大家好,我是kafka,可能很多人都听说过我,知道我是2011年出生在LinkedIn的,从那会儿到现在我的功能越发强大了。作为一个优秀而又完整的平台,你可以在我上面冗余地存储巨大的数据量,我有一个具有高吞吐量(数百万/秒)的消息总线,你可以在这上面对经过我的数据进行实时流处理。
如果你认为我就只有上面的这些特点的话,那么你真的是太肤浅了。
上面虽然说的很好,但是并未触及到我的核心,这里我给你几个关键字:分布式,水平可扩展,容错,提交日志。
上面这些抽象的词语,我会一一解释它们的含义,并告诉你们我是如何工作的。
内心独白: 本来我是想要以第一人称来写这篇文章的,但是我发现我只能写出上面的了,再多的我就憋不出来了,于是我决定不要为难自己,还是用用第三人称写吧(写作的功底仍然需要锻炼)
分布式
分布式系统由多个运行的计算机系统组成,所有这些计算机在一个集群中一起工作,对终端用户来讲只是一个单一节点。
kafka也是分布式的,因为它在不同的节点(又被称为broker)上存储,接受以及发送消息,这样做的好处是具有很高的可扩展性和容错性。
水平可扩展性
在这之前,先看看什么是垂直可扩展,比如你有一个传统的数据库服务器,它开始过度负载,解决这个问题的办法就是给服务器加配置(cpu,内存,SSD),这就叫做垂直扩展。但是这种方式存在两个巨大的劣势
- 硬件存在限制,不可能无限的添加机器配置
- 它需要停机时间,通常这是很多公司无法容忍的
水平可扩展就是通过添加更多的机器来解决同样的问题,添加新机器不需要停机,而且集群中也不会对机器的数量有任何的限制。问题在于并非所有系统都支持水平可伸缩性,因为它们不是设计用于集群中(集群中工作更加复杂)。
容错性
非分布式系统中容易最致命的问题就是单点失败,如果你唯一的服务器挂掉了,那么我相信你会很崩溃。
而分布式系统的设计方式就是可以以配置的方式来容许失败。在5个节点的kafka集群中,你仍然可以继续工作即使其中两个节点挂掉了。
需要注意的是,容错与性能直接相关,你的系统容错程度越高,性能就越差。
提交日志(commit log)
提交日志(也被称为预写日志或者事物日志)是仅支持附加的持久有序数据结构,你无法修改或者删除记录,它从左往右读并且保证日志的顺序。
是不是觉得kafka的数据结构如此简单?
是的,从很多方面来讲,这个数据结构就是kafka的核心。这个数据结构的记录是有序的,而有序的数据可以确保我们的处理流程。这两个在分布式系统中都是及其重要的问题。
kafka实际上将所有消息存储到磁盘并在数据结构中对它们进行排序,以便利用顺序磁盘读取。
- 读取和写入都是常量时间O(1)(当确定了record id),与磁盘上其他结构的O(log N)操作相比是一个巨大的优势,因为每个磁盘搜索都很耗时。
- 读取和写入不会相互影响,写不会锁住读,反之亦然。
这两点有着巨大的优势, 因为数据大小与性能完全分离。无论你的服务器上有100KB还是100TB的数据,Kafka都具有相同的性能
如何工作
应用程序(producer)发送消息(record)到kafka服务器(broker),这些消息会被其他应用程序(consumer)所处理,这些消息存储在主题(topic)中,并且消费者订阅该主题以接收新消息。是不是感觉很像你平时写的代码——生产者消费者模式。
随着主题变得非常大,它们会分成更小的分区(partition),以获得更好的性能和可伸缩性(比如存储了用户相互发送的消息,你可以根据用户名的第一个字母来进行拆分)。Kafka保证分区内的所有消息都按照它们的顺序排序,区分特定消息的方式是通过其偏移量(offset),你可以将其视为普通数组索引,即为分区中的每个新消息递增的序列号。
kafka遵守着愚蠢的broker和聪明的consumer的准则。这意味着kafka不会跟踪消费者读取了哪些记录并删除它们,而是会将它们存储一定的时间(比如1天,以log.retention开头的来决定日志保留时间),直到达到某个阈值。消费者自己轮询kafka的新消息并且告诉它自己想要读取哪些记录。这允许它们按照自己的意愿递增/递减它们所处的偏移量,从而能够重放和重新处理事件。
需要注意的是消费者是属于消费者组的,消费者组有一个或多个消费者。为了避免两个进程读取同样的消息两次,每个partition只能被一个消费者组中的一个消费者访问。
持久化到硬盘
正如我之前提到的,kafka实际上是将所有记录存储到硬盘而不在RAM中保存任何内容。你想知道这个如何做出这个选择的,其实这背后有很多优化使得这个方案可行。
- kafka有一个将消息分组的协议,这允许网络请求将消息组合在一起并减少网络开销,服务器反过来一次性保留大量消息,消费者一次获取大量线性块。
- 磁盘上线性读写非常快,现代磁盘非常慢的概念是由于大量磁盘寻址,但是在大量的线性操作中不是问题。
- 操作系统对线性操作进行了大量优化,通过预读(预取大块多次)和后写(将小型逻辑写入组成大型物理写入)技术。
- 操作系统将磁盘文件缓存在空闲RAM中。这称为pagecache,而kafka的读写都大量使用了pagecahce
- 写消息的时候消息先从java到page cache,然后异步线程刷盘,消息从page cache刷入磁盘
- 读消息的时候先从page cache找,有就直接转入socket,没有就先从磁盘load到page cache,然后直接从socket发出去
- 由于Kafka在整个流程(producer - >broker - >consumer)中以未经修改的标准化二进制格式存储消息,因此它可以使用零拷贝优化。那时操作系统将数据从pagecache直接复制到socket,有效地完全绕过了Kafka broker。
所有这些优化都使Kafka能够以接近网络的速度传递消息。
数据分发和复制
我们来谈谈Kafka如何实现容错以及它如何在节点之间分配数据。
为了使得一个borker挂掉的时候,数据还能得以保留,分区(partition)数据在多个broker中复制。
在任何时候,一个broker拥有一个partition,应用程序读取/写入都要通过这个节点,这个节点叫做—-partition leader。它将收到的数据复制到N个其他broker,这些接收数据的broker叫做follower,follower也存储数据,一旦leader节点死掉的时候,它们就准备竞争上岗成为leader。
这可以保证你成功发布的消息不会丢失,通过选择更改复制因子,你可以根据数据的重要性来交换性能以获得更强的持久性保证
这样如果leader挂掉了,那么其中一个follower就会接替它称为leader。包括leader在内的总副本数就是副本因子(使用–replication-factor指定)上图有1个leader,2个follower,所以副本因子就是3。
但是你可能会问:producer或者consumer怎么知道partition leader是谁?
对生产者/消费者对分区的写/读请求,它们需要知道分区的leader是哪一个,对吧?这个信息肯定是可以获取到的,Kafka使用zookeeper来存储这些元数据。
什么是ZooKeeper
Zookeeper是一个分布式键值存储。它针对读取进行了高度优化,但写入速度较慢。它最常用于存储元数据和处理群集的机制(心跳,分发更新/配置等)。
它允许服务的客户(Kafka broker)订阅并在发生变更后发送给他们,这就是Kafka如何知道何时切换分区领导者。ZooKeeper本身维护了一个集群,所以它就有很高的容错性,当然它也应该具有,毕竟Kafka很大程度上是依赖于它的。
zookeeper用于存储所有的元数据信息,包括但不限于如下几项:
- 消费者组每个分区的偏移量(现在客户端在单独的kafka topic上存储偏移量)
- ACL —— 权限控制
- 生产者/消费者的流量控制——每秒生产/消费的数据大小。可以参考Kafka-流量控制Quota功能
- partition leader以及它们的健康信息
那么produer/consumer是如何知道谁是partition leader的呢?
生产者和消费者以前常常直接连接ZooKeeper来获取这些信息,但是Kafka从0.8和0.9版本开始移除了这种强耦合关系。客户端直接从kafka broker直接获取这些元数据,而让kafka broker从zookeeper那里获取这些元数据。
更多zookeeper的讲解可以参考:漫画:什么是ZooKeeper?
流式处理(Streaming)
在Kafka中,流处理器是指从输入主题获取连续数据流,对此输入执行某些处理并生成数据流以输出到其他主题(或者外部服务,数据库,容器等等).
什么是数据流呢?首先,数据流是无边界数据集的抽象表示。无边界意味着无限和持续增长。无边界数据集之所以是无限的,是因为随着时间推移,新的记录会不断加入进来。比如信用卡交易,股票交易等事件都可以用来表示数据流
我们可以使用producer/consumer的API直接进行简单处理,但是对于更加复杂的转换比如将流连接到一起,kafka提供了集成Stream API库
这个API是在你自己的代码中使用的,它并不是运行在broker上,它的工作原理和consumer API类似,可帮助你在多个应用程序(类似于消费者组)上扩展流处理工作。
无状态处理
流的无状态处理是确定性处理,其不依赖于任何外部条件,对于任何给定的数据,将始终生成与其他任何内容无关的相同输出。举个例子,我们要做一个简单的数据转换—-“zhangsan” —> “Hello,zhangsan”
流-表二义性
重要的是要认识到流和表实质上是一样的,流可以被解释称为表,表也可以被解释称为流.
流作为表
流可以解释为数据的一系列更新,聚合后得结果就是表的最终结果,这项技术被称为事件溯源(Event Sourcing)
如果你了解数据库备份同步,你就会知道它们得技术实现被称为流式复制—-将对表的每个更改都发送报副本服务器.比如redis中的AOF以及Mysql中的binlog
Kafka流可以用相同的方式解释 - 当累积形成最终状态时的事件。此类流聚合保存在本地RocksDB中(默认情况下),被称为KTable。
表作为流
可以将表视为流中每个键的最新值的快照。以流记录可以生成表一样,表更新可以生成更改日志流。
有状态处理
我们在java中常用的一些操作比如map()或者filter()是没有状态的,它不会要求你保留任何原始数据。但是现实中,大多数的操作都是有状态的(比如count()),因为就需要你存储当前累计的状态。
在流处理器上维护状态的问题是流处理器可能会失败!你需要在哪里保持这种状态才能容错?
一种简单的方法是简单地将所有状态存储在远程数据库中,并通过网络连接到该存储,这样做的问题是大量的网络带宽会使得你的应用程序变慢。一个更微妙但重要的问题是你的流处理作业的正常运行时间将与远程数据库紧密耦合,并且作业将不是自包含的(其他team更改数据库可能会破坏你的处理)。
那么什么是更好的办法呢?
回想一下表和流的二元性。这允许我们将流转换为与我们的处理位于同一位置的表。它还为我们提供了一种处理容错的机制 - 通过将流存储在Kafka broker中。
流处理器可以将其状态保持在本地表(例如RocksDB)中,该表将从输入流(可能在某些任意转换之后)更新。当进程失败时,它可以通过重放流来恢复其数据。
你甚至可以将远程数据库作为流的生产者,有效地广播用于在本地重建表的更改日志。
KSQL
通常,我们不得不使用JVM语言编写流处理,因为这是唯一的官方Kafka Streams API客户端。
2018年4月,KSQL作为一项新特性被发布,它允许你使用熟悉的类似SQL的语言编写简单的stream jobs。你安装了KSQL服务器并通过CLI以交互方式查询以及管理。它使用相同的抽象(KStream和KTable),保证了Streams API的相同优点(可伸缩性,容错性),并大大简化了流的工作。
这听起来可能不是很多,但在实践中对于测试内容更有用,甚至允许开发之外的人(例如产品所有者)使用流处理,可以看看Confluent提供的这篇关于ksql的使用
什么时候使用kafka
正如我们已经介绍的那样,Kafka允许你通过集中式介质获取大量消息并存储它们,而不必担心性能或数据丢失等问题。
这意味着它非常适合用作系统架构的核心,充当连接不同应用程序的集中式媒体。Kafka可以成为事件驱动架构的中心部分,使你可以真正地将应用程序彼此分离.
Kafka允许你轻松地分离不同(微)服务之间的通信。使用Streams API,现在可以比以往更轻松地编写业务逻辑,从而丰富Kafka主题数据以供服务使用。可能性很大,我恳请你探讨公司如何使用Kafka。
总结
Apache Kafka是一个分布式流媒体平台,每天可处理数万亿个事件。Kafka提供低延迟,高吞吐量,容错的发布和订阅管道,并能够处理事件流。我们回顾了它的基本语义(生产者,代理,消费者,主题),了解了它的一些优化(pagecache),通过复制数据了解了它的容错能力,并介绍了它不断增长的强大流媒体功能。Kafka已经在全球数千家公司中大量采用,其中包括财富500强企业中的三分之一。随着Kafka的积极开发和最近发布的第一个主要版本1.0(2017年11月1日),有预测这个流媒体平台将会与关系数据库一样,是数据平台的重要核心。我希望这篇介绍能帮助你熟悉Apache Kafka。
参考文章
https://tech.meituan.com/2015/01/13/kafka-fs-design-theory.html
https://shiyueqi.github.io/2017/04/27/Kafka-%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6Quota%E5%8A%9F%E8%83%BD/
http://kafka.apache.org/documentation.html#introduction
https://docs.confluent.io/current/