kafka设计
- 微信公众号:阿俊的学习记录空间
- 小红书:ArnoZhang
- wordpress:arnozhang1994
- 博客园:arnozhang
- CSDN:ArnoZhang1994
一、目标
能够作为一个统一的平台,处理大型公司可能拥有的所有实时数据流。(更像是数据库日志)
- 高吞吐量:Kafka必须具有高吞吐量,以支持高容量的事件流,例如实时日志聚合。
- 处理数据积压:它需要能够优雅地处理大量数据积压,以支持离线系统的周期性数据加载。
- 低延迟传输:这也意味着系统必须能够处理低延迟传输,以满足更传统的消息传递用例。
- 分区与实时处理:我们希望支持这些数据流的分区、分布式、实时处理,以创建新的派生数据流。这激发了我们的分区和消费者模型。
- 容错性:最后,在将数据流传输到其他数据系统以提供服务的场景中,我们知道系统必须能够在机器故障时保证容错性。
二、持久化
Kafka严重依赖文件系统来存储和缓存消息。设计良好的磁盘结构通常可以和网络一样快。
磁盘性能的关键在于,过去十年里,硬盘的吞吐量与磁盘寻道延迟的差距正在扩大。结果是,配置六个7200rpm SATA RAID-5阵列的JBOD(只是一堆磁盘)可以提供约600MB/秒的线性写入性能,但随机写入的性能却只有约100k/秒——差距超过6000倍。这些线性读写操作是所有使用模式中最可预测的,操作系统对此进行了大量优化。现代操作系统提供了预读和回写技术,这些技术可以预取大块数据并将较小的逻辑写操作组合成较大的物理写操作。关于这个问题的进一步讨论可以参考ACM Queue的文章;他们实际上发现顺序磁盘访问有时比随机内存访问更快!
为了弥补这种性能差异,现代操作系统在磁盘缓存上变得越来越积极。现代操作系统会愉快地将所有空闲内存用于磁盘缓存,而当内存被回收时,性能损失很小。所有磁盘的读写操作都会经过这个统一的缓存。如果不使用直接I/O,很难关闭此功能,因此即使一个进程维护了一个进程内缓存,这些数据可能仍然会在操作系统页面缓存中被复制,实际上将所有内容存储了两次。
此外,kafka是基于JVM的,任何使用过Java内存的人都知道两件事:
- 对象的内存开销非常高,通常会使存储的数据量加倍(甚至更糟)。
- 随着堆内数据的增加,Java垃圾回收变得越来越复杂和缓慢。
磁盘页面文件是指操作系统用来构建虚拟内存的硬盘空间。
页缓存是Linux内核一种重要的磁盘高速缓存,它通过软件机制实现。
由于这些因素,使用文件系统并依赖页面缓存要优于维护内存缓存或其他结构——通过自动访问所有空闲内存,我们至少可以将可用缓存容量加倍,并且由于存储的是压缩的字节结构而不是单个对象,可能会再次加倍。这样可以在32GB机器上得到高达28-30GB的缓存空间,并且不会有GC(垃圾回收)带来的性能损失。此外,即使服务重新启动,这个缓存仍然保持热态,而进程内缓存则需要在内存中重建(对于一个10GB的缓存,这可能需要10分钟),否则将不得不从完全冷态缓存开始(这可能意味着初始性能非常差)。这也大大简化了代码,因为所有维护缓存和文件系统一致性的逻辑现在都由操作系统处理,操作系统通常比一次性进程内尝试更有效、更正确。如果你的磁盘使用偏向于线性读取,那么预读操作实际上是在每次磁盘读取时用有用数据预填充这个缓存。
这表明了一种非常简单的设计:与其尽可能多地在内存中维护数据,并在空间不足时恐慌地将其全部刷新到文件系统中,我们反其道而行之。所有数据都立即写入文件系统上的持久日志,而不一定要刷新到磁盘上。实际上,这意味着数据被传输到了内核的页面缓存中。
三、效率
我们投入了大量精力提高效率。我们最主要的用例之一是处理网页活动数据,这是一种非常高容量的数据:每个页面浏览可能会生成几十次写操作。此外,我们假设每个发布的消息至少会被一个消费者读取(通常是多个),因此我们努力使消费变得尽可能便宜。
我们还从构建和运行许多类似系统的经验中发现,效率是实现多租户操作的关键。如果下游基础设施服务由于应用程序使用中的一个小突增而轻易成为瓶颈,这样的小变化往往会引发问题。通过确保基础设施不会成为性能瓶颈,我们可以帮助确保在负载下首先崩溃的是应用程序,而不是基础设施。这一点在运行一个支持几十或上百个应用程序的集中式集群时尤为重要,因为使用模式的变化几乎是每天都会发生的。
我们在前一节讨论了磁盘效率。一旦消除了不良的磁盘访问模式,这类系统中的两个常见低效原因是:过多的小I/O操作和过多的字节复制。
小I/O问题发生在客户端与服务器之间以及服务器自身的持久操作中。
为避免此问题,我们的协议围绕着“消息集”抽象构建,自然地将消息分组在一起。这允许网络请求将消息分组在一起,分摊网络往返的开销,而不是一次只发送一条消息。服务器则一次将大量消息附加到日志中,消费者也一次获取大块的线性数据。
这一简单的优化带来了数量级的加速。批处理使网络包更大,顺序磁盘操作更大,内存块更连续,所有这些都允许Kafka将随机的消息写入流转变为线性写入,从而流向消费者。
四、生产者
负载均衡
生产者将数据直接发送到分区的主副本,避免了中间路由层的复杂性。所有Kafka节点都可以响应元数据请求,告知主题的各个分区的主副本位置,允许生产者适当地定向请求。
生产者可以选择通过分区键对数据进行语义分区,确保同一键值的数据发送到同一个分区,方便消费者进行本地化处理。
异步发送
为了提高效率,Kafka生产者积累内存中的数据并批量发送。批量大小和等待时间可配置,例如64k或10毫秒,提供了在延迟和吞吐量之间的权衡。生产者通过批处理减少服务器上较小I/O操作的开销。
五、消费者
Kafka消费者通过向分区的主代理发出"fetch"请求来工作,并指定从何处开始消费日志。消费者对消费位置有完全控制,支持倒回重新消费。
推送与拉取
Kafka采用拉取模式,允许消费者控制数据消费速度,避免了推送模式下因消费速率不足导致的过载问题。拉取模式支持数据批处理,确保不影响延迟的情况下优化传输效率。
Kafka通过长轮询避免紧密轮询轮询的忙等待问题,消费者可以等待数据的到来或设定一个字节限制,确保批量数据传输。
拉取模式更适合多生产者、大规模数据管道的应用场景。
消费者位置
Kafka的分区是有序的,每个分区的消费者位置仅是一个整数,代表下一个要消费的消息偏移量。这使得已消费数据的状态非常小,并可以定期进行检查点操作。消费者可以重新消费数据,支持对Bug修复后的回溯处理。
离线数据加载
Kafka支持批量数据加载,如加载到Hadoop或数据仓库中。Hadoop的每个节点/主题/分区组合一个任务,支持完全并行加载。
六、静态成员身份
静态成员身份用于提高基于组rebalance协议构建的流应用程序的可用性。动态成员身份在任务重新分配时可能导致有状态应用程序的任务恢复时间过长,影响可用性。Kafka通过允许组成员提供持久的实体ID来避免不必要的重平衡,保证组成员身份不变,从而提升系统的可用性。
使用静态成员身份步骤:
- 将代理集群和客户端应用程序升级到2.3或更高版本。
- 为每个消费者实例设置唯一的
GROUP_INSTANCE_ID_CONFIG
值。 - 对Kafka Streams应用程序,使用
application.server
参数,确保部署的实例具有唯一值。