【学习点滴】kafka

目录

Kafka 简介

为何使用消息系统

Kafka 架构

Kafka 拓扑结构

Topic & Partition

Producer 消息路由

Consumer Group

Kafka  High Available

为何需要 Replication

为何需要 Leader Election

Kafka HA 设计解析

如何将所有 Replica 均匀分布到整个集群

Data Replication

broker failover 过程简介

创建 / 删除 Topic

Broker 响应请求流程

LeaderAndIsrRequest 响应过程

Broker 启动过程

一些tips

高效使用磁盘

零拷贝

减少网络开销

kafka消息保留策略 

kafka offset的存储问题


一点学习记录,摘自  https://www.infoq.cn/article/kafka-analysis-part-1 

Kafka 简介

Kafka 是一种分布式的,基于发布 / 订阅的消息系统。主要设计目标如下:

  • 以时间复杂度为 O(1) 的方式提供消息持久化能力,即使对 TB 级以上数据也能保证常数时间复杂度的访问性能。(顺序读写)
  • 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒 100K 条以上消息的传输。
  • 支持 Kafka Server 间的消息分区,及分布式消费,同时保证每个 Partition 内的消息顺序传输。
  • 同时支持离线数据处理和实时数据处理。
  • Scale out:支持在线水平扩展。

为何使用消息系统

  • 解耦在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
  • 冗余有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的 " 插入 - 获取 - 删除 " 范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
  • 扩展性因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。
  • 灵活性 & 峰值处理能力在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
  • 可恢复性系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
  • 顺序保证在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka 保证一个 Partition 内的消息的有序性。
  • 缓冲在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。
  • 异步通信很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们

 

Kafka 架构

  • Broker  Kafka 集群包含一个或多个服务器,这种服务器被称为 broker
  • Topic  每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。(物理上不同 Topic 的消息分开存储,逻辑上一个 Topic 的消息虽然保存于一个或多个 broker 上但用户只需指定消息的 Topic 即可生产或消费数据而不必关心数据存于何处)
  • Partition  Parition 是物理上的概念,每个 Topic 包含一个或多个 Partition.
  • Producer  负责发布消息到 Kafka broker
  • Consumer  消息消费者,向 Kafka broker 读取消息的客户端。
  • Consumer Group  每个 Consumer 属于一个特定的 Consumer Group(可为每个 Consumer 指定 group name,若不指定 group name 则属于默认的 group)。

Kafka 拓扑结构

Kafka设计解析(一):Kafka背景及架构介绍

  如图所示,一个典型的 Kafka 集群中包含若干 Producer(可以是 web 前端产生的 Page View,或者是服务器日志,系统 CPU、Memory 等),若干 broker(Kafka 支持水平扩展,一般 broker 数量越多,集群吞吐率越高),若干 Consumer Group,以及一个 Zookeeper 集群。Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 Consumer Group 发生变化时进行 rebalance。Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。

Topic & Partition

Topic 在逻辑上可以被认为是一个 queue,每条消费都必须指定它的 Topic,可以简单理解为必须指明把这条消息放进哪个 queue 里。为了使得 Kafka 的吞吐率可以线性提高,物理上把 Topic 分成一个或多个 Partition,每个 Partition 在物理上对应一个文件夹,该文件夹下存储这个 Partition 的所有消息和索引文件。若创建 topic1 和 topic2 两个 topic,且分别有 13 个和 19 个分区,则整个集群上会相应会生成共 32 个文件夹(本文所用集群共 8 个节点,此处 topic1 和 topic2 replication-factor 均为 1),如下图所示。

Kafka设计解析(一):Kafka背景及架构介绍

每个日志文件都是一个 log entrie 序列,每个 log entrie 包含一个 4 字节整型数值(值为 N+5),1 个字节的 "magic value",4 个字节的 CRC 校验码,其后跟 N 个字节的消息体。每条消息都有一个当前 Partition 下唯一的 64 字节的 offset,它指明了这条消息的起始位置。磁盘上存储的消息格式如下:

message length : 4 bytes (value: 1+4+n)
"magic" value : 1 byte 
crc : 4 bytes 
payload : n bytes 

这个 log entries 并非由一个文件构成,而是分成多个 segment,每个 segment 以该 segment 第一条消息的 offset 命名并以“.kafka”为后缀。另外会有一个索引文件,它标明了每个 segment 下包含的 log entry 的 offset 范围,如下图所示。

Kafka设计解析(一):Kafka背景及架构介绍

因为每条消息都被 append 到该 Partition 中,属于顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是 Kafka 高吞吐率的一个很重要的保证)。

Kafka设计解析(一):Kafka背景及架构介绍

对于传统的 message queue 而言,一般会删除已经被消费的消息,而 Kafka 集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要),因此 Kafka 提供两种策略删除旧数据。一是基于时间,二是基于 Partition 文件大小。例如可以通过配置 $KAFKA_HOME/config/server.properties,让 Kafka 删除一周前的数据,也可在 Partition 文件超过 1GB 时删除旧数据,配置如下所示。

