kafka思维导图:
Messaging System 消息引擎系统
常见的消息传输协议:
点对点模型:
A发送的消息只能被B接收,其他任何系统都不能读取A发送的消息;
发布/订阅模型:
多个发布者可以向相同的主题发送消息,订阅者也可能存在多个,它们都能接收到相同主题的消息。
kafka同时支持以上两种消息模型。
为什么要使用消息引擎系统?
削峰填谷:
缓冲上下游瞬时突发流量,使其平滑。特别是对于那种发送能力很强的上游系统,如果没有消息引擎保护,下游系统可能会由于没有消息引擎的保护被压垮导致全链路服务“雪崩”。消息引擎能够有效的对抗上有的冲击,真正做到将上游的“峰”填到“谷”,避免流量的震荡。
消息引擎另一大好处是发送方与接收方的松耦合,简化应用的开发和系统间不必要的交互。
kafka属于分布式的消息引擎系统,主要功能是提供一套完备的消息发布与订阅系统。
kafka集群架构
kafka特性:
- 消息持久化
- 高吞吐,充分利用磁盘的顺序读写,采用零拷贝(Zero-Copy)技术;
- 扩展性
- 多客户端支持
- 流处理
- 安全机制
- 数据备份
- 轻量级
- 消息压缩,支持Gzip,Snappy,LZ4这三种方式,通常把多条消息放在一起组词MessageSet,然后再把MessageSet放到一条消息里面去,从而提高压缩比率,进而提高吞吐量。
kafka的三层消息架构:
第一层是主题层:每个主题配置M个分区,每个分区又可以配置N个副本;
- 第二层是分区层:每个分区的N个副本只能有一个充当领导者(Leader)的角色,并对外提供服务,其他的N-1个副本只是追随者副本(Follower),只提供数据冗余用;
- 第三层是消息层:分区中包含若干个消息,每条消息的位移从0开始,一次递增;
客户端程序只能与分区的领导者副本(Leader)进行交互。
kafka中的名词术语
消息:record 消息引擎处理的主要对象;
- 主题:Topic 承载消息的逻辑容器,实际应用中多用来区分不同的业务;
- 分区:Partition 有序不变的消息序列,每个主题可以有多个分区;
- 消息位移:Offset 表示分区中每条消息的位置,单调递增且不变的值;
- 副本:Replica 一条消息被拷贝到多个地方进行数据冗余,副本分为Leader和Follower
- 生产者:Producer 向主题发布新消息的应用程序;
- 消费者:Consumer 从主题订阅新消息的应用程序;
- 消费者位移:Consumer Offset 表征消费者消费进度,每个消费者都有自己的消费者位移;
- 消费者组:Consumer Group 多个消费者实例组成的一个组,同时消费多个分区以实现高吞吐;
重平衡:Rebalance 消费者组内某个消费者实例挂掉后,其它消费者实例自动重新分配订阅主题分区的过程。Rebanlance是kafka实现消费者端实现高可用的重要手段。
Kakfa Broker Leader的选举
Kakfa Broker集群受Zookeeper管理。
所有的Kafka Broker节点一起去Zookeeper上注册一个临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Kafka Broker会成为Kafka Broker Controller,其他的Kafka broker叫Kafka Broker follower。(这个过程叫Controller在ZooKeeper注册Watch)。
这个Controller会监听其他的Kafka Broker的所有信息,如果这个kafka broker controller宕机了,在zookeeper上面的那个临时节点就会消失,此时所有的kafka broker又会一起去Zookeeper上注册一个临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Kafka Broker会成为Kafka Broker Controller,其他的Kafka broker叫Kafka Broker follower。
例如:一旦有一个broker宕机了,这个kafka broker controller会读取该宕机broker上所有的partition在zookeeper上的状态,并选取ISR列表中的一个replica作为partition leader(如果ISR列表中的replica全挂,选一个幸存的replica作为leader; 如果该partition的所有的replica都宕机了,则将新的leader设置为-1,等待恢复,等待ISR中的任一个Replica“活”过来,并且选它作为Leader;或选择第一个“活”过来的Replica(不一定是ISR中的)作为Leader),这个broker宕机的事情,kafka controller也会通知zookeeper,zookeeper就会通知其他的kafka broker。
kafka Broker是如何持久化数据的?
kafka使用消息日志(Log)保存数据,一个日志就是磁盘上一个只能追加写(append only)消息的物理文件。追加写避免了缓慢的随机访问I/O操作改为性能较好的顺序I/O写操作,这也是kafka实现高吞吐量的一个重要手段。
不停的向一个日志里面写消息,最终会耗尽磁盘空间,因此必须定期的删除消息回收磁盘空间。怎么删除呢?
通过日志段机制(Log segment),在kafka的底层,一个日志又进一步细分为多个日志段,消息被追加写到当前最新的日志段中,当写满一个日志段后,kafka会自动切分出一个新的日志段,并将老的日志段封存起来。kafka在后台还有定时任务定期的检查老的日志段是否能够被删除,从而实现磁盘回收的目的。
在partition中如何通过offset查找message
第一步查找segment file
上述图2为例,其中00000000000000000000.index表示最开始的文件,起始偏移量(offset)为0.第二个文件 00000000000000368769.index的消息量起始偏移量为368770 = 368769 + 1.同样,第三个文件00000000000000737337.index的起始偏移量为737338=737337 + 1,其他后续文件依次类推,以起始偏移量命名并排序这些文件,只要根据offset 二分查找文件列表,就可以快速定位到具体文件。当offset=368776时定位到00000000000000368769.index|log
第二步通过segment file查找message通过第一步定位到segment file,当offset=368776时,依次定位到00000000000000368769.index的元数据物理位置和 00000000000000368769.log的物理偏移地址,然后再通过00000000000000368769.log顺序查找直到 offset=368776为止
record物理结构
参数说明:
关键字 | 解释说明 |
---|---|
8 byte offset | 在parition(分区)内的每条消息都有一个有序的id号,这个id号被称为偏移(offset),它可以唯一确定每条消息在parition(分区)内的位置。即offset表示partiion的第多少message |
4 byte message size | message大小 |
4 byte CRC32 | 用crc32校验message |
1 byte “magic" | 表示本次发布Kafka服务程序协议版本号 |
1 byte “attributes" | 表示为独立版本、或标识压缩类型、或编码类型 |
8 timestamp | 消息时间戳 |
4 byte key length | 表示key的长度,当key为-1时,K byte key字段不填 |
K byte key | 可选 |
4 payload-length | 消息实际数据长度 |
value bytes payload | 表示实际消息数据 |
kafka中一些比较重要的参数配置
Broker端参数:
存储信息相关:
- log.dirs: 无默认值,CSV格式
- log.dir: 单个目录,是对上一个目录的补充
配置多个路径的意义:
- 提升读写性能,比起单块磁盘,多块磁盘同时读写具有更高的吞吐量;
- 能够实现故障转移:Failover,1.1版本前,任何一个磁盘挂掉Broker进程会关闭,1.1版本后坏的的磁盘的数据会自动的转移到其他正常的磁盘上,同时Broker还能正常工作;
Broker连接相关:
- listeners: 告诉外部连接者要通过什么协议访问指定主机名和端口开放的kafka服务;
- advertised.listeners: Broker用于对外发布;
- host.name/port: 过期参数
监听器的概念:若干个逗号分隔的三元组,每个三元组为<协议名称,主机名,端口号>,协议名称可能是标准的名字,比如:PLAINTEXT表示明文传输,SSL表示用SSL或TLS加密传输等。
关于Topic管理:
- auto.create.topics.enable: 是否允许自动创建Topic;
- unclean.leader.election.enable: 是否允许Unclean Leader选举;
- auto.leader.rebalance.enable: 是否允许定期进行Leader选举;
数据留存相关:
- log.retention.{hour|minutes|ms}: 控制一条消息被保存多长时间;
- log.retention.bytes:指定Broker为消息保存的总磁盘容量大小;
- message.max.bytes:控制Broker能接收的最大消息大小
Topic级别参数:
注意:如果同时设置了Topic级别的参数和Broker级别的参数,Topic级别的参数会覆盖全局的Broker参数
- retention.ms: 规定该Topic被保存的时长,默认七天;
- retention.bytes: 要为该Topic预留多大的磁盘空间,默认值-1,表示可以无限制使用磁盘空间;
JVM参数:
操作系统参数:
生产者消息分区机制
主题-分区-消息三次结构:
为什么要分区?
分区的主要原因是:提供负载均衡的能力,提高系统的伸缩性(Scalability),不同的分区放到不同的Broker节点上,数据的读写也是针对分区这个粒度进行的,这样每个节点的机器都能独立的执行各自分区的读写请求处理,同时还可以通过添加新的节点来提升系统的吞吐量。
在MongoDB/Elasticksearch/HBase等系统也有分区的思想,Shard,Region
常见的分区策略
- 轮询策略
- 随机策略
- 按消息键保序策略
kafka允许为每条消息定义消息键,简称key。它可以是一个有着明确业务含义的字段,也可以用来表征消息元数据。一旦消息被定义了Key就可以保证同一个Key的所有消息都进入相同的分区里面,由于单个分区的消息处理是有序的,因此这个策略被称为消息键保序策略。
生产者压缩算法
压缩的目的:时间换空间,带来更少的磁盘占用以及更少的网络I/O传输。
何时压缩?
压缩可能发生在两个地方:生产者端和Broker端。
生产者配置:compression.type 参数表示启用指定类型的压缩算法。
大部分情况下Broker接收到Producer的消息只是原封不动的将其保存而不会对其进行任何修改。
特殊情况:
- Broker端指定了和Producer端不同的压缩算法。例如:Producer端使用了GZIP但Broker指定了Snappy,那么Broker端就需要先把GZIP格式的消息解压再压缩成Snappy格式。这种情况会导致CPU使用率飙升。
- Broker端发生了消息格式转换。
何时解压缩?
有压缩就有解压缩,通常情况下生产者发送压缩消息到Broker后,Broker只是原封不动的保存起来,当消费者请求这部分数据的时候Broker依然原样发送出去,当消息到达Consumer端后,由Consumer自行解压缩还原成之前的消息。
Consumer怎么知道要使用哪种解压缩算法?
kafka会把启用了哪种压缩算法封装进消息集合中,当Consumer读取到消息集合时自然知道用哪种解压算法。
Producer端压缩,Broker保持,Consumer解压缩。
生产者CPU资源紧张不建议开启压缩,带宽紧张建议开启压缩。
无消息丢失配置实现
kafka只对已提交的消息做有限度的持久化保证。
生产者丢失数据案例:
Producer应用向kafka发送消息,最后发现kafka并没有保存。
kafka Producer是异步发送消息的,如果调用了producer.send(msg)这个API,通常会立即返回,但此时我们并不能认为消息已经成功发送完成。
producer.send(msg)属于典型的“fire and forget”,因此如果出现消息丢失我们是无法知晓的,因此这个方式不太靠谱。
使用这个方式哪些情况会出现消息丢失?
网络抖动,消息压根就没有发送的Broker端,消息不合格被Broker拒绝接收等。
怎么解决?
使用带有回调通知的发送API,producer.send(msg, callback),callback能准确的告诉你消息是否发送成功,当出现发送失败的情况我们可以做相应的处理。比如那些瞬时错误可以通过重试,消息不合格造成的可以通过调整消息格式后再发送。
消费者程序丢失数据
Consumer端丢失数据主要体现在Consumer要消费的消息不见了。Consumer使用位移的概念来表示这个Consumer当前消费到的Topic分区的位置
如果先更新位移再消费消息,那么当消费失败时这部分消息就丢失了。因此需要维持先消费消息再更新位移,虽然会出现消息重复处理但是不会丢消息。
另一种比较隐蔽的消息丢失场景:Consumer程序从kafka获取消息后开启多个线程处理消息,而Consumer程序自动的向前更新位移,当某个线程在处理过程中失败了,消息没有被正常处理,但是位移已经被更新,此时该消息实际上是丢失了。
怎么解决?
如果是多线程异步处理消息,Consumer程序不要开启自动提交位移,而是要应用程序手动提交位移。
最佳实践
kafka无消息丢失的配置:
- 不要使用producer.send(msg),而要使用producer.send(msg,calback);
- 设置acks=all。acks是Producer的一个参数,代表对已提交消息的定义,设置成all表示所有的副本Broker都要接收到消息该消息才算提交成功,这是最高等级的“已提交”定义;
- 设置retries为一个较大的值。也是Producer的参数,自动重试;
- 设置unclean.leader.election.enable=false。这是Broker端的参数,控制哪些Broker有资格竞选分区的Leader.如果一个Broker落后原先的Leader太多,那么它一旦成为新的Leader,必然会造成消息丢失。故一般都要把改参数设置成false,避免该情况发生;
- 设置replication.factor >= 3。Broker端参数,把消息多保存几份。毕竟防止消息丢失的主要机制是数据冗余。
- 设置min.insyc.replicas > 1。Broker端参数,表示消息至少被写入多少个副本才算“已提交”。默认值为1.
- 确保replication.factor > min.insyc.replicas。如果两者相等,那么只要有个副本挂机,整个分区就无法正常工作了,改善消息的持久性,防止数据丢失的前提是不降低可用性。推荐 replication.factor = min.insyc.replicas + 1。
- 确保消息消费完再提交。Consumer端有个参数enable.auto.commit,最好设置成false,并采用手动提价位移的方式。
消费者组 Consumer Group
- Consumer Group下可以有一个或多个Consumer实例;
- Group ID标识一个唯一的消费者组
- Consumer Group订阅的主题的单个分区只能分配给组内的某个Consumer实例。这个分区可以被其他Consumer Group消费
“大名鼎鼎”和“臭名昭著”的Rebalance
Rabalance本质是一种协议,规定了一个Consumer Group下的所有Consumer如何达成一致来分配订阅的Topic的每个分区。
ex:一个包含100个分区的Topic分配给一个包含20个Cobsumer实例的消费者组,正常情况下每个Consumer会被分配5个分区
Consumer Group什么时候会触发Rebalance?
- 组成员数发生变更,比如:增加、离开、被踢除
- 订阅主题数发生变更,Consumer Group可以使用正则表达式的方式订阅主题,当新创建了一个满足条件的主题时就会触发Rebalance
- 消费者组所订阅的主题的分区数发生变更,kafka只允许增加一个主题的分区数,当分区数增加时就会触发订阅该主题的所有Consumer Group发生Rebalance
公平的分区策略,避免出现“闲死”和“忙死”
ex:加入Consumer实例C
为什么臭名昭著?
- Rebalance对Consumer Group的消费过程有极大的影响,类似java中STW,在Rebalance的过程中所有Consumer的也会停止消费,等待Rebalance的完成;
- 目前Rebalance的设计是所有Consumer实例共同参与,全部重新分配分区;
- Rebalance的过程很慢,Group内有上百个实例时一次Rebalance需要几个小时!因此尽量避免Rebalance的发生。
位移主题
__consumer_offsets
当kafka集群中的第一个Consumer程序启动时,kafka会自动创建位移主题;
将Consumer的位移数据作为普通的kafka消息提交到__consumer_offsets中,__consumer_offsets的主要作用是保存消费者的位移信息;
位移主题是个普通的kafka主题,但它的消息格式由kafka自定义,用户不能修改,一旦写入的消息不满足格式,kafka内部无法解析就会造成Broker崩溃;
- 消息格式:类似K,V,其中K包含Group id/topic/分区号
- 分区数由ofssets.topic.num.partitions参数控制,默认为50,副本数由offsets.topic.replication.factor参数控制,默认为3;
如果不满意kafka自动创建位移主题的默认值(比如分区数太多),可以在集群中没有任何consumer启动的情况下通过kafka API手动创建位移主题。
目前kafka Consumer支持自动/手动提交位移
- 自动提交的好处是不用关心位移提交的事情,但也丧失了灵活性和可控性,完全没法把控Consumer端的位移管理,是否自动提交由参数enable.auto.commit=true/false控制,如果设置了True后台自动定期的帮你提交位移,提交间隔由参数auto.commit.interval.ms控制;
- 手动提交需要应用开发者负责位移提交的责任,kafka Consumer API提供了相应的方法;
- 自动提交存在的一个问题:Consumer只要启动着就会无限期的向位移主题写入消息。
kafka使用Compact策略来删除位移主题中的过去消息:
图中位移为0、2和3的消息的Key都是K1,Compact之后分区只需要保存最新发送的位移为3的消息。
kafka提供了专门的后台线程(Log Cleaner)定期的检查待Compact的主题,看是否存在满足条件的可删除数据。如果生产环境位移主题无限膨胀占用了过多的磁盘空间,那么有可能是因为Log Cleaner线程挂了导致的。