基本概念
- Kafka 最初有Linkedin公司开发,是一个分布式、支持多分区、多副本、多订阅者,基于ZooKeeper 的分布式消息流平台。主要Producer 将消息发送到Broker,Broker 负责将收到的消息存储在磁盘中,而Consumer负责从Broker 订阅并消费消息。
- Producer生产者,发送消息的一方,负责发布消息到Kafka。
- Consumer消费者,消费消息的一方,连接到Kafka上并接收消息,并进行相应的业务逻辑处理。
- Consumer Group消费组:Kafka是按消费组来消费消息,一个消费组下面的所有机器可以组成一个Consumer Group,每条消息只能被该Consumer Group一个Consumer消费,不同Consumer Group可以消费同一条消息。
- Broker服务代理节点,可以简单的看做一个独立的Kafka服务节点或Kafka服务实例。
- ZooKeeper 管理Kafka 集群,负责存储集群Broker、Topic、Partition等meta数据存储,同时也负责Broker故障发现,Partition Leader选举,负载均衡等功能。
- Topic Kafka中的消息以主题为单位进行归类,是一个逻辑上的概念。生产者负责将消息发送到特定的主题,消费者负责订阅主题并进行消费。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上,但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)。
- Partition分区是物理上的概念,每个Topic包含一个或多个Partition。一个分区只属于一个主题,又叫主题分区,存储层面体现为一个日志文件。同一分区下的所有副本保存有相同的消息序列,这些副本分散在不同的Broker上,从而能够对抗部分Broker宕机带来的数据不可用。Partition物理上由多个Segment 文件组成,每个Segment 大小相等,顺序读写,每个Segment数据文件以该段中最小的offset,文件扩展名为.log,生产者发送消息到partition,消费者从partition拉取消息进行消费。
- Consumer Group 每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。
- offset 是消息在分区中的唯一标示(指向partition中下一条将要被消费的消息位置),Kafka 通过它来保证消息在分区内的顺序性,但是offset 并不跨越分区(Partition),Kafka保证的是分区的有序而不是主题有序。
- 如果分区规则设定得合理,所有的消息都可以均匀地分配到不同的分区中。如果一个主题只对应一个文件,那么这个文件所在的机器I/O将成为这个主题的性能瓶颈,而分区解决了这个问题。//一个主题对应了8个分区,部署在两地,分别4个。
- Kafka 为分区引入了多副本(Replica)机制,通过增加副本的数量可以提升容灾的能力。同一分区的不同副本中保存的相同的消息,注意同一时刻,由于主从数据同步延迟,副本之间并非完全一致。注意到生产者和消费者只连接着Leader ,Follower副本只与Leader副本交互, 从Leader中异步拉取消息,不对外提供服务。当Leader副本挂掉后,或者说Leader副本所在的Broker宕机时,Kafka依托于ZooKeeper提供的监控功能实时感知到,并立即开启新一轮的领导选举,从Follower副本中选一个新的Leader,老Leader副本重启回来后,只能作为Follower加入到集群中。
- 可以串联起Kafka的三层消息架构,第一层是主题层,每个主题可以配置M个分区(垂直),而每个分区又可以配置N个副本;第二层是分区层,每个分区的N个副本只能有一个充当Leader角色,对外提供服务,其他N-1副本是Follower副本,只是提供数据冗余;第三层是消息层,分区中包含若干条消息,每条消息的位移从0开始,依次递增。
- Consumer 使用拉(Pull)模式从服务端拉取消息,并且保存消费的具体位置,当消费者宕机后恢复上线时可以根据之前保证的消费位置重新拉取需要的消息进行消费。
- Kafka 引入ISR(In-of-Sync Replicas)机制,ISR副本都是与Leader副本同步的副本,相反不在ISR中的Follower副本就被认为是与Leader不同步的。注意ISR必然包括Leader副本,甚至某些情况下,ISR只有Leader副本。
- Leader 副本负责维护和跟踪ISR集合中所有Follower 副本的滞后状态,当Follower副本滞后太多而失效时,Leader 副本会把它从ISR 集合中剔除,转移到OSR集合中(Out-of-Sync Replicas)。如果OSR集合中Follower 副本追上了Leader 副本,那么转移到ISR集合中。注意默认情况下当Leader 副本发生故障时,只有ISR 集合中的副本才有资格被选举为新的Leader,OSR集合中副本没有机会。
- 注意Broker参数replica.lag.time.max.ms 代表着Follower副本能够落后Leader副本的最长时间间隔,当前默认值是10秒。也就是说只要一个Follower副本落后Leader副本的时间不连续超过10秒,那么Kafka就认为该Follower副本与Leader是同步的。所以说ISR集合时动态变化的,不是静态不变的。
- 同步复杂要求所有能工作的Follower 副本都复制完,这条消息才会被确认为已成功提交,这种方式极大影响了性能。而在异步复制方式下,Follower 副本异步从leader副本复制数据,数据只要被leader 副本写入就被认为已经成功提交,这种情况Follower 都还没有复制完落后于Leader 副本,如果Leader 突然宕机,则会造成数据丢失。Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制,而是ISR 方式,有效地权衡数据可靠性和性能之间的关系。
- ISR 与HW(High Watermark)和LEO(Log End Offset)有紧密的关系,HW标志了一个特定的消息偏移量,消费者只能拉取到这个offset之前的消息。LEO标示昂前日志文件中下一条待写入的消息offset,分区ISR集合中每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区HW。
生产者
- 生产逻辑:配置生产者客户端参数及创建相应的生产者实例 -> 构建待发送的消息 -> 发送消息 -> 关闭生产者实例 。//把大象装机冰箱步骤
- 一个topic可以有若干个分区,且分区可以动态修改,但是只允许增加不允许减少,否则 Kafka 会抛出 InvalidPartitionsException 异常,因为减少分区要考虑数据保留问题。
- 每个分区中的消息是有序的,各个分区之间的消息是无序的。新消息根据分区规则,采用追加的方式写入到对应分区日志文件的尾部。
- key用来指定消息的键,它不仅是消息的附加信息,还可以用来计算分区号进而可以让消息发往特定的分区。消息以主题(Topic)为单位进行归类,而key可以让消息再进行二次归类,同一个key的消息会被划分到同一个分区中。
- 同分区的不同副本中保存的是相同的消息(在同一时刻,副本之间并非完全一样),副本之间是“一主多从”的关系,其中leader副本负责处理读写请求, follower副本只负责与 leader副本的消息同步,不支持读写分离。
- 副本处于不同的 broker中,当 leader副本出现故障时,从 follower副本中重新选举新的 leader副本对外提供服务。
- acks 是生产者中非常重要的的参数,它涉及消息的可靠性和吞吐量之间的权衡。acks= 1,生产者发送消息之后,只要分区Leader副本成功写入消息,那么他就会收到来自服务端的成功响应,是消息可靠性和吞吐量之间的折中方案;acks=0,生产者发送消息之后不需要等待任何服务端的响应,优势是最大吞吐量,劣势是消息从发送到写入kafka 过程中出现某些异常,生产者无从感知,消息也就丢失了;acks=-1,生产者在消息发送之后,需要等待ISR中的所有副本都成功写入才能够收到来自服务端的成功响应,优势是可靠性很强。
- compression参数指定消息的压缩方式,默认为none 不压缩,可以设置为“gzip”、“snappy”、“lz4”,消息压缩可以极大地减少网络传输量、降低网络I/O,从而提高整体性能。压缩是一种使用时间换空间的优化方式。
- max.request.size用来限制生产者客户端能发送的消息最大值,默认为1M。retries配置生产者重试的次数,默认为0次。retry.backoff.ms用来设定两次重试之间的时间间隔,避免无效频繁重试。request.timeout.ms配置Producer等待响应的最长时间,默认为30000ms
消费者
- 每一个分区只能被一个消费组中一个消费者所消费,一个消费者可以消费多个分区。如果消费者过多,出现了消费者的个数大于分区个数的情况,就会出现消费者分配不到任务分区。
- 对于消息中间件而言,一般有两种消息投递模式:点对点(P2P)模式和发布/订阅(Pub/Sub)模式。点对点是基于队列的,发布定于模式定义了如何向一个内容节点发布和订阅,这个内容节点成为主题,主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,消息订阅者从主题中订阅消息,一对多广播时采用发布/订阅模式。
- 如果所有的消费者都隶属于同一个消费组,那么所有的消息都会被均衡地投递给每个消费者,即每条消息只会被一个消费者处理,这就相当于点对点模式的应用。
- 如果所有的消费者都隶属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者处理,这就相当于发布/订阅模式的应用。
- 消费步骤:创建消费者实例 -> 订阅主题 -> 拉取消息并消费 -> 提交消费位移 -> 关闭消费者实例。
- Kafka 中消费时基于拉模式的。是补一个不断轮询的过程,消费者要做的就是不断重复地调用poll()
主题与分区
- 主题(Topic)和分区(Partition)都是逻辑上概念,分区可以有一个至多个副本,每个副本对应一个日志文件,每个日志文件对应一至多个日志分段(LogSegment),每个日志分段还可以细分为索引文件、日志存储文件和快照文件等。
问答
消息队列适用于哪些场景?
- 异步处理:消息发送者 可以发送一个消息而无须等待响应,耗时久的服务异步处理。
- 应用解耦:发送者和接受者不必了解对方、只需要确认消息,比如发送和接收者可以是不同的系统,不同的语言编写的,地理上可以不在同一个地域,发送者和接受者不必同时在线。
- 流量削峰:当在线接口在应对高峰流量时,比如“秒杀”,“流量激增”时,如果接口处理能力有限,可以先将无法及时处理的请求发送给消息队列,后台处理,防止将api接口打死。
- 扩展性:只要增加处理过程即可,不需要改变代码、不需要调整参数,便于分布式扩容。(开闭原则,应该对扩展开放,而对修改关闭,应该通过扩展实现相应的功能变化,而不是通过修改代码来实现)
- Pub/Sub模型:一条消息,可以广播给任意个收听方。
- 分布式事务一致性:多个应用系统之间的数据状态同步。
- 流数据处理:分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为,针对这些数据流进行实时或者批量采集汇总,然后导入到大数据实时计算引擎。
MQ如何保证高可用?
- Broker分散到不同机器上,即使其中一台机器宕机,其他机器上的Broker也能够对外提供服务。
- 备份机制(Replication),把相同的数据拷贝到多台机器上,这些相同的数据拷贝在Kafka中称为副本(Replica)。Kafka定义了两类副本:Leader Replica对外提供服务,指与客户端程序进行交互,Follower Replica只是被动追随Leader,不能与外界进行交互。属于主备不是主从。
Partition机制有哪些?
Partition机制也就是Poducer消息partitioning策略,主要有一下几种:
- 轮询,默认策略,表现优秀,保证消息最大限度地被平均分配到所有分区上
- 随机
- 按消息键保序,Kafka允许为每条消息定义消息键,简称为Key,可以具有业务含义,一旦消息被定义了Key,可以保证同一个Key的所有消息都进入到相同的分区里,由于每个分区下的消息处理都是顺序的
- Hash
- 自定义,实现org.apache.kafka.clients.producer.Partitioner接口
MQ如何保证传输可靠性?
生产者丢失
生产者丢失消息的可能点在于程序发送失败抛异常了没有重试处理,或者发送的过程成功但是过程中网络闪断MQ没收到,或者消息本身不合格(消息太大超过了Broker的承受能力)导致Broker拒绝接受,消息就丢失了。
- 同步处理:如RabbitMQ提供事务功能channel.txSelect,生产者发生数据MQ没有接受到,生产者会收到报错,然后回滚事务,重新发送消息;如果收到消息则可以提交事务,缺点是开启事务后吞吐量低,耗费性能。
- 异步处理:生产者使用带有回调通知的发送API,producer.send(msg, callback)如果超过一定时间还没接收到这个消息的回调,重发消息。通常采用confirm方法。
- retries=N,Producer参数,让生产者发送失败多次重试,避免消息丢失;
MQ丢失
如果生产者保证消息发送到MQ,而MQ收到消息后还存在内存中,这时候宕机了又没来得及同步给其他节点就有可能导致消息丢失。
- RocketMQ分为同步刷盘和异步刷盘两种方式,默认的是异步刷盘,就有可能导致消息还未刷到硬盘上就丢失了,可以通过设置为同步刷盘的方式来保证消息可靠性,这样即使MQ挂了,恢复的时候也可以从磁盘中去恢复消息。
- 上面介绍Kafka的acks =-1 或者all,生产者在消息发送之后,需要等待ISR中的所有副本都成功写入才能够收到来自服务端的成功响应
- Kafka的其他配置replication.factor=N,要求每个partition至少有2个副本;min.insync.replicas=N,Broker端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”,设置大于1可以提升消息持久性;设置replication.factor > min.insync.replicas ,如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作,不仅要改善消息的持久性,防止数据丢失,还要不降低可用性的基础上完成。
- unclean.leader.election.enable=false,Broker端参数,控制的是哪些Broker有资格竞选分区的Leader,如果一个Broker中Follower副本落后原先的Leader副本太多,那么它一旦成为新的Leader副本,必然会造成消息的丢失。故一般都要将参数设置为false,即不允许这种情况的发生。选择false即选择了CAP中的C(Consistency),选择true即选择了CAP中的A(Availability)。不能单纯说Kafka是CP架构或CA架构
消费者丢失
消费端刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。
- enable.auto.commit,Consumer端参数,设置为false, 手动开启配置关闭自动offset,确保消息消费完成再提交,确保处理完后通过API调用发送ack,如果MQ没收到ack,则重发数据。
Kafka高性能在哪些方面?
- 批量发送消息:Kafka瓶颈通常不在于磁盘,而在于网络。Kafka通过将Topic划分成多个Partition,Producer将消息分发到多个本地Partition的消息队列(双端队列)中,每个Partition消息队列中的消息会写入到不同的Leader节点。Producer先生产消息、序列化消息并压缩消息后,追加到本地的记录收集器(RecordAccumulator),Sender不断轮询记录收集器,当满足一定条件时(消息大小达到阈值,消息等待发送时间达到阈值),将队列中的数据发送到Partition Leader节点。其中队列的每个元素是一个批记录(ProducerBatch),批记录使用createdMs表示批记录的创建时间(批记录中第一条消息加入的时间), topicPartion表示对应的Partition元数据。记录的批处理通过使用更大的数据包,以及提高带宽效率来分摊网络往返的开销。
- Netty异步非阻塞IO模型
- 分区分段:Kafka采取了分区的模式,而每一个分区又对应到一个物理分段,而查找的时候可以根据二分查找快速定位。
- 顺序写:Kafka采用顺序写的方式来做消息持久化,顺序I/O比随机I/O性能要好很多,减少了寻址耗费时间。服务端会将每条消息的顺序值转换成绝对偏移量(Broker从Partition维度来标记消息的顺序,用于控制Consumer消费消息的顺序),Kafka通过nextOffset(下一个偏移量)来记录存储在日志中最近一条消息的偏移量。由于Broker是将消息持久化到当前日志的最后一个分段中,写入文件的方式是追加写,采用了对磁盘文件的顺序写。对磁盘的顺序写以及索引文件加快了Broker查询消息的速度。
- 数据传输压缩:上文有谈到可压缩参数,一方面减少带宽,也减少数据传输的消耗。补充
- 时间轮:定时任务插入删除,时间复杂度为O(1)。
- 零拷贝:Linux系统提供了系统事故调用函数sendfile(),可以直接把内核缓冲区里的数据拷贝到Socket缓冲区中,不用拷贝到用户态了。因此减少CPU的上下文切换和磁盘IO,上下文切换次数由4个减少到2个,数据拷贝由5次减少到3次。(下图来自公众号“腾讯技术工程”)。零拷贝建立在:Java Nio channel.transforTo()方法 实际是调用 Linux sendfile 系统调用。sendfile()通过DMA将文件内容拷贝到一个读取缓冲区,然后由内核将数据拷贝到与输出套接字相关联的内核缓冲区。
为什么Kafka使用拉模型?
基于推送模型的消息系统,由消息代理记录消费状态。消息代理将消息推送到消费者后,标记这条消息为已经被消费,但是这种方式无法很好地保证消费的处理语义。
比如当我们已经把消息发送给消费者之后,由于消费进程挂掉或者由于网络原因没有收到这条消息,如果我们在消费代理将其标记为已消费,这个消息就永久丢失了。所以如果采用 Push,消息消费的速率就完全由消费代理控制,一旦消费者发生阻塞,就会出现问题。
Kafka 采取拉取模型(Poll),由自己控制消费速度,以及消费的进度,消费者可以按照任意的偏移量进行消费。比如消费者可以消费已经消费过的消息进行重新处理,或者消费最近的消息等等。
Push与Pull模式对比
- Push模式对用户要求低,方便用户获取需要的信息;Pull对于客户端用户要求较高,需要维护offset
- Push及时性好,服务端即时地向客户端推送更新的动态信息;Pull及时性较差,客户端难以获取实时信息
- Push推送的信息可能并不能满足客户端个性化需求;Pull可满足客户端个性化需求
- Push信息大于消费者消费速率,需要有协调QoS机制做到消费端反馈;Pull按需获取
如果对Kafka调优?
应用层
- 不要频繁地创建Producer和Consumer对象,尽量复用。把Producer定义为Spring的单例Bean,应用启动阶段初始化Producer对象
@Component
public class PushMessageProducer {
private static IProducerProcessor producer;
@PostConstruct
public static void init() {
try {
Properties properties = new Properties();
properties.setProperty(ConsumerConstants.KafkaClientAppkey, "appkey");
producer = KafkaClient.buildProduceFactory(properties, "message");
} catch (Exception e) {
logError(e);
}
}
- 用完及时关闭,这些对象底层会创建很多物理资源,如Socket、ByteBuffer缓冲区。
- 合理利用多线程改善性能。设置多个消费者线程数。
框架层
合理配置Kafka集群的各种参数。
JVM层
- 设置Broker端堆大小,一般JVM堆大小设置成6~8G
在使用消息队列中会遇到哪些问题?
可能原因 | 解决方案 | |
---|---|---|
消息积压 | 1)生产端没有限流;2)消费端耗时久;3)消费消费端消费线程少;4)客户端在消费失败后设置CONSUME_FAILURE,一旦不能恢复会导致一直重试 | 1)生产端增加限流;2)发现问题及时扩容Partition并扩容消费者机器;3)增加消费端线程数;4)消费失败不要使用 CONSUME_FAILURE,可以使用RECONSUME_LATER,一段时间后再进行消费,避免该消息一直处理失败 |
消息丢失 | 1)数据可靠性未设置acks=-1;2)消息过大造成发送失败;3)集群机器宕机 | 1)如果对消息丢失0容忍可设置客户端 ack=-1;2)不要发送超过1M以上消息;3)做好集群容灾处理,尽量保证partition均匀分布在所有Broker中。 |
重复消费 | 生产者重复生产消息 | 消息消费严格幂等控制 |
MQ技术选型
Kafka | RocketMQ | RabbitMQ | ActiveMQ | |
---|---|---|---|---|
单机吞吐量 | 10万级 | 10万级 | 万级 | 万级 |
开发语言 | Scala | Java | Erlang | Java |
协议 | 自定义协议 | 自定义协议 | 基于AMQP | 基于JMS |
高可用 | 分布式架构 | 分布式架构 | 主从架构 | 主动架构 |
性能 | ms级 | ms级 | us级 | ms级 |
功能 | 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用 | MQ功能较为完善,扩展性好 | 并发性能强,延时低 | MQ功能完善 |
社区活跃度 | 高 | 高 | 中 | 低 |
模式 | 拉模式 | 拉模式 | 推+拉模式 | 推+拉模式 |
远程调用RPC和消息MQ的区别
本质上是网络通讯的两种不同的实现机制,核心区别是RPC是双向直接网络通讯,MQ是单向引入中间载体的网络通讯
- 架构上,MQ存在中间Broker,可以把消息存储起来
- 同步调用:对于要立即等待返回结果的场景,RPC是首选
- MQ的使用:一方面是基于性能考虑,如服务器不能快速响应客户端,需要放进队列里;另一方面,MQ更侧重于数据的传输,方式更加多样化,除了点对点外,还支持订阅发布功能。