# The minimum age of a log file to be eligible for deletion
log.retention.hours=168
# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824
# The interval at which log segments are checked to see if they can be deleted according to the retention policies
log.retention.check.interval.ms=300000
# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction.
log.cleaner.enable=false

    这里要注意,因为 Kafka 读取特定消息的时间复杂度为 O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。选择怎样的删除策略只与磁盘以及具体的需求有关。另外,Kafka 会为每一个 Consumer Group 保留一些 metadata 信息——当前消费的消息的 position,也即 offset。这个 offset 由 Consumer 控制。正常情况下 Consumer 会在消费完一条消息后递增该 offset。当然,Consumer 也可将 offset 设成一个较小的值,重新消费一些消息。因为 offet 由 Consumer 控制,所以 Kafka broker 是无状态的,它不需要标记哪些消息被哪些消费过,也不需要通过 broker 去保证同一个 Consumer Group 只有一个 Consumer 能消费某一条消息,因此也就不需要锁机制,这也为 Kafka 的高吞吐率提供了有力保障。

Producer 消息路由

    Producer 发送消息到 broker 时,会根据 Paritition 机制选择将其存储到哪一个 Partition。如果 Partition 机制设置合理,所有消息可以均匀分布到不同的 Partition 里,这样就实现了负载均衡。如果一个 Topic 对应一个文件,那这个文件所在的机器 I/O 将会成为这个 Topic 的性能瓶颈,而有了 Partition 后,不同的消息可以并行写入不同 broker 的不同 Partition 里,极大的提高了吞吐率。可以在 $KAFKA_HOME/config/server.properties 中通过配置项 num.partitions 来指定新建 Topic 的默认 Partition 数量,也可在创建 Topic 时通过参数指定,同时也可以在 Topic 创建之后通过 Kafka 提供的工具修改。

    在发送一条消息时,可以指定这条消息的 key,Producer 根据这个 key 和 Partition 机制来判断应该将这条消息发送到哪个 Parition。Paritition 机制可以通过指定 Producer 的 paritition. class 这一参数来指定,该 class 必须实现 kafka.producer.Partitioner 接口。本例中如果 key 可以被解析为整数则将对应的整数与 Partition 总数取余,该消息会被发送到该数对应的 Partition。(每个 Parition 都会有个序号, 序号从 0 开始)

    则 key 相同的消息会被发送并存储到同一个 partition 里,而且 key 的序号正好和 Partition 序号相同。(Partition 序号从 0 开始,本例中的 key 也从 0 开始)。下图所示是通过 Java 程序调用 Consumer 后打印出的消息列表。

Kafka设计解析(一):Kafka背景及架构介绍

Consumer Group

(本节所有描述都是基于 Consumer hight level API 而非 low level API)。

使用 Consumer high level API 时,同一 Topic 的一条消息只能被同一个 Consumer Group 内的一个 Consumer 消费,但多个 Consumer Group 可同时消费这一消息。

Kafka设计解析(一):Kafka背景及架构介绍

    这是 Kafka 用来实现一个 Topic 消息的广播(发给所有的 Consumer)和单播(发给某一个 Consumer)的手段。一个 Topic 可以对应多个 Consumer Group。如果需要实现广播,只要每个 Consumer 有一个独立的 Group 就可以了。要实现单播只要所有的 Consumer 在同一个 Group 里。用 Consumer Group 还可以将 Consumer 进行自由的分组而不需要多次发送消息到不同的 Topic。

    下面这个例子清晰地展示了 Kafka Consumer Group 的特性。首先创建一个 Topic (名为 topic1,包含 3 个 Partition),然后创建一个属于 group1 的 Consumer 实例,并创建三个属于 group2 的 Consumer 实例,最后通过 Producer 向 topic1 发送 key 分别为 1,2,3 的消息。结果发现属于 group1 的 Consumer 收到了所有的这三条消息,同时 group2 中的 3 个 Consumer 分别收到了 key 为 1,2,3 的消息。如下图所示。

Kafka设计解析(一):Kafka背景及架构介绍

Kafka 保证同一 Consumer Group 中只有一个 Consumer 会消费某条消息,实际上,Kafka 保证的是稳定状态下每一个 Consumer 实例只会消费某一个或多个特定 Partition 的数据,而某个 Partition 的数据只会被某一个特定的 Consumer 实例所消费。也就是说 Kafka 对消息的分配是以 Partition 为单位分配的,而非以每一条消息作为分配单元。这样设计的劣势是无法保证同一个 Consumer Group 里的 Consumer 均匀消费数据,优势是每个 Consumer 不用都跟大量的 Broker 通信,减少通信开销,同时也降低了分配难度,实现也更简单。另外,因为同一个 Partition 里的数据是有序的,这种设计可以保证每个 Partition 里的数据可以被有序消费。

如果某 Consumer Group 中 Consumer(每个 Consumer 只创建 1 个 MessageStream)数量少于 Partition 数量,则至少有一个 Consumer 会消费多个 Partition 的数据,如果 Consumer 的数量与 Partition 数量相同,则正好一个 Consumer 消费一个 Partition 的数据。而如果 Consumer 的数量多于 Partition 的数量时,会有部分 Consumer 无法消费该 Topic 下任何一条消息

 

 

Kafka  High Available

