消息队列(message queue)

1.message queue 简称 mq, 主要解决应用耦合、异步消息、流量削锋等问题。实现高性能、高可用、可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。

2.缺点

1)系统可用性降低:系统引入的外部依赖越多,越容易挂掉。

2)系统复杂度提高了

3)一致性问题:消息传递给多个系统,部分执行成功,部分执行失败,容易导致数据不一致

3.目前在市面上比较主流的消息队列中间件主要有,Kafka、ActiveMQ、RabbitMQ、RocketMQ等这几种。

ActiveMQ和RabbitMQ这两着因为吞吐量还有GitHub的社区活跃度的原因,在各大互联网公司都已经基本上绝迹了, 现在各大公司用得最多的就是Kafka和RocketMQ




利用kafka实现消息队列优缺点 kafka消息队列的缺点_Powered by 金山文档


编辑切换中

RocketMQ是阿里开源的项目,git活跃度还可以。基本上你push了自己的bug确认了有问题都有阿里大佬跟你试试解答并修复的,他的架构设计部分跟同样是阿里开源的一个RPC框架(Dubbo)很像。

这里重点讲Kafka,Kafka起初是由 Linkedin公司采用 Scala语言开发的一个多分区、多副本且基于 ZooKeeper 协调的分布式消息系统,现己被捐献给 Apache 基金会 。 目前 Kafka 已经定位为一个分布式流式 处理平台,它以高吞吐、可持久化、可水平扩展、支持流数据处理等多种特性而被广泛使用。

kafka基础认识


利用kafka实现消息队列优缺点 kafka消息队列的缺点_kafka_02


整个 Kafka 体系结构中引入了以下 3 个术语。

( 1) Producer: 生产者,也就是发送消息的一方。生产者负责创建消息 , 然后将其投递到Kafka 中 。

( 2 ) Consumer:消费者,也就是接收消息的 一方。消费者连接到 Kafka 上并接收消息,进而进行相应的业务逻辑处理 。

(3) Broker:服务代理节点。对于 Kafka 而言, Broker 可以简单地看作一个独立的 Kafka 服务节点或 Kafka服务实例。大多数情况下也可以将 Broker看作一台 Kafka服务器,前提是这 台服务器上只部署了一个 Kafka 实例。一个或多个 Broker 组成了 一个 Kafka 集群 。一般而言, 我们更习惯使用首字母小写的 broker 来表示服务代理节点 。

主题跟分区

在Kafka中还有两个特别重要的概念一一主题(Topic)与分区(Partition)。 Kafka中的消息以主题为单位进行归类,生产者负责将消息发送到特定的主题(发送到 Kafka 集群中的每一条消息都要指定一个主题),而消费者负责订阅主题并进行消费。

主题是一个逻辑上的概念,它还可以细分为多个分区,一个分区只属于单个主题。同一主题下的不同分区包含的消息是不同的,消息在被追加到分区的时候都会分配一个特定的偏移量(offset)。 offset是消息在分区中的唯一标识, Kafka通过它来保证消息在分区内的顺序性,不过 offset并不跨越分区,也就是说Kafka保证的是分区有序而不是主题有序。每一条消息被发送到 broker 之前,会根据分区规则选择存储到哪个具体的分区 。 如果分区规则设定得合理,所有的消息都可以均匀地分配到不同的分区中 。

多副本机制

同一分区的不同副本中保存的是相同的消息(在同一时刻,副本之间并非完全一样),副本之间是 “一主多从”的关系,其中 leader副本负责处理读写请求,follower副本只负责与 leader副本的 消息同步。副本处于不同的 broker 中 ,当 leader 副本出现故障时,从 follower 副本中重新选举 新的 leader副本对外提供服务。 Kafka通过多副本机制实现了故障的自动转移,当 Kafka集群中某个 broker 失效时仍然能保证服务可用。

Producer详解

生产者发送消息共有三种模式:

1.fire-and-forget 发后即忘:只管往Kafka里发送消息,不管消息发送的结果。有可能在异常情况下造成消息丢失,可靠性最差,性能最高

