kafka 1.0 中文文档(八):实现

5.1网络层

网络层是一个NIO服务器,这里不做过多秒速。sendfile是通过给MessageSet接口地址的writeTo方法实现。这使得支持文件备份的信息集合,可以使用更有效的transferTo方法,而不是使用内置的缓冲写数据。该线程模型是一个单一的acceptor线程和n个processor线程组成。每个都处理一个固定数量的连接。

5.2消息

消息由可变长度的header,可变长度不可见key字节数组和可变长度不可见的值得数组组成。header的格式在下一节中描述。让key和value不可见是一个正确的决定:目前在序列化库方面正在取得很大的进展,任何特定的选择都不太可能适合所有用途。编写Kafka的应用程序可能会指定序列化类型。RecordBatch接口只是一个迭代器,通过使用专门的方法来批量读取和写入NIO通道。

5.3消息格式

消息(aka Records)总是分批写入。一个批次消息的技术术语就是record batch。并且一个record batch包含一个或多个record。退一步说,我们可以有一个包含单个record的rebord batch。Record batches 和records有他们自己的headers。0.11.0或更高版本(消息格式版本V2,或者magic=2)描述了每种格式。Click here

5.3.1 Record Batch

以下是RecordBatch在磁盘上的格式。

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
    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]

请注意,启用压缩时,压缩的rebord数据将直接按照record数进行序列化。
CRC覆盖从属性到批次结束的数据(即CRC后面的所有字节)。它位于magic字节之后,这意味着客户端在决定如何解析批处理长度和魔术字节之间的字节之前必须解析magic字节。在CRC计算中不包括分区领导epoch字段,以避免在为代理接收的每个批次分配此字段时需要重新计算CRC。 CRC-32C(Castagnoli)多项式用于计算。

压缩:与旧的消息格式不同,magic v2及以上版本在日志清理时保留原始批次的第一个和最后一个偏移/序列号。这是能够在日志重新加载时恢复生产者的状态时所必需的。例如,如果我们没有保留最后一个序列号,那么在分区引导失败之后,producter可能会看到一个OutOfSequence错误。必须保留基本序列号以进行重复检查(代理通过验证传入批次的第一个和最后一个序列号与来自该生产者的最后一个序列号相匹配,来检查传入的Produce的去重请求),因此,当批次中的所有记录都被清除但批次仍被保留以保留生产者的最后序列号时,可能在记录中有空的批次。baseTimestamp字段在压缩过程中没有保留,所以如果批量中的第一条记录被压缩,它将会改变。

5.3.1.1 Control Batches

一个control batch包含一条叫做control record的单个record。control record不应该传递给应用程序。相反,它们被消费者用来过滤掉事务退出的消息。

控制记录的key符合以下模式:

version: int16 (current version is 0)
type: int16 (0 indicates an abort marker, 1 indicates a commit)

control batch的值的架构取决于类型。 对客户来说价值是不透明的。

5.3.2 Record

record级别headers是在Kafka 0.11.0中引入的。 带有headers的记录的磁盘格式如下所示。

length: varint
attributes: int8
bit 0~7: unused
timestampDelta: varint
offsetDelta: varint
keyLength: varint
key: byte[]
valueLen: varint
value: byte[]
Headers => [Header]
5.4.2.1 Record Header
headerKeyLength: varint
headerKey: String
headerValueLength: varint
Value: byte[]

5.4 日志

一个有两个分区的名为“my_topic”的主题的日志保存在两个目录(my_topic_0和my_topic_1),日志文件包含该主题的消息。 日志文件的格式是一系列的“日志条目”:每个日志条目是一个4字节的整数N,存储着消息长度,后面跟着的N个字节组成的消息,每个消息都由一个64位的整数标识的唯一偏移量,指定消息字节在该分区发送到该主题中的所有消息流中的开始位置,每个消息的磁盘格式如下:每个日志文件都以第一个消息的偏移量命名,因此,第一个创建的文件将是00000000000.kafka,后面增加的文件都由一个整数(大约等于前一个文件的字节数)命名,这个整数是配置文件中指定的最大日志文件大小。

record的二进制格式是版本化的,并作为标准接口进行维护,因此 record batches可以在生产者,broker和客户端之间传输,无需重新复制或转换。 record的磁盘格式的详细信息在5.3节。

