我们先从流和表的处理容错能力开始,然后再介绍弹性。我们将会看到,它们实际上是一枚硬币的两面。
容错处理
流和表具有容错能力,因为它们的数据被可靠地存储在 Kafka 中。对于流来说,这个相对好理解,因为流是直接与主题对应起来的,如果在处理过程中出了问题,重新读取主题的数据就可以了。
这对于表来说就相对复杂了,因为表必须维护额外的信息,也就是它们的状态,这样才能进行有状态的操作,比如 COUNT() 或 SUM()。在 Kafka Streams 应用程序或 ksqlDB 服务器中,为了确保在保持高性能的同时实现有状态处理,表需要被物化到本地磁盘。但是,机器或容器会宕机,本地保存的数据也会随之丢失,那么我们该如何确保表的容错能力呢?
表和状态被物化到本地磁盘:
与关系型数据库的重做日志类似,变更流就是表的事实来源。变更流持续不断地被保存到 Kafka 主题中,所以这个主题也被叫作变更日志主题。所以,表的容错能力利用了流和表的二元性。在 stream 任务或运行任务的容器 / 虚拟机 / 机器发生故障时,表的数据可以通过变更流来恢复,数据的处理也因此不会被中断,不会有数据丢失或产生错误的处理结果。
如果一个容器发生故障,那么就需要在另一个容器上重建账户表,这样就不需要重新运行整个处理过程。我们可以直接从变更日志主题恢复表的状态。变更日志主题经过压缩,所以整个恢复过程非常快,稍后我们将会看到。
运行在机器 A 上的一个任务。如果机器发生宕机,任务会被迁移到另一台机器上。在新机器上,表的状态被恢复到发生故障时的那个时刻,恢复完成之后,任务继续执行:
弹性与上一小节讲到的容错能力有关。分布式系统处理故障(比如容器崩溃)所需要做的与实现弹性(例如,通过增加容器或移除容器实现应用程序的伸缩)所需要做的实际上很相似。至于容器是因为有意被移除还是因为无意发生故障,这个并不重要。换句话说,弹性和容错能力是一枚硬币的两面!
假设我们有两个 Kafka Streams 应用程序实例。输入数据是一个 Kafka 主题,这个主题有 4 个分区,那么就会有 4 个 stream 任务。这 4 个任务被均匀地分配给两个应用程序实例。如果现在加入第三个和第四个应用程序实例,那么之前的任务及其表分区的一部分会被迁移到新的应用程序实例上。
在加入新的应用程序实例之前:
在新增应用程序实例之后:
表和主题的压缩
一般来说,表底层的主题应该是压缩的。但有一种情况例外,比如基于一个已有的 Kafka 主题创建 ksqlDB 表,对于这种情况,与主题相关的配置都会被保留下来。压缩是 Kafka 的一个特性,确保 Kafka 对主题分区里的每一个键保留最新的事件,如图 5 所示。它会定时移除同一个键的旧事件(如图 5 示例中,Alice 之前访问过的网站),以此来减少表的变更流所占用的存储空间。
同一个键的旧事件被定期移除:
压缩的第二个好处是减少了应用程序在发生再均衡时所需要的恢复时间,因为从 Kafka 代理传输给 ksqlDB 服务器或 Kafka Streams 应用程序的数据减少了,这同时也提高了弹性和故障处理能力。假设我们有一张包含一百万用户的表,每天会发生很多变更事件,到现在已经有 4 亿个事件了。在启用了压缩功能之后,恢复用户表就会快很多,因为只需要读取最新的一百万个事件,而不是所有的 4 亿个事件。
所以说,压缩是很有用的。但要注意的是,压缩会清除表的历史事件,例如图 5 中被虚线框起来的部分。如果你需要所有的历史事件,那么可以考虑禁用压缩功能。但请注意,对于流,不应该启用压缩功能,因为具有相同键的新事件不应该被认为是可以“取代”旧事件!
弹性和容错能力的背后
在故障处理和弹性的背后实际上是 Kafka 的再均衡过程。在生产环境中运行 Kafka Streams 应用程序和 ksqlDB 服务器时,我们需要明白,有那么一小段时间(通常很短),应用程序有一部分是不可用的,直到再均衡结束。在这一小段时间内,ksqlDB 或 Kafka Streams 应用程序会对受影响的任务和表或者状态进行迁移。
迁移任务涉及的数据越多,恢复所需的时间也就越长。如果需要传输的数据太多,那么客户端应用程序实例(保存表分区的地方)和服务器端的 Kafka 代理(包含主题分区,可以基于这些分区来恢复表的分区)之间的带宽就会成为瓶颈。
之前提到的压缩功能(默认是启用的)在减少数据方面非常有效。另一个可用于缩短恢复时间的功能是待命副本( standby replica ),这个选项是可选的,但在生产环境中建议开启。
以 Kafka Streams 为例,应用程序实例可以被配置成其他实例的被动数据副本。在发生故障时,应用程序实例的任务被迁移到另一个已经包含了原有数据副本的实例上,这就极大地加快了恢复速度。不过,待命副本也有缺点,因为它增加了应用程序实例和 Kafka 代理之间的网络通信,而对于应用程序来说,因为增加了额外的数据副本,本地存储消费也随之增加。
待命副本默认是禁用的:
最后,我想分享一个容量规划技巧:在规划本地数据存储容量时,不要忘了考虑弹性和容错能力需要额外的空间,因为 stream 任务及其相关的表分区可能会在 Kafka Streams 应用程序实例或 ksqlDB 服务器之间移动。如果预期的本地表数据为 50GB,并且有 5 个应用程序实例,那么每个应用程序只分配 10GB 空间是不够的,如果这样的话,应用程序就没有办法在其他实例发生故障时接管它们的工作。
分区和并行处理
Kafka 的并行处理程度是由输入数据的分区数决定的,不管是流、表还是主题。如果有 20 个输入分区,那么就会有 20 个 stream 任务。也就是说,你可以运行 20 个 Kafka Streams 应用程序实例(或者一个包含 20 个服务器节点的 ksqlDB 集群),然后这些任务均匀地分配给这些实例。其他多余的应用程序实例将会空闲。
并行处理度不会超过输入分区的数量 :
这是 ksqlDB 的实例代码:
gist.github.com/confluentgi… 。
解决数据倾斜问题
在进行并行处理时,可能会遇到这种情况:有些 stream 任务需要处理的数据很多,有些则很少。我们通过监控相关的指标(例如消费延迟)就可以知道是否发生了这种情况。
Confluent Control Center 的指标监控 :
这是系列文章的最后一篇。在本系列文章中,我们先是介绍了基础元素——事件、流和表,然后了解了 Kafka 的存储层,然后是 Kafka 的处理层,还介绍了 ksqlDB 和 Kafka Streams。最后,我们探讨了这些应用程序的弹性和容错能力是如何实现的。