消息中间件笔记

924 阅读13分钟

一、使用场景

  • 异步处理
  • 流量控制(削峰)
  • 解耦

二、常见消息队列对比

  • RabbitMQ:路由灵活、性能相对较差、非分布式架构、消息堆积严重影响性能
  • RocketMQ:低延时、稳定
  • Kafka:高吞吐量、适合海量数据、生态系统完善

三、消息模型(业务层面)

  • 队列模型

    消费者之间是竞争关系,一条消息只能被一个消费者消费(producer->queue->consumer)
  • 发布-订阅模型

    在每个订阅中,订阅者都可以接收到主题的所有消息。如果只有一个订阅者就和队列模型基本一致(现代消息队列大多使用)(publisher->topic->subscriber)
  • RabbitMQ的消息模型

    使用队列模型,对于一份消息需要被多个消费者消费,使用Exchange将消息发送到多个队列,每个队列存放一份完整消息数据(producer->exchange->queue->consumer)
  • RocketMQ的消息模型

    在topic下面存在queue概念,订阅者概念通过消费者组(consumer group)实现。每个主题包含多个队列,通过多个队列来实现多实例并行生存消费。只在队列层面保证消息有序性。
  • Kafka的消息模型

    与RocketMQ一致,队列(queue)对应的名字叫分区

四、消息丢失问题

  • 生产阶段

    请求确认机制,只要producer收到了broker的确认响应就可以保证在生产阶段不会丢失,需要捕获消息发送的错误并重发(特别是异步发送时需要在回调中检查发送结果)
  • 存储阶段

    如果对消息可靠性要求高;对于单个节点的broker,需要配置参数在收到消息后写入磁盘成功再给producer返回确认。如果是broker集群,需要配置成至少发送到2个以上的节点再给客户端返回确认。
  • 消费阶段

    请求确认机制,客户端从broker拉取消息执行消费逻辑成功后才返回确认响应,如果没有收到消费确认下次拉取还会返回同一消息。需要设置手动ack,并且在执行完所有消费逻辑之后再发送消费确认。

五、重复消息问题

消费操作具备幂等性(任意多次执行所产生的影响与一次的影响相同)

  • 业务逻辑设计成具备幂等性操作

  • 利用数据库的唯一索引约束实现幂等

  • 为更新的数据设置前置条件

  • 记录并检查操作(需要保证原子性)

六、消息积压问题

  • 发送端性能优化

    • 增加并发
    • 增加每次发送的批量
  • 消费端性能优化

    • 一定要保证消费端的消费性能高于生产端的发送性能,这样的系统才能持续健康的运行
    • 水平扩容,扩容consumer实例数量的同时必须同步扩容partition数量
  • 解决积压问题

    • 发送过快,扩容消费实例
    • 发送过快,系统降级,关闭不重要的业务减少消息发送
    • 消费过慢,查看是否大量错误消费,是否阻塞
    • 发送消费速度无异常,可能是消费失败导致同样消息反复消费

七、RocketMQ

消息发送

  • 同步发送:消息发送后会等待broker的响应
  • 异步发送:消息发送后不等待响应,后续响应返回触发回调函数
  • 单向发送:发送消息后不等待响应且没有回调函数触发

消息消费

  • 拉取型消费者:主动从服务器拉取消息
  • 推送型消费者:封装了消息的拉取、消费进度和其他内部维护工作,将消息到达时执行的回调接口留给用户实现。实现上还是从服务端拉取消息,不同的是先要注册消费监听器,当监听器被触发才开始工作
  • 消费模式

    • 集群消费 :默认,一个消费者集群共同消费一个主题的多个队列,一个队列只会被一个消费者消费
    • 广播模式:将消息发给消费者组中的每一个消费者消费

消息重试

  • 生产者端重试

    向broker发送消息时,如果由于网络抖动等原因导致消息发送失败,可以设置失败重试次数让消息重发
  • 消费者端重试

    • 由于网络等原因导致消息没法从broker发送到消费者端,此时会重试直到发送成功(可以切换broker)
    • 消费者端已经正常接收到消息但是在执行后续消息处理时发生了异常,最终返回处理失败。broker会在某个时间后(默认10秒后)重新投递,如果一直失败积累到一定次数(默认16)之后会将消息投递到死信队列,此时需要监控死信队列处理

定时消息

