kafka官方文档翻译-design

1.动机

我们设计的Kafka能够作为统一的平台来处理大型公司可能拥有的所有实时数据。为了做到这一点,我们不得不考虑一系列相当广泛的用例。
它必须具有高吞吐量来支持大量事件流,例如实时日志聚合。
它将需要正常处理大量数据积压,以便能够支持从离线系统定期进行数据加载。
这也意味着系统必须处理低延迟传递来处理更传统的消息传递用例。
我们希望支持可分区的,分布式的,实时的数据反馈处理,并创建和分发新的反馈。
最后,在将流发送到其他数据系统进行服务的情况下,我们知道系统必须能够在存在机器故障的情况下保证容错。
支持这些使用导致我们进行了一系列独特的元素的设计,比传统的消息系统更像数据库日志。 我们将在以下部分中概述设计的一些要素。
2.持久化
不要担心文件系统!
kafka严重依赖文件系统来存储和缓存消息。
人们普遍认为“磁盘是缓慢的”,这使得人们怀疑一个持久化结构能否提供有竞争力的性能。事实上,磁盘速度的快慢取决于它们的使用方式;一个设计合理的磁盘结构通常可以和网络一样快。
关于磁盘性能的关键事实是,在过去十年中,硬盘驱动器的吞吐量与磁盘寻址的延迟就无关了(磁盘寻址数据已经达到极致)。因此,使用六个7200rpm SATA RAID-5阵列的JBOD配置上的线性写入性能约为600MB /秒,但是随机写入的性能只有大约100k/sec,超过6000X的差异。这些线性读取和写入是所有使用模式中最可预测的,并且由操作系统进行了大量优化。现代操作系统提供预读和写入技术,它们以大块多重预取数据,并将较小的逻辑写入组合成大型物理写入。在这个ACM队列文章中可以找到关于这个问题的进一步讨论。他们实际上发现顺序磁盘访问在某些情况下比随机内存访问速度更快!
为了弥补这种性能差异,现代操作系统在使用主存储器进行磁盘缓存时变得越来越激进。当内存被回收时,现代操作系统很乐意将所有可用内存转移到磁盘缓存,虽然性能会降低一点。所有磁盘读写都将通过这个统一缓存。除非使用直接 I/O,否则此功能不能轻易关闭,因此即使一个进程维护一个进程内部数据缓存,这些数据也可能在操作系统页面缓存中进行复制,从而有效地存储两次。
此外,我们正在建立在JVM之上,任何花费任何时间使用Java内存使用的人都知道两件事情:        
     1.对象的内存开销非常高,通常将数据的大小加倍(或更差)。
     2.随着堆内数据的增加,Java垃圾收集变得越来越繁琐和缓慢。
由于这些因素(使用文件系统和依赖pagecache)优于维护内存中缓存或其他结构,因此通过自动访问所有可用内存,至少可以使可用缓存增加一倍,并且可能通过存储紧凑的字节结构而不是单独的对象再次加倍。这样做会导致在32GB机器上缓存高达28-30 GB,而不受GC惩罚。此外,即使重新启动服务,此缓存也将保持温和,而进程内缓存将需要在内存中重建(对于10GB缓存可能需要10分钟),否则将需要从完全冷却缓存开始 (这可能意味着可怕的初始性能)。这也大大简化了代码,因为在缓存和文件系统之间保持一致性的所有逻辑现在都在OS中,这往往比一次性进程中的尝试更有效和更正确地执行。 如果您的磁盘使用偏向于线性读取,那么每次磁盘读取时,预读将有效地预填充此缓存。
这表明一个非常简单的设计:相对于在内存中维护尽可能多的内存,并在空间不足时将其全部刷新到文件系统,我们反过来做。 所有数据立即写入文件系统上的持久化日志,而不必刷新到磁盘。 实际上这只是意味着它被转移到内核的页面缓存中。
满足常量时间
用于消息系统的持久化数据结构通常是一个消费者队列,这些消息消费者队列与BTree或者其他一般用途随机访问数据结构关联,以维护关于消息的元数据。BTree是最通用的数据结构,在消息系统中可以支持各种事务性和非事务性语义。它们的成本相当高,虽然Btree操作是O(log N),通常认为O(log N)基本上等同于常量时间,但对于磁盘操作来说,这是不正确的。磁盘每次寻址需要10毫秒,每个磁盘一次只能执行一次寻址,因此并行性是有限的。因此,即使是少量的磁盘寻址也会导致很高的开销。由于存储系统将非常快的缓存操作与非常慢的物理磁盘操作混合在一起,随着数据增加(缓存大小固定),观察到的树结构性能通常是超线性的--也就是说,你的数据翻倍会让事情变得比原来慢了两倍还多。

