文章目录
4.2 持久化
不要害怕文件系统
磁盘读写的快慢和如何使用有关,合理设计可以使磁盘和网络一样快。
配置为six 7200rpm SATA RAID-5 array的JBOD,顺序写的性能为600MB/sec,随机写的性能为100k/sec,相差6000倍。顺序读写是磁盘最希望被使用的方式,而且操作系统做了很多优化。
现代操作系统提供预读,后写的功能,(内存中的buff/cache)。应用程序不用再实现一遍。而且关于java内存,我们知道:
- 一个对象所需要的内存经常是实际存储数据大小的两倍甚至更多。
- 随着堆中数据增加,垃圾回收变得越来越慢。
使用操作系统的缓存,可以降低JVM的内存占用,重启应用时,也不用重新加载数据。
稳定的时间复杂度
4.3 性能
端到端的批量压缩
有些场景下瓶颈不是CPU或者磁盘,而是网络。用户可以每次发送数据前自己压缩数据,但是这样会有很低的压缩比。很多场景下的数据冗余都是因为有很多相同类型的数据,高效的压缩是将很多同类数据一起压缩而不是单条压缩。
producer将一个批次的数据压缩,发送到broker,broker存储压缩过的数据,consumer端接收到数据后解压。
支持GZIP, Snappy, LZ4 and ZStandard compression protocols
consumer端可以同时消费压缩和未压缩的数据。所以header中有标识是否压缩。
ByteBufferMessageSet格式如下。
1 byte magic | 1 byte compression-attributes | 4 byte CRC32 of the payload |
4.4 The Producer
负载均衡
producer直接发送数据到对应leader partition所在的broker,不需要任何中转。所以每一个kafka节点都需要知道哪个节点是活着的,对应leader partition在哪个节点上,并且能够响应相应的请求。
异步发送
批量操作是提交效率的高效方法之一,kafka在内存中缓存数据,每次请求发送一批数据。支持两个参数,缓存数据不能超过 X 字节,或者不能超过 n 毫秒。
4.5 The Consumer
consumer从leader partition所在的broker拉取数据。每次请求传一个offset,broker返回从该offset开始往后的一批数据。consumer可以控制offset来重复消费。
Consumer 偏移量
离线数据加载
静态组
每当consumer个数发生改变(新的consumer加入或者旧的consumer掉线),会发生rebalance, rebalance期间所有consumer无法消费数据。rebalance严重影响消费组吞吐,特别是group比较大,consumer个数比较多的时候,rebalance需要很长时间。为了解决这个问题,新版的group管理协议允许consumer提供一个持久的entity ids。基于这些id,之前分配的partition不变,不重新rebalance。
如果要使用这个特性,broker和client都要升级到2.3版本
4.6 消息发送语义
At most once
At least once
Exactly once
4.7 复制
5 实现
5.1 网络层
网络层是一个简单直接的NIO server。
5.2 消息
消息包含一个变长的header,一个变长的key的字节数组,和一个变长的value的字节数组。根据场景选用不同的序列化方式。RecordBatch类提供简单批量读写message的方法
5.3 消息格式
一个批次(Record Batch)包含很多条消息(Record), 批次(Record Batch)和每一条消息(Record)都有自己的header。
5.3.1 Record Batch 格式
下面的格式就是存在磁盘上的格式
baseOffset: int64
batchLength: int32
partitionLeaderEpoch: int32
magic: int8 (current magic value is 2)
crc: int32
attributes: int16
bit 0~2:
0: no compression
1: gzip
2: snappy
3: lz4
4: zstd
bit 3: timestampType
bit 4: isTransactional (0 means not transactional)
bit 5: isControlBatch (0 means not a control batch)
bit 6~15: unused
lastOffsetDelta: int32
firstTimestamp: int64
maxTimestamp: int64
producerId: int64
producerEpoch: int16
baseSequence: int32
records: [Record]
5.3.2 Record格式
磁盘上的存储格式如下
length: varint
attributes: int8
bit 0~7: unused
timestampDelta: varint
offsetDelta: varint
keyLength: varint
key: byte[]
valueLen: varint
value: byte[]
Headers => [Header]
5.3.2.1 Record Header 格式
headerKeyLength: varint
headerKey: String
headerValueLength: varint
Value: byte[]
5.4 Log
文件存储如下,每个partition建一个文件夹,以topic-partition命名。数据文件以该文件存储的起始offset命名,数据文件是一个"log entrie"的队列,每个"log entrie"包含一个4字节的整数表示message大小N,和N个字节的message。
drwxr-xr-x 2 root root 4096 Aug 10 15:14 access-0
drwxr-xr-x 2 root root 4096 Aug 10 14:50 access-1
drwxr-xr-x 2 root root 4096 Aug 10 15:14 access-6
-rw-r--r-- 1 root root 546784 Aug 10 08:13 00000000024134685350.index
-rw-r--r-- 1 root root 1073737636 Aug 10 08:13 00000000024134685350.log
-rw-r--r-- 1 root root 818724 Aug 10 08:13 00000000024134685350.timeindex
-rw-r--r-- 1 root root 372720 Aug 10 12:12 00000000024161786878.index
-rw-r--r-- 1 root root 1073695705 Aug 10 12:12 00000000024161786878.log
-rw-r--r-- 1 root root 555840 Aug 10 12:12 00000000024161786878.timeindex
-rw-r--r-- 1 root root 20 Aug 10 10:43 leader-epoch-checkpoint
用offset当做消息的id是一个亮点。开始的想法是用producer生成的GUID, 然后每个broker维护一个GUID到offset的映射。但是既然每个consumer都要持有一个serverId,那全局唯一的GUID就不能发挥它的价值。另外,维持一个随机ID到offset的映射需要一个大量随机读写磁盘的索引结构。所以为了简化查找,我们决定用brokerId,partitionId和一个递增的counter去唯一确定一条消息。用offset去标识消息就变得自然而然了。
写磁盘
数据会顺序的写到文件末尾,数据文件会滚动切换到下一个文件(根据配置决定一个文件的大小)。两个配置控制操作系统强制刷新数据到磁盘上,消息的条数和缓存时间。
读数据
每次读会提供一个offset和读取的最大字节数S。如果单条消息大于S,则会重试,每次重试S扩大两倍直到该条消息读取成功。broker端可以设置接收的单条消息的最大字节,超过该大小的丢弃。
consumer端试图消费一个不存在的offset时,会收到OutOfRangeException,可以根据情况重置消费或者失败。
返回给consumer的数据格式
MessageSetSend (fetch result)
total length : 4 bytes
error code : 2 bytes
message 1 : x bytes
...
message n : x bytes
MultiMessageSetSend (multiFetch result)
total length : 4 bytes
error code : 2 bytes
messageSetSend 1
...
messageSetSend n
删除数据
两个参数控制过期策略:时间,大小
为了避免在删除数据的时候禁止读,删除的时候使用copy-on-write
5.5 其他
Consumer追踪offset
kafka允许group里的consumer存储offset到指定的broker(group coordinator)。consumerGroup 根据groupName被分配到对应的coordinator。
当group coordinator收到一个OffsetCommitRequest,它会把数据写入一个特殊的topic(__consumer_offsets),当所有的副本都写入消息后,才会返回response给consumer。coordinator会在内存中维护提交的offset。