只支持固定精度级的定时消息,按照1~n定义了1、5、10、30s、1、2、3、4、5、6、7、8、9、10、20、30m、1、2h

批量发消息

相同topic且相同发送状态的非定时消息可以选择批量发送方式,但是一次不能超过1MB,需要手动编码处理

事务消息

开启事务->发送半消息->提交或回滚->投递消息,实际上消息已经发送至broker只是没提交前无法消费。对于事务提交或回滚消息broker没有收到的情况,broker会定期去producer反查这个事务对应本地事务的状态,根据结果确定提交或回滚,业务代码需要实现一个反查本地事务的接口

Broker集群

Broker分为Master(BrokerId为0)和Slave(BrokerId 不为0)两种,Master可以部署多个,Master和Slave存储的数据一样,Slave根据配置从Master中同步数据。每个broker会与nameserver集群中的所有节点建立长连接定时注册topic信息到所有nameserver。

队列会平均分散在多个broker实例上,生产者的发送机制保证消息尽量被平均分摊到所有队列。队列本身不存储消息,消息真正被存储在CommitLog文件中,队列只是存储CommitLog中对应的位置信息。

刷盘

  • 同步刷盘 :生产者发送的消息都要等到消息落盘成功才能返回成功消息,可以避免消息丢失,对性能有一定影响。
  • 异步刷盘: 先缓存起来就向生产者返回成功,后续再将缓存的数据异步落盘。有两种情况:一是定期刷盘,二是缓存中的数据达到设定的阀值。

复制(master到slave)

  • 同步:至少步复制到一个slave之后才向生产者返回成功
  • 异步:消息只要写入master就向生产者返回成功消息,然后异步复制到slave

八、Kafka

分区(Partition)

Kafka的基本存储单元,实际虽然物理上最小单位是Segment,但是Kafka并不提供一个Partition内不同Segment的并行处理能力。每次只会写Partition内的一个Segment,顺序读一个Partition内的不同Segment。不同Partition可位于不同的机器上也可位于同一台服务器上(一个Partition对应一个文件夹)。在生产者和broker角度,不同Partition的写操作完全并行,对于消费者并发数取决于Partition数量

复制

通过使用ZooKeeper提供的leader选举方式实现数据复制。首先选举出一个分区leader,其他副本作为follower,所有写操作先发给leader然后由leader发给follower。复制是针对分区的,所有生产者和消费者的请求都会经过leader,follower是从leader处复制消息数据。

消息发送

  • 消息发送方式

    • 立即发送,不关心消息发送结果
    • 同步发送,发送消息后获取Future对象查看发送结果
    • 异步发送,注册一个回调函数,生产者收到服务器响应时会触发执行回调函数
  • 发送错误

    • 可重试错误如连接错误,可通过配置生产者自动重试来解决。如果还是不行应用会收到一个重试异常
    • 不能通过重试来解决的错误,直接抛出异常给生产者
  • 消息发送确认

    • 不等Broker确认,消息被发送出去就认为是成功的
    • 由leader确认,当leader确认接收到消息就认为发送是成功的,然后再由其他副本通过异步方式拉取
    • 由所有leader和follower都确认接收到消息才认为是成功的,可靠性最高,性能稍有影响
  • 消息重发

    生产者提供自动重试机制但不能无限重发,由初始化生产者对象时的retries属性决定,默认会在重试后等待100ms(retry.backoff.ms)
  • 批次发送

    当有多条消息要被发往同一分区时,生产者会把它们放到同一批次里,通过批次的概念来提高吞吐量,同时也会增加延时。对批次的控制由生产者对象的两个属性实现,batch.size(发往每个分区的缓存消息数量达到这个值就会触发网络请求发送批次里所有消息),linger.ms(每条消息在缓存中的最长时间,超过这个时间就会忽略batch.size由客户端发送出去)
  • 事务消息

    解决的问题是确保在一个事务中发送的多条消息要么都成功要么都失败,这里的多条消息不一定要在同一个主题和分区中。可以在Kafka事务执行过程中加入本地事务实现和RocketMQ中事务类似效果,但Kafka中是没有事务反查机制的

消费者组