2.sync 同步:同步发送可靠性很高,发送消息时要么返回发送成功,要么返回异常。但是性能 很差,需要阻塞等待消息发送完后才会发送下一条

3.async异步:指定一个回调方法,会在发送完成后把结果回调到指定方法内。

消息在send()发送到broker的过程中,会经过拦截器,序列化器和分区器:

拦截器:在调用序列化器和分区器之前会调用配置的拦截器。可以对发出去的消息作统一处理。

序列化器:生产者用序列化将对象转成字节数组传送到Kafka中,消费者通过反序列化器转成对应的对象。生产者与消费者使用的序列化器必须是一一对应关系。

分区器:计算消息发送的分区ID。默认的分区器是org che.kafka.clients.producer intemals.DefaultPartitioner。会对key进行哈希,最终根据得到的哈希值计算分区。所以拥有同一个key消息会放到同一个分区里,如有没有指定key,那么就会轮询到任意一个可用的分区当中。


利用kafka实现消息队列优缺点 kafka消息队列的缺点_利用kafka实现消息队列优缺点_03


编辑切换为居

整个生产者客户端由主线程和sender线程组成:主线程负责从消息创建到拦截器,序列化器,分区器后将消息缓存到消息累加器。sender线程则负责从消息累加器中获取消息然后发送到Kafka中

消息累加器RecordAccumulator:

消息累加器主要是用来缓存消息方便sender批量发送,减少网络传输提升性能,可以通过生产者的buffer.memory进行设置大小,默认值是32mb。如果生产者生产消息的速度比传输到broker中的速度要快很多,那么累加器中空间不足的话。在发送消息调用send()方法时就会消息阻塞或者抛异常。

消息累加器工作流程:从主线程发送过来的消息都会放入累加器的一个双端队列里。在累加器的内部为每一个分区都维护了一个双端队列。队列里的元素都是一个个的ProducerBatch,消息发送过来时都会放到队列的尾部,sender线程读取的时候都会从头部读取。一个ProducerBatch指的是一个消息批次,由多个消息组成,可以减少网络请求和提高吞吐量。ProducerBatch的大小由参数batch.size控制。默认时16kb,如果新消息的大小比batch.size大,那么会重新创建一个消息大小的ProducerBatch,但是这个ProducerBatch内存将不能复用。sender线程从累加器中获取到消息后,将会转换成<Node,List< ProducerBatch>的形式,其中node代表的是Kafka集群的broker节点。然后再传给InFlightRequests,InFlightRequests存储的是已经发出去但是没有收到响应的请求,它会获取到负载最小的broker节点进行发送。

重要的生产者参数:

1.ack

这个参数用来指定分区中必须要有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入的。 acks 是生产者客户端中一个非常重要参数 ,它涉 消息的可靠性和吞吐之间的权衡.

1):ack=1.默认值就是1,即只要分区的leader副本写入这条消息,那么服务端就会返回写入成功,如果分区leader副本写入成功后奔溃了,其他follower副本还没来得及进行同步,那么在重新进行选举后新的leader副本就会丢失这条消息。

2):ack=0 生产者发送消息后不需要等服务端的返回。默认就是成功。如果在写入过程中有异常,那么生产者也不知道。能够保证最大的吞吐量

3):ack=-1或者ack=ALL 生产者在发送消息后需要等待leader副本和所有的可用的follower副本全部成功写入后才会返回成功。

2.max.request.size(重要)

这个参数代表生产者能够发送的消息最大值,默认值是1mb。不要盲目增大这个配置,如果需要增大这个配置。还需要增大服务端和消费者端对应的配置,比如服务端有个message.max.bytes:broker能接收消息的最大字节数,如果超过就会抛异常。

3.retries和retry.backoff.ms

retries参数代表的是生产者重试次数,默认为0,retry.backoff.ms代表的是每次重试的间隔时间

4.max.flight.requests.per.connection 默认5