为何需要 Replication

    在 Kafka 在 0.8 以前的版本中,是没有 Replication 的,一旦某一个 Broker 宕机,则其上所有的 Partition 数据都不可被消费,这与 Kafka 数据持久性及 Delivery Guarantee 的设计目标相悖。同时 Producer 都不能再将数据存于这些 Partition 中。

  • 如果 Producer 使用同步模式则 Producer 会在尝试重新发送message.send.max.retries(默认值为 3)次后抛出 Exception,用户可以选择停止发送后续数据也可选择继续选择发送。而前者会造成数据的阻塞,后者会造成本应发往该 Broker 的数据的丢失。
  • 如果 Producer 使用异步模式,则 Producer 会尝试重新发送message.send.max.retries(默认值为 3)次后记录该异常并继续发送后续数据,这会造成数据丢失并且用户只能通过日志发现该问题。同时,Kafka 的 Producer 并未对异步模式提供 callback 接口。

    由此可见,在没有 Replication 的情况下,一旦某机器宕机或者某个 Broker 停止工作则会造成整个系统的可用性降低。随着集群规模的增加,整个集群中出现该类异常的几率大大增加,因此对于生产系统而言 Replication 机制的引入非常重要。

为何需要 Leader Election

注意:本文所述 Leader Election 主要指 Replica 之间的 Leader Election。

    引入 Replication 之后,同一个 Partition 可能会有多个 Replica,而这时需要在这些 Replication 之间选出一个 Leader,Producer 和 Consumer 只与这个 Leader 交互,其它 Replica 作为 Follower 从 Leader 中复制数据。

    因为需要保证同一个 Partition 的多个 Replica 之间的数据一致性(其中一个宕机后其它 Replica 必须要能继续服务并且即不能造成数据重复也不能造成数据丢失)。如果没有一个 Leader,所有 Replica 都可同时读 / 写数据,那就需要保证多个 Replica 之间互相(N×N 条通路)同步数据,数据的一致性和有序性非常难保证,大大增加了 Replication 实现的复杂性,同时也增加了出现异常的几率。而引入 Leader 后,只有 Leader 负责数据读写,Follower 只向 Leader 顺序 Fetch 数据(N 条通路),系统更加简单且高效。

Kafka HA 设计解析

如何将所有 Replica 均匀分布到整个集群

为了更好的做负载均衡,Kafka 尽量将所有的 Partition 均匀分配到整个集群上。一个典型的部署方式是一个 Topic 的 Partition 数量大于 Broker 的数量。同时为了提高 Kafka 的容错能力,也需要将同一个 Partition 的 Replica 尽量分散到不同的机器。实际上,如果所有的 Replica 都在同一个 Broker 上,那一旦该 Broker 宕机,该 Partition 的所有 Replica 都无法工作,也就达不到 HA 的效果。同时,如果某个 Broker 宕机了,需要保证它上面的负载可以被均匀的分配到其它幸存的所有 Broker 上。

Kafka 分配 Replica 的算法如下:

  1. 将所有 Broker(假设共 n 个 Broker)和待分配的 Partition 排序
  2. 将第 i 个 Partition 分配到第(i mod n)个 Broker 上
  3. 将第 i 个 Partition 的第 j 个 Replica 分配到第((i + j) mode n)个 Broker 上

Data Replication

Kafka 的 Data Replication 需要解决如下问题:

  • 怎样 Propagate 消息
  • 在向 Producer 发送 ACK 前需要保证有多少个 Replica 已经收到该消息
  • 怎样处理某个 Replica 不工作的情况
  • 怎样处理 Failed Replica 恢复回来的情况

Propagate 消息

    Producer 在发布消息到某个 Partition 时,先通过 ZooKeeper 找到该 Partition 的 Leader,然后无论该 Topic 的 Replication Factor 为多少(也即该 Partition 有多少个 Replica),Producer 只将该消息发送到该 Partition 的 Leader。Leader 会将该消息写入其本地 Log。每个 Follower 都从 Leader pull 数据。这种方式上,Follower 存储的数据顺序与 Leader 保持一致。Follower 在收到该消息并写入其 Log 后,向 Leader 发送 ACK。一旦 Leader 收到了 ISR 中的所有 Replica 的 ACK,该消息就被认为已经 commit 了,Leader 将增加 HW 并且向 Producer 发送 ACK。

为了提高性能,每个 Follower 在接收到数据后就立马向 Leader 发送 ACK,而非等到数据写入 Log 中。因此,对于已经 commit 的消息,Kafka 只能保证它被存于多个 Replica 的内存中,而不能保证它们被持久化到磁盘中,也就不能完全保证异常发生后该条消息一定能被 Consumer 消费。但考虑到这种场景非常少见,可以认为这种方式在性能和数据持久化上做了一个比较好的平衡。在将来的版本中,Kafka 会考虑提供更高的持久性。

Consumer 读消息也是从 Leader 读取,只有被 commit 过的消息(offset 低于 HW 的消息)才会暴露给 Consumer。

Kafka Replication 的数据流如下图所示:

ACK 前需要保证有多少个备份

和大部分分布式系统一样,Kafka 处理失败需要明确定义一个 Broker 是否“活着”。对于 Kafka 而言,Kafka 存活包含两个条件,一是它必须维护与 ZooKeeper 的 session(这个通过 ZooKeeper 的 Heartbeat 机制来实现)。二是 Follower 必须能够及时将 Leader 的消息复制过来,不能“落后太多”。

