各大分布式系统如何实现EOS(exactly-once)

背景

现代很多分布式系统都实现了,EOS的数据处理、消息投递语义。EOS可以避免消息重复处理引发的一些数据问题,在一些场景下是是十分有用的。今天根据不同的分布式系统类别,分别讨论下他们如何是实现EOS的。

分布式数据库

由于数据库本身在事务实现方面的长期积累,分布式数据库通过实现数据库领域的分布式事务,即可达成EOS的处理语义。这里的关键就是实现分布式事务,现代分布式数据库实现分布式事务主流方式是依赖共识算法、改进的2PC(类似precolator采用更细粒度的锁控制避免锁范围太大的问题)。关于数据库分布式事务其他实现方式有兴趣的也可以参考:分布式事务的几种实现模式

消息系统

Kafka

主要依赖以下两种方式:

  • 幂等写入:这个其实比较容易想到,如果写入的接收端本身是支持幂等写入的,那么自然也就满足了EOS。Kafka给每个生产者发送的消息分配一个单调递增的ID,通过过滤重复的producer id确保幂等写入。现在新版的Kafka也是支持多分区幂等,但是仍然只能支持单个会话幂等,会话重启后TC会根据时序epoch分配新的ID.
    • 优点:简单易用,实现起来容易(因为不需要协调生产者和消费者)并且开销很小
    • 缺点:这个EOS仅针对生产者这一端,最大问题是消费端仍然不能保证消费是EOS的,除非自己实现一个EOS consumer来管理中间状态,做一致性提交。
  • 事务写入:类似数据库的实现,发送消息后需要提交才生效。消费者可以隔离级别设定成RC即可。注意Kafka事务是针对单个producer实例的。Kafka基于事务的EOS实现主要依赖事务型producer和consumer以及server的transaction log topic。
    • 优点:生产者和消费者端都可以达成EOS。Kafka实现提供了事务型生产者和事务性消费者。配合server侧的存储事务元数据的内部topic可以达成端到端的EOS。
    • 缺点:实现会有一些复杂度,例如消费需要通过控制消息来感知消息的提交状态并且移动offset。此外server侧的这些元数据也会影响整体server的性能。

Pulsar

总体也是实现了幂等写入和事务写入两种模式,思路上没有太大的差别。事务实现EOS也是基于TC和transaction log topic。主要差别在于:

  • Pulsar在消费消息上性能更好:这个主要是因为Kafka是基于pull模型的(吞吐量优先),而pulsar是基于push模型的。pulsar可以将一个分区内的数据并发的push给多个消费者,不像Kafka只能一个消费者去消费一个分区中事务内的消息,有对头阻塞的问题。

RocketMQ

EOS语义实现

RocketMQ的java SDK是支持EOS的。本质上也是基于事务实现EOS,不过事务型的消费者需要用户自己创建一个消费事务表然后强依赖一个支持本地事务的数据库,相对重些。核心是基于事务,然后同时在生产者和消费者端做好EOS控制。

区分事务消息功能和EOS

RocketMQ一个比较有名的特性是事务消息功能,可以用来做跨系统的分布式事务。这个事务消息功能和实现EOS是两个东西,切勿弄混。事务消息主要是配合生产者本地执行以及RocketMQ自己的半事务消息实现的。

基因决定结果

Kafka、Pulsar、RocketMQ实现EOS上思路是类似的,但是一些性能和使用上的差别其实本质是由于他们基因不同导致。消息系统基因可以从消费模型、存储模型、计算存储架构几个角度来看。

  • 计算存储架构:像rocketMQ、kafka都是将topic、分区、broker等概念强关联存储的(例如数据存储在broker节点上),也就意味着计算存储强耦合。而pulsar是计算存储分离的架构。计算存储分离的架构在功能拓展上会更加灵活,消费模型上实现push也比较容易(关键是消费者变化时,负载均衡的职责重点再server还是consumer group)。
  • 消费模型:push和pull。Kafka基于pull和offset这套体系,有更好的吞吐。像pulsar基于push模型,处理好backpressure,我理解是比pull具备更好的延迟、消费性能的(可以并发push)。基于push相当于很多控制主动权就在server了。这个消费模型的差异自然也会导致大家同样思路实现的EOS能力上有一些差异。RocketMQ初期不是基于存储计算分离架构做的,实现push不太好弄,所以本质是个pull模型,但是为了有更好的低延迟表现,通过consumer短轮询实现了一个“伪push”,本质就是拉的勤快点。
  • 存储模型:存储模型主要差别就是log based还是index based。Kafka面向吞吐,logbased利用partition和offset有很好的性能。基于 Index based的好处没有将物理存储强依赖功能逻辑。基于Index-based的存储可以做更多功能特性。Kafka由于物理分区文件和他运行时能力强绑定了,功能扩展上就容易受限了。RocketMQ的存储模型设计是借鉴了Kafka的,本质上也是log based,但是数据存储上有所不同,没有像Kafka一样按照分区存储配合offset,而是用commit log混合存储再配合consumer queue文件来管理消费状态,好处是更加彻底利用顺序写的优势。[3]中的图表可供参考。

image.png

流计算

同理,类似MQ,实现端到端的EOS,需要整个端到端链路上涉及的组件均要支持EOS.对于Flink来说就是:

  • Source: 可以利用Flink提供的2pc接口或者自己实现事务型source
  • Flink Server: 内部利用checkpoint机制把状态存盘,Flink将数据流分成多个分区,每个分区都有一个Checkpoint Coordinator。Checkpoint Coordinator负责协调分区之间的状态同步,以保证所有分区的状态都能被恢复。执行ckpt的时候会STW。事务提交可以做一次ckpt,失败的话根据上一次ckpt做rollback
  • Sink: 可以依赖flink提供的2pc接口或者自己实现事务型sink

Kafka Stream

Kafka stream是依托于Kafka本身而存在的,所以实现EOS上也依赖KAFKA本身的EOS能力。

RocketMQ Stream

也依赖RocketMQ,不过作用是拿RocketMQ作外部存储(这点和Kafka Stream完全依赖Kafka不同),他自己基于checkpoint也有实现EOS。

参考资料