1、系统架构
image.png2、网络模型
3、commit日志
一个commit日记类似预写式日记(WAL)和事务日记,它是可追加的有序的持久化数据,无法进行修改或者删除
image.png这种结构是Kafka的核心,它具备排序功能,而排序则可以保证确定性的处理,这两者都是分布式系统中的重要问题
Kafka通常会将消息持久化到磁盘上,它充分利用磁盘的有序读取特性,读写的时间复杂度都为O(1),这是相当了不起的,另外读取和写入操作不会相互影响,写入不会加锁阻塞读取操作
4、持久化至磁盘
正如前面提及的,Kafk将消息存储至磁盘而不是内存RAM,你或许会惊讶它是如何做出这种选择的,背后应该有许多优化使其可行,没错,事实上优化点包括:
- Kafka的通信协议支持消息合并,减少网络流量传输,Broker节点一次持续存储大量数据,消费者可以一次获取大量的消息
- 操作系统通过提前读入(read-ahead)和write-behind缓存技术,使得磁盘上的线性读写速度快,现代磁盘速度慢的结论是基于需要磁盘搜索的场景
- 现代操作系统引入页面缓存(Page cache)技术,页缓冲由多个磁盘块构造,在linux读写文件时,它用于缓存文件的逻辑内容,从而加块对磁盘映射和数据的访问
- Kafka存储消息使用的是不可变的标准二进制格式,可以充分利用零拷贝技术(zero-copy),将数据从页缓存直接复制到socket通道中
- 数据磁盘持久化:消息不在内存中cache,直接写入到磁盘,充分利用磁盘的顺序读写性能
知识扩展:
readahead-维基百科
零拷贝的原理及Java实现
5、Zookeeper服务
Zookeeper是一个分布式KV对目录存储系统,特点是可靠性高、读取性能高,但是写入性能差,常被用于存储元数据和保存集群状态,包括心跳、配置等等
1、消费者组的每个分区的偏移量,不过后来Kafka将其保存至内部主题__consumer_offsets中
2、访问权限列表
3、生产者和消费者速率限定额度
4、分区leader信息和它们的健康状态
6、Controller控制器
一个分布式系统肯定是可协调的,当事件发生时,节点必须以某种方式做出反应,控制器负责决定集群如何做出反应并指示节点做某事,它是功能不能过于复杂的Broker节点,最主要的职责是负责节点下线和重新加入时重平衡和分配新的分区leader
控制器从ZooKeeper Watch事件中可以得知某个Broker节点实例下线(或者节点过期,一般发生于Broker长时间繁忙导致心跳异常)的情况,然后做出反应,决定哪些节点应成为受影响分区的新leader,然后通知每个相关的follower通过leaderAndlsr请求开始从新的leader复制数据
image.png从上面可以得知,原本作为分区leader的Broker节点实例重启后,它将不再担任任何分区的leader,消费者也不会从这个节点上读取消息,这导致了资源的浪费,幸运的是,Kafka有一个被称为优先副本(preferred leader replica)的概念-你可以理解成原先为该分区leader节点(通过broker id区分)的副本,如果该副本可用,Kafka会将集群恢复成之前状态,通过设置auto.leader.rebalance.enabled=true可以使得这个过程自动触发,默认值为true
Broker节点下线通常都是短暂的,这意味着一段时间后会恢复,这就是为什么当一个节点离开集群时,与之关联的元数据不会被删除,如果它是一个分区的跟随者,系统也不会为此分区重新分配新的跟随者
但是需要注意的是,恢复加入的节点不能立即拿回其上次的leader地位,它还没有资格
7、ISR
副本同步队列ISR(in-sync replicas),它是由leader维护的,follower从leader同步数据是有延迟的,任意一个超过阈值都会被剔除出ISR列表, 存入OSR(Outof-Sync Replicas)列表中,新加入的follower也会先存放在OSR中
一个follower想被选举成leader,它必须在ISR队列中才有资格,不过,在没有同步副本存在并且已有leader都下线的边缘情况下,可以选择可用性而不是一致性
ISR列表维护标准如下:
- 它在过去的X秒内有完整同步leader消息,通过replica.lag.time.max.ms配置约定
- 它在过去的X秒内向Zookeeper发送了一个心跳,通过zookeeper.session.timeout.ms配置约定
8、生产者acks设置
明显,存在一系列意外事件会导致leader下线,假如leader节点接收到生产者的消息,在存储并且响应ack后节点崩溃了,此时Kafka会从ISR列表中选举一个新的leader,但是由于生产者ack配置默认为1,意思是只考虑leader接收情况不考虑follower同步情况,最终导致部分消息丢失了,所以我们应该在生产者端设置acks=all,要求每条数据必须是写入所有副本之后,才能认为是写成功,另外一层意思是起码有一个leader和一个follower。不过这种设置影响集群性能,降低了吞吐量,使得生产者需要在发送下一批消息之前等待更多时间
9、脑裂
想象一下,当正常存活的controller控制器由于长时间GC-STW导致不可用,然后Zookeeper会认为/controller节点(节点3)已经过期随即删除并发送通知到其他broker节点,其他每个broker节点都尝试升级为控制器节点,假设节点2从竞争中胜出成功新的控制器节点并在ZK中创建/controller节点
然后其他节点接收到通知,了解到节点2成为了新的控制器节点,除了还在GC暂停的节点3,或者通知压根没到达的节点3,也就是说节点3不知道leadership已经发生了变化,它还以为自己是控制器节点。此时,同时存在两个控制器,并行发出可能存在冲突的命令,导致严重的后果
幸运的是,Kafka提供了epoch number的方式可以轻松区分出真实的控制器,它是自增长的序列号,信息存储在ZooKeeper中,显然序列号最大的那个节点才是真实的
9、生产者消费者读取写入分区策略
生产者往kafka中发送消息,定义消息的topic,如果不指定partition,消息会采用默认的负载均衡策略分发到不同的partition中,kafka只保证了每个partiton中的消息是有序的,不保证整体topic消息有序,这也就是之前所说的局部有序。消费者从kafka中拉取数据进行消费,注意这里是拉取而不是kafka去推送消息,这样设计的好处就在消费端可以根据自身的消费能力去消费数据,消费时指定消费主题topic和消费群组,也可以指定partition进行消费,不指定的话默认全部消费。
10、分区与segment的设计
在Kafka文件存储中,同一个topic下有多个不同的partition,每个partiton为一个目录,partition的名称规则为:topic名称+有序序号,第一个序号从0开始计,最大的序号为partition数量减1,partition是实际物理上的概念,而topic是逻辑上的概念。
上面提到partition还可以细分为segment,这个segment又是什么?如果就以partition为最小存储单位,我们可以想象当Kafka producer不断发送消息,必然会引起partition文件的无限扩张,这样对于消息文件的维护以及已经被消费的消息的清理带来严重的影响,所以这里以segment为单位又将partition细分。每个partition(目录)相当于一个巨型文件被平均分配到多个大小相等的segment(段)数据文件中(每个segment 文件中消息数量不一定相等)这种特性也方便old segment的删除,即方便已被消费的消息的清理,提高磁盘的利用率。每个partition只需要支持顺序读写就行,segment的文件生命周期由服务端配置参数(log.segment.bytes,log.roll.{ms,hours}等若干参数)决定。
segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值,数值大小为64位,20位数字字符长度,没有数字用0填充,如下:
- 00000000000000000000.index
- 00000000000000000000.log
- 00000000000000170410.index
- 00000000000000170410.log
- 00000000000000239430.index
- 00000000000000239430.log
11、消费
订阅topic是以一个消费组来订阅的,一个消费组里面可以有多个消费者。同一个消费组中的两个消费者,不会同时消费一个partition。换句话来说,就是一个partition,只能被消费组里的一个消费者消费,但是可以同时被多个消费组消费。因此,如果消费组内的消费者如果比partition多的话,那么就会有个别消费者一直空闲。
12、消息投递语义
kafka支持3种消息投递语义
At most once:最多一次,消息可能会丢失,但不会重复
At least once:最少一次,消息不会丢失,可能会重复
Exactly once:只且一次,消息不丢失不重复,只且消费一次(0.11中实现,仅限于下游也是kafka)
在业务中,常常都是使用At least once的模型,如果需要去重入的话,往往是业务自己实现。
-
At least once
先获取数据,再进行业务处理,业务处理成功后commit offset。
1、生产者生产消息异常,消息是否成功写入不确定,重做,可能写入重复的消息
2、消费者处理消息,业务处理成功后,更新offset失败,消费者重启的话,会重复消费 -
At most once
先获取数据,再commit offset,最后进行业务处理。
1、生产者生产消息异常,不管,生产下一个消息,消息就丢了
2、消费者处理消息,先更新offset,再做业务处理,做业务处理失败,消费者重启,消息就丢了
常用配置项
-
broker配置
配置项作用broker.idbroker的唯一标识auto.create.topics.auto设置成true,就是遇到没有的topic自动创建topic。log.dirslog的目录数,目录里面放partition,当生成新的partition时,会挑目录里partition数最少的目录放。 -
topic配置
配置项作用num.partitions新建一个topic,会有几个partition。log.retention.ms对应的还有minutes,hours的单位。日志保留时间,因为删除是文件维度而不是消息维度,看的是日志文件的mtime。log.retention.bytespartion最大的容量,超过就清理老的。注意这个是partion维度,就是说如果你的topic有8个partition,配置1G,那么平均分配下,topic理论最大值8G。log.segment.bytes一个segment的大小。超过了就滚动。log.segment.ms一个segment的打开时间,超过了就滚动。message.max.bytesmessage最大多大
参考资料:
深入理解分布式系统kafka知识点
深入理解Apache Kafka
kafka深入理解和实践
震惊了!原来这才是 Kafka!(多图+深入)
kafka深入研究之路(1)-剖析各原理01
网友评论