目录
目标
kafka,仅支持拉取的分布式流式平台。本文从简介、使用场景、设计、实现四个方面阐述kafka。
kafka系统关系图
what
kafka是仅支持拉取pull方式的分布式流式平台。
三个主要能力
- 针对记录流进行发布与订阅,类似于消息队列
- 存储记录流并且保证故障恢复
- 实时地执行产生的记录流
两种典型的应用
- 构建实时流式数据管道,用于系统间可靠的获取数据
- 构建实时流式应用,依据数据进行转换或响应
集群
kafka以集群的方式运行,其中的服务器可以分为多个数据中心;集群以主题维度存储数据;每个记录包含key/value/timestamp。
四种api
Producer API,向一个或多个主题发布记录
Consumer API ,订阅一个或多个主题
Streams API,应用作为流处理器,消费来自一个或多个主题的输入记录流,生产一个或多个主题的输出记录流,在输入和输出流之间高效的转换。
Connector API,构建和运行可重用的生产者或消费者,它们将主题与已存在的应用或数据系统进行连接。例如与关系型数据库连接的连接器,可能捕获每个变更并入表。
介绍
Topics and Logs 主题和日志
主题
对记录流的抽象,对发布的记录的分类,允许有多个订阅者。
kafka Logs
图示
由图可了解
- 针对每个主题,kafka集群会维护一个分区式log,也就是一个主题分为多个分区
- 每个分区可视为一个commit log,有序的且不可修改的记录序列,可持续追加,顺序写随机读
- 分区中的每个记录都分配一个offset,在每个分区中,offset就是唯一标识
- kafka集群会持久化所有发布的记录,无论是否被消费,保留时长是可以配置的,超过时长后就会丢弃并释放空间
- 实际上,每个消费者需要保留这个offset,且offset可由消费者随意控制
- 在log中这些分区有两个作用
- 使得log的大小可伸缩,每个分区的大小伸缩必须与其建立连接的servers匹配,因为一个主题有多个分区,所以主题可以控制任意数量的数据。
- 分区是平行单元
Distribution 分布式
- Log的所有分区,在kafka集群中是分布式的
- 每一个server会对一部分分区的数据和请求进行处理
- 每个分区在集群中会复制多份,以达到容错的目的,复制份数是可以配置的。
- 针对每个分区,有一个server作为leader,多个server作为followers;
- leader控制这个分区的所有读写请求,同时followers会复制leader数据。
- leader故障时,一个follower会自动成为新的leader
- zk上生成leader节点
- 针对每个server,既作为某些分区的leader,也作为其它某些分区的follower,因此集群负载是平衡的
Producer 发布者
负责向选定的主题发布记录,并决定记录发布到哪个分区,路由机制:轮询 或其他(例如基于记录中某个key)
Consumer 消费者
图示
由图可了解
- 通过group name将consumer划分为一组
- 主题的记录log分为多个分区,每个分区的记录只能由组中一个consumer消费
- kafka仅保证单个分区内记录的顺序性,如果需要主题的所有记录必须被顺序消费,则主题必须仅有一个分区,因此组内只有一个consumer可以进行消费。
Kafka作为消息系统
传统消息系统的优缺点
queue
- 消费者池从queue取数据,使得一个记录被一个消费者处理
- 优点:将queue中所有记录的处理分割给了多个消费者,这样可以伸缩记录处理能力
- 缺点:不支持多个订阅者,而仅是一个进程在读queue中记录。
发布-订阅
- 发布的每个记录会广播给所有订阅者
- 优点:每个记录广播给所有订阅者,有多个进程在读和处理记录
- 缺点:无法伸缩处理能力
Kafka作为存储系统
写入kafka的数据会落盘并进行容灾复制
生产者等待回告,以判断"数据的持久化和复制"是否成功
Kafka作为流式进程
作为一个流式进程,不断的从输入主题获取数据流,处理数据,不断的向输出主题生成数据流
使用场景
消息系统
网站用户活动跟踪
各种类型的用户行为被发布到不同的主题,然后被多种消费者订阅,以便完成实时处理\实时监控\存到hadoop或其它离线仓库系统以进行离线处理。
监测数据
日志聚合
流处理
事件发起源
状态改变是以发生的时间顺序进行存储
commit log提交日志
设计
持久化
- kafka基于文件系统进行数据存储和缓存,(使用"作为磁盘缓存的内存-pagecache",磁盘数据预读到pagecache,数据直接写入pagecache而不是刷新到磁盘,提升磁盘线性读写的速度)
- 消息日志存在一个目录中,每个目录对应一个消息序列
效率,为什么快
提升读效率
零拷贝
pagecache预读
从文件到字节码传输的传统过程,进行4次拷贝,耗时
- 操作系统执行读数据 磁盘-->内核空间的pagecache
- 应用读数据 内核空间pagecache-->用户空间buffer
- 应用写数据 用户空间-->内核空间的socket buffer
- 操作系统执行 内核空间的socket buffer-->NIC buffer(数据从此发送到网络)
使用零拷贝后,用户空间不参与拷贝过程,经历3次拷贝,效率提升
- 操作系统执行读数据 磁盘-->内核空间的pagecache
- 操作系统执行 内核空间pagecache-->内核空间的socket buffer
- 操作系统执行 内核空间的socket buffer-->NIC buffer(数据从此发送到网络)
提升写效率
直接写入内存的pagecache,之后按照规则刷盘
顺序写
点对点传输,批压缩
一批消息被捆绑压缩,以这种模式发送到一个server;这批消息以压缩的方式写入log;并且只有消费者才可以解压。
kafka支持GZIP, Snappy and LZ4压缩协议
生产者
负载均衡
- 生产者直接将数据发送给"分区的leader",而不需要"选择leader"的路由规则,因为集群中每个server都具有一份元数据,其中包含"存活的所有server"和"每个主题的所有分区的leader信息"
- 数据发送到哪个分区,需要通过路由规则,如随机或自定义方式(根据数据中某个key进行hash)
异步发送
kafka可以配置为"针对一个请求,积累(不超过)一定大小的数据 或 等待(不超过)一定的时间,如64K或10ms,然后发送给消费者"。优点:一次发送更多的数据,减少IO操作。对单个请求而言增加了延迟,却提升了集群的吞吐量。
消费者
kafka提供消费者pull拉取数据方式
消费位置
- 清楚哪些数据已经被消费过,这是消息系统的一个关键能力。
- kafka在消费者端仅维护分区的消费位置,即offset,下一个消费位置。优点:避免broker维护消费情况的复杂问题,消费者自行控制消费offset,更具有灵活性。
离线数据加载
消费者周期性加载数据到离线数据系统
消息交付机制
3种可提供的消息交付保证机制
理解为生产者和消费者两者的交付保证
- 至多一次,消息丢失后不再交付
- 至少一次,消息丢失后再次交付
- 仅一次,消息仅交付一次
"仅一次"的交付机制
从0.11.0.0版本开始,保证生产者生产消息"仅一次"的交付机制
- 在生产者向broker发送数据时,会携带两个数据"broker会为每个生产者分配一个ID","生产者为每个消息设置一个不重复的序号",以标示数据唯一性。
- 使用类似事务机制,保证"向多个主题分区发送数据时原子性"
kafka stream 以及 在生产者和消费者中使用事务,保证"主题间的传输和处理"的仅一次交付。
复制
复制单元是分区,是一个可复制的log
一个分区具有一个独立的leader和多个followers,所有的读写都与leader交互。
存活的节点需要满足两个条件(满足两个条件的节点称为"in sync")
- 与zk保持心跳
- 如果是follower,必须从leader复制发生的写,而且不能落后很多
leader会维护'ISR-in sync replicate同步备份'集合,一旦follower宕机/卡顿/落后很多,leader会将其从ISR集合中删除。
仅当消息在所有ISR的followers中都完成复制后,这个消息才称之为"commited",所以消费者不必担心leader宕机时的消息丢失
考虑消息持久性和可用性进行主题维度配置
- 关闭 unclean leader election
- 特定的ISR数量且acks=all,需要权衡可用性和延迟
log压缩
图示
log压缩详情
log cleaner,后台线程池完成"再次复制log/删除记录"
过程
- 选择log,依据为:log head与log tail的比例较大时
- 对log head中每个key的最新offset进行简洁的汇总
- 从头到尾重新拷贝log-->删除最近出现key的历史记录-->立即与log进行交换,因此需要额外的磁盘空间
- 2中的汇总作为一个压缩hash表
配额
两个方面的配置
- 带宽,字节维度
- 请求维度,如网络百分比 IO线程数
实现
网络层
是一个公平直接的NIO服务器,发送文件的实现是通过MessageSet.writeTo方法,其允许基于文件的消息集合使用更加高效的transferTo方法,而不使用进程内buffer写,其实就是零拷贝。
线程模型:一个接收器线程,多个处理器线程。
消息构成
可变长度的header+可变长度不透明的key字节数据+可变长度不透明的value字节数据
header的格式是既定的,但key和value字节数据与实际实用的序列化器有关
消息格式
多个消息通常写入到批中,称为a record batch批记录,批记录和记录具有各自的header。
批记录格式
记录格式
Log
主题的log有N个分区,就会对应N个目录,目录名为主题名+分区序号,目录中是存放消息的数据文件。例如:主题名"my_topic",其log具有两个分区,对应两个目录"my_topic_0""my_topic_1"。
log file的内容为 "多个log entry构成的序列"。log entry=代表消息长度的4子节数字 + 消息的子节。
每个消息使用一个64位整数的offset作为ID,offset表示"在这个分区的消息流中,该消息开始的字节位置"。
log file名称为该文件中第一个消息的offset,后缀名为.kafka。
写
线性追加到最后一个文件,当文件达到最大大小(1G)时会使用新建的文件。
log 写有两个配置项
- 强制OS进行刷盘前,可以写入的消息数量M
- 刷盘的间隔时间S
在持久性上做了保证,最多丢失M个数据或S时间间隔内的数据
读
读时需要传递64位offset和块的上限大小S,返回'S字节缓冲区中包含的消息的遍历器';S应该比单个消息大,但是对于大消息需要进行多次读,且每次S需要翻倍,直到读取成功。
消息的上限大小和缓冲区大小是可以配置的,这样让server可以拒绝大消息,而且也给了client一个最大量的约束-每次都要读取整个消息。
通过offset读取消息的过程
- 二分查找方式定位所在文件
- 计算在该文件中的offset
- 从该offset进行读取
result格式
删除
文件维度进行删除,规则为N天前修改/保存最近NG大小的log
为了不阻塞读,在文件删除时使用copyOnWrite方式,删除同时提供当前log view的快照,删除完成后使用新log view
存储保证
强制刷盘前可写入M个消息,broker每次启动时都会对log分区最新的文件进行遍历并验证消息的合法性(消息大小+其offset要小于文件的长度 且 冗余校验码正确)
分布式
消费者offset追踪
高级消费者需要做两点
- 对自己消费的分区,记录下消费的最高offset
- 提交"分区-消费的最高offset"集合,便于重启时重新处理
kafka提供配置,为消费者组分配一个broker作为offset manager,因此该组内的所有消费者都要向该offset manager提交和获取offset集合。
高级消费者会自动完成offset追踪,但是对于简单消费者可能需要自行完成。
zk节点结构
broker
broker注册
/brokers/ids/[0...N] --> {"jmx_port":...,"timestamp":...,"endpoints":[...],"host":...,"version":...,"port":...} (ephemeral node)
其代表所有存活broker的节点,每个节点使用一个唯一的逻辑ID
borker topic 注册
/brokers/topics/[topic]/partitions/[0...N]/state --> {"controller_epoch":...,"leader":...,"version":...,"leader_epoch":...,"isr":[...]} (ephemeral node)
broker将自己注册到某个主题的某个分区的状态节点中
consumer
consumer注册
/consumers/[group_id]/ids/[consumer_id] --> {"version":...,"subscription":{...:...},"pattern":...,"timestamp":...} (ephemeral node)
代表当前存活的consumer,znode具有临时的consumer_id,格式为hostname:uuid,其数据包括该consumer订阅的主题
可以用zk存储consumer offset
/consumers/[group_id]/offsets/[topic]/[partition_id] --> offset_counter_value (persistent node)
记录下某个分区已经消费的最大offset
分区归属哪个消费者
/consumers/[group_id]/owners/[topic]/[partition_id] --> consumer_node_id (ephemeral node)
每个分区由哪个消费者负责
cluster集群ID
/cluster/id
consumer启动流程
consumer_id注册
监听group内consumer_id的变化,以便在consumer间重平衡各自的职责
监听broker的变化
如果使用topic filter,需要监听broker topic,当topic变化时再次判断哪些topic能通过filter
group内进行一次重新平衡
consumer平衡调整
group内消费者重新平衡的作用:在分区分配方面,让消费者保持一致。
重平衡过程
- topic的所有分区P并排序
- group内的所有消费者C并排序
- i作为消费者结合C的index,N作为分区P数量/消费者C数量的比值
- i*N to (i+1)*N - 1的分区分配给index为i的消费者
- 删除原来的"分区归属哪个消费"的节点
- 新建"分区归属哪个消费"节点