❝
何为后kafka时代那?kafka自从 2011 年被捐献给 Apache 基金会到现在,已经发展到现如今的消息队列事实标准。作为一个优秀的分布式消息系统,Kafka 已经被许多企业采用并成为其大数据架构中不可或缺的一部分。Kafka也 已经不再只是分布式消息队列,而是想做集成了分发、存储和计算的“流式数据平台”。与此同时,大数据“新秀 Pulsar” 站在的kafka的肩膀上,针对kafka使用与维护过程中的痛点问题,明确提到“Pulsar 旨在取代 Apache Kafka 多年的主宰地位”的口号。
❞
kafka作为一个老的基础组件,很多读者都已经对其设计和原理十分熟悉,面向Pulsar的冲击下,很多人或许会犹豫究竟要选择哪个技术?
本人在 Tencent 中负责维护数据总线与数据集成服务,kafka与pulsar是消息总线中的基本组件需求,并且我们的系统在具体的大数据消息队列之上,又抽象了一层管道(channel)的概念,使得可以将两种消息队列可以可插拔的嵌入服务中。因此,我们团队同时在使用Kafka与Pulsar。不可否认,Pulsar确实十分优秀,站在了前人的基础上,在架构底层重新定义的大数据消息队列。
但是,即使是Pulsar时代下,kafka的存在价值和使用场景是否发生了变化那?
本系列文章会带大家重新梳理和回顾Kafka的概念、设计、发展历史和其当下使用的场景。
发展历程和底层设计
Kafka 是 2010 年左右在 LinkedIn 研发的一套流数据处理平台。LinkedIn发现这些 MQ 系统都有两个比较通用的缺陷:一是当消费者出现,无法及时消费的时候,数据就会丢掉;二是可延展性问题,MQ 系统很难很好地配合数据的波峰或波谷。
这些需求正好对应当时的消息队列系统不能解决的一些问题:「横向拓展、持久化、高吞吐、高性能、甚至是低成本」。因此Kafka这一流处理系统出现后,瞬间成为大数据流处理系统的事实标准。
2010 年LinkedIn开始自己开发 Kafka。设计理念非常简单,就是一个以「append-only 日志作为核心的数据存储结构」。简单来说,就是我们把数据以日志的方式进行组织,所有对于日志的写操作,都提交在日志的最末端,对日志也只能是顺序读取。
从我们当下的角度看待当时的kafka设计确实什么简单又十分优雅!kafka能实现上述的:横向拓展、持久化、高吞吐、高性能都得益于这个基础设计。
「顺序读写,高吞吐」: HDD 的随机读取和写入因为其本身原因会非常慢,但其实如果能够把所有的读和写都按照顺序来进行操作,会发现它几乎可以媲美内存的随机访问。kafka利用append-only 日志作为核心的数据存储结构,只会对文件进行顺序的读写操作,大大的利用了这一优点。【机械硬盘的连续读写性能很好,但随机读写性能很差,这主要是因为磁头移动到正确的磁道上需要时间,随机读写时,磁头需要不停的移动,时间都浪费在了磁头寻址上,所以性能不高。衡量磁盘的重要主要指标是IOPS和吞吐量。】
「物理分区,多分区扩展」:在Kafka中,Topic 只是一个逻辑的概念。每个 Topic 都包含一个或多个 Partition,不同 Partition 可位于不同节点。一方面,由于不同 Partition 可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。另一方面,由于 Partition 在物理上对应一个文件夹,即使多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘的优势。
「多副本,保证高可用」:每个broker中的partition都会设置有replication(副本)的个数,生产者写入的时候首先根据分发策略(有partition按partition,有key按key,都没有轮询)写入到leader中,follower(副本)再跟leader同步数据,这样有了备份,也可以保证消息数据的不丢失。
「低成本」:Kafka 的日志存储持久化到磁盘,在部署时可以使用成本较低的HDD磁盘。
「页缓存加速」:顺序写文件时,「读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据」;页缓存会将连续的小块写组装成大块的物理写从而提高性能;页缓存会 尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间;【Cache 层在内存中缓存了磁盘上的部分数据。当数据的请求到达时,如果在 Cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能】
「零拷贝,充分利用IO」:Kafka 在这里采用的方案是通过 NIO 的transferTo/transferFrom调用操作系统的 sendfile 实现零拷贝。总共发生 2 次内核数据拷贝、2 次上下文切换和一次系统调用,消除了 CPU 数据拷贝。【零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载】
从我们当下的角度看待当时的kafka设计还是那么简洁和优雅,kafka能实现上述的:横向拓展、持久化、高吞吐、高性能都得益于这个基础设计。但是,这种简单的设计也有一些弊端,但是在kafka刚出的那个时候,这个设计和功能确实太优秀了。
Kafka使用痛点
首先先说结论,「后kafka时代下,kafka的某些设计已经比较落后了」。在运营/运维kafka的过程中,其实遇到了很多问题。
而很多问题是在基础架构确定之后,就决定了会有这样的结果。
单机器的分区上限问题
虽然 Kafka 的 topic partition 是顺序写入,但是当 broker上有成百上千个topic partition 时,从磁盘角度看就变成了随机写入,此时磁盘读写性能会随着 topic partition 数量的增加而降低,因此「Kafka broker 上存储的 topic partition 数量是有限制的」。
这大大限制了kafka在多主题情况下的使用。
非存储与计算分离的架构
kafka并不是一个存储与计算分离的架构,因此无法从存储和计算单个维度进行扩容。
数据存储和消息队列服务绑定,集群扩缩容/分区均衡需要大量拷贝数据,造成集群性能下降,并且带来了很大的运维成本。
一个分区只能归属于一台机器带来的文件存储
Kafka中根据设置的保留期来删除消息。有可能消息没被消费,过期后被删除。 不支持TTL。
但是这其中的本质问题来自于:一个分区只能归属于一台Broker机器,如果想要扩容的话,只能扩分区,拆分区
在极端情况下,如果原有kafka集群负载到达50%,流量这时如果翻三四倍,这对kafka的运维来说简直是个灾难!
运维成本高
如果某个机器磁盘满了,需要显式的使用工具迁移分区(kafka-reassign-partitions.sh)
数据存储和消息队列服务绑定,集群扩缩容/分区均衡需要大量拷贝数据,会带来了很大的运维成本。
随着 Kafka 集群规模的增长,Kakfa 集群的运维成本急剧增长,需要投入大量的人力进行日常运维。在某互联网公司中,扩容一台机器到 Kafka 集群并进行分区均衡,需要 0.5人/天;缩容一台机器需要 1 人/天。
PageCache 污染问题,造成读写性能下降
Kafka对page cache需求巨大。根据经验值,为Kafka分配6~8GB的堆内存就已经足足够用了,将剩下的系统内存都作为page cache空间,可以最大化I/O效率。
另一个需要特别注意的问题是lagging consumer,即那些消费速率慢、明显落后的consumer。它们要读取的数据有较大概率不在broker page cache中,因此会增加很多不必要的读盘操作。
比这更坏的是,「lagging consumer读取的“冷”数据仍然会进入page cache,污染了多数正常consumer要读取的“热”数据」,连带着正常consumer的性能变差。在生产环境中,这个问题尤为重要。
在 catch-up 读场景下,容易出现 PageCache 污染,造成读写性能下降。虽然kafka可以利用PageCache进行读取加速,在一些场景下实践效果不佳。
「Kafka不支持读写分离」
在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从 而实现的是一种「主写主读」的生产消费模型。Kafka 并不支持「主写从读」。
其实kafka的「主写主读」也是有一些优点的:
可以简化代码的实现逻辑,减少出错的可能;
将负载粒度细化均摊,与主写从读相比,不仅负载效能更好,而且对用户可控;
没有延时的影响;
在副本稳定的情况下,不会出现数据不一致的情况。
但是这些也不能算是完全的优点,「只是在当前kafka架构下,做到读写分离的收益不如主写主读方案。」
kafka中IO 不隔离,因此「消费者在清除 Backlog 时会影响其他生产者和消费者」。
后Kafka时代的使用场景
什么样的场景下可以继续用kafka?
在集群内topic不多或增长速度不是特别快的情况下,kafka依旧是很好的选择。
不需要复杂的企业级场景的时候,kafka仍旧是首选。
Kafka 原生的集群模式使用简单,能满足少量业务的需要。但对于大型企业(网站),大量的业务使用 Kafka,数据量、流量极大,必然同时存在多个集群,需要对用户的接入、运行时监控、集群运维提供统一的解决方案
版本变更
kafka 0.7 版本
kafka开源后的第一个正式的版本
提供了数据的压缩以及 MirrorMaker,也就是跨集群之间的数据拷贝。
这个版本太过久远,我们就不再占用篇幅介绍了
kafka 0.8 版本
Kafka 0.8.0 里面加入了多副本功能,也就是基于备份的一个高可用性的特性。
该版本中提出了一个叫做 ISR,或者叫做实时备份列表的机制。我们把所有的备份分为已同步和未同步的备份。
已同步的备份指,Leader 所有的 Data 在 replica 里面都有;未同步的很简单,由于可能比较慢,或者备份还不完整,也许有些数据在 Leader 上面有,但是在 replica 上没有。Leader 可以通过来管理这样的一个列表来做到实时的修改。同时,发布者发布信息的时候,可以要求备份方式。
kafka 0.9 版本
当 Kafka 集群不断变大、使用场景不断增多的时候,多租户之间的影响就会非常显著,一个人可以影响其他所有用户。
有一个员工写的客户端,当获取元数据失败时会一直发请求,并部署到了几十台机器上,结果就影响了所有的其他用户。所以我们在 0.9 里第一个要加的重大机制就是配额,限定每一个 user 能够用多大的流量跟 Kafka 交互。如果你超过配额,Kafka broker 就故意延迟你的请求,使一个 User 不会影响别人。这就是 0.9 的配额机制。
之前版本,Kafka其实存在一个比较大的隐患,就是利用 Zookeeper 来存储记录每个消费者/组的消费进度。虽然,在使用过程当中,JVM帮助我们完成了一些优化,但是消费者需要频繁的去与 Zookeeper 进行交互,而利用ZKClient的API操作Zookeeper频繁的Write其本身就是一个比较低效的Action,对于后期水平扩展也是一个比较头疼的问题。如果期间 Zookeeper 集群发生变化,那 Kafka 集群的吞吐量也跟着受影响。
从kafka-0.9版本及以后,kafka的消费者组和offset信息就不存zookeeper了,而是存到broker服务器上,所以,如果你为某个消费者指定了一个消费者组名称(group.id),那么,一旦这个消费者启动,这个消费者组名和它要消费的那个topic的offset信息就会被记录在broker服务器上。
kafka 0.10 版本
Kafka Streams 是在 0.10 里面加入的,它是一个流处理的平台,或者叫它是流处理的一个库。它是基于发布端和消费端的处理平台,它能够做到的是 Event-at-a-time、Stateful,并且支持像 Windowing 这样的操作,支持 Highly scalable、distributed、fault tolerant。所有这些都很大程度上利用了 Kafka broker,也就是服务器端本身的延展性和高可用性。
简单来说,Kafka Streams 所做的就是从 Kafka 的 Topic 里实时地抓取数据。这个数据会通过用户所写拓扑结构,把所有的 record 实时进行 transform 之后,最终再写回到 Kafka 里面,是个很简单的流数据处理。那么它怎么做延展性呢?也很简单,当用户写好一个拓扑结构以后,可以在多个机器,或者多个容器、多个虚拟机、甚至是多个 CPU 上面,部署多个应用,当应用同时进行的时候,会利用 Kafka 自动地划分每一个不同的应用所抓取的不同 partition 的数据
kafka 1.0版本
在该版本下做到 Exactly-Once,这才能使 Kafka 作为一个成熟的流数据平台
非 Exactly-Once 是指由于网络延迟或其他各种原因,导致消息重复发送甚至重复处理。那么直白来说 Exactly-Once 的定义,就是从应用的角度来说,当发生了错误,希望做到每一个接收到的 record,处理结果会被反映到它的处理状态中,一次且仅有一次,也就是 Exactly-Once。
之后的版本中,虽然在持续迭代,但是从kafka的底层架构来看,没有其他的重大更新了
参考资料:
https://mp.weixin.qq.com/s/p-jJVMjh9yr8QVqgRKtPtA
https://zhuanlan.zhihu.com/p/337861077
https://zhuanlan.zhihu.com/p/344277683
原文地址:https://mp.weixin.qq.com/s/2ob3tDnu1EBD8QMXmv8llA
首发在该平台上:【大数据技术事务所】