亚线性

超线性

直观地,持久化队列可以建立在简单的读取和追加到文件上,就像普通的日志解决方案。这种结构的优点是所有的操作都是O(1),并且读和写彼此互不阻塞。 这具有明显的性能优势,因为性能与数据大小完全分离 - 一个服务器现在可以充分利用一些便宜,低转速的1 + TB SATA驱动器。虽然这些驱动器的寻址性能不佳,但是对于大量的读写而言,这些驱动器具有可接受的性能,并且1/3的价格,3倍的容量。
无需任何性能损失就可以访问几乎无限的磁盘空间,这意味着我们可以提供消息系统中通常没有的一些功能。例如,在Kafka,替代消息一消费就马上尝试删除,我们可以保留消息相对较长的时间(例如一个星期)。这将为消费者带来很大的灵活性,正如我们将要描述的那样。
3.效率
我们在效率上付出了很大的努力。我们的主要用例之一是处理Web活动数据,这是非常大容量的数据:每个页面视图可能会生成数十个写入。此外,我们假设每个发布的消息都是由至少一个消费者(通常是多个)读取的,因此我们努力使消费尽可能便宜。
我们还发现,从建立和运行多个类似系统的经验来看,效率是有效的多租户操作的关键。如果下游基础设施服务由于应用程序使用中产生小的碰撞而容易成为瓶颈,那么这种小的更改常常会产生问题。通过非常快的速度,我们帮助确保应用程序将在基础设施之前被加载。这对于在集中式集群中运行支持数十或数百个应用程序的集中服务时尤为重要,因为使用模式的变化几乎每天都在发生。
我们讨论了上一节中的磁盘效率。一旦不良的磁盘访问模式被消除,这种类型的系统有两个低效率的常见原因:太多小的I/O操作和过多的字节复制。
在客户端和服务器之间以及服务器自身的持久化操作中都会发生小的I/O问题。
为了避免这种情况,我们的协议是围绕一个“消息集”抽象构建的,它可以自然地将消息分组。这允许网络请求将消息分组并分摊网络往返的开销,而不是一次发送一条消息。服务器依次将大量的消息附加到它的日志中,而消费者一次就能获取大的线性块。
这种简单的优化产生了数量级的加速。 批处理导致更大的网络数据包,更大的顺序磁盘操作,连续的内存块等,这些都允许Kafka将一串连续的随机消息写入流向消费者的线性写入流中。
另一个低效率是字节复制。在低消息率下,这不是一个问题,但在负载下,影响是显著的。为了避免这种情况,我们使用了一个标准化的二进制消息格式,由生产者、代理和使用者共享(因此数据块可以在不修改的情况下传输)。
由Broker维护的消息日志本身只是一个文件目录,每个文件都由一系列以相同格式(这种格式被生产者和消费者共同使用)写入磁盘的消息集填充。 维护这种通用格式允许优化最重要的操作:网络传输持久化日志块。 现代unix操作系统提供了高度优化的代码路径,用于将数据从页面缓存传输到套接字; 在Linux中,这是通过sendfile系统调用完成的。
要了解sendfile的影响,了解将数据从文件传输到套接字的通用数据路径很重要:
1.操作系统将数据从磁盘读取到内核空间中的pagecache
2.应用程序从内核空间读取数据到用户空间缓冲区
3.应用程序将数据写回内核空间到套接字缓冲区
4.操作系统将数据从套接字缓冲区复制到通过网络发送的NIC缓冲区
这显然是低效的,有四个副本和两个系统调用。 使用sendfile,通过允许操作系统将数据从pagecache直接发送到网络来避免重新复制。 所以在这个优化的路径中,只需要最后的拷贝到NIC缓冲区。
我们期望一个常见的用例是一个主题(Topic)有多个消费者。 使用上面的零拷贝优化,数据被精确地复制到pagecache一次,并且在每次消费上重用,而不是先存储在存储器中,然后每次读取时再复制到用户空间。 这允许以接近网络连接限制的速率消费消息。
这种pagecache和sendfile的组合意味着在消费者大多赶上生产消息的生存率的Kafka群集上,您将看到磁盘上没有任何读取活动,因为它们将完全从缓存中提供数据。
有关Java中支持sendfile和零拷贝的更多信息,请参阅这篇文章。
端到端批量压缩
在某些情况下,瓶颈实际上不是CPU或磁盘,而是网络带宽。 对于需要通过广域网络在数据中心之间发送消息的数据管道尤其如此。 当然,用户可以一次压缩一条消息,而不需要Kafka的任何支持,但这可能导致非常差的压缩比,因为冗余是由于相同类型的消息之间的重复(例如, JSON中的字段名称或Web日志中的用户代理或通用字符串值)。 有效的压缩需要压缩多个消息,而不是单独压缩每个消息。
Kafka支持这种高效的批处理格式。 一批消息可以一起压缩并以此形式发送到服务器。 这批消息将以压缩形式写入,并将在日志中保持压缩,并且只能由消费者解压缩。
Kafka支持GZIP,Snappy和LZ4压缩协议。 有关压缩的更多细节可以在这里找到。
4.生产者
负载均衡
在没有任何中介路由层的情况下,生产者直接将数据发送给作为分区领导者的broker。为了帮助生产者实现这一点,在任何给定的时间内,所有Kafka节点都可以回应元数据的请求,这些请求涉及哪些服务器是活跃的和Topic分区的领导者在哪。这样就可以合理的指导生产者发送请求了。
客户端控制向哪个分区发布消息。这可以随机地进行,实现一种随机的负载平衡,或者可以通过一些语义分区函数来实现。我们公开语义分区接口,这个接口允许用户为分区指定key,并且应用这个接口hash到一个分区。(如果需要,还可以重写分区函数)。例如,如果选择的键是用户id,那么给定用户的所有数据将被发送到相同的分区。这反过来将允许消费者对他们的消费做出局部性的假设。这种类型的分区是明确设计的,以允许在消费者中进行本地敏感的处理。
异步发送
批处理是效率的主要推动力之一,启动批量操作,Kafka生产者将尝试在内存中积累数据,并在一个请求中发送更大批量的数据。 批处理可以被配置为累积不超过固定数量的消息和等待不超过固定的延迟范围(例如64k或10ms)。 这允许多个字节的累积发送和在服务器上少量更大的I/O操作。 这种缓冲是可配置的,并且提供了一种机制来权衡少量额外的延迟以获得更好的吞吐量。
有关配置的详细信息和生产者的api可以在文档的其他地方找到。
5.消费者
Kafka消费者向相应的broker(作为分区的领导者)发出“提取”请求。 消费者在每次请求中指定其在日志中的偏移量,并从该位置返回一大堆日志。 因此,消费者对偏移量具有绝对的控制,在需要时,还可以将偏移量倒回重新消费数据。
推vs.拉
我们考虑的一个初步问题是消费者从broker拉数据还是broker将数据推送给消费者。在这方面,kafka遵循更传统的设计,数据从生产者推送给broker,然后消费者从broker拉取。一些以日志为中心的系统,例如Scribe和Apache Flume,遵循非常不同的推送式路径,其中数据被推送到下游。这两种方法都有利弊。然而,随着broker控制数据传输的速度,基于推送的系统难以处理不同的消费者。消费者的目标通常是以最大可能的速度消费;不幸的是,在推动系统中,这意味着消费者在消费率低于生产率(实质上是拒绝服务攻击)时往往不堪重负。基于拉式的系统具有更好的性能,消费者只需要简单地落后并在有能力的时候进行追赶。消费者可以通过某种回馈协议来缓解这种情况,消费者通过这种方式表明自己不堪重负,转移速度已经提高到完全利用(但没有过度利用),这样消费者比看起来更为机警。所以我们采用了更传统的拉模式。
基于拉式系统的另一个优点在于它适用于对发送给消费者的数据进行大量批处理。在不知道下游用户是否能够立即处理消息的情况下,基于推送的系统必须选择立即发送一个请求或累积更多数据然后再发送。 如果调整为低延迟,这将导致一次发送单个消息,这条消息用于传输最终却被缓冲了,这是浪费的。 基于拉式的设计可以解决这个问题,因为消费者总是在日志中的当前位置(或最多可配置的最大大小)之后拉出所有可用的消息。 因此,无需引入不必要的延迟即可获得最佳批量数据。
一个简单的基于拉的系统的缺陷是,如果broker没有数据,消费者就要循环检测, 使用空轮询频繁检测的方式等候数据到来。为了避免这种情况,我们在pull请求中设置参数,允许阻塞消费者请求直到数据到达(也可以等待给定字节数到达再响应请求,这样可以确保大的传输量)。
你可以想象其他可能的设计,端到端只有拉操作。 生产者会在本地写一个本地的日志,broker从这些日志拉取消息,消费者从broker拉取消息。 类似“存储和转发”的生产者经常被推荐。 这是有趣的,但我们觉得非常不适合有成千上万的生产者的目标用例。 我们在大规模运行持久数据系统的经验使我们感到,跨越多个应用程序并涉及数以千计磁盘的系统实际上不会使事情变得更加可靠,而且会成为一个噩梦般的操作。 在实践中,我们发现可以大规模地运行强大SLA流水线,而不需要生产者进行持久化操作。
消费者位置
令人惊讶的是,跟踪所使用的内容是消息传递系统的关键性能点之一。
大多数消息传递系统在broker上保存关于所使用的消息的元数据。也就是说,当消息被分发给消费者时,broker要么在本地立即记录这个事实,要么等待消费者的确认。这是一个相当直观的选择,实际上对于单个机器服务器来说,不清楚这个状态可以去哪里。由于在许多消息传递系统中存储的数据结构很难扩展,因此另一个实用的选择是,一旦broker知道哪些消息被消费,就立即删除它,以维护一个较小的数据集。
让broker和消费者就所消费的东西达成一致并不是一个小问题。如果消息每次通过网络发送后broker立即记录该消息已被消费,那么如果消费者未能处理该消息(比如说因为崩溃或者请求超时等等),则该消息将会丢失。为了解决这个问题,许多消息传递系统增加了一个确认功能,这意味着消息在被发送时只被标记为发送而不是消费。broker等待来自消费者的特定确认以将消息标记为消费。这个策略解决了丢失信息的问题,但是却产生了新的问题。首先,如果消费者在发送确认前被标记为失败(比如超过指定时间没有收到确认就认为失败),那么消息将被消费两次。第二个问题是性能问题,现在,broker必须保持每个消息的多个状态(首先锁定它,以便不会再次发出消息,然后将其标记为永久消耗,以便将其删除)。这样棘手的问题必须得到处理,比如如何处理被发送但未被确认的消息。
Kafka处理这个不同。 我们的Topic被分成一组完全有序的分区,每个分区在任何给定的时间都被每个订阅消费者组中的一个消费者消费。 这意味着消费者在每个分区中的位置只是一个整数,即要消耗的下一个消息的偏移量。 这使得消费状态非常小,每个分区只有一个数字。 这个状态可以定期检查。 这使消息确认的等价物非常便宜。
这个决定有一个好处。 消费者可以故意倒回到旧的偏移量并重新使用数据。这违反了队列的公共契约,但事实证明,这是许多消费者的基本特征。 例如,如果消费者代码有一个错误,并且在一些消息被消费之后被发现,则消费者可以在错误得到修复后重新使用这些消息。
离线数据加载
可伸缩的持久性允许消费者仅仅周期性地消费的可能性,诸如批量数据加载,其周期性地将数据批量加载到诸如Hadoop或关系数据仓库之类的离线系统中。
在将数据加载到Hadoop的用例中,我们通过在单个map任务上分割加载的数据来并行化数据加载,每个节点/主题/分区组合构成一个数据切片,在加载中允许完全并行。 Hadoop提供了任务管理功能,失败的任务可以重新启动,而没有重复数据的危险,只需从原始位置重新启动即可。
6.消息传递语义
现在我们稍微了解了生产者和消费者是如何工作的,让我们来讨论一下Kafka在生产者和消费者之间提供的语义保证。很明显,可以提供多种可能的消息传递保证:
  • 最多一次 - 消息可能会丢失,但永远不会重新发送。
  • 至少一次 - 消息永远不会丢失,但可以重新发送。
  • 恰好一次 - 这是人们真正想要的,每个消息只传递一次。