Leader 会跟踪与其保持同步的 Replica 列表,该列表称为 ISR(即 in-sync Replica)(包含 Leader 自己)。这种方案,与同步复制非常接近,但不同的是,这个 ISR 是由 Leader 动态维护的。如果 Follower 不能紧“跟上”(即落后太多)Leader,它将被 Leader 从 ISR 中移除,待它又重新“跟上”Leader 后,会被 Leader 再次加加 ISR 中。每次改变 ISR 后,Leader 都会将最新的 ISR 持久化到 Zookeeper 中。这里所描述的“落后太多”指 Follower 复制的消息落后于 Leader 后的条数超过预定值(该值可在 $KAFKA_HOME/config/server.properties 中通过replica.lag.max.messages配置,其默认值是 4000)或者 Follower 超过一定时间(该值可在 $KAFKA_HOME/config/server.properties 中通过replica.lag.time.max.ms来配置,其默认值是 10000)未向 Leader 发送 fetch 请求。

Kafka 的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,完全同步复制要求所有能工作的 Follower 都复制完,这条消息才会被认为 commit,这种复制方式极大的影响了吞吐率(高吞吐率是 Kafka 非常重要的一个特性)。而异步复制方式下,Follower 异步的从 Leader 复制数据,数据只要被 Leader 写入 log 就被认为已经 commit,这种情况下如果 Follower 都复制完都落后于 Leader,而如果 Leader 突然宕机,则会丢失数据。而 Kafka 的这种使用 ISR 的方式则很好的均衡了确保数据不丢失以及吞吐率。Follower 可以批量的从 Leader 复制数据,这样极大的提高复制性能(批量写磁盘),极大减少了 Follower 与 Leader 的差距。

需要说明的是,Kafka 只解决 fail/recover,不处理“Byzantine”(“拜占庭”)问题。一条消息只有被 ISR 里的所有 Follower 都从 Leader 复制过去才会被认为已提交(in-sync Replica)。这样就避免了部分数据被写进了 Leader,还没来得及被任何 Follower 复制就宕机了,而造成数据丢失(Consumer 无法消费这些数据)。而对于 Producer 而言,它可以选择是否等待消息 commit,这可以通过request.required.acks来设置。这种机制确保了只要 ISR 有一个或以上的 Follower,一条被 commit 的消息就不会丢失。

Leader Election 算法

上文说明了 Kafka 是如何做 Replication 的,另外一个很重要的问题是当 Leader 宕机了,怎样在 Follower 中选举出新的 Leader。因为 Follower 可能落后许多或者 crash 了,所以必须确保选择“最新”的 Follower 作为新的 Leader。一个基本的原则就是,如果 Leader 不在了,新的 Leader 必须拥有原来的 Leader commit 过的所有消息。这就需要作一个折衷,如果 Leader 在标明一条消息被 commit 前等待更多的 Follower 确认,那在它宕机之后就有更多的 Follower 可以作为新的 Leader,但这也会造成吞吐率的下降。

一种非常常用的选举 leader 的方式是“Majority Vote”(“少数服从多数”),但 Kafka 并未采用这种方式。这种模式下,如果我们有 2f+1 个 Replica(包含 Leader 和 Follower),那在 commit 之前必须保证有 f+1 个 Replica 复制完消息,为了保证正确选出新的 Leader,fail 的 Replica 不能超过 f 个。因为在剩下的任意 f+1 个 Replica 里,至少有一个 Replica 包含有最新的所有消息。这种方式有个很大的优势,系统的 latency 只取决于最快的几个 Broker,而非最慢那个。Majority Vote 也有一些劣势,为了保证 Leader Election 的正常进行,它所能容忍的 fail 的 follower 个数比较少。如果要容忍 1 个 follower 挂掉,必须要有 3 个以上的 Replica,如果要容忍 2 个 Follower 挂掉,必须要有 5 个以上的 Replica。也就是说,在生产环境下为了保证较高的容错程度,必须要有大量的 Replica,而大量的 Replica 又会在大数据量下导致性能的急剧下降。这就是这种算法更多用在 ZooKeeper 这种共享集群配置的系统中而很少在需要存储大量数据的系统中使用的原因。例如 HDFS 的 HA Feature 是基于 majority-vote-based journal ,但是它的数据存储并没有使用这种方式。

实际上,Leader Election 算法非常多,比如 ZooKeeper 的 Zab Raft  Viewstamped Replication 。而 Kafka 所使用的 Leader Election 算法更像微软的 PacificA 算法。

Kafka 在 ZooKeeper 中动态维护了一个 ISR(in-sync replicas),这个 ISR 里的所有 Replica 都跟上了 leader,只有 ISR 里的成员才有被选为 Leader 的可能。在这种模式下,对于 f+1 个 Replica,一个 Partition 能在保证不丢失已经 commit 的消息的前提下容忍 f 个 Replica 的失败。在大多数使用场景中,这种模式是非常有利的。事实上,为了容忍 f 个 Replica 的失败,Majority Vote 和 ISR 在 commit 前需要等待的 Replica 数量是一样的,但是 ISR 需要的总的 Replica 的个数几乎是 Majority Vote 的一半。

虽然 Majority Vote 与 ISR 相比有不需等待最慢的 Broker 这一优势,但是 Kafka 作者认为 Kafka 可以通过 Producer 选择是否被 commit 阻塞来改善这一问题,并且节省下来的 Replica 和磁盘使得 ISR 模式仍然值得。

