kafka的设计以及如何保证可靠性

Push vs. pull

最初我们考虑的问题是:究竟是由 consumer 从 broker 那里 pull 数据,还是由 broker 将数据 push 到 consumer。Kafka 在这方面采取了一种较为传统的设计方式,也是大多数的消息系统所共享的方式:即 producer 把数据 push 到 broker,然后 consumer 从 broker 中 pull 数据。 也有一些 logging-centric 的系统,比如 ScribeApache Flume,沿着一条完全不同的 push-based 的路径,将数据 push 到下游节点。这两种方法都有优缺点。然而,由于 broker 控制着数据传输速率, 所以 push-based 系统很难处理不同的 consumer。让 broker 控制数据传输速率主要是为了让 consumer 能够以可能的最大速率消费;不幸的是,这导致着在 push-based 的系统中,当消费速率低于生产速率时,consumer 往往会不堪重负(本质上类似于拒绝服务攻击)。pull-based 系统有一个很好的特性, 那就是当 consumer 速率落后于 producer 时,可以在适当的时间赶上来。还可以通过使用某种 backoff 协议来减少这种现象:即 consumer 可以通过 backoff 表示它已经不堪重负了,然而通过获得负载情况来充分使用 consumer(但永远不超载)这一方式实现起来比它看起来更棘手。前面以这种方式构建系统的尝试,引导着 Kafka 走向了更传统的 pull 模型。

另一个 pull-based 系统的优点在于:它可以大批量生产要发送给 consumer 的数据。而 push-based 系统必须选择立即发送请求或者积累更多的数据,然后在不知道下游的 consumer 能否立即处理它的情况下发送这些数据。如果系统调整为低延迟状态,这就会导致一次只发送一条消息,以至于传输的数据不再被缓冲,这种方式是极度浪费的。 而 pull-based 的设计修复了该问题,因为 consumer 总是将所有可用的(或者达到配置的最大长度)消息 pull 到 log 当前位置的后面,从而使得数据能够得到最佳的处理而不会引入不必要的延迟。

简单的 pull-based 系统的不足之处在于:如果 broker 中没有数据,consumer 可能会在一个紧密的循环中结束轮询,实际上 busy-waiting 直到数据到来。为了避免 busy-waiting,我们在 pull 请求中加入参数,使得 consumer 在一个“long pull”中阻塞等待,直到数据到来(还可以选择等待给定字节长度的数据来确保传输长度)。

消费者的位置

如果 broker 在每条消息被发送到网络的时候,立即将其标记为 consumed,那么一旦 consumer 无法处理该消息(可能由 consumer 崩溃或者请求超时或者其他原因导致),该消息就会丢失。 为了解决消息丢失的问题,许多消息系统增加了确认机制:即当消息被发送出去的时候,消息仅被标记为sent 而不是 consumed;然后 broker 会等待一个来自 consumer 的特定确认,再将消息标记为consumed。这个策略修复了消息丢失的问题,但也产生了新问题。 首先,如果 consumer 处理了消息但在发送确认之前出错了,那么该消息就会被消费两次。第二个是关于性能的,现在 broker 必须为每条消息保存多个状态(首先对其加锁,确保该消息只被发送一次,然后将其永久的标记为 consumed,以便将其移除)。 还有更棘手的问题要处理,比如如何处理已经发送但一直得不到确认的消息。

Kafka 使用完全不同的方式解决消息丢失问题。Kafka的 topic 被分割成了一组完全有序的 partition,其中每一个 partition 在任意给定的时间内只能被每个订阅了这个 topic 的 consumer 组中的一个 consumer 消费。这意味着 partition 中 每一个 consumer 的位置仅仅是一个数字,即下一条要消费的消息的offset。这使得被消费的消息的状态信息相当少,每个 partition 只需要一个数字。这个状态信息还可以作为周期性的 checkpoint。这以非常低的代价实现了和消息确认机制等同的效果。

这种方式还有一个附加的好处。consumer 可以回退到之前的 offset 来再次消费之前的数据,这个操作违反了队列的基本原则,但事实证明对大多数 consumer 来说这是一个必不可少的特性。 例如,如果 consumer 的代码有 bug,并且在 bug 被发现前已经有一部分数据被消费了, 那么 consumer 可以在 bug 修复后通过回退到之前的 offset 来再次消费这些数据。

  • 消费端自动提交带来的重复消息主要原因是提交不及时,假设我们仍然使用默认的 5s提交时间间隔,在最近一次提交之后的 3s发生了再均衡,再 均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后 了 3s,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复消息的时间窗,不过这种情况是无法完全避免的 。

    • kafka手动提交当前偏移量,取消自动提交,把 auto.commit.offset 设为 false
  • 消费端commitSync()同步提交:同步提交有一个不足之处,在 broker对提交请求作出回应之前,应用程序会一直阻塞,这样会限制应用程序的吞吐量。我们可以通过降低提交频率来提升吞吐量,但如果发生了再均衡, 会增加重复消息的数量。

  • 消费端commitAsync()异步提交:它之所以不进行重试,是因为在它收到 服务器响应的时候,可能有一个更大的偏移量已经提交成功。假设我们发出一个请求用于提交偏移量 2000,这个时候发生了短暂的通信问题 ,服务器收不到请求,自然也不会 作出任何响应。与此同时,我们处理了另外一批消息,并成功提交了偏移量 3000。如果 commitAsync() 重新尝试提交偏移量 2000,它有可能在偏移量 3000之后提交成功。这个时 候如果发生再均衡,就会出现重复消息。