值得注意的是,这分成两个问题:发布消息的持久性保证和消费消息时的保证。
许多系统声称提供“恰好一次”的交付语义,但重要的是要阅读细则,这些声明大多是误导性的(即它们用例不包括消费者或生产者可能失败的情况, 多个消费者处理的情况,或写入磁盘的数据可能丢失的情况)。
Kafka的语义是直接的。在发布消息时,我们有一个消息被“提交”到日志的概念。一旦发布的消息被提交,它将不会丢失,只要有一个复制分区的broker仍然“存活”。提交的消息的定义、活的分区以及我们尝试处理的失败类型的描述将在下一节中详细描述。现在,让我们假设一个完美的、无损的broker,并尝试理解对生产者和消费者的保证。如果一个生产者试图发布一条消息并遇到一个网络错误,那么它就不能确定这个错误发生在消息被提交之前还是之后。这类似于用自动生成的key插入到数据库表的语义。
在0.11.0.0之前,如果一个生产者没有收到一个表明消息已经提交的响应,那么除了重新发送消息之外别无选择。 这提供了至少一次的传送语义,如果原始请求实际上已经成功,则在重新发送期间可以将消息再次写入日志。 自0.11.0.0开始,Kafka生产者也支持一个幂等递送选项,保证重新发送不会在日志中导致重复条目。 为了达到这个目的,broker为每个生产者分配一个ID,并使用生产者发送的序列号(每条消息一个序列号)去除重复消息。 也从0.11.0.0开始,生产者支持使用类似事务的语义将消息发送到多个主题分区的能力:即,所有消息要么都写入成功,要不都写入失败。 这种用例在Kafka的主题之间进行了恰好一次处理(如下所述)。
并不是所有的用例都需要这样的强力保证。对于延迟敏感的用例,我们允许生产者指定它想要的耐久性级别。 如果生产者指定它想要等待消息被提交,则可以采用10ms的量级。 然而,生产者也可以指定它想要完全异步发送,或者它只想等待直到领导者(不一定包含跟随者)接收到消息。
现在让我们从消费者的角度来描述语义。 所有副本都具有相同的日志和相同的偏移量。 消费者控制在这个日志中的位置。 如果消费者没有崩溃,它可以将这个位置存储在内存中,但是如果消费者失败了,我们希望这个主题分区被另一个进程接管,那么新进程需要选择一个合适的位置来开始处理。 假设消费者读取一些消息 - 它具有处理消息和更新其位置的多个选项。
1.它可以读取消息,然后保存其在日志中的位置,最后处理消息。在这种情况下,消费者进程在保存其位置后可能会崩溃,但在保存其消息处理输出之前。在这种情况下,接管处理的进程将从保存的位置开始处理,即使在该位置之前的一些消息未被处理。这相当于“至多一次”的语义,消费者失败消息可能无法被处理。
2.它可以读取消息,处理消息,最后保存它的位置。在这种情况下,消费者进程在处理消息后崩溃,但在保存它的位置之前。在这种情况下,当新进程接管了它接收到的最初几条消息时,它就已经被处理了。在消费者失败的情况下,这相当于“至少一次”的语义。在许多情况下,消息有主键,因此更新是幂等的(接收相同的消息两次,只是用该记录的另一个副本覆盖该记录)。
那么,到底什么是精确的一次性语义(即你真正想要的东西)呢?在使用Kafka主题并生成另一个主题(如在Kafka Streams应用程序中)时,我们可以利用上面提到的0.11.0.0的生产者新事务功能。消费者的位置被存储为一个主题中的消息,因此我们可以将偏移量写入kafka与接收处理数据的输出主题置于相同的事务中。如果事务被中止,消费者的位置将恢复到原来的值,而输出主题的生成数据将不被其他消费者看到,这取决于他们的“隔离级别”。在默认的“read_uncommitted”隔离级别,所有消息对于消费者都是可见的,即使它们是被中止的事务的一部分,但在“read_committed”中,消费者只会从提交的事务中返回消息(以及不属于事务的任何消息)。
写入外部系统时,限制在于需要将消费者的位置与实际存储为输出的位置进行协调。 实现这一目标的经典方法是在消费者位置的存储和消费者输出的存储之间引入两阶段提交。 但是,这可以更简单地处理,并且通常通过让消费者将其偏移存储在与其输出相同的位置来进行。 这样做更好,因为消费者可能想要写入的许多输出系统不支持两阶段提交。 作为一个例子,考虑一个Kafka Connect连接器,它将读取的数据及其偏移量一起填充到HDFS中,以保证数据和偏移量都被更新,或者两者都不被更新。 对于许多其他需要这些更强的语义的数据系统,并且这些数据系统中消息没有主键用于去重,我们也遵循类似的模式。
因此,Kafka在Kafka Streams中有效地支持恰好一次交付,并且在Kafka主题之间传输和处理数据时,事务性生产者/消费者通常用于提供恰好一次交付。 对其他目的系统的恰好一次交付通常需要与此类系统合作,但Kafka提供了实现这一可行的偏移量(参见Kafka Connect)。 否则,Kafka默认保证至少一次交付,并且允许用户在处理一批消息之前通过禁止生产者重试和在消费者中提交偏移量来实现至多一次交付。
7.复制
Kafka为每个主题分区复制日志,复制数量可配置,而且每个分区副本在单独的服务器上,有几个副本就有几台服务器(您可以每个主题设置单独的复制因子)。 这样,当群集中的服务器出现故障时,可以自动故障转移到这些副本,以便在发生故障时保持可用状态。
其他消息传递系统提供了一些复制有关的特性,但是在我们(完全有偏见的)看来,这似乎是一个不太常用的东西,而且有很大的缺点:从属处于非活动状态,吞吐量受到严重影响, 繁琐的手动配置等等。Kafka默认使用复制,实际上我们将未复制的主题实现为复制因子为1的复制主题。
复制单元是主题分区。 在非失败条件下,kafka的每个分区都有一个单独的领导者和零个或更多的追随者。 包括了领导者的副本总数构成复制因子。 所有的读写操作都转到分区的领导。 通常情况下,有比broker更多的分区,而且领导者在broker之间平均分配。 追随者的日志与领导者的日志完全相同 - 都有相同的偏移量和消息,并且消息的顺序相同(当然,在任何给定的时间,领导者可能在其日志末尾有几个尚未被复制的消息)。
追随者会像普通的kafka消费者一样,从领导者那里消费信息,并将其应用到自己的日志中。让追随者从领导者那里拉取消息的好处是,可以让追随者自然地将他们正在应用于日志的日志条目一起批量处理。
与大多数分布式系统一样,自动处理故障需要精确定义节点“活着”的含义。 对于kafka节点活跃有两个条件:
1.节点必须能够维护与ZooKeeper的会话(通过ZooKeeper的心跳机制)
2.如果它是一个slave节点,它必须复制发生在领导者上的写操作,而不是落后于“太远”
我们将满足这两个条件的节点称为“同步”,以避免“存在”或“失败”的模糊性。 领导跟踪“同步”节点的集合。 如果追随者死亡,被卡住或落后,领导将从同步副本列表中删除它。 停滞和滞后副本由replica.lag.time.max.ms配置确定。
在分布式系统术语中,我们仅仅尝试处理失败的“失败/恢复”模式,节点突然停止工作,然后恢复(可能不知道已经死亡)。 Kafka不处理所谓的“拜占庭式”的故障,其中节点产生任意或恶意的反应(可能是由于错误或犯规)。
现在我们可以更准确地定义,当该分区的所有同步副本将其应用于其日志时,将认为该消息已被提交。只有承诺的消息才会被发送给消费者。这意味着消费者不必担心如果领导者失败,可能会看到可能丢失的消息。另一方面,生产者可以选择是否等待消息被提交,这取决于他们在延迟和持久性之间的权衡。这个首选项由生产者使用的acks设置控制。请注意,主题有一个“最小数量”的同步副本的设置,当生产者请求确认消息已写入完整的同步副本集时,将检查这个设置。如果生产者请求较不严格的确认,则即使同步副本的数量低于最小值(例如,它可以只有领导者一个副本),也可以提交和消费该消息。
kafka提供的保证是,在任何时候,只要至少有一个同步副本存在,就不会丢失提交的消息。
kafka在短暂的故障切换后仍然可以在节点故障的情况下使用,但在网络分区存在时可能无法使用。
复制日志:法定人数,ISR和状态机(哦,我的!)
kafka分区的核心是一个复制日志。复制日志是分布式数据系统中最基本的原语之一,有许多实现方法。复制日志可以被其他系统作为原语使用,用来以状态机方式实现其他分布式系统。
一个复制日志按照一系列值的顺序(通常是对日志条目编号0,1,2,...)进行建模。 有很多方法可以实现这一点,但最简单和最快的方法是以领导者为准,领导者选择提供给它的值的排序。 只要领导者还活着,所有的追随者只需要复制值和顺序(领导者选择的)。
当然,如果领导者没有失败,我们就不需要追随者!当领导失败时,我们需要从追随者中选出一位新的领导人。但追随者本身可能会落后或崩溃,因此我们必须确保我们选择一个最新的追随者。日志复制算法必须提供的基本保证是,如果我们告诉客户端消息是提交的,而领导者失败了,我们选择的新领导人也必须有这个消息。这就产生了一个权衡:如果领导者在宣布承诺之前等待更多的追随者承认一条信息,那么将会有更多潜在的选举领导人。
如果您选择必需的确认数量和日志数量,这两个数量必须比较来选举一个领导者,以保证有一个重叠,那么这就称为Quorum(仲裁)。
这种权衡的一个常见方法是在提交决定和领导者选举中使用多数票。 这不是kafka所做的,但是我们仍然可以通过探索来了解这个权衡。 假设我们有2f + 1个副本。 如果f + 1副本必须在领导者声明提交之前接收到一条消息,并且如果我们通过从至少f + 1个副本中选择具有最完整日志的跟随者来选择一个新领导者,则不超过f 失败,领导者保证有所有承诺的信息。 这是因为在任何f + 1副本中,必须至少有一个副本包含所有提交的消息。 这个副本的日志将是最完整的,因此将被选为新的领导者。 每个算法都必须处理许多其他细节(例如,精确定义了什么使得日志更加完整,确保了领导失败期间的日志一致性或更改副本集中的服务器集),但是现在我们将忽略这些细节。
这种多数投票方式有一个非常好的属性:延迟依赖于最快的服务器。也就是说,如果复制因子是3,延迟是由更快的slave而不是较慢的slave决定的。
在这个家族中,有多种多样的算法,包括ZooKeeper的   Zab Raft Viewstamped Replication 。我们知道的最类似的关于kafka的实际实现的学术刊物是来自微软的 PacificA
大多数投票的缺点是,不允许发生多个故障。 要容忍一个故障需要三份数据拷贝,而要容忍两个故障则需要五份数据拷贝。 根据我们的经验,只有足够的冗余来容忍单个故障对于一个实际的系统来说是不够的,但是对于大容量数据问题来说,每次操作写五次,磁盘空间要求是5倍,吞吐量是1/5,是不实用的。 这可能是为什么仲裁算法更常用于共享群集配置(如ZooKeeper)的原因,但是对于主数据存储则不太常见。 例如,在HDFS中,namenode的高可用性功能是建立在基于多数投票的journal上的,但是这种更昂贵的方法不适用于数据本身。
kafka采取了一种稍微不同的方法来选择法定人数。 kafka不是多数投票,而是动态地维护一组被引导到领导者的同步副本(ISR)。 只有这一组的成员才有资格当选领导。 写入Kafka分区不会被视为提交,直到所有的同步副本收到写入。 这个ISR集合发生变化时会被持久化到ZooKeeper中。 正因为如此,ISR中的任何副本都有资格当选领导者。 对于kafka的使用模型来说,这是一个重要的因素,在那里有很多分区,确保领导平衡很重要。 有了这种ISR模型和f + 1个副本,一个Kafka主题可以容忍f个故障,而不会丢失承诺的消息。
对于我们希望处理的大多数用例,我们认为这种权衡是合理的。 在实践中,为了容忍f失败,多数投票和ISR方法都会等待相同数量的副本在提交消息之前确认(例如,在一次失败后仍然存在,多数仲裁需要三次副本和一次确认,ISR方法要求两个副本和一个确认)。 不受最慢服务器影响的提交能力是多数投票方法的一个优点。 但是,我们认为通过允许客户端选择是否阻塞消息提交来改善(ISR),并且由于所需的复制因子较低而产生的额外的吞吐量和磁盘空间是值得的。
另一个重要的设计区别是,Kafka不要求崩溃的节点恢复完整的数据。在这个空间中,复制算法依赖于“稳定存储”并不罕见。“稳定存储”在任何故障恢复场景中都不能丢失,而且没有潜在的一致性违规行为。这个假设有两个主要问题。首先,磁盘错误是我们在持久数据系统的实际操作中观察到的最常见的问题,它们通常不会完整地保存数据。其次,即使这不是一个问题,我们也不需要在每次写一致性保证时使用fsync,因为这样可以减少2到3个数量级的性能。我们允许一个副本重新加入ISR的协议,但是必须确保副本在重新连接之前必须再次完全重新同步,这样即使它在崩溃中丢失了未刷新的数据,也没有关系。
不干净的领导人选举:如果他们都死了呢?
请注意,Kafka关于数据丢失的保证是基于至少一个副本保持同步。 如果复制分区的所有节点都死亡,则此保证不再成立。
然而,当所有的副本都死亡的时候,一个实际的系统需要做一些合理的事情。如果你不幸地发生了这种情况,重要的是要考虑会发生什么。有两种行为可以实现:
1.等待ISR的一个副本重新激活,并选择这个副本作为领导者(希望它仍然拥有所有的数据)。
2.选择第一个副本(不一定在ISR中)作为领导者复活。
这是可用性和一致性之间的简单折衷。 如果我们在ISR中等待副本,那么只要这些副本停机,我们将保持不可用状态。 如果这样的复制品被毁坏或者他们的数据丢失了,那么我们永远是失败的。 另一方面,如果一个不同步的复制品恢复生机,并且我们允许它成为领导者,那么它的日志就成为真相的来源,即使它不能保证每一个提交的信息都是如此。 默认情况下,Kafka选择第二个策略,并且当ISR中的所有副本都已经死掉时,选择可能不一致的副本。 可以使用配置属性unclean.leader.election.enable来禁用此行为,以支持停机时间优于不一致的用例。
这种困境并不是kafka所特有的。 它存在于任何法定人数的计划中。 例如,在大多数投票计划中,如果大多数服务器遭受永久性故障,那么您必须选择丢失100%的数据,或者通过将现有服务器上剩下的内容作为新的事实来源,但这样会违反一致性。
可用性和耐久性保证
当对Kafka进行写操作时,生产者可以选择是否等待消息被0,1或全部(-1)副本确认。 请注意,“所有副本确认”并不保证已分配的全部副本已收到消息。 默认情况下,当acks = all时,只要所有当前的同步副本收到消息,确认就会发生。 例如,如果一个主题只配置了两个副本,一个失败(即只有一个同步副本保留),那么指定acks = all的写入将会成功。 但是,如果剩余副本也失败,这些写入可能会丢失。 尽管这确保了分区的最大可用性,但是对于偏好耐久性而不是可用性的一些用户,这种行为可能是不希望的。 因此,我们提供了两个主题级配置,可用于偏好耐久性:
1.禁用不干净的领导者选举 - 如果所有副本都不可用,那么分区将保持不可用,直到最近的领导者再次可用。 这有效地避免了消息丢失的风险。 请参阅上一节有关不洁领导人选举的澄清。
2.指定最小的ISR大小 - 如果ISR的大小大于某个最小值,分区才接受写入操作,以防止消息丢失(比如消息仅仅写入单个副本,随后该副本不可用)。 这个设置只有在生产者使用acks = all的情况下才会生效,并保证消息至少被这么多的同步副本确认。 此设置提供了一致性和可用性之间的权衡。 对于最小ISR大小的更高设置保证了更好的一致性,因为信息被保证写入更多的副本,这减少了丢失的可能性。 但是,这会降低可用性,因为如果同步副本的数量低于最小阈值,则分区将无法写入。
副本管理
上面关于复制日志的讨论实际上只包含一个日志,即一个主题分区。然而,Kafka集群将管理成百上千个这样的分区。我们试图以轮询的方式在集群内平衡分区,以避免将所有高容量主题分区集中在少数节点上。同样,我们试图平衡领导,使每个节点都是其分区的比例份额(ISR)的领导者。
同样重要的是,要优化领导选举过程。一个直观的选举实现是如果一个节点宕机了, 那么这个节点托管的每个分区都独立选举。相反,我们选择一个broker作为“控制器”。该控制器检测broker级别的故障,并负责更改故障broker中所有受影响的分区的领导者。其结果是,我们能够将许多需要的领导变更通知一起进行批量处理,这使得选举过程对大量的分区来说变得更加便宜和快捷。如果控制器失败,一个幸存的broker将成为新的控制器。
8.日志压缩
日志压缩确保了kafka在单个主题分区的数据日志中始终保留了每个消息键的最后一个已知值。它涉及用例和场景,例如在应用程序崩溃或系统故障后恢复状态,或在运行维护期间重新启动应用程序后重新加载缓存。让我们深入了解这些用例,然后描述压缩是如何工作的。
到目前为止,我们只描述了更简单的数据保留方法,在一段固定的时间之后或日志达到某个预定的大小,旧的日志数据被丢弃。 这适用于时间事件数据,这些记录彼此间没有联系。 然而,有一种重要的数据流类型,是根据主键,变化的数据(例如对数据库表的更改)。
我们来讨论这样流的一个具体例子。 假设我们有一个包含用户邮箱地址的主题, 每当用户更新他们的电子邮件地址时,我们都会使用他们的用户ID作为主键向该主题发送一条消息。 现在假设我们在一段时间内为id为123的用户发送以下消息,每个消息对应于电子邮件地址的改变(其他id的消息被省略):
1
2
3
4
5
6
7
8
9
123 => bill@microsoft.com
        .
        .
        .