该参数指定了sender在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量。把它设为 1 可以保证消息是按照发送的顺序写入服务器的,即使发生了重试。如果将 acks 参数配置为非零值,并且max.flight.requests.per.connection 参数配置为大于1的值,那可能会出现错序的现象:如果第一批次消息写入失败,而第二批次消息写入成功,那么生产者会重试发送第一批次的消息,此时如果第一次的消息写入成功,那么这两个批次的消息就出现了错序。一般而言,在需要保证消息顺序的场合建议把参数 max.in.flight.requests.per.connection 配置为 1 ,而不是把 acks 配置为 0,不过这样也会影响整体的吞吐。

5.compression.type

这个参数指定消息的压缩方式,默认值是none,是一种时间换空间的方式,对消息延时性要求较高的不推荐使用

6.linger.ms

这个值代表生产者发送ProducerBatch等待的时间。默认值是0,sender线程会在ProducerBatch被填满或者等待时间超过linger.ms设置的值后拿取消息发送出去。增大这个值的话会增加消息的延迟性。

7.buffer.memory:

消息累加器的大小值,如果生产者生产消息的速度比传输到broker中的速度要快很多,那么累加器中空间不足的话。在发送消息调用send()方法时就会消息阻塞或者抛异常。

8.batch.size

用于指定 ProducerBatch 可以复用内存区域的大小。

Consumer详解

消费者与消费组

消费者订阅Kafka中的主题,并且从Kafka中拉取消息。在kafka中,还有一个消费组的概念,每一个消费者都对应一个消费组,当消息发布到主题后,只会被每个订阅主题的其中一个消费者进行消费。


利用kafka实现消息队列优缺点 kafka消息队列的缺点_Powered by 金山文档_04


比如:某个主题有4个分区,p0,p1,p2,p3。有两个消费组A,B订阅了这个主题,消费组A中有4个消费者C0 Cl C2 C3 ,消费组B中有两个消费者 C4 C5。那么最后分配的规则就是A消费组中每个消费者对应一个分区,B消费组中每个消费者对应两个分区。

