流处理中的状态
最近要为自己开发的流处理引擎设计状态存储,在流处理当中,状态存储的关键点,就是要将事件源的消费指针与流处理的计算状态作为同一个事务进行操作,保障两者之间的一致性,只有这样,才能够通过事件重放来实现系统整体的高可用。
基于这个指导前提,架构设计的主要工作就成了选择事件源和状态存储。对于事件源的选择,并不需要太多纠结,Kafka已经成为了这方面的标准;而对于状态存储,可选择的方案就有多种,并且不同的方案会对系统的延迟和运维成本造成很大的影响。对于流处理引擎的实现,最好能够通过抽象来与具体的方案隔离,并将选择权交给最终面对具体业务的用户。
本地存储 + 分布式文件系统
一种目前广受采用的存储方案,就是本地存储 + 分布式文件系统。本地存储需要提供高性能读写,并且支持事务(至少需要原子的批量写入),当前常见的选择便是RocksDB,在流处理引擎中,所有对状态的读写操作都是直接通过RocksDB。但由于RocksDB本身并不支持高可用,因此,就需要借助分布式文件系统,比如JuiceFS,来实现高可用,方法就是将RocksDB周期性地增量备份至分布式文件系统当中。
当本地存储的数据惨遭破坏,或者是分区要被迁移到另一个节点,那么就可以通过分布式文件系统中的备份来恢复本地存储,然后事件源会基于最新的可用备份进行重放,状态也就会被还原,整个系统从而优雅地实现无缝运转。
尽管RocksDB本身提供了持久化功能,但在这里最好将其看作是一个临时状态的缓冲层,并不为持久化做担保。如果规模允许,不使用RocksDB而是采用内存快照其实也是完全允许的,RocksDB在这里真正的作用是让状态的存储规模可以超越内存,而非是提供持久化。在此方案中,真正为持久化兜底的,实际上是事件源和分布式文件系统。
优点
该架构最大的优点就是可以让状态的读写性能达到最大化,在不出故障的情况下,状态的读写操作将完全不依赖于网络。另外,对于流处理引擎自身的设计而言,也无需担心状态的一致性和可用性,从而避免了分布式系统设计中那些最让人头疼的纠结,大幅降低了实现的复杂度。
缺点
数据恢复的延迟非常大
如果一旦出现故障,或者要进行集群内的rebalance,那么数据恢复的延迟将是非常大的:
- 首先需要考虑到增量备份是无法实时进行的,而是需要周期,并且周期往往是分钟级别,而备份的间隔周期越长,就意味着在恢复时需要重放的数据量越大。
- 将备份写入到分布式文件系统需要耗时,这部分耗时取决于增量备份的数据量以及分布式文件系统的写入性能,备份写入耗时过多,也同样意味着在恢复时需要重放大量的数据。
- 执行恢复操作时,需要将备份从分布式文件系统下载到本地文件系统。
- RocksDB在执行恢复操作时,需要对SST文件进行大量的校验,并重建本地数据库,如果数据量大,这部分也相当耗时。
- 事件源需要进行回放,直到追赶上最新的事件,这部分取决于上述操作总共消耗的时间以及当前业务的流量。
要降低恢复的延迟,主要有两个办法:
- 将整个系统划分为非常多的小分区,每个分区拥有独立的状态存储,并且每个分区严格规定容量上限,避免单个分区拥有过多的数据,一旦达到了上限,就不要再为该分区分配新的任务,而是扩展新的分区。这意味着事件源和流处理引擎都需要支持动态分区,或者是对分区进行预分配,流处理引擎要承担分区所带来的新的复杂度,比如最棘手的事件路由和顺序问题。
- 提升分布式文件系统的读写性能:提升写性能,意味着可以缩短备份的周期和耗时;提升读性能,意味着可以减少恢复的耗时。
在这两点上不断进行优化,就能够将延迟控制在一个可接受的范围。
抖动带来的不确定性
将计算和存储放在同一个节点,尽管能使得IO吞吐最大化并简化设计,但两者也不可避免会有资源争夺,尤其是对于RocksDB,在其进行增量备份以及Compaction时,都会消耗大量的CPU资源,从而造成系统抖动。这种抖动在资源匮乏和流量峰值时,可能会引起连锁反应。要应对这种抖动,需要在上线前对生产环境模拟测量,并对RocksDB进行精心配置,但RocksDB一向以配置和优化复杂著称。
适合场景
纯实时计算
可以看到,这种架构最大的“软肋”是数据恢复时的延迟,因此,最适合的场景就是纯实时计算。计算场景对于延迟往往要求不高,另外,计算场景也是天然支持通过重放事件来实现真正的"Exactly Once"语义的。
能够容忍偶尔延迟的业务
对于非计算类型的业务,这种架构其实也有很大的“诱惑”,无论是在性能还是运维成本方面。在性能方面,它有非常大的调优空间,性能可以被“压榨”到极致,极大降低业务延迟;而在运维成本方面,它仅依赖分布式文件系统,现在不同以往,使用像JuiceFS这样的云原生文件系统,可能要比维护分布式数据库的成本要低几个数量级。
对于开发者,重要的是,要搞清楚两个问题:
- 事件重放会对业务逻辑产生怎样的影响?尤其是如果你的业务中包含着副作用?
- 如果数据恢复不可避免地发生,它产生的延迟(往往是分钟级的延迟),最坏的影响是什么?能否通过额外手段进行补偿?
如果我们对以上问题很有信心,那么就可以放心地去采用这种架构。仅使用一些简单的组件,就构建起了高性能、可扩展、高可用、事务性的分布式系统。