如何处理所有 Replica 都不工作

上文提到,在 ISR 中至少有一个 follower 时,Kafka 可以确保已经 commit 的数据不丢失,但如果某个 Partition 的所有 Replica 都宕机了,就无法保证数据不丢失了。这种情况下有两种可行的方案:

  • 等待 ISR 中的任一个 Replica“活”过来,并且选它作为 Leader
  • 选择第一个“活”过来的 Replica(不一定是 ISR 中的)作为 Leader

这就需要在可用性和一致性当中作出一个简单的折衷。如果一定要等待 ISR 中的 Replica“活”过来,那不可用的时间就可能会相对较长。而且如果 ISR 中的所有 Replica 都无法“活”过来了,或者数据都丢失了,这个 Partition 将永远不可用。选择第一个“活”过来的 Replica 作为 Leader,而这个 Replica 不是 ISR 中的 Replica,那即使它并不保证已经包含了所有已 commit 的消息,它也会成为 Leader 而作为 consumer 的数据源(前文有说明,所有读写都由 Leader 完成)。Kafka0.8.* 使用了第二种方式。根据 Kafka 的文档,在以后的版本中,Kafka 支持用户通过配置选择这两种方式中的一种,从而根据不同的使用场景选择高可用性还是强一致性。

如何选举 Leader

最简单最直观的方案是,所有 Follower 都在 ZooKeeper 上设置一个 Watch,一旦 Leader 宕机,其对应的 ephemeral znode 会自动删除,此时所有 Follower 都尝试创建该节点,而创建成功者(ZooKeeper 保证只有一个能创建成功)即是新的 Leader,其它 Replica 即为 Follower。

但是该方法会有 3 个问题:

  • split-brain 这是由 ZooKeeper 的特性引起的,虽然 ZooKeeper 能保证所有 Watch 按顺序触发,但并不能保证同一时刻所有 Replica“看”到的状态是一样的,这就可能造成不同 Replica 的响应不一致
  • herd effect 如果宕机的那个 Broker 上的 Partition 比较多,会造成多个 Watch 被触发,造成集群内大量的调整
  • ZooKeeper 负载过重 每个 Replica 都要为此在 ZooKeeper 上注册一个 Watch,当集群规模增加到几千个 Partition 时 ZooKeeper 负载会过重。

Kafka 0.8.* 的 Leader Election 方案解决了上述问题,它在所有 broker 中选出一个 controller,所有 Partition 的 Leader 选举都由 controller 决定。controller 会将 Leader 的改变直接通过 RPC 的方式(比 ZooKeeper Queue 的方式更高效)通知需为为此作为响应的 Broker。同时 controller 也负责增删 Topic 以及 Replica 的重新分配。

broker failover 过程简介

  1. Controller 在 ZooKeeper 注册 Watch,一旦有 Broker 宕机(这是用宕机代表任何让系统认为其 die 的情景,包括但不限于机器断电,网络不可用,GC 导致的 Stop The World,进程 crash 等),其在 ZooKeeper 对应的 znode 会自动被删除,ZooKeeper 会 fire Controller 注册的 watch,Controller 读取最新的幸存的 Broker。
  2. Controller 决定 set_p,该集合包含了宕机的所有 Broker 上的所有 Partition。
  3. 对 set_p 中的每一个 Partition

    3.1 从/brokers/topics/[topic]/partitions/[partition]/state读取该 Partition 当前的 ISR

    3.2 决定该 Partition 的新 Leader。如果当前 ISR 中有至少一个 Replica 还幸存,则选择其中一个作为新 Leader,新的 ISR 则包含当前 ISR 中所有幸存的 Replica。否则选择该 Partition 中任意一个幸存的 Replica 作为新的 Leader 以及 ISR(该场景下可能会有潜在的数据丢失)。如果该 Partition 的所有 Replica 都宕机了,则将新的 Leader 设置为 -1。

    3.3 将新的 Leader,ISR 和新的leader_epochcontroller_epoch写入/brokers/topics/[topic]/partitions/[partition]/state。注意,该操作只有其 version 在 3.1 至 3.3 的过程中无变化时才会执行,否则跳转到 3.1

  4. 直接通过 RPC 向 set_p 相关的 Broker 发送 LeaderAndISRRequest 命令。Controller 可以在一个 RPC 操作中发送多个命令从而提高效率。

    broker failover 顺序图如下所示。

创建 / 删除 Topic

  1. Controller 在 ZooKeeper 的/brokers/topics节点上注册 Watch,一旦某个 Topic 被创建或删除,则 Controller 会通过 Watch 得到新创建 / 删除的 Topic 的 Partition/Replica 分配。
  2. 对于删除 Topic 操作,Topic 工具会将该 Topic 名字存于/admin/delete_topics。若delete.topic.enable为 true,则 Controller 注册在/admin/delete_topics上的 Watch 被 fire,Controller 通过回调向对应的 Broker 发送 StopReplicaRequest,若为 false 则 Controller 不会在/admin/delete_topics上注册 Watch,也就不会对该事件作出反应。
  3. 对于创建 Topic 操作,Controller 从/brokers/ids读取当前所有可用的 Broker 列表,对于 set_p 中的每一个 Partition:

    3.1 从分配给该 Partition 的所有 Replica(称为 AR)中任选一个可用的 Broker 作为新的 Leader,并将 AR 设置为新的 ISR(因为该 Topic 是新创建的,所以 AR 中所有的 Replica 都没有数据,可认为它们都是同步的,也即都在 ISR 中,任意一个 Replica 都可作为 Leader)

    3.2 将新的 Leader 和 ISR 写入/brokers/topics/[topic]/partitions/[partition]

  4. 直接通过 RPC 向相关的 Broker 发送 LeaderAndISRRequest。

    创建 Topic 顺序图如下所示。

