基本概念
broker
一个单独的kafka server,主要工作是接收producer生产的消息持久化到磁盘中并分配offset;接收customer的fetch消息的请求返回对应的消息响应;在cluster集群模式下,也会接收其他broker的请求,比如follower分区备份leader的消息
message
消息是kafka中最小的的单位,主要包括key(根据策略路由到具体分区,可保证相同key保存在统一分区)、value、时间戳
topic
主题,用于存储消息的概念,表示一类消息的集合。一个topic可以接收多个生产者push消息,同时也支持多个订阅该topic的消费者pull消息
partition
分区,是一种物理概念。 每一个Topic都可以分为多个partition(至少有一个),同一topic的不同分区消息内容不同,分别分散在不同的broke上以对kafka做水平扩展(受限于broke存储空间)从而增强并行处理能力。每一个消息发送到分区的时候,都会分配一个offset参数是分区的唯一标识,kafka根据它来保证分区内消息的顺序,但不能保证不同分区的顺序。
producer
生产消息,按照一定规则将消息push到指定的topic的分区内。
customer
消费消息,消费者通过向订阅的topic中pull消息进行消费,consumer还会维护offset值,表示已经消费到哪个offset位置。在kafka中,多个consumer组成一个consumer group,如果实现消息队列模式,一个consumer只能存在一个group中,group保证其订阅的topic的每一个分区被分配给group中的一个消费者进行消费;如果要实现订阅广播模式,则将消费者放到多个group中即可。当consumer group中增加、减少消费者时,kafka通过重新调整分区与消费者的关系,从而实现水平扩展。
副本
kafka对消息进行了冗余处理,每个分区都会有多个副本,每个副本的内容是相同(不能保证同一时刻完全相同)。副本类型分为leader、follower,如果分区只有一个副本的时候只有leader没有follower。在每个副本集合中,都会选举出一个leader以及多个follower,所有的读写请求都是在leader副本,follower负责同步复制,所以有多少个partition就有多少个leader,在集群模式下一般选择分散到各个节点上,分散服务压力。follower与leader在不同的broker,防止leader宕机还可以,保证可用性。
ISR集合
表示目前可用并消息量与leader的差距没超过阈值的副本集合。
设计思想
动机
kafka被设计成一个平台式的数据实时馈送,需要满足一下几点:
- 支持高吞吐量支持高流量场景,如实时日志聚合
- 支持大量数据积压,如离线消息存储
- 支持低延迟消息转发,如传统消息队列场景
- 支持系统容错,防止出现系统不可用导致的不可用、数据丢失等问题
持久化
kafka的消息持久化是保存在磁盘中,传统印象中,磁盘io的速度是非常慢的,但是线性顺序写入磁盘远优于随机写入磁盘,在某些情况下,甚至比随机访问内存的速度还要快。kafka是运行在JVM中,内存开销比较昂贵,并且当内存中的数据量增大后,垃圾回收会变得更加复杂。Btree是比较常用的数据索引结构,但是在磁盘存储上不适用,因为磁盘寻址不支持并行执行,相反采用最简单的按照顺序查询和在其后插入数据的方案比较有效,写操作不会阻塞读操作。
影响磁盘性能的原因还有两个:
- 大量的小数据IO操作,kafka协议建立了一个抽象消息块概念,合理的将消息进行分组打包。网络请求时,将多个消息打包成一组,而不是一次发送一个消息,在持久化的时候也采用消息块一起存储,消费者消费时,每次获取多个有序的消息块。
- 字节拷贝,kafka采用跳过内核到用户空间的的数据复制,直接将数据从内核pagecache复制到网络连接中
load balance
生产者发送消息到指定topic的leader分区,不需要经常任何中间路由。kafka中的所有子节点都清楚:哪个节点alive、主分区在哪一台节点。客户端发送消息的时候也可以根据某个规则指定消息发送到某个分区,如轮训、随机、hash。
push vs pull
kafka采用传统的设计方案,生产者push消息到broker,消费者从broker中pull消息。在push-base的方案中,当生产消息的速率远高于消费能力时,消费者可能会不堪重负,出现阻塞;而相反在pull-base时,可以在适当的时间(生产速率降低)时,完成对积压消息的消费。
pull-base另一个好处是:如果采用push,当生产者生产大量消息时,系统是低延迟状态,broker只能接收到消息就立马发送到消费者端,传输的数据得不到缓冲,很浪费网络资源;pull则可以将可用的消息(不超过阈值)fetch到消费者端,资源得到更有效的利用。
消费者的位置
一般的消息队列如rabbitmq,当消息传递给customer之后,broker将保存消息的状态,等待customer回复确认后,系统将删除此消息,让系统的数据量保持在一个较低的水平。
kafka采用了另外一种截然相反的方式,将topic分割成一组完全有序的partition,其中一个partition中的消息只能被订阅了它的consumer group中的一个消费者消费。这也就意味着每个customer只需要保存一个offset位置,表示下一个消费消息的位置即可,以非常低的代价实现了其他消息队列保护消息消费的ack机制。
还有一个优点就是,在消费者出现bug时,可以指定offset回到原先的位置,重新消费消息。
工作原理
核心组成
生产者生产消息分析
- producer将消息发送到broker cluster,先从zookeeper中找到对应partition的leader的服务器,并发送消息到leader
- 消息将追加到分区的log文件中,属于顺序写磁盘,但是不会立刻写入磁盘,而是先存入buffer,然后过指定时间或者达到一定的数据量再写入(减少IO次数提高性能)
- follower将pull同步leader的消息,并返回ack
- 当leader收到所有ack回复,将响应producer发送成功
消费者消费消息分析
- 消费者根据Range、Round Robin的方式匹配到订阅的topic的对应的分区
- 从分区pull消息进行消费
- 一个分区只能被一个订阅该topic的消费者消费(保证消息被消费的顺序)
消费者与分区的匹配规则
Range策略
针对于topic,每个topic之间没有任何联系
实现类:org.apache.kafka.clients.consumer.RangeAssignor
- 分配时,取出一个topic下的所有有效分区
- 取出订阅该topic的所有消费者,并排好序
- 分区的数量 / 消费者数量 = m
- 分区的数量 % 消费者数量 = n
- 则前面n个消费者按照顺序各取m+1个分区,后面剩余消费者按照顺序取n个分区
缺点:前面消费者负责分区数量多,负载压力较大
Round Robin轮询策略
针对全局的topic和消费者进行处理
实现类:org.apache.kafka.clients.consumer.RoundRobinAssignor
- topic按照字典排序,并取出topic下的所有有效分区进行遍历
- 消费者按照字典排序,并形成一个环形迭代器
- 遍历第1步,并轮询消费者
- 如果轮询到的消费者订阅的topic与分区的topic一致,则关联;否则跳过继续比较next消费者直到找到对应的或者结束
自定义策略
实现类:实现抽象类AbstractPartitionAssignor
Rebalance算法(0.8.2.2之前版本)
触发条件:当消费者数量的变化、分区数量的变化
1. 将目标 topic 下的所有 partirtion 排序,存于PT
2. 对某 consumer group 下所有 consumer 排序,存于 CG,第 i 个consumer 记为 Ci
3. N=size(PT)/size(CG),向上取整
4. 解除 Ci 对原来分配的 partition 的消费权(i从0开始)
5. 将第i*N到(i+1)*N-1个 partition 分配给 Ci zookeeper作用
- broker注册,路径:/brokers/ids,保存一些broker的IP和port之类的信息。其中,创建的是临时节点,当broker宕机后将自动删除
- topic注册,路径:/borkers/topics