集群与可靠的数据传输
1.集群的成员关系
- Kafka 使用 zookeeper 来维护集群成员的信息。每个 broker 都有个唯一标识符, 这个标识符可以在配置文件里指定, 也可以自动生成。 在 broker 启 动的时候, 它通过创建临时节点把自己的 ID 注册到 zoo-keeper。 Kafka 组件订阅 Zookeeper 的/brokers/ids 路径(broker 在 zookeeper 上的注册路径) , 当 有 broker 加入集群或退出集群时, 这些组件就可以获得通知 。
- 如果你要启动另一个具有相同 ID 的 broker, 会得到一个错误。新 broker 会试着进行注册,但不会成功, 因为 zookeeper 里已经有一个具有相同 ID 的 broker。
- 在 broker 停机、 出现网络分区或长时间垃圾回收停顿时, broker 会从 Zookeeper 上断开连接, 此时 broker 在启动时创建的临时节点会自动从 Zookeeper 上移除。 监听 broker 列表的 Kafka 组件会被告知该 broker 已移除。
- 在关闭 broker 时, 它对应的节点也会消失, 不过它的 ID 会继续存在于其他数据结构中。 例如,主题的副本列表里就可能包含这些 ID。在完全关闭一 个 broker 之后, 如果使用相同的 ID 启动另一个全新的 broker, 它会立刻加入集群, 并拥有与旧 broker 相同的分区和主题。
2.什么是控制器
- 控制器其实就是一个 broker, 只不过它除了具有一般 broker 的功能之外, 还负责分区首领的选举。 集群里第一个启动的 broker 通过在 Zookeeper 里创建一个临时节点/controuer 让自己成为控制器。 其他 broker 在启动时也会尝试创建这个节点,不过它们会收到一个“节点已存在”的异常,然后“意 识”到控制器节点已存在, 也就是说集群里已经有一个控制器了 。 其他 broker 在控制器节点上创建 Zookeeperwatch 对象,这样它们就可以收到这个节点 的变更通知。这种方式可以确保集群里一次只有一个控制器存在。
- 如果控制器被关闭或者与 Zookeeper 断开连接, zookeeper 上的临时节点就会消失。 集群里的其他 broker 通过 watch 对象得到控制器节点消失的通 知, 它们会尝试让自己成为新的控制器。 第一个在 Zookeeper 里成功创建控制器节点的 broker 就会成为新的控制器, 其他节点会收到“节点已存在”的 异常,然后在新的控制器节点上再次创建 watch 对象。
- 当控制器发现一个 broker 已经离开集群,它就知道,那些失去首领的分区需要一个新首领 (这些分区的首领刚好是在这个 broker 上)。 控制器遍历这 些分区, 并确定谁应该成为新首领 (简单来说就是分区副本列表里的下一个副本) , 然后向所有包含新首领或现有跟随者的 broker 发送请求。该请求消息 包含了谁是新首领以及谁是分区跟随者的信息。随后,新首领开始处理来自生产者和消费者的请求,而跟随者开始从新首领那里复制消息。
- 当控制器发现一个 broker 加入集群时, 它会使用 broker ID 来检査新加入的 broker 是否包含现有分区的副本。 如果有, 控制器就把变更通知发送给 新加入的 broker 和其他 broker, 新 broker 上的副本开始从首领那里复制消息。
- 简而言之, Kafka 使用 Zookeeper 的临时节点来选举控制器,并在节点加入集群或退出集群时通知控制器。控制器负责在节点加入或离开集群时进行分区首领选举。
3.复制-Kafka 的核心
- 复制功能是 Kafka 架构的核心。在 Kafka 的文档里, Kafka 把自己描述成“一个分布式的、可分区的、可复制的提交日志服务”。复制之所以这么关键, 是因为它可以在个别节点失效时仍能保证 Kafka 的可用性和持久性。
- Kafka 使用主题来组织数据, 每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在 broker 上, 每个 broker 可以保存成百上千个属于 不同主题和分区的副本。
3.1replication-factor
- 用来设置主题的副本数。每个主题可以有多个副本,副本位于集群中不同的 broker 上,也就是说副本的数量不能超过 broker 的数量,否则创建主题 时会失败。
- 比如 partitions 设置为 2,replicationFactor 设置为 1. Broker 为 2 个的话,分区会均匀在
broker kafka-topics.bat --zookeeper localhost:2181/kafka --create --topic topicB --replication-factor 1 --partitions 2
- 比如 partitions 设置为 2,replicationFactor 设置为 2. Broker 为 2 个的话,每个 broker 都有副本存在
kafka-topics.bat --zookeeper localhost:2181/kafka --create --topic topicA --replication-factor 2 --partitions 2
3.2副本类型
- 首领副本
每个分区都有一个首领副本。为了保证一致性,所有生产者请求和消费者请求都会经过这个副本 。 - 跟随者副本
首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,它们唯一的任务就是从首领那里复制消息, 保持与首领一致的状态 。 如 果首领发生崩溃, 其中的一个跟随者会被提升为新首领 。 - 优先副本
除了当前首领之外, 每个分区都有一个优先副本(首选首领),创建主题时选定的首领分区就是分区的优先副本。 之所以把它叫作优先副本, 是因 为在创建分区时, 需要在 broker 之间均衡首领副本。 因此, 我们希望首选首领在成为真正的首领时, broker 间的负载最终会得到均衡。 默认情况下, Kafka 的 auto.leader.rebalance.enable 被设为 true,它会检査优先副本是不是当前首领,如果不是,并且该副本是同步副本, 那么就会触发首领选举, 让优先副本成为 当前首领。
3.3工作机制
- 首领的另一个任务是搞清楚哪个跟随者的状态与自己是一致的。跟随者为了保持与首领的状态一致,在有新消息到达时尝试从首领那里复制消息, 不 过有各种原因会导致同步失败。例如,网络拥塞导致复制变慢, broker 发生崩演导致复制滞后,直到重启 broker 后复制才会继续。
- 为了与首领保持同步, 跟随者向首领发送获取数据的请求, 这种请求与消费者为了读取消息而发送的请求是一样的。首领将响应消息发给跟随者。请 求消息里包含了跟随者想要获取消息的偏移量, 而且这些偏移量总是有序的 。
- 一个跟随者副本先请求消息 1,接着请求消息 2,然后请求消息 3,在收到这 3 个请求的响应之前,它是不会发送第 4 个请求消息的。如果跟随者发送了请 求消息 4,那么首领就知道它已经收到了前面 3 个请求的响应。 通过査看每个跟随者请求的最新偏移量, 首领就会知道每个跟随者复制的进度。如果跟随者在 10s 内没有请求任何消息,或者虽然在请求消息,但在 10s 内没有请求最新的数据,那么它就会被认为是不同步的。如果一个副本无法与首领保持一致, 在首领发生失效时,它就不可能成为新首领,因为它没有包含全部的消息。
- 相反,持续请求得到的最新消息副本被称为同步副本。在首领发生失效时,只有同步副本才有可能被选为新首领。
4.处理请求的内部机制
- broker 的大部分工作是处理客户端、分区副本和控制器发送给分区首领的请求。 Kafka 提供了一个二进制协议(基于 TCP),指定了请求消息的格式以及 broker 如何对请求作出响应——包括成功处理请求或在处理请求过程中遇到错误。
- 客户端发起连接并发送请求,broker 处理请求并作出响应。 broker 按照请求到达的顺序来处理它们这种顺序保证让 Kaka 具有了消息队列的特性,同时 保证保存的消息也是有序的。
- 所有的请求消息都包含一个标准消息头:
Request type(也就是 API key)
Request version( broker 可以处理不同版本的客户端请求,并根据客户端版本作出不同的响应)
Correlation id-一个具有唯一性的数字,用于标识请求消息,同时也会出现在响应消息和错误日志里(用于诊断问题)
Client Id 用于标识发送请求的客户端
broker 会在它所监听的每一个端口上运行一个 Acceptor 线程,这个线程会创建一个连接并把它交给 Processor 线程去处理。 Processor 线程(也被叫作 “网络线程”)的数量是可配置的。网络线程负责从客户端获取请求消息,把它们放进请求队列,然后从响应队列获取响应消息,把它们发送给客户端。
- 请求消息被放到请求队列后,IO 线程会负责处理它们。比较常见的请求类型有:
生产请求:生产者发送的请求,它包含客户端要写入 broker 的消息。
获取请求:在消费者和跟随者副本需要从 broker 读取消息时发送的请求。
- 生产请求和获取请求都必须发送给分区的首领副本。 如果 broker 收到一个针对特定分区的请求,而该分区的首领在另一个 broker 上,那么发送请求 的客户端会收到一个“非分区首领”的错误响应。当针对特定分区的获取请求被发送到一个不含有该分区首领的 broker 上,也会出现同样的错误。Kafka 客 户端要自己负责把生产请求和获取请求发送到正确的 broker 上。
- 那么客户端怎么知道该往哪里发送请求呢?客户端使用了另一种请求类型,也就是元数据请求。这种请求包含了客户端感兴趣的主题列表。服务器 端的响应消息里指明了这些主题所包含的分区、每个分区都有哪些副本,以及哪个副本是首领。元数据请求可以发送给任意一个 broker ,因为所有 broker 都缓存了这些信息。
- 一般情况下,客户端会把这些信息缓存起来,并直接往目标 broker 上发送生产请求和获取请求。它们需要时不时地通过发送元数据请求来刷新这些 信息(刷新的时间间隔通过 metadata.max.age.ms 参数来配置,2.1.3 的客户端默认参数 30S),从而知道元数据是否发生了变更, 比如,在新 broker 加入集群时,部分副本会被移动到新的 broker 上。另外,如果客户端收到“非首领”错误,它会在尝试重发请求之前先刷新元数据,因为这个错误说明了客 户端正在使用过期的元数据信息,之前的请求被发到了错误的 broker 上。
4.1生产请求
- acks 这个配置参数,该参数指定了需要多少个 broker 确认才可以认为一个消息写入是成功的。不同的配置对“写入成功”的界定是 不一样的,如果 acks=1,那么只要首领收到消息就认为写入成功;如果 acks=all,那么需要所有同步副本收到消息才算写入成功; 如果 acks=0, 那么生产者在把 消息发出去之后, 完全不需要等待 broker 的响应。
- 包含首领副本的 broker 在收到生产请求时, 会对请求做一些验证。
- 发送数据的用户是否有主题写入权限?
- 请求里包含的 acks 值是否有效(只允许出现 0、1 或 all) ?(ack=-1 等同于 ack=all)
- 如果 acks=all, 是否有足够多的同步副本保证消息已经被安全写入?
- 之后,消息被写入本地磁盘。在 Linux 系统上,消息会被写到文件系统缓存里,并不保证它们何时会被刷新到磁盘上。Kafka 不会一直等待数据被写到磁盘 上,它依赖复制功能来保证消息的持久性。
- 在消息被写入分区的首领之后, broker 开始检査 acks 配置参数一如果 acks 被设为 0 或 1, 那么 broker 立即返回响应;如果 acks 被设为 all,那么请求 会被保存在一个缓冲区里, 直到首领发现所有跟随者副本都复制了消息, 响应才会被返回给客户端。
4.2获取请求
- broker 处理获取请求的方式与处理生产请求的方式很相似。客户端发送请求,向 broker 请求主题分区里具有特定偏移量的消息, 好像在说: “请把主 题 Test 分区 0 偏移量从 53 开始的消息以及主题 Test 分区 3 偏移量从 64 开始的消息发给我。”客户端还可以指定 broker 最多可以从一个分区里返回多 少数据。 这个限制是非常重要的, 因为客户端需要为 broker 返回的数据分配足够的内存。 如果没有这个限制, broker 返回的大量数据有可能耗尽客户端 的内存。
- 请求需要先到达指定的分区首领上,然后客户端通过査询元数据来确保请求的路由是正确的。首领在收到请求时,它会先检査请求是否有效,比如,指定的偏移量在分区上是否存在?如果客户端请求的是已经被删除的数据,或者请求的偏移量不存在, 那么 broker 将返回一个错误。
- 如果请求的偏移量存在, broker 将按照客户端指定的数量上限从分区里读取消息, 再把消息返回给客户端。 Kafka 使用零复制技术向客户端发送消息 一一也就是说, Kafka 直接把消息从文件(或者更确切地说是 Linux 文件系统缓存)里发送到网络通道,而不需要经过任何中间缓冲区。 这是 Kafka 与其他大 部分数据库系统不一样的地方, 其他数据库在将数据发送给客户端之前会先把它们保存在本地缓存里。 这项技术避免了字节复制, 也不需要管理内存缓 冲区, 从而获得更好的性能。
- 客户端除了可以设置 broker 返回数据的上限, 也可以设置下限。 例如, 如果把下限设置为 10KB,就好像是在告诉 broker:“等到有 10KB 数据的时候再 把它们发送给我。”在主题消息流量不是很大的情况下,这样可以减少 CPU 和网络开销。 客户端发送一个请求, broker 等到有足够的数据时才把它们返回 给客户端, 然后客户端再发出情求, 而不是让客户端每隔几毫秒就发送一次请求,每次只能得到很少的数据甚至没有数据。对比这两种情况, 它们最终读取 的数据总量是一样的, 但前者的来回传送次数更少, 因此开销也更小。
- 当然,我们不会让客户端一直等待 broker 累积数据。在等待了一段时间之后,就可以把可用的数据拿回处理,而不是一直等待下去。所以,客户端可以定义一个超时时间,告诉 broker: “如果你无法在 K 毫秒内累积满足要求的数据量, 那么就把当前这些数据返回给我。”
4.3ISR
- 并不是所有保存在分区首领上的数据都可以被客户端读取。大部分客户端只能读取已经被写入所有同步副本的消息。 分区首领知道每个消息会被复 制到哪个副本上, 在消息还没有被写入所有同步副本之前, 是不会发送给消费者的,尝试获取这些消息的请求会得到空的响应而不是错误。
- 因为还没有被足够多副本复制的消息被认为是“不安全”的,如果首领发生崩横,另一 个副本成为新首领,那么这些消息就丢失了。如果我们允许消 费者读取这些消息,可能就会破坏一致性。试想, 一个消费者读取并处理了这样的一个消息,而另一个消费者发现这个消息其实并不存在。所以,我们会等到 所有同步副本复制了这些消息,才允许消费者读取它们。这也意味着,如果 broker 间的消息复制因为某些原因变慢,那么消息到达消费者的时间也会随之变长 (因为我们会先等待消息复制完毕) 。延迟时间可以通过参数 replica. lag. time. max. ms 来配置, 它指定了副本在复制消息时可被允许的最大延迟时间。
- Kafka 的数据复制是以 Partition 为单位的。而多个备份间的数据复制,通过 Follower 向 Leader 拉取数据完成。从一这点来讲,有点像 Master-Slave 方 案。不同的是,Kafka 既不是完全的同步复制,也不是完全的异步复制,而是基于 ISR 的动态复制方案。
- ISR,也即 In-Sync Replica。每个 Partition 的 Leader 都会维护这样一个列表,该列表中,包含了所有与之同步的 Replica(包含 Leader 自己)。每次数据写入时,只有 ISR 中的所有 Replica 都复制完,Leader 才会将其置为 Commit,它才能被 Consumer 所消费。
- 这种方案,与同步复制非常接近。但不同的是,这个 ISR 是由 Leader 动态维护的。如果 Follower 不能紧“跟上”Leader,它将被 Leader 从 ISR 中移除, 待它又重新“跟上”Leader 后,会被 Leader 再次加加 ISR 中。每次改变 ISR 后,Leader 都会将最新的 ISR 持久化到 Zookeeper 中。
- 至于如何判断某个 Follower 是否“跟上”Leader,不同版本的 Kafka 的策略稍微有些区别。
- 从 0.9.0.0 版本开始,replica.lag.max.messages 被移除,故 Leader 不再考虑 Follower 落后的消息条数。 另外,Leader 不仅会判断 Follower 是否在 replica.lag.time.max.ms 时间内向其发送 Fetch 请求,同时还会考虑 Follower 是否在该时间内与之保持同步。
示例
- 在第一步中,Leader A 总共收到 3 条消息,但由于 ISR 中的 Follower 只同步了第 1 条消息(m1),故只有 m1 被 Commit,也即只有 m1 可被 Consumer 消费。此时 Follower B 与 Leader A 的差距是 1,而 Follower C 与 Leader A 的差距是 2,虽然有消息的差距,但是满足同步副本的要求保留在 ISR 中。
- 在第二步中,由于旧的 Leader A 宕机,新的 Leader B 在 replica.lag.time.max.ms 时间内未收到来自 A 的 Fetch 请求,故将 A 从 ISR 中移除,此时 ISR={B, C}。同时,由于此时新的 Leader B 中只有 2 条消息,并未包含 m3(m3 从未被任何 Leader 所 Commit),所以 m3 无法被 Consumer 消费。
- 上图中就是因为 acks 不为 all 或者-1,不全部复制,就会导致单台服务器宕机时的数据丢失 m3 丢失了。
使用 ISR 方案的原因
- 由于 Leader 可移除不能及时与之同步的 Follower,故与同步复制相比可避免最慢的 Follower 拖慢整体速度,也即 ISR 提高了系统可用性。
- ISR 中的所有 Follower 都包含了所有 Commit 过的消息,而只有 Commit 过的消息才会被 Consumer 消费,故从 Consumer 的角度而言,ISR 中的所有 Replica 都始终处于同步状态,从而与异步复制方案相比提高了数据一致性。
ISR 相关配置说明
- Broker 的 min.insync.replicas 参数指定了 Broker 所要求的 ISR 最小长度,默认值为 1。也即极限情况下 ISR 可以只包含 Leader。但此时如果 Leader 宕 机,则该 Partition 不可用,可用性得不到保证。
- 只有被 ISR 中所有 Replica 同步的消息才被 Commit,但 Producer 发布数据时,Leader 并不需要 ISR 中的所有 Replica 同步该数据才确认收到数据。Producer 可以通过 acks 参数指定最少需要多少个 Replica 确认收到该消息才视为该消息发送成功。acks 的默认值是 1,即 Leader 收到该消息后立即告诉 Producer 收到该消息,此时如果在 ISR 中的消息复制完该消息前 Leader 宕机,那该条消息会丢失。而如果将该值设置为 0,则 Producer 发送完数据后,立即认为 该数据发送成功,不作任何等待,而实际上该数据可能发送失败,并且 Producer 的 Retry 机制将不生效。更推荐的做法是,将 acks 设置为 all 或者-1,此 时只有 ISR 中的所有 Replica 都收到该数据(也即该消息被 Commit),Leader 才会告诉 Producer 该消息发送成功,从而保证不会有未知的数据丢失。
5.物理存储机制
- Kafka 的基本存储单元是分区。分区无法在多个 broker 间进行再细分,也无法在同一个 broker 的多个磁盘上进行再细分。
- 在配置 Kafka的时候, 管理员指定了一个用于存储分区的目录清单——也就是log.dirs参数的值 (不要把它与存放错误日志的目录混淆了, 日志目录是 配置在1og4j.properties 文件里的)。 该参数一般会包含每个挂载点的目录。
5.1分区分配
- 在创建主题时, Kafka 首先会决定如何在 broker 间分配分区。假设你有 6 个 broker, 打算创建一个包含 10 个分区的主题,并且复制系数为 3(确保至少 有 3 台 broker)。那么 Kafka 就会有 30 个分区副本, 它们可以被分配给 6 个 broker。 在进行分区分配时, 我们要达到如下的目标。
- 在 broker 间平均地分布分区副本。对于我们的例子来说, 就是要保证每个 broker 可以分到 5 个副本。
- 确保每个分区的每个副本分布在不同的 broker 上。假设分区 0 的首领副本在 broker2 上,那么可以把跟随者副本放在 broker3 和 broker4 上, 但 不能放在 broker2 上,也不能两个都放在 broker3 上。
- 如果为 broker 指定了机架信息,那么尽可能把每个分区的副本分配到不同机架的 broker 上 。 这样做是为了保证一个机架的不可用不会导致整体 的分区不可用 。
- 为了实现这个目标, 我们先随机选择一个 broker(假设是 4) , 然后使用轮询的方式给每个 broker 分配分区来确定首领分区的位置。于是,首领分区 0 会在 broker4 上,首领分区 l 会在 broker5 上, 首领分区 2 会在 broker 0 上(只有 6 个 broker), 并以此类推。然后, 我们从分区首领开始,依次分配跟随者副 本。如果分区 0 的首领在 broker4 上,那么它的第一个跟随者副本会在 broker5 上,第二个跟随者副本会在 broker 0 上。分区 1 的首领在 broker5 上,那么它 的第一个跟随者副本在 broker0 上,第二个跟随者副本在 broker1 上。
- 为分区和副本选好合适的 broker 之后, 接下来要决定这些分区应该使用哪个目录。 我们单独为每个分区分配目录, 规则很简单: 计算每个目录里的 分区数量, 新的分区总是被添加到数量最小的那个目录里。 也就是说, 如果添加了一个新磁量, 所有新的分区都会被创建到这个磁盘上。因为在完成分配 工作之前,新磁盘的分区数量总是最少的。 (最少使用原则)
5.2文件管理
- 保留数据是 Kafka 的一个基本特性, Kafka 不会一直保留数据, 也不会等到所有消费者都读取了消息之后才删除消息。 相反, Kafka 管理员为每个主题 配置了数据保留期限, 规定数据被删除之前可以保留多长时间, 或者清理数据之前可以保留的数据量大小 。
- 因为在一个大文件里査找和删除消息是很费时的, 也很容易出错, 所以分区分成若干个片段。默认情况下,每个片段包含 1GB 或一周的数据,以较小的 那个为准。在 broker 往分区写入数据时,如果达到片段上限,就关闭当前文件,并打开一个新文件。
- 当前正在写入数据的片段叫作活跃片段。 活动片段永远不会被删除, 所以如果你要保留数据 1 天,但片段里包含了 5 天的数据,那么这些数据会被保留 5 天,因为在片段被关闭之前这些数据无法被删除。如果你要保留数据一周,而且每天使用一个新片段,那么你就会看到,每天在使用一个新片段的同时会删除 一个最老的片段一所以大部分时间该分区会有 7 个片段存在。
5.3文件格式
- Kafka 的消息和偏移量保存在文件里。保存在磁盘上的数据格式与从生产者发送过来或者发送给消费者的消息格式是一样的 。 因为使用了相同的消 息格式进行磁盘存储和网络传输, Kafka 可以使用零复制技术给消费者发送消息, 同时避免了对生产者已经压缩过的消息进行解压和再压缩。
- 除了键、值和偏移量外,消息里还包含了消息大小、校验和、消息格式版本号、压缩算法(snappy、 Gzip 或 Lz4)和时间戳(在 0.10.0 版本里引入的)。时 间戳可以是生产者发送消息的时间, 也可以是消息到达 broker 的时间, 这个是可配置的。
- 如果生产者发送的是压缩过的消息, 那么同一个批次的消息会被压缩在一起, 被当作 “包装消息”进行发送。于是, broker 就会收到一个这样的消息, 然后再把它发送给消费者。 消费者在解压这个消息之后, 会看到整个批次的消息, 它们都有自己的时间戳和偏移量。
- 如果在生产者端使用了压缩功能(极力推荐),那么发送的批次越大,就意味着在网络传输和磁盘存储方面会获得越好的压缩性能, 同时意味着如果修改 了消费者使用的消息格式 (例如, 在消息里增加了时间戳) , 那么网络传输和磁盘存储的格式也要随之修改, 而且 broker 要知道如何处理包含了两种消息 格式的文件。一种是普通消息,一种是包装消息。
- Kafka 附带了一个叫 DumpLogSegment 的工具, 可以用它査看片段的内容。 它可以显示每个消息的偏移量、校验和、魔术数字节、消息大小和压缩算法。
5.4索引
- 消费者可以从Kafka的任意可用偏移量位置开始读取消息。假设消费者要读取从偏移量100开始的1MB消息,那么 broker必须立即定位到偏移量100(可 能是在分区的任意一个片段里), 然后开始从这个位置读取消息。为了帮助 broker 更快地定位到指定的偏移量, Kafka 为每个分区维护了一个索引。索引把 偏移量映射到片段文件和偏移量在文件里的位置。
- 索引也被分成片段, 所以在删除消息时, 也可以删除相应的索引 。 Kafka 不维护索引的校验和。 如果索引出现损坏, Kafka 会通过重新读取消息并录 制偏移量和位置来重新生成索引。 如果有必要, 管理员可以删除索引, 这样做是绝对安全的, Kafka 会自动重新生成这些索引 。
5.5超时数据的清理机制
- 一般情况下, Kafka 会根据设置的时间保留数据,把超过时效的旧数据删除掉。不过,试想一下这样的场景,如果你使用 Kafka 保存客户的收货地址,那么保 存客户的最新地址比保存客户上周甚至去年的地址要有意义得多,这样你就不用担心会用错旧地址,而且短时间内客户也不会修改新地址。另外一个场景, 一个应用程序使用 Kafka 保存它的状态,每次状态发生变化,它就把状态写入 Kafka。在应用程序从崩溃中恢复时,它从 Kafka 读取消息来恢复最近的状态。 在这种情况下,应用程序只关心它在崩溃前的那个状态,而不关心运行过程中的那些状态。
- Kafka 通过改变主题的保留策略来满足这些使用场景 。 早于保留时间的事件会被删除, 为每个键保留最新的值, 从而达到清理的效果
- 每个日志片段可以分为以下两个部分 。
- **干净的部分,**这些消息之前被清理过, 每个键只有一个对应的值, 这个值是上一次清理时保留下来的。 **污浊的部分,**这些消息是在上一次清理之后 写入的。
- 为了清理分区, 清理线程会读取分区的污独部分, 并在内存里创建一个 map。 map 里的每个元素包含了消息键的散列值和消息的偏移量,键的散列值 是 16B,加上偏移量总共是 24B。如果要清理一个 1GB 的日志片段,并假设每个消息大小为 1KB,那么这个片段就包含_一百万个消息,而我们只需要用 24MB 的 map 就可以清理这个片段。 (如果有重复的键, 可以重用散列项, 从而使用更少的内存。)
- 清理线程在创建好偏移量 map 后,开始从干净的片段处读取消息,从最旧的消息开始,把它们的内容与 map 里的内容进行比对。它会检査消息的键是否 存在于 map 中, 如果不存在, 那么说明消息的值是最新的,就把消息复制到替換片段上。如果键已存在,消息会被忽略, 因为在分区的后部已经有一个具有 相同键的消息存在。在复制完所有的消息之后,我们就将替換片段与原始片段进行交换,然后开始清理下一个片段。完成整个清理过程之后,每个键对应一个 不同的消息一这些消息的值都是最新的。 清理前后的分区片段如图所示。
- 清理的思想就是根据 Key 的重复来进行整理,注意,它不是数据删除策略,而是类似于压缩策略,如果 key 送入了值,对于业务来说,key 的值应该 是最新的 value 才有意义,所以进行清理后只会保存一个 key 的最新的 value,这个适用于一些业务场景,比如说 key 代表用户 ID,Value 用户名称,如果使 用清理功能就能够达到最新的用户的名称的消息(这个功能有限,请参考使用)
6.可靠的数据传递
6.1Kafka 提供的可靠性保证和架构上的权衡
- 对于可靠性,一般使用保证这个词,它是指确保系统在各种不同的环境下能够发生一致的行为。
- ACID 大概是大家最熟悉的一个例子,它是关系型数据库普遍支持的标准可靠性保证。ACID 指的是原子性、一致性、隔离性和持久性。如果一个供应 商说他们的数据库遵循 ACID 规范,其实就是在说他们的数据库支持与事务相关的行为。
- 有了这些保证,我们才能相信关系型数据库的事务特性可以确保应用程序的安全。我们知道系统承诺可以做到些什么,也知道在不同条件下它们会 发生怎样的行为。我们了解这些保证机制,并基于这些保证机制开发安全的应用程序。
- 所以,了解系统的保证机制对于构建可靠的应用程序来说至关重要,这也是能够在不同条件下解释系统行为的前提。那么 Kafka 可以在哪些方面作 出保证呢?
- Kafka 可以保证分区消息的顺序。如果使用同一个生产者往同一个分区写入消息,而且消息 B 在消息 A 之后写入,那么 Kafka 可以保证消息 B 的 偏移量比消息 A 的偏移量大,而且消费者会先读取消息 A 再读取消息 B 。
- 只有当消息被写入分区的所有同步副本时(但不一定要写入磁盘),它才被认为是“ 已提交”的。生产者可以选择接收不同类型的确认,比如在 消息被完全提交时的确认,或者在消息被写入首领副本时的确认,或者在消息被发送到网络时的确认。
- 只要还有一个副本是活跃的,那么已经提交的消息就不会丢失。消费者只能读取已经提交的消息。
- 这些基本的保证机制可以用来构建可靠的系统,但仅仅依赖它们是无法保证系统完全可靠的。构建一个可靠的系统需要作出一些权衡, Kafka 管理 员和开发者可以在配置参数上作出权衡,从而得到他们想要达到的可靠性。这种权衡一般是指消息存储的可靠性和一致性的重要程度与可用性、高吞吐 量、低延迟和硬件成本的重要程度之间的权衡。
6.2复制
- Kafka 的复制机制和分区的多副本架构是 Kafka 可靠性保证的核心。把消息写入多个副本可以使 Kafka 在发生崩愤时仍能保证消息的持久性。
- Kafka 的主题被分为多个分区,分区是基本的数据块。分区存储在单个磁盘上, Kafka 可以保证分区里的事件是有序的,分区可以在线(可用), 也可以离线(不可用) 。每个分区可以有多个副本,其中一个副本是首领。所有的事件都直接发送给首领副本,或者直接从首领副本读取事件。其他副 本只需要与首领保持同步,并及时复制最新的事件。当首领副本不可用时,其中一个同步副本将成为新首领。
- 分区首领是同步副本,而对于跟随者副本来说,它需要满足以下条件才能被认为是同步的
- 与 Zoo keeper 之间有一个活跃的会话,也就是说,它在过去的 6 秒(可配置)内向 Zoo keeper 发送过心跳。
- 在过去的 10s 内(可配置)从首领那里获取过消息。
- 在过去的 10s 内从首领那里获取过最新的消息。光从首领那里获取消息是不够的,它还必须是几乎零延迟的。
- 如果跟随者副本不能满足以上任何一点,比如与 Zookeeper 断开连接,或者不再获取新消息,或者获取消息滞后了 10s 以上,那么它就被认为是不 同步的。一个不同步的副本通过与 Zookeeper 重新建立连接,并从首领那里获取最新消息,可以重新变成同步的。这个过程在网络出现临时问题并很快 得到修复的情况下会很快完成,但如果 broker 发生崩愤就需要较长的时间。
- 注意: 如果一个或多个副本在同步和非同步状态之间快速切换,说明集群内部出现了问题,通常是 Java 不恰当的垃圾回收配置导致的。不恰当的垃 圾回收配置会造成几秒钟的停顿,从而让 broker 与 Zoo keeper 之间断开连接,最后变成不同步的,进而发生状态切换。
7.Broker 配置对可靠性的影响
7.1复制系数
- 主题级别的配置参数是 replication.factor,而在 broker 级别则可以通过 default. replication.factor 来配置自动创建的主题。
- Kafka 的默认复制系数就是 3,不过用户可以修改它。
- 如果复制系数为 N,那么在1个 broker 失效的情况下,仍然能够从主题读取数据或向主题写入数据。所以,更高的复制系数会带来更高的可用性、 可靠性和更少的故障。另一方面,复制系数 N 需要至少 N 个 broker ,而且会有 N 个数据副本,也就是说它们会占用 N 倍的磁盘空间。我们一般会在可 用性和存储硬件之间作出权衡。
- 那么该如何确定一个主题需要几个副本呢?这要看主题的重要程度,以及你愿意付出多少成本来换取可用性。
- 如果因 broker 重启导致的主题不可用是可接受的(这在集群里是很正常的行为),那么把复制系数设为 1 就可以了。在作出这个权衡的时候,要确 保这样不会对你的组织和用户造成影响,因为你在节省了硬件成本的同时也降低了可用性。复制系数为 2 意味着可以容忍 1 个 broker 发生失效,看起来 已经足够了。不过要记住,有时候 1 个 broker 发生失效会导致集群不稳定(通常是旧版的 Kafka ),迫使你重启另一个 broker-一集群控制器。也就是 说,如果将复制系数设为 2 ,就有可能因为重启等问题导致集群不可用。
- 基于以上几点原因,在要求可用性的场景里把复制系数设为 3 。在大多数情况下,这已经足够安全了,不过要求更可靠时,可以设为更高,比如我 5 个副本,以防不测。
- 副本的分布也很重要。默认情况下, Kafka 会确保分区的每个副本被放在不同的 broker 上。不过,有时候这样仍然不够安全。如果这些 broker 处 于同一个机架上, 一旦机架的交换机发生故障,分区就会不可用,这时候把复制系数设为多少都不管用。为了避免机架级别的故障,我们建议把 broker 分布在多个不同的机架上。
7.2不完全的首领选举
- unclean.leader.election 只能在 broker 级别(实际上是在集群范围内)进行配置, 它的默认值是 true。
- 当分区首领不可用时, 一个同步副本会被选为新首领。如果在选举过程中没有丢失数据,也就是说提交的数据同时存在于所有的同步副本上,那么 这个选举就是“完全”的。
- 但如果在首领不可用时其他副本都是不同步的,我们该怎么办呢?
- 这种情况会在以下两种场景里出现。
- 分区有 3 个副本,其中的两个跟随者副本不可用(比如有两个 broker 发生崩溃)。这个时候,如果生产者继续往首领写入数据,所有消息都会得到确认并被提交(因为此时首领是唯一的同步副本)。现在我们假设首领也不可用了(又一个 broker 发生崩溃),这个时候,如果之前的一个跟随者重 新启动,它就成为了分区的唯一不同步副本。
- 分区有 3 个副本,因为网络问题导致两个跟随者副本复制消息滞后,所以尽管它们还在复制消息,但已经不同步了。首领作为唯一的同步副本继 续接收消息。这个时候,如果首领变为不可用,另外两个副本就再也无法变成同步的了。
- 对于这两种场景,我们要作出一个两难的选择。
- 如果不同步的副本不能被提升为新首领,那么分区在旧首领(最后一个同步副本)恢复之前是不可用的。有时候这种状态会持续数小时(比如更换 内存芯片)。
- 如果不同步的副本可以被提升为新首领,那么在这个副本变为不同步之后写入旧首领的消息、会全部丢失,导致数据不一致。为什么会这样呢? 假设在副本 0 和副本 1 不可用时,偏移量 100-200 的消息被写入副本 2 (首领)。现在副本 2 变为不可用的,而副本 0 变为可用的。副本 0 只包含偏 移量 0~ 100 的消息,不包含偏移量 100~ 200 的悄息。如果我们允许副本 0 成为新首领,生产者就可以继续写人数据,消费者可以继续读取数据。于 是,新首领就有了偏移量 100 ~200 的新消息。这样,部分消费者会读取到偏移量 100 ~200 的旧消息,部分消费者会读取到偏移量 100~200 的新消 息,还有部分消费者读取的是二者的混合。这样会导致非常不好的结果,比如生成不准确的报表。另外,副本 2 可能会重新变为可用,并成为新首领的 跟随者。这个时候,它会把比当前首领旧的消息全部删除,而这些消息对于所有消费者来说都是不可用的。
- 简而言之,如果我们允许不同步的副本成为首领,那么就要承担丢失数据和出现数据不一致的风险。如果不允许它们成为首领,那么就要接受较低 的可用性,因为我们必须等待原先的首领恢复到可用状态。
- 如果把 unclean.leader.election 设为 true ,就是允许不同步的副本成为首领(也就是“ 不完全的选举”),那么我们将面临丢失消息的风险。如果把这个参数设为 false ,就要等待原先的首领重新上线,从而降低了可用性。
- 我们经常看到一些对数据质量和数据一致性要求较高的系统会禁用这种不完全的首领选举( 把这个参数设为 false ) 。比如银行系统,大部分银行 系统宁愿选择在几分钟甚至几个小时内不处理信用卡支付事务,也不会冒险处理错误的消息。不过在对可用性要求较高的系统里,比如实时点击流分析 系统, 一般会启用不完全的首领选举。
7.3最少同步副本
- 在主题级别和 broker 级别上,这个参数都叫 min.insync.replicas。
- 我们知道,尽管为一个主题配置了 3 个副本,还是会出现只有一个同步副本的情况( acks=0 或 1)。如果这个同步副本变为不可用,我们必须在可 用性和一致性之间作出选择—这又是一个两难的选择。根据 Kafka 对可靠性保证的定义,消息只有在被写入到所有同步副本之后才被认为是已提交的。但 如果这里的“所有副本”只包含一个同步副本,那么在这个副本变为不可用时,数据就会丢失。
- 如果要确保已提交的数据被写入不止一个副本,就需要把最少同步副本数量设置为大一点的值。对于一个包含 3 个副本的主题,如果 min.insync.replicas 被设为 2 ,那么至少要存在两个同步副本才能向分区写入数据。
- 如果 3 个副本都是同步的,或者其中一个副本变为不可用,都不会有什么问题。不过,如果有两个副本变为不可用,那么 broker 就会停止接受生产 者的请求。尝试发送数据的生产者会收到 NotEnoughReplicasException 异常。消费者仍然可以继续读取已有的数据。实际上,如果使用这样的配置,那么 当只剩下一个同步副本时,它就变成只读了,这是为了避免在发生不完全选举时数据的写入和读取出现非预期的行为。为了从只读状态中恢复,必须让两个不可用分区中的一个重新变为可用的(比如重启 broker ),并等待它变为同步的。
8.可靠系统里的生产者
- 即使我们尽可能把 broker 配置得很可靠,但如果没有对生产者进行可靠性方面的配置, 整个系统仍然有可能出现突发性的数据丢失。
- 请看以下两个例子。
- 为 broker 配置了 3 个副本,并且禁用了不完全首领选举,这样应该可以保证万无一失。我们把生产者发送消息的 acks 设为 1 (只要首领接收到消 息就可以认为消息写入成功)。生产者发送一个消息给首领,首领成功写入,但跟随者副本还没有接收到这个消息。首领向生产者发送了一个响应,告 诉它“消息写入成功”,然后它崩溃了,而此时消息还没有被其他副本复制过去。另外两个副本此时仍然被认为是同步的(毕竟判断一个副本不同步需 要一小段时间),而且其中的一个副本成了新的首领。因为消息还没有被写入这个副本,所以就丢失了,但发送消息的客户端却认为消息已成功写入。因为消费者看不到丢失的消息,所以此时的系统仍然是一致的(因为副本没有收到这个消息,所以消息不算已提交),但从生产者角度来看,它丢失了 一个消息。
- 为 broker 配置了 3 个副本,并且禁用了不完全首领选举。我们接受了之前的教训, 把生产者的 acks 设为 all 。假设现在往 Kafka 发送消息,分 区的首领刚好崩溃,新的首领正在选举当中, Kafka 会向生产者返回“首领不可用”的响应。在这个时候,如果生产者没能正确处理这个错误,也没有 重试发送消息直到发送成功,那么消息也有可能丢失。这算不上是 broker 的可靠性问题,因为 broker 并没有收到这个消息。这也不是一致性问题,因为 消费者并没有读到这个消息。问题在于如果生产者没能正确处理这些错误,弄丢消息的是它们自己。
- 那么,我们该如何避免这些悲剧性的后果呢?从上面两个例子可以看出,每个使用 Kafka 的开发人员都要注意两件事情。
- 根据可靠性需求配置恰当的 acks 值。
- 在参数配置和代码里正确处理错误
8.1发送确认
- acks=0 意味着如果生产者能够通过网络把消息发送出去,那么就认为消息已成功写入 Kafka 。在这种情况下还是有可能发生错误,比如发送的对象 无法被序列化或者网卡发生故障,但如果是分区离线或整个集群长时间不可用,那就不会收到任何错误。即使是在发生完全首领选举的情况下,这种模 式仍然会丢失消息,因为在新首领选举过程中它并不知道首领已经不可用了。在 acks=0 模式下的运行速度是非常快的(这就是为什么很多基准测试都是 基于这个模式),你可以得到惊人的吞吐量和带宽利用率,不过如果选择了这种模式, 一定会丢失一些消息。
- acks=1 意味若首领在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应。在这个模式下,如果发生正常的首领 选举,生产者会在选举时收到一个 LeadeNotAvailableExcepti.on 异常,如果生产者能恰当地处理这个错误,它会重试发送消息,最终消息会安全到达新的 首领那里。不过在这个模式下仍然有可能丢失数据,比如消息已经成功写入首领,但在消息被复制到跟随者副本之前首领发生崩溃。
- acks=all 意味着首领在返回确认或错误响应之前,会等待所有同步副本都收到悄息。如果和 min.insync.replicas 参数结合起来,就可以决定在返回确认 前至少有多少个副本能够收到消息。这是最保险的做法——生产者会一直重试直到消息被成功提交。 不过这也是最慢的做法,生产者在继续发送其他消 息之前需要等待所有副本都收到当前的消息。可以通过使用异步模式和更大的批次来加快速度,但这样做通常会降低吞吐量。
8.2配置生产者的重试参数
- 生产者需要处理的错误包括两部分: 一部分是生产者可以自动处理的错误,还有一部分是需要开发者手动处理的错误。
- 如果 broker 返回的错误可以通过重试来解决,那么生产者会自动处理这些错误。生产者向 broker 发送消息时, broker 可以返回一个成功响应码或 者一个错误响应码。错民响应码可以分为两种, 一种是在重试之后可以解决的,还有一种是无法通过重试解决的。例如,如果 broker 返回的是 LEADER_NOT_AVAILABLE 错误,生产者可以尝试重新发送消息。也许在这个时候一个新的首领被选举出来了,那么这次发送就会成功。也就是说, LEADER_NOT_AVAILABLE 是一个可重试错误。
- 另一方面,如果 broker 返回的是 INVALID_CONFIG 错误,即使通过重试也无能改变配置选项,所以这样的重试是没有意义的。这种错误是不可重试 错误。
- 一般情况下,如果你的目标是不丢失任何消息,那么最好让生产者在遇到可重试错误时能够保持重试。为什么要这样?因为像首领选举或网络连接 这类问题都可以在几秒钟之内得到解决,如果让生产者保持重试,就不需要额外去处理这些问题了。
- 那么为生产者配置多少重试次数比较好?这个要看在生产者放弃重试并抛出异常之后想做些什么。如果你想抓住异常并再多重试几次,那么就可以 把重试次数设置得多一点, 让生产者继续重试;如果你想直接丢弃消息,多次重试造成的延迟已经失去发送消息的意义;如果你想把消息保存到某个地 方然后回过头来再继续处理,那就可以停止重试。
- Kafka 的跨数据中心复制工具( MirrorMaker)默认会进行无限制的重试。作为一个具有高可靠性的复制工具,它决不会丢失消息。
- 要注意,重试发送一个已经失败的消息会带来一些风险,如果两个消息都写入成功,会导致消息重复。例如,生产者因为网络问题没有收到 broker 的确认,但实际上消息已经写入成功,生产者会认为网络出现了临时故障,就重试发送该消息(因为它不知道消息已经写入成功)。在这种情况下,broker 会收到两个相同的消息。重试和恰当的错误处理可以保证每个消息“至少被保存一次”,但无法保证每个消息“只被保存一次”。现实中的很多应用程 序在消息里加入唯一标识符,用于检测重复消息,消费者在读取消息时可以对它们进行清理。还要一些应用程序可以做到消息的“幂等”,也就是说, 即使出现了重复消息,也不会对处理结果的正确性造成负面影响。
8.3额外的错误处理
- 使用生产者内置的重试机制可以在不造成消息丢失的情况下轻松地处理大部分错误,不过对于开发人员来说,仍然需要处理其他类型的错误,包括:
- 不可重试的 broker 错误,例如消息大小错误、认证错误等 . 在消息发送之前发生的错误,例如序列化错误;
- 在生产者达到重试次数上限时或者在消息占用的内存达到上限时发生的错误。
- 错误处理器的代码逻辑与具体的应用程序及其目标有关。丢弃“不合理的消息”?把错误记录下来?把这些消息保存在本地磁盘上?具体使用哪一 种逻辑要根据具体的架构来决定。如果错误处理只是为了重试发送消息,那么最好还是使用生产者内置的重试机制。
9.可靠系统里的消费者
- 可以看到,只有那些被提交到 Kafka 的数据,也就是那些已经被写入所有同步副本的数据,对消费者是可用的,这意味着消费者得到的消息已经具 备了一致性。
- 消费者唯一要做的是跟踪哪些消息是已经读取过的,哪些是还没有读取过的。这是在读取消息时不丢失消息的关键。
- 在从分区读取数据时,消费者会获取一批事件,检查这批事件里最大的偏移量,然后从这个偏移量开始读取另外一批事件。这样可以保证消费者总 能以正确的顺序获取新数据, 不会错过任何事件。
- 如果一个消费者退出,另一个消费者需要知道从什么地方开始继续处理,它需要知道前一个消费者在退出前处理的最后一个偏移量是多少。所谓的 “另一个”消费者,也可能就是它自己重启之后重新回来工作。这也就是为什么消费者要“提交”它们的偏移量。它们把当前读取的偏移量保存起来, 在退出之后,同一个群组里的其他消费者就可以接手它们的工作。如果消费者提交了偏移量却未能处理完消息,那么就有可能造成消息丢失,这也是消 费者丢失消息的主要原因。在这种情况下,如果其他消费者接手了工作,那些没有被处理完的消息就会被忽略,永远得不到处理。所以我们要重视偏移 量提交的时间点和提交的方式。
9.1消费者的可靠性配置
- 为了保证消费者行为的可靠性,需要注意以下 4 个非常重要的配置参数。
- 第 1 个是 group.id 。如果两个消费者具有相同的 group.id , 并且订阅了同一个主题,那么每个消费者会分到主题分区的一个子集, 也就是说它们 只能读到所有消息的一个子集(不过群组会读取主题所有的消息)。如果你希望消费者可以看到主题的所有消息,那么需要为它们设置唯一的 group.id 。
- 第 2 个是 auto.offset.reset 。这个参数指定了在没有偏移量可提交时(比如消费者第 1 次启动时)或者请求的偏移量在 broker 上不存在时,消费者 会做些什么。这个参数有两种配置。一种是 earliest ,如果选择了这种配置,消费者会从分区的开始位置读取数据,不管偏移量是否有效,这样会导致消 费者读取大量的重复数据,但可以保证最少的数据丢失。一种是 latest,如果选择了这种配置, 消费者会从分区的末尾开始读取数据,这样可以减少重 复处理消息,但很有可能会错过一些消息。
- 第 3 个是 enable.auto.commit 。这是一个非常重要的配置参数,你可以让消费者基于任务调度自动提交偏移量,也可以在代码里手动提交偏移量。 自动提交的一个最大好处是,在实现消费者逻辑时可以少考虑一些问题。如果你在消费者轮询操作里处理所有的数据,那么自动提交可以保证只提交已 经处理过的偏移量。自动提交的主要缺点是,无法控制重复处理消息(比如消费者在自动提交偏移量之前停止处理消息),而且如果把消息交给另外一 个后台线程去处理,自动提交机制可能会在消息还没有处理完毕就提交偏移量。
- 第 4 个配置参数 auto.commit.interval.ms 与第 3 个参数有直接的联系。如果选择了自动提交偏移盘,可以通过该参数配置提交的频度, 默认值是每 5 秒钟提交一次。一般来说,频繁提交会增加额外的开销,但也会降低重复处理消息的概率。
9.2显式提交偏移量
- 如果选择了自动提交偏移量,就不需要关心显式提交的问题。不过如果希望能够更多地控制偏移量提交的时间点,那么就要仔细想想该如何提交偏 移量了一一要么是为了减少重复处理消息,要么是因为把消息处理逻辑放在了轮询之外。
- 在开发具有可靠性的消费者应用程序时需要注意的事项。
- 总是在处理完事件后再提交偏移量
如果所有的处理都是在轮询里完成,而且消息处理总是幂等的,或者少量消息丢失无关紧要, 那么可以使用自动提交,或者在轮询结束时进行手动 提交。 - 提交频度是性能和重复消息数量之间的权衡
即使是在最简单的场景里,比如所有的处理都在轮询里完成,并且不需要在轮询之间维护状态,你仍然可以在一个循环里多次提交偏移量(甚至可 以在每处理完一个事件之后),或者多个循环里只提交一次,这完全取决于你在性能和重复处理消息之间作出的权衡。 - 确保对提交的偏移量心里有数
在轮询过程中提交偏移量有一个不好的地方,就是提交的偏移量有可能是读取到的最新偏移量,而不是处理过的最新偏移量。要记住,在处理完消 息后再提交偏移量是非常关键的,否则会导致消费者错过消息。 - 再均衡
在设计应用程序时要注意处理消费者的再均衡问题。一般要在分区被撤销之前提交偏移量,并在分配到新分区时清理之前的状态。 - 消费者可能需要重试
有时候,在进行轮询之后,有些消息不会被完全处理,可能稍后再来处理。例如,假设要把 Kafka 的数据写到数据库里,不过那个时候数据库不可 用,于是你想稍后重试。要注意,你提交的是偏移量,而不是对消息的“确认”,这个与传统的发布和订阅消息系统不太一样。如果记录的#30 处理失 败,但记录的#31 处理成功,那么你不应该提交#31, 否则会导致的#31 以内的偏移量都被提交,包括的#30 在内。不过可以采用下面这种模式来解决这 个问题。
在遇到可重试错误时,把错误写入一个独立的主题,然后继续。一个独立的消费者群组负责从该主题上读取错误消息,并进行重试,或者使用其中 的一个消费者同时从该主题上读取错误消息并进行重试,不过在重试时需要暂停该主题。这种模式有点像其他消息系统里的死信队列 。 - 消费者可能需要维护状态
有时候你希望在多个轮询之间维护状态,例如,你想计算消息的移动平均数,希望在首次轮询之后计算平均数,然后在后续的轮询中更新这个结果。 如果进程重启,你不仅需要从上一个偏移量开始处理数据,还要恢复移动平均数。有一种办法是在提交偏移量的同时把最近计算的平均数写到一个“结 果”主题上。消费者线程在重新启动之后,它就可以拿到最近的平均数并接着计算。不过这并不能完全地解决问题,因为 Kafka 并没有提供事务支持。 消费者有可能在写入平均数之后来不及提交偏移量就崩溃了,或者反过来也一样。这是一个很复杂的问题,你不应该尝试自己去解决这个问题,建议尝 试一下 Kafka 流计算,它为聚合、连接、时间窗和其他复杂的分析提供了高级的 API 。 - 长时间处理
有时候处理数据需要很长时间:你可能会从发生阻塞的外部系统获取信息,或者把数据写到外部系统,或者进行一个非常复杂的计算,但是我们要尽量保持轮询。在这种情况下, 一种常见的做法是使用一个线程来处理数据,因为使用多个线程可以进行并行处理,从而加快处理速度。在把数据移交给线程去处理之后,你就可以暂停消费者,然后保持轮询,但不获取新数据,直到工作线程处理完成。在工作线程处理完成之后,可以让消费者继 续获取新数据。 - 仅一次传递
有些应用程序不仅仅需要“至少一次”(意味着没有数据丢失),还需要“仅一次”语义。Kafka 现在还不能完全支持仅一次语义,消费者还是有一 些办法可以保证 Kafka 里的每个消息只被写到外部系统一次(但不会处理向 Kafka 写入数据时可能出现的重复数据) 。
实现仅一次处理最简单且最常用的办能是把结果写到一个支持唯一键的系统里,比如键值存储引擎、关系型数据库、ElasticSearch 或其他数据存储引 擎。在这种情况下,要么消息本身包含一个唯一键(通常都是这样),要么使用主题、分区和偏移量的组合来创建唯一键一一-它们的组合可以唯一标 识一个 Kafka 记录。如果你把消息和一个唯一键写入系统,然后碰巧又读到一个相同的消息,只要把原先的键值覆盖掉即可。数据存储引擎会覆盖已经 存在的键值对,就像没有出现过重复数据一样。这个模式被叫作幂等性写入,它是一种很常见也很有用的模式。
如果写入消息的系统支持事务, 那么就可以使用另一种方法。最简单的是使用关系型数据库。我们把消息和偏移量放在同一个事务里,这样它们就 能保持同步。在消费者启动时,它会获取最近处理过的消息偏移量,然后调用 seek ()方也从该偏移量位置继续读取数据。