Broker 响应请求流程

Broker 通过kafka.network.SocketServer及相关模块接受各种请求并作出响应。整个网络通信模块基于 Java NIO 开发,并采用 Reactor 模式,其中包含 1 个 Acceptor 负责接受客户请求,N 个 Processor 负责读写数据,M 个 Handler 处理业务逻辑。

Acceptor 的主要职责是监听并接受客户端(请求发起方,包括但不限于 Producer,Consumer,Controller,Admin Tool)的连接请求,并建立和客户端的数据传输通道,然后为该客户端指定一个 Processor,至此它对该客户端该次请求的任务就结束了,它可以去响应下一个客户端的连接请求了。其核心代码如下。

Processor 主要负责从客户端读取数据并将响应返回给客户端,它本身并不处理具体的业务逻辑,并且其内部维护了一个队列来保存分配给它的所有 SocketChannel。Processor 的 run 方法会循环从队列中取出新的 SocketChannel 并将其SelectionKey.OP_READ注册到 selector 上,然后循环处理已就绪的读(请求)和写(响应)。Processor 读取完数据后,将其封装成 Request 对象并将其交给 RequestChannel。

RequestChannel 是 Processor 和 KafkaRequestHandler 交换数据的地方,它包含一个队列 requestQueue 用来存放 Processor 加入的 Request,KafkaRequestHandler 会从里面取出 Request 来处理;同时它还包含一个 respondQueue,用来存放 KafkaRequestHandler 处理完 Request 后返还给客户端的 Response。

Processor 会通过 processNewResponses 方法依次将 requestChannel 中 responseQueue 保存的 Response 取出,并将对应的SelectionKey.OP_WRITE事件注册到 selector 上。当 selector 的 select 方法返回时,对检测到的可写通道,调用 write 方法将 Response 返回给客户端。

KafkaRequestHandler 循环从 RequestChannel 中取 Request 并交给kafka.server.KafkaApis处理具体的业务逻辑。

LeaderAndIsrRequest 响应过程

对于收到的 LeaderAndIsrRequest,Broker 主要通过 ReplicaManager 的 becomeLeaderOrFollower 处理,流程如下:

  1. 若请求中 controllerEpoch 小于当前最新的 controllerEpoch,则直接返回 ErrorMapping.StaleControllerEpochCode。
  2. 对于请求中 partitionStateInfos 中的每一个元素,即((topic, partitionId), partitionStateInfo):

    2.1 若 partitionStateInfo 中的 leader epoch 大于当前 ReplicManager 中存储的 (topic, partitionId) 对应的 partition 的 leader epoch,则:

    2.1.1 若当前 brokerid(或者说 replica id)在 partitionStateInfo 中,则将该 partition 及 partitionStateInfo 存入一个名为 partitionState 的 HashMap 中

    2.1.2 否则说明该 Broker 不在该 Partition 分配的 Replica list 中,将该信息记录于 log 中

    2.2 否则将相应的 Error code(ErrorMapping.StaleLeaderEpochCode)存入 Response 中

  3. 筛选出 partitionState 中 Leader 与当前 Broker ID 相等的所有记录存入 partitionsTobeLeader 中,其它记录存入 partitionsToBeFollower 中。
  4. 若 partitionsTobeLeader 不为空,则对其执行 makeLeaders 方。
  5. 若 partitionsToBeFollower 不为空,则对其执行 makeFollowers 方法。
  6. 若 highwatermak 线程还未启动,则将其启动,并将 hwThreadInitialized 设为 true。
  7. 关闭所有 Idle 状态的 Fetcher。

LeaderAndIsrRequest 处理过程如下图所示

Broker 启动过程

Broker 启动后首先根据其 ID 在 ZooKeeper 的/brokers/idszonde 下创建临时子节点( Ephemeral node ),创建成功后 Controller 的 ReplicaStateMachine 注册其上的 Broker Change Watch 会被 fire,从而通过回调 KafkaController.onBrokerStartup 方法完成以下步骤:

  1. 向所有新启动的 Broker 发送 UpdateMetadataRequest,其定义如下。

  2. 将新启动的 Broker 上的所有 Replica 设置为 OnlineReplica 状态,同时这些 Broker 会为这些 Partition 启动 high watermark 线程。
  3. 通过 partitionStateMachine 触发 OnlinePartitionStateChange。

 

 

一些tips

高效使用磁盘

顺序写磁盘

根据《一些场景下顺序写磁盘快于随机写内存》所述,将写磁盘的过程变为顺序写,可极大提高对磁盘的利用率。(提高吞吐量,还有一个设计点是将partition分布式存储,同时跟多个broker机器进行交互)