消息交付语义

  • 至少一次语义:如果生产者收到了 Kafka broker 的确认,并且生产者的acks配置项设置为all(或-1),这就意味着消息已经被精确一次写入 Kafka topic 了。然而,如果生产者接收ack超时或者收到了错误,它就会认为消息没有写入 Kafka topic 而尝试重新发送消息。如果broker恰好在消息已经成功写入 Kafka topic 后,发送ack前,出了故障,生产者的重试机制就会导致这条消息被写入 Kafka 两次,从而导致同样的消息会被消费者消费不止一次。

  • 至多一次语义:如果生产者在ack超时或者返回错误的时候不重试发送消息,那么消息有可能最终并没有写入 Kafka topic 中,因此也就不会被消费者消费到。但是为了避免重复处理的可能性,我们需要接受有些消息可能被遗漏的情况。

  • 精确一次语义:即使生产者重试发送消息,也只会让消息被发送给消费者一次。精确一次语义是最令人满意的保证,但也是最难实现的。因为它需要消息系统本身和生产消息的应用程序还有消费消息的应用程序一起合作。比如,在成功消费一条消息后,你又把消费的offset重置到之前的某个offset位置,那么你将收到从那个offset到最新的offset之间的所有消息。这解释了为什么消息系统和客户端程序必须合作来保证精确一次语义。

    • 幂等性:每个分区中精确一次且有序

      一个幂等性的操作就是一种被执行多次造成的影响和只执行一次造成的影响一样的操作。现在生产者发送的操作是幂等的。如果出现导致生产者重试的错误,同样的消息,仍由同样的生产者发送多次,将只被写到 Kafka broker 的日志中一次。对于单个分区,幂等生产者不会因为生产者或broker故障而发送多条重复消息。想要开启这个特性,获得每个分区内的精确一次语义,也就是说没有重复,没有丢失,并且有序的语义,只需要设置生产者配置中的enable.idempotence=true

      这个特性是怎么实现的呢?在底层,它和 TCP 的工作原理有点像,每一批发送到 Kafka 的消息都将包含一个序列号,broker将使用这个序列号来删除重复的发送。和只能在瞬态内存中的连接中保证不重复的 TCP 不同,这个序列号被持久化到副本日志,所以,即使分区的leader挂了,其他的broker接管了leader,新leader仍可以判断重新发送的是否重复了。这种机制的开销非常低:每批消息只有几个额外的字段。

必须被处理的故障

  • broker可能发生故障:Kafka 是一个高可用、持久化的系统,每一条写入一个分区的消息都会被持久化并且多副本备份(假设有n个副本)。所以,Kafka 可以容忍n-1broker故障,意味着一个分区只要至少有一个broker可用,分区就可用。Kafka 的副本协议保证了只要消息被成功写入了主副本,它就会被复制到其他所有的可用副本。
  • 生产者到broker的 RPC 调用可能失败:Kafka 的持久性依赖于生产者接收brokerack。没有接收成功ack不代表生产请求本身失败了。broker可能在写入消息后,发送ack给生产者的时候挂了。甚至broker也可能在写入消息前就挂了。由于生产者没有办法知道错误是什么造成的,所以它就只能认为消息没写入成功,并且会重试发送。在一些情况下,这会造成同样的消息在 Kafka 分区日志中重复,进而造成消费端多次收到这条消息。
  • 客户端可能发生故障:精确一次传递也必须考虑客户端故障。但是我们如何知道一个客户端已经故障而不是暂时和broker断开,或者经历一个程序短暂的暂停?区分永久性故障和临时故障是很重要的,为了正确性,broker应该丢弃僵住的生产者发送来的消息,同样,也应该不向已经僵住的消费者发送消息。一旦一个新的客户端实例启动,它应该能够从失败的实例留下的任何状态中恢复,从一个安全点开始处理。这意味着,消费的偏移量必须始终与生产的输出保持同步。
  • Unclean leader 选举: 如果节点全挂了:这里有两种实现的方法
    • 等待一个 ISR 的副本重新恢复正常服务,并选择这个副本作为领 leader (它有极大可能拥有全部数据)。-- 可以通过配置属性 unclean.leader.election.enable 禁用此策略,采取此措施
    • 选择第一个重新恢复正常服务的副本(不一定是 ISR 中的)作为leader。 – 默认

kafka再均衡(Rebalance)

一个主题可以有多个分区,而订阅该主题的消费组中可以有多个消费者。每一个分区只能被消费组中的一个消费者消费,可认为每个分区的消费权只属于消费组中的一个消费者。例如消费者会宕机,还有新的消费者会加入,而为了应对这些变化,让分区所属权的分配合理,这都需要对分区所属权进行调整,也就是所谓的“再均衡”。

kafka零拷贝

零拷贝技术减少了用户态与内核态之间的切换,让拷贝次数降到最低,从而实现高性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值