123 => bill@gatesfoundation.org
        .
        .
        .
123 => bill@gmail.com
日志压缩为我们提供了更为细化的保留机制,因此我们保证至少保留每个主键的最新更新(例如bill@gmail.com)。 通过这样做,我们保证日志包含每个key最终值的完整快照,而不仅仅是最近更改的key。 这意味着下游消费者可以从这个主题中恢复自己的状态,而不必保留所有更改的完整日志。
我们先看几个有用的用例,然后看看如何使用它。
1.数据库更改订阅。 通常需要在多个数据系统中拥有一个数据集,而且这些系统中的一个往往是某种数据库(要么是一个RDBMS,要么是一个新开发的键值存储)。 例如,您可能有一个数据库,一个缓存,一个搜索集群和一个Hadoop集群。 每次对数据库的更改都需要反映在缓存,搜索群集中,最终在Hadoop中。 在只处理实时更新的情况下,您只需要最近的日志。 但是,如果您希望能够重新加载缓存或恢复失败的搜索节点,则可能需要完整的数据集。
2.事件采购。 这是一种应用程序设计风格,它将查询处理与应用程序设计协同定位,并使用更改日志作为应用程序的主存储。
3.高可用性的日志记录。执行本地计算的进程可以通过记录它对本地状态所做的更改来实现容错,以便出现故障时另一个进程可以重新加载这些更改并继续运行。一个具体的例子是在流查询系统中处理计数、聚合和其他“group by”类似的处理。Samza,一个实时的流处理框架,正是为了这个目的而使用这个特性。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值