Kafka 的整个设计中,Partition 相当于一个非常长的数组,而 Broker 接收到的所有消息顺序写入这个大数组中。同时 Consumer 通过 Offset 顺序消费这些数据,并且不删除已经消费的数据,从而避免了随机写磁盘的过程。

由于磁盘有限,不可能保存所有数据,实际上作为消息系统 Kafka 也没必要保存所有数据,需要删除旧的数据。而这个删除过程,并非通过使用“读 - 写”模式去修改文件,而是将 Partition 分为多个 Segment,每个 Segment 对应一个物理文件,通过删除整个文件的方式去删除 Partition 内的数据。这种方式清除旧数据的方式,也避免了对文件的随机写操作。

通过如下代码可知,Kafka 删除 Segment 的方式,是直接删除 Segment 对应的整个 log 文件和整个 index 文件而非删除文件中的部分内容。

零拷贝

Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件通过网络发送(Broker 到 Consumer)的过程。这一过程的性能直接影响 Kafka 的整体吞吐量。

传统模式下的四次拷贝与四次上下文切换

以将磁盘文件通过网络发送为例。传统模式下,一般使用如下伪代码所示的方法先将文件数据读入内存,然后通过 Socket 将内存中的数据发送出去。

buffer = File.read
Socket.send(buffer)

这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态 Buffer(DMA 拷贝),然后应用程序将内存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝),接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝),最后通过 DMA 拷贝将数据拷贝到 NIC Buffer。同时,还伴随着四次上下文切换,如下图所示。

Kafka设计解析(六):Kafka高性能关键技术解析

sendfile 和 transferTo 实现零拷贝

而 Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer,无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件 - 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示。

Kafka设计解析(六):Kafka高性能关键技术解析

 

从具体实现来看,Kafka 的数据传输通过 TransportLayer 来完成,其子类 PlaintextTransportLayer 通过 Java NIO 的 FileChannel 的 transferTo 和 transferFrom 方法实现零拷贝,

   @Override
   public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
       return fileChannel.transferTo(position, count, socketChannel);
   }

注: transferTo 和 transferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。

减少网络开销

批处理

批处理是一种常用的用于提高 I/O 性能的方式。对 Kafka 而言,批处理既减少了网络传输的 Overhead,又提高了写磁盘的效率。

Kafka 0.8.1 及以前的 Producer 区分同步 Producer 和异步 Producer。同步 Producer 的 send 方法主要分两种形式。一种是接受一个 KeyedMessage 作为参数,一次发送一条消息。另一种是接受一批 KeyedMessage 作为参数,一次性发送多条消息。而对于异步发送而言,无论是使用哪个 send 方法,实现上都不会立即将消息发送给 Broker,而是先存到内部的队列中,直到消息条数达到阈值或者达到指定的 Timeout 才真正的将消息发送出去,从而实现了消息的批量发送。

Kafka 0.8.2 开始支持新的 Producer API,将同步 Producer 和异步 Producer 结合。虽然从 send 接口来看,一次只能发送一个 ProducerRecord,而不能像之前版本的 send 方法一样接受消息列表,但是 send 方法并非立即将消息发送出去,而是通过 batch.size 和 linger.ms 控制实际发送频率,从而实现批量发送。

由于每次网络传输,除了传输消息本身以外,还要传输非常多的网络协议本身的一些内容(称为 Overhead),所以将多条消息合并到一起传输,可有效减少网络传输的 Overhead,进而提高了传输效率。

零拷贝章节的图中可以看到,虽然 Broker 持续从网络接收数据,但是写磁盘并非每秒都在发生,而是间隔一段时间写一次磁盘,并且每次写磁盘的数据量都非常大(最高达到 718MB/S)。

数据压缩降低网络负载

Kafka 从 0.7 开始,即支持将数据压缩后再传输给 Broker。除了可以将每条消息单独压缩然后传输外,Kafka 还支持在批量发送时,将整个 Batch 的消息一起压缩后传输。数据压缩的一个基本原理是,重复数据越多压缩效果越好。因此将整个 Batch 的数据一起压缩能更大幅度减小数据量,从而更大程度提高网络传输效率。

Broker 接收消息后,并不直接解压缩,而是直接将消息以压缩后的形式持久化到磁盘。Consumer Fetch 到数据后再解压缩。因此 Kafka 的压缩不仅减少了 Producer 到 Broker 的网络传输负载,同时也降低了 Broker 磁盘操作的负载,也降低了 Consumer 与 Broker 间的网络传输量,从而极大得提高了传输效率,提高了吞吐量。

高效的序列化方式

Kafka 消息的 Key 和 Payload(或者说 Value)的类型可自定义,只需同时提供相应的序列化器和反序列化器即可。因此用户可以通过使用快速且紧凑的序列化 - 反序列化方式(如 Avro,Protocal Buffer)来减少实际网络传输和磁盘存储的数据规模,从而提高吞吐率。这里要注意,如果使用的序列化方法太慢,即使压缩比非常高,最终的效率也不一定高。

 

kafka消息保留策略 

Kafka Broker默认的消息保留策略是:要么保留一定时间,要么保留到消息达到一定大小的字节数。

当消息达到设置的条件上限时,旧消息就会过期并被删除,所以,在任何时刻,可用消息的总量都不会超过配置参数所指定的大小。