一个Topic可以有多个消费者组,topic的消息会被复制(不是真正的复制)到所有的消费者组中。在一个消费者组内可以有多个消费者,共享同一个分组ID。组内所有消费者协调消费它们订阅主题下的所有分区消息,一个分区只能由一个消费者组里的消费者来消费

  • 广播和单播

    实现广播方式只需每个消费者都分配一个单独的消费组。实现单播方式则每个Topic只能有一个消费者组

  • Rebalance

    Rebalance本质上是一种协议,规定了一个消费者组下的所有消费者如何达成一致来分配主题下的每个分区。

    • 触发Rebalance的场景
      • 消费者组内成员发送变更(消费者加入或离开)
      • 订阅的主题数量发生变化
      • 订阅主题的分区数量发生变化
    • 消费者每次调用poll方法时都会向集群coordinator节点Broker发送心跳消息,如果超过指定时间没有收到对应的心跳消息,则broker会认为该消费者已经死亡,因此将该消费者负责的分区派给其他消费者消费
    • Rebalance操作影响整个消费者组,消费全部暂停消费直到结束。通过控制发送心跳频率和会话过期时间来尽量避免这种情况发生
  • 消费偏移量

    Kafka服务端并不保存消息的状态,消费的时候需要消费者自己做很多事情,消费者每次调用poll方法时返回的总是没有被消费者消费的消息。

    Kafka中有一个叫作_consumer_offset的特殊主题用来保存消息在每个分区的偏移量,消费者每次消费时都会往这个主题中发送消息,消息包含每个分区的偏移量。Rebanlance之后为了能继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量,开始继续消费

    • 提交方式会影响消息的处理
      • 自动提交:默认定期自动提交(enable.auto.commit默认true),默认时间是5秒(auto.commit.interval.ms),简单但是会产生消息重复问题
      • 手动提交:在程序中自己决定何时提交的方式来解决丢失消息可能并减少消息重复数量,需要关闭自动提交。使用commitSync()提交,在broker响应之前是阻塞的,并且会在成功或者遇到不可恢复错误之前一直重试。每提交一次就等待一次限制了消费端的吞吐量,降低提交频率又会增加重复消费概率
      • 异步提交:使用commitAsync()提交,它不会一直重试(防止接收响应时已有更大偏移量提交而导致覆盖)。可以增加回调,broker响应之后执行回调,常用于记录提交错误或应用度量指标

九、RabbitMQ

消息保存

  • disk:集群中必须有一个disk方式节点,供启动时ram节点同步消息。在发送消息时指明需要写入磁盘。当消息服务器内存紧张时会将部分内存中的消息转移到磁盘.rdq文件。文件达到16m(默认)时会生成一个新文件,文件中已被删除的消息比例大于阀值会触发文件合并
  • ram:不保存消息、消息存储索引、队列索引和其他节点状态等数据
  • 分别支持Queue、Message、Exchange持久化

消息确认模式

  • 事务模式:生产者同步等待broker的执行结果
  • 发送方确认:把channel设置为确认模式,该channel发布的消息会分配一个唯一ID,一旦消息投递到队列channel就会向生产者发送确认消息,确认消息中包含消息ID。
    • 普通确认:发送完消息调用waitForConfirms等待broker的确认
    • 批量确认:每发送完一批消息再调用waitForConfirms等待确认
    • 异步确认:通过addConfirmListener方法注册回调,broker确认了一条或者多条后回调该方法

消费者应答

  • 自动回执

    当broker成功发送消息给消费者之后就会立即把此消息从队列中删除,不需要等待消费者返回确认消息

  • 手动回执

    当broker发送消息给消费者后并不会立即把此消息删除,而是要等收到消费者返回的确认消息后才删除。消费者收到消息处理完成后需要向broker显示发送ack

  • 拒绝消息

    当消费者处理消息失败或者当前不能处理消息时可以给broker发送一个拒绝消息的指令,要求broker将该消息丢弃或者重新放入队列中。当队列只有一个消费者时需要确认不会因为拒绝消息并选择重新放入队列中而导致消息在同一消费者发生死循环

  • 消息预取

    通过设置预取数量限制每个消费者在收到下一个确认回执前一次最多可以接收多少条消息

  • 流控机制

    当rabbitmq服务器出现内存或磁盘等资源的使用量达到设置的阀值时(消息大量堆积)会触发流控机制,阻塞客户端的连接,阻止生产者继续发送消息,服务端消息推送也会受到极大影响直到警告解除。