一般不会用消息偏移量用作消息ID。我们最初的想法是使用生产者生成的GUID,并在每个broker上维护从GUID到偏移量的映射。但是既然消费者必须为每个服务器维护一个ID,那么全局唯一性的GUID就没有任何价值了。而且,维护GUID到偏移的映射需要一个重量级的索引结构,它必须同步到磁盘,基本上需要一个完整的持久随机访问数据结构。因此,为了简化查找结构,我们决定使用一个简单的每个分区的原子计数器(offset),它可以结合分区ID和节点ID来唯一标识一个消息。这使查找结构更简单,虽然每个消费者仍然可能请求多个查找。然而,一旦我们用一个计数器,直接使用偏移量就很自然了 - 毕竟计数器对一个分区是唯一递增的整数。由于偏移量的具体实现对consumer api是隐藏的,因此我们采用了更高效的方法。
image

日志允许依次追加到文件最后。当文件达到可配置的大小(比如1GB)时,该文件将被转存到新文件中。该日志有两个配置参数:M,它指定操作系统强制将文件刷新到磁盘之前能写入的消息的数量,以及S,在指定的秒数后强制刷新到磁盘。 这提供了一个持久保证,即在系统崩溃的情况下,最多丢失M条消息或S秒的数据。

读取是通过指定消息的64位逻辑偏移量(offset)和S字节的最大块大小来完成。 这将返回一个消息迭代器(在S个字节的缓冲区中。 S的意图是大于任何单个消息,但是在消息异常大的情况下,可以多次重试读取,每次将缓冲区大小加倍,直到消息被成功读取。 可以指定最大消息和缓冲区大小,以使服务器拒绝大于指定大小的消息,并为客户端绑定这个指定的最大值,以便获取完整的消息。 缓冲区可能以不完整的部分消息结束,这很容易通过大小限制来检测。

实际从偏移量读取进程,首先找到存储数据的日志段文件,从全局偏移值计算文件特定的偏移量,然后从文件偏移量读取数据。检索是通过简单的二分查找,而不是内存内的遍历查找。

日志提供了获取最近写入的消息的能力,以允许客户从“现在”开始订阅。这对于那些因为消息过期数据(SLA指定的天数retention.ms)失败的消息都很有用。这种情况,如果客户端指定了一个不存在偏移量,将返回OutOfRangeException,可以重置它自己。

以下是发送给消费者的结果格式。

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

删除

数据删除是:一次只能删除一个日志段。日志管理器允许插入删除策略,以选择哪些文件符合删除条件。虽然保留N GB再删的策略也很有用,但是我们目前默认的策略是保留N天。为了避免加读锁的时候还允许删除,我们使用了写时复制(copy-on-write style)段列表模式实现,它提供了一致的视图,允许在删除的同时搜索日志段在不可变静态快照视图上继续进行。

保证

日志提供了配置参数M,该参数控制在强制刷新到磁盘之前能写入的最大消息数。启动时,将运行一个日志恢复过程,对最新日志段中的所有消息进行迭代,并验证每个消息条目是否有效。如果消息条目的大小和偏移量之和小于文件的长度,则消息条目有效,并且消息有效载荷的CRC32与存储在消息中的CRC相匹配。如果检测到损坏,日志将被截断为最后一个有效的偏移量。

请注意,必须处理两种损坏:由于崩溃而丢失未写入的块,以及将损坏的块添加到文件。这是因为操作系统通常不能保证文件索引节点和实际块数据之间的写入顺序,所以除了丢失写入的数据之外,如果索引节点更新为新的大小,文件将获得无意义的数据,在写入包含该数据的块之前发生崩溃。 CRC检测到这个情况,并防止它损坏日志(当然未写入的消息丢失了)。

5.5分布式

消费者偏移量跟踪

high-level 消费者跟踪它在每个分区中消耗的最大偏移量,并周期性地提交其偏移量,以便在重新启动时从这些偏移量恢复。kafka提供了一个选项,可以将给定消费group的所有偏移量存储在指定的经纪人(对于该组)中,称为偏移量管理员( offset manager)。即该消费组中的任何消费者实例应该将其偏移提交和提取发送给该偏移量管理员(broker)。high-level 消费者自动处理这个问题。如果您使用简单的消费者,则需要手动管理偏移量。这不支持Java简单消费者,它只能在ZooKeeper中提交或提取偏移量。如果您使用Scala简单消费者,您可以检测偏移量管理员,并明确提交或提取偏移量管理员的偏移量。消费者可以通过向任何Kafka broker发出GroupCoordinatorRequest并读取含有offset manager的GroupCoordinatorResponse来查找 offset manager。然后,消费者可以继续从 offset manager代理处提交或提取偏移量。在 offset manager变化的情况下,消费者将需要重新发现 offset manager。如果你想手动管理你的偏移,你可以看看这些代码示例,如何发出OffsetCommitRequest和OffsetFetchRequest。

当offset manager接收到OffsetCommitRequest时,它将请求添加到名为__consumer_offsets的特定的Kafka压缩主题。 只有在偏移量主题的所有副本都接收到偏移量后,offset manager才会向使用者发送成功提交偏移量的响应。 如果偏移无法在可配置的超时内复制,则偏移提交将失败,并且consumer 可能会在退出后重试提交(high-level 消费者是自动完成)。broker定期压缩偏移量主题,因为它只需要维护每个分区的最近偏移量提交。 offset manager还将偏移量缓存在内存表中以快速提供偏移量提取。

当offset manager接收到偏移量提取请求时,它只是从偏移量缓存中返回最后提交的偏移量。 如果offset manager刚刚启动,或者它刚刚成为一组新的消费组的offset manager(通过成为偏移量主题分区的leader),则可能需要将偏移量主题分区加载到高速缓存中。 在这种情况下,偏移读取将失败,并产生OffsetsLoadInProgress异常,并且消费者可能在退出后重试OffsetFetchRequest(高级用户是自动完成的。)

把offset从ZooKeeper迁移到Kafka

早期版本中的Kafka使用者默认在ZooKeeper中存储它们的偏移量。 可以通过以下步骤将这些消费者迁移到Kafka中:

  1. 在消费者配置中设置offsets.storage = kafka和dual.commit.enabled = true。
  2. 为消费者做一个rolling bounce,确认你的消费者是健康的。
  3. 在消费者配置中设置dual.commit.enabled = false。
  4. 为消费者做一个rolling bounce,然后确认你的消费者是健康的。
    如果您设置了offsets.storage = zookeeper,则还可以使用上述步骤执行回滚(即,从Kafka迁移回ZooKeeper)。

ZooKeeper目录

下面给出了用于消费者和经纪人之间协调的ZooKeeper结构和算法。

Notation

当一个路径中的元素被表示为[xyz]时,这意味着xyz的值不是固定的,并且实际上对于xyz的每个可能值都有一个ZooKeeper znode。 例如/topics/[topic]将是一个名为/topics的目录,其中包含每个主题名称的子目录。 还给出了数值范围,例如[0 … 5]来指示子目录0,1,2,3,4。箭头 ->用于指示znode的内容。 例如/hello -> world会指示一个znode /hello下包含值“world”。

Broker Node Registry代理节点注册

 /brokers/ids/[0...N] --> {"jmx_port":...,"timestamp":...,"endpoints":[...],"host":...,"version":...,"port":...} (ephemeral node)

这是所有当前代理节点的列表,每个代理节点都提供一个唯一的逻辑代理标识符,它将消息标识给消费者(必须作为其配置的一部分给出)。 启动时,代理节点通过在/ brokers/ids下用broke id 创建一个znode进行注册。 broker id 的目的是允许broker移动到不同的物理机器而不影响用户。 尝试注册已在使用的broker id (例如,因为两个服务器配置了相同的broker id )会导致错误。

由于代理使用临时znode在ZooKeeper中进行注册,因此该注册是动态的,并且在代理关闭或死亡(从而通知消费者不再可用)时将消失。

Broker Topic Registry

 /brokers/topics/[topic]/partitions/[0...N]/state --> {"controller_epoch":...,"leader":...,"version":...,"leader_epoch":...,"isr":[...]} (ephemeral node)

每个代理在其维护的主题下注册自己,并存储该主题的分区数量。

Consumers and Consumer Groups

主题的消费者也在ZooKeeper中注册,以便相互协调并平衡数据的消费。 消费者也可以通过设置offsets.storage = zookeeper将他们的偏移量存储在ZooKeeper中。 但是,这个偏移量存储机制将在未来的版本中被弃用。 因此,建议将offset存储迁移到Kafka。

多个消费者可以组成一个组,共同消费一个主题。 在同一组中的每个消费者被指定一个共享的的group_id。 该group id是在消费者的配置中提供的,通过设置groupid的方式告诉消费者它属于哪个组。

一个组中的消费者尽可能公平地划分分区,每个分区恰好被消费者组中的一个消费者消费。

Consumer Id Registry

除了组中的所有消费者共享的group_id之外,每个消费者还被赋予一个临时唯一的consumer_id(形式为hostname:uuid)用于识别。 消费者ID在以下目录中注册。

/consumers/[group_id]/ids/[consumer_id] --> {"version":...,"subscription":{...:...},"pattern":...,"timestamp":...} (ephemeral node)

组中的每个消费者都在其组中注册,并使用其consumer_id创建一个znode。 znode的值包含

Consumer Offsets

消费者跟踪他们在每个分区中消耗的最大偏移量。 如果offsets.storage = zookeeper,则此值存储在ZooKeeper目录中。

/consumers/[group_id]/offsets/[topic]/[partition_id] --> offset_counter_value (persistent node)

Partition Owner registry

每个代理分区由给定消费组中的单个消费者使用。 在消费开始之前,消费者必须确定分区的所有权。为了建立对它的所有权,消费者将自己的ID写在它声明所有权的特定经纪人分区下的临时节点中。

 /consumers/[group_id]/owners/[topic]/[partition_id] --> consumer_node_id (ephemeral node)

Cluster Id

集群ID是分配给Kafka集群的唯一且不可变的标识符。 集群ID最多可以有22个字符,允许的字符是由正则表达式[a-zA-Z0-9 _ \ - ] +定义的,对应于不带填充的URL安全的Base64变体使用的字符。 从概念上讲,它是在第一次启动集群时自动生成的。

当第一次成功启动版本为0.10.1或更高版本的broker时生成。 broker尝试在启动期间从/cluster/id znode获取集群标识。如果znode不存在,那么代理会生成一个新的集群标识并使用此集群标识并创建znode。

Broker node registration

Broker节点基本上是独立的,所以他们只发布他们有什么信息。 Broker加入时,会在Broker节点注册表目录下进行注册,并写入有关其主机名和端口的信息。 Broker还在broker topic registry中注册现有主题及其逻辑分区的列表。 在代理上创建新主题时会动态注册。

消费者注册算法

当消费者启动时,它执行以下操作:

  1. 在其消费组的consumer ID注册中注册自己。
  2. 在 consumer ID注册表下注册关于更改(新消费者加入或任何现有的消费者离开)的监听。 (每次更改都会触发更改的消费者所属组中的所有消费者之间的再平衡。)
  3. 在经纪人ID注册表下注册一个监视变更(新的经纪人加入或任何现有的经纪人离开)。 (每个变化都会触发所有消费群体中的所有消费者重新平衡。)
  4. 如果消费者使用主题过滤器创建消息流,则还会在 broker topic registry下注册关于更改(新增主题)的监听。 (每次更改都会触发对可用主题的重新评估,以确定主题过滤器允许哪些主题。新的允许主题将触发消费者组中所有消费者之间的重新平衡)。
  5. 强迫自己在消费组内重新平衡。

消费者重新平衡算法

消费者重新平衡算法允许组中的所有消费者就哪个消费者正在消费哪个分区达成共识。 消费者重新平衡是在每次添加或删除代理节点或者同一组内的其他消费者时被触发。 对于给定的主题和给定的消费者组,broker分区在组内的消费者之间平均分配。 分区总是由单个消费者使用。 这个设计简化了实现。 如果我们允许一个分区被多个消费者同时使用,那么分区上就会出现竞争,并且需要某种锁定。 如果消费者比分区多,一些消费者就根本得不到任何数据。 在重新平衡期间,我们尝试以这种方式将分区分配给消费者,以减少每个消费者必须连接的代理节点的数量。

每位消费者在重新平衡期间执行以下操作
1. 对于Ci订阅的每个主题T.
2. 让PT成为话题T的所有分区
3. 让CG成为消费主题T的与Ci相同的所有消费者
4. 排序PT(因此同一个代理上的分区被聚集在一起)
5. 排序CG
6. 让我作为CG中Ci的指标位置,让N = size(PT)/ size(CG)
7. 将分区从i * N分配到(i + 1)* N-1到消费者Ci
8. 从分区所有者注册表中删除Ci拥有的当前条目
9. 将新分配的分区添加到分区所有者注册表
(我们可能需要重新尝试,直到原始分区所有者释放其所有权)
当一个消费者触发再平衡时,同一时间内同一群体内的其他消费者也重新平衡。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值