Abstract
本文翻译自Error Handling Patterns for Apache Kafka Applications, 介绍了当kafka处理消息/事件失败时的处理模式.
模式1 失败时停止
针对有些场景是,事件必须按顺序处理. 例如从数据库采集CDC (Change Data Capture)变动数据流.
如下的图展示了源topic中的数据是如何处理或转到并发布到目标topic.如果在处理中遇到错误,应用必须停止并需要人工介入. 注意源topic中的事件没有任何其他的路径.
模式2 死信队列
这种模式是当应用无法处理时,将消息路由到另外一个error topic. 然后应用本身的流继续执行. 这种模式要求处理逻辑并不需要重复处理的过程. 换言之,一个事件要么被成功处理 要么路由到一个error topic.
下图展示了这种case, 可能发生的2种路径:
- 正常情况就是消息被处理并放到目标topic
- 当无法处理时(比如格式不对或者缺少属性),这样的消息被路由到error topic
模式3 重试topic和重试应用
如果应用处理消息时, 它依赖的条件并不满足时我们怎么办呢? 比如某应用用于处理购买商品请求. 商品的价格是由另外的一个应用处理, 并且可能在应用收到请求时,价格丢失了.(依赖缺失)
这时候,我们可以添加一个重试topic来处理那些依赖并不满足的消息(直到依赖满足). 与此同时继续处理其他正常满足条件的消息. 比如: 当价格信息不可用时, 你可以把这样的消息路由到retry topic. 可以周期性重试知道条件满足.
在容器化环境中,你可以专门部署1-2个实例来处理重试过程, 这种情况下异常case应该越少越好.
下图展示了这种case可能发生的3种路径:
- 正常情况下消息被正确处理并放到目标topic
- 对于无法处理的消息比如格式不对,被路由到error topic
- 对于那些缺少依赖数据的消息被路由到retry topic. 然后有一个专门的retry 引用来周期性尝试重新处理消息
可能发生的关联事件的乱序问题
如果使用模式3,这是我们必须要特殊重点考虑的case: 事件无法保证与源topic的顺序一致. 这是因为retry 的路径一般"更长更久"并且比正常执行路径更慢, 因为只有少量的retry实例app和需要retry的事件.
下图展示了这种问题: (事件1在目标topic中后于事件2收到). 这个模式需要结合业务确定这种case是否有问题.
模式4 保证重定向事件的顺序
在模式3中, 我们加入了一个retry topic来延迟处理那些缺少依赖的消息. 但是上面的模式有个问题就是目标topic中收到消息的顺序可能发生改变.
在某些情况下,顺序改变是不可接受的. 比如一个更新商品库存的应用如果被处理的顺序不一致可能处理商品被超卖的问题.
比如某商品目前库存10个.
请求1说要买10个
请求2说店家要增加10个库存
请求3说要买10个
如果请求3先于请求2被处理,那么便会发生问题. 最后的这个模式便用于解决上述问题.
在这个模式中,主应用需要维护每个被路由到retry topic的应用. 当某个依赖条件不满足时,(比如某个商品的价格不知道),主应用需要在内存中维护一个唯一的标志来标志该事件. 这个唯一标志应该按照业务逻辑归组(比如这个日志中,所有这个商品的事件应该被归于1组). 这可以帮助区分哪些事件与商品A相关,与商品A相关的事件也应该被路由到retry topic,以此来保证顺序.
当第一个不满足消息依赖的事件收到时,主应用执行如下任务:
- 记录这个消息唯一ID并放到内存中.
- 在该消息的header种加入该唯一ID. 在header种加可以避免对原消息做个事的改变. 另一个原因是,当retry app处理完时,可以方便的发布第一个tombstone消息.
- 发布该唯一ID到redirect topic.
当下一个消息收到时,主应用需要检查消息,如果发现该消息所属商品已经被路由到retry topic (从内存中查找), 那么这个消息也应该被路由到retry topic.
比如下面的例子, 主应用收到商品A的消息,发现该商品已经被路由到retry topic(比如商品A的价格暂时不知道), 那么这条消息并不会在主应用处理而是被路由到retry topic. 这保证了商品A的所有事件都被按照与收到消息时相同的顺序进行处理. 主应用也会给消息加上header并且在内存中记录该消息被发送到retry topic.
retry app会处理retry topic中的数据,并且这些消息的顺序是一致的.当某个事件呗正确重新处理,并且发布到target topic, retry app回发送一个tombstone事件到redirect topic. 每一个retry event都会发送一个tombstone事件.
主应用负责监听redirect topic中的tombstone事件. 这样主应用可以移除内存中的事件记录. 这样可以保证对于同一个商品的后续事件可以被正常的流程处理.
下图展示了完成的事件路径(如果某个事件缺少依赖的话): 主应用需要做如下处理:
- 读取源topic中的事件
- 检查本地存储来验证是否有在retry路径上的节点存在(对于有同一依赖的事件)
- 处理事件并发送到目标topic
错误恢复
当主应用crash或者异常退出时,主应用中的内存存储会消失. 然而这个可以很容易的通过读取redirect topic中的消息来重新初始化内存中的存储.
总结
错误处理和重试对于所有应用都非常重要. 对kafka也不例外. 本文提供的这些方法无法覆盖所有的场景, 但是希望可以对你的需求提供指导.