topic可以配置自己的保留策略,可以将消息保留到不再使用他们为止。

因为在一个大文件里查找和删除消息是很费时的事,也容易出错,所以,分区被划分为若干个片段。默认情况下,每个片段包含1G或者一周的数据,以较小的那个为准。在broker往leader分区写入消息时,如果达到片段上限,就关闭当前文件,并打开一个新文件。当前正在写入数据的片段叫活跃片段。当所有片段都被写满时,会清除下一个分区片段的数据,如果配置的是7个片段,每天打开一个新片段,就会删除一个最老的片段,循环使用所有片段。
 

 

kafka offset的存储问题

注意:从kafka-0.9版本及以后,kafka的消费者组和offset信息就不存zookeeper了,而是存到broker服务器上,所以,如果你为某个消费者指定了一个消费者组名称(group.id),那么,一旦这个消费者启动,这个消费者组名和它要消费的那个topic的offset信息就会被记录在broker服务器上

1.概述
Kafka版本[0.10.1.1],已默认将消费的 offset 迁入到了 Kafka 一个名为 __consumer_offsets 的Topic中。其实,早在 0.8.2.2 版本,已支持存入消费的 offset 到Topic中,只是那时候默认是将消费的 offset 存放在 Zookeeper 集群中。那现在,官方默认将消费的offset存储在 Kafka 的Topic中,同时,也保留了存储在 Zookeeper 的接口,通过 offsets.storage 属性来进行设置。

2.内容
其实,官方这样推荐,也是有其道理的。之前版本,Kafka其实存在一个比较大的隐患,就是利用 Zookeeper 来存储记录每个消费者/组的消费进度。虽然,在使用过程当中,JVM帮助我们完成了一些优化,但是消费者需要频繁的去与 Zookeeper 进行交互,而利用ZKClient的API操作Zookeeper频繁的Write其本身就是一个比较低效的Action,对于后期水平扩展也是一个比较头疼的问题。如果期间 Zookeeper 集群发生变化,那 Kafka 集群的吞吐量也跟着受影响。

在此之后,官方其实很早就提出了迁移到 Kafka 的概念,只是,之前是一直默认存储在 Zookeeper集群中,需要手动的设置,如果,对 Kafka 的使用不是很熟悉的话,一般我们就接受了默认的存储(即:存在 ZK 中)。在新版 Kafka 以及之后的版本,Kafka 消费的offset都会默认存放在 Kafka 集群中的一个叫 __consumer_offsets 的topic中。

当然,其实她实现的原理也让我们很熟悉,利用 Kafka 自身的 Topic,以消费的Group,Topic,以及Partition做为组合 Key。所有的消费offset都提交写入到上述的Topic中。因为这部分消息是非常重要,以至于是不能容忍丢数据的,所以消息的 acking 级别设置为了 -1,生产者等到所有的 ISR 都收到消息后才会得到 ack(数据安全性极好,当然,其速度会有所影响)。所以 Kafka 又在内存中维护了一个关于 Group,Topic 和 Partition 的三元组来维护最新的 offset 信息,消费者获取最新的offset的时候会直接从内存中获取。

kafka 提供三种语义的传递:

               1至少一次

               2至多一次

               3精确一次

  首先在 producer 端保证1和2的语义是非常简单的,至少一次只需要同步确认即可(确认方式分为只需要 leader 确认以及所有副本都确认,第二种更加具有容错性),至多一次最简单只需要异步不断的发送即可,效率也比较高。目前在 producer 端还不能保证精确一次,在未来有可能实现,实现方式如下:在同步确认的基础上为每一条消息加一个主键,如果发现主键曾经接受过,则丢弃

  在 consumer 端,大家都知道可以控制 offset,所以可以控制消费,其实 offset 只有在重启的时候才会用到。在机器正常运行时我们用的是 position,我们实时消费的位置也是 position 而不是 offset。我们可以得到每一条消息的 position。如果我们在处理消息之前就将当前消息的 position 保存到 zk 上即 offset,这就是只多一次消费,因为我们可能保存成功后,消息还没有消费机器就挂了,当机器再打开时此消息就丢失了;或者我们可以先消费消息然后保存 position 到 zk 上即 offset,此时我们就是至少一次,因为我们可能在消费完消息后offset 没有保存成功。而精确一次的做法就是让 position的保存和消息的消费成为原子性操作,比如将消息和 position 同时保存到 hdfs 上 ,此时保存的 position 就称为 offset,当机器重启后,从 hdfs重新读入offset,这就是精确一次。

  • consumer可以先读取消息,然后将offset写入日志文件中,然后再处理消息。这存在一种可能就是在存储offset后还没处理消息就crash了,新的consumer继续从这个offset处理,那么就会有些消息永远不会被处理,这就是上面说的“最多一次”。
  • consumer可以先读取消息,处理消息,最后记录offset,当然如果在记录offset之前就crash了,新的consumer会重复的消费一些消息,这就是上面说的“最少一次”。
  • “精确一次”可以通过将提交分为两个阶段来解决:保存了offset后提交一次,消息处理成功之后再提交一次。但是还有个更简单的做法:将消息的offset和消息被处理后的结果保存在一起。比如用Hadoop ETL处理消息时,将处理后的结果和offset同时保存在HDFS中,这样就能保证消息和offser同时被处理了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值