消费者与消费组这种模型可以让整体的消费能力具备横向伸缩性,可以通过增加(或减少)消费者的个数来提高(或降低〕整体的消费能力,对于分区数固定的情况,一昧地增加消费者并不会让消费能力 直得到提升,如果消费者过多,出现了消费者的个数大于分区个数的情况,就会有消费者分配不到任何分区。

这些分配逻辑是根据Kafka默认的分区分配逻辑进行分析的。

消费者在进行消费前需要指定消费组名称,通过参数group.id进行配置,默认值为空字符串。

位移提交

kafka的消息消费实采用的基于拉模式。Kafka的消息消费是一个不断轮询的过程,消费者重复调用poll()方法,获取订阅主题分区上的消息。

对于分区而言,每一个消息都有一个唯一的offset,用来表示消息在分区中对应的位置。消费者也有一个offset的概念,表示消费到分区中某个消息的位置。Kafka将对消费者提交上来的位移进行持久化,存储在Kafka的内部主题__consumer_offsets中,消费者在消费完消息后进行的位移提交就是将消费位移存储在内部主题当中。

Kafka内部默认的位移提交方式是自动提交,由消费者端参数enable.auto.commit配置,默认true,这个自动提交是定时提交,默认是5秒提交一次,如果要修改的话,需要修改auto.commit.interval.ms值,自动提交的动作是在poll()方法里进行的。每次向服务器进行拉去消息的时候都会判断是否进行了位移提交,如果没有提交,那么就会进行提交。

自动提交的问题:

自动提交非常简便,免去了复杂的位移提交问题。但是自动提交会带来重复消费和消息丢失的情况。自动提交相当于延迟提交。

比如:刚提交完位移,然后拉取了一批消息,但是消费者崩溃了,那么下次又会从上次提交位移的地方拉取消息,就会造成重复消费。

手动提交:手动提交细分可以分为同步提交和异步提交,可以根据具体的业务来选择提交时间。同步提交每次消费一条消息提交一次消费位移,在提交完成之前会阻塞消费线程等到提交完成后再消费下一条消息。缺点是损耗性能,会降低吞吐量。但是能保证消息的安全性。异步提交不会阻塞消费线程。当执行提交操作后立刻消费下一条消息,使消费者的性能和吞吐量得到一定的提高。异步提交结果会采用回调的方式返回,异步提交也会有失败的情况,如果异步提交失败,如果直接进行重试也会有重复消费的风险。可以采用位移判断的方式检测是否重试来增加容错。

1.ack-mode 位移提交模式


利用kafka实现消息队列优缺点 kafka消息队列的缺点_Powered by 金山文档_05


2.auto-commit-interval 自动提交的时间间隔

消费者重均衡


利用kafka实现消息队列优缺点 kafka消息队列的缺点_kafka_06


触发 Kafka 重平衡的有以下几种情况:


消费组成员发生变更,有新消费者加入或者离开,或者有消费者崩溃;


消费组订阅的主题数量发生变更;


消费组订阅的分区数发生变更。

主要涉及到参数:

1.session.timeout.ms 默认10秒

该参数是 Coordinator 检测消费者失败的时间,即在这段时间内客户端是否跟 Coordinator 保持心跳,如果该参数设置数值小,可以更早发现消费者崩溃的信息,从而更快地开启重平衡,避免消费滞后,但是这也会导致频繁重平衡,这要根据实际业务来衡量。

2.max.poll.interval.ms 默认300秒

消费者处理消息逻辑的最大时间,对于某些业务来说,处理消息可能需要很长时间,比如需要 1分钟,那么该参数就需要设置成大于 1分钟的值,否则就会被 Coordinator 剔除消息组然后重平衡。

3.heartbeat.interval.ms 默认3秒kafka会有一个心跳线程来同步服务端,告诉服务端自己是正常可用的,默认是3秒发送一次心跳。该参数跟 session.timeout.ms 紧密关联,前面也说过,只要在 session.timeout.ms 时间内与 Coordinator 保持心跳,就不会被 Coordinator 剔除,那么心跳间隔的时间就是 heartbeat.interval.ms,因此,该参数值必须小于 session.timeout.ms,以保持 session.timeout.ms 时间内有心跳。

重要的消费者参数

1.fetch.min.bytes

该参数用来配置 Consumer 在一次拉取请求(调用 poll () 方法)中能从 Kafka 中拉取的最小数据量,默认值为1(B) Kafka 在收到 Consumer 拉取请求时,如果返回给 Consumer 的数据量 于这个参数所配置的值,那么它就需要进行等待,直到数据量满足这个参数的配置大小。可以适当调大这个参数的值以提高一定的吞吐量,不过也会造成额外的延迟,对于延迟敏感的应用可能就不可取了。

2.fetch.max.bytes

消费者从Kafka一次拉取的最大数据量。默认是50mb,但是当这个值很小时,消费者还是会正常返回。Kafka中能接收的最大消息是通过服务端的message.max.bytes控制的。

3. fetch.max.wait.ms

用于指定拉取消息时最大等待时间。默认是500ms,如果超过这个时间就算没有满足最小的数据量也会返回。

4. max.poll.records

代表消费者一次拉取请求中拉取的消息最大数,默认是500条,如果消息体比较小可以适当调大。

分区分配策略

Kafka在默认的情况下采用的是RangeAssignor分配策略,除此之外,Kafka还提供了另外两种分区分配的策略,RoundRobinAssignor和StickyAssignor,可以通过消费者客户端参数partition.assignment.strategy参数配置。

RangeAssignor:分配原理是根据消费者总数和分区总数进行整除运算,然后将分区进行平均分配,保证尽可能的把分区均匀分配给所有的消费者。每一个主题,会按照字典对订阅该主题的消费者进行排序,如果不够平均分配,那么排前面的就会多分配一个。

RoundRobinAssignor:分配原理是将所有的消费者和所有的主题分区按照字典排序,然后通过轮询的方法将分区依次分给每个消费者。如果消费组里面所有的消费者订阅的主题是一样的,那么是均匀分配。

StickyAssignor:比前两种复杂,但是会使分区分配更合理,如发生再均衡操作时,会保留原来消费者的分区,然后再均衡分配。

自定义分配策略:实现PartitionAssignor接口,可以打破一个分区只能被一个消费者消费的规则,但是要注意位移提交的覆盖。