你可能用错了 kafka 的重试机制( 三 )


它忽略了不同类型的错误第一个问题是 , 它没有考虑到导致事件消费失败的两大原因:可恢复错误和不可恢复错误 。
可恢复错误指的是 , 如果我们多次重试 , 这些错误最终将得以解决 。 一个简单的示例是将数据保存到数据库的消费者 。 如果数据库暂时不可用 , 那么当下一条消息通过时 , 消费者将失败 。 一旦数据库再次变得可用 , 消费者就能够再次处理该消息 。
从另一个角度来看:可恢复错误指的是那些根源在消息和消费者外部的错误 。 解决这种错误后 , 我们的消费者将继续前进 , 好像无事发生一样 。 (很多人在这里被弄糊涂了 。 “可恢复”一词并不意味着应用程序本身——在我们的示例中为消费者——可以恢复 。 相反 , 它指的是某些外部资源——在此示例中为数据库——会失败并最终恢复 。 )
关于可恢复错误需要注意的是 , 它们将困扰主题中的几乎每一条消息 。 回想一下 , 主题中的所有消息都应遵循相同的架构 , 并代表相同类型的数据 。 同样 , 我们的消费者将针对该主题的每个事件执行相同的操作 。 因此 , 如果消息A由于数据库中断而失败 , 那么消息B、消息C等也将失败 。
不可恢复错误指的是无论我们重试多少次都将失败的错误 。 例如 , 消息中缺少字段可能会导致一个NullPointerException , 或者包含特殊字符的字段可能会使消息无法解析 。
与可恢复错误不同 , 不可恢复错误通常会影响单个孤立消息 。 例如 , 如果只有消息A包含不可解析的特殊字符 , 则消息B将成功 , 消息C等也将成功 。
与可恢复错误不同 , 解决不可恢复错误意味着我们必须修复消费者本身(永远不要“修复”消息本身——它们是不可变的记录!)例如 , 我们可能会修复消费者以便正确处理空值 , 然后重新部署它 。
那么 , 这与重试主题解决方案有什么关系?
对于初学者来说 , 它对可恢复错误不是特别有用 。 请记住 , 在解决外部问题之前 , 可恢复错误将影响每一条消息 , 而不仅仅是当前的一条消息 。 因此可以肯定的是 , 将失败的消息分流到重试主题将为下一条消息清理出通道 。 但接下来的消息也将失败 , 下一条以及再下一条也将失败 。 我们最好还是让消费者自己重试 , 直到问题解决为止 。
不可恢复的错误呢?重试队列可以在这些情况下提供帮助 。 如果一条麻烦的消息阻止了所有后续消息的消费 , 那么毫无疑问 , 分流该消息肯定会为我们的用户消费清除障碍(当然 , 多个重试主题是没必要的) 。
但是 , 虽然重试队列可以帮助受不可恢复错误困扰的消息消费者继续前进 , 但它也可能带来更多隐患 。 下面我们就进一步分析背后的原因 。
它会忽略排序我们简要回顾一下跨边界事件发布的一些重要环节 。 在有界上下文中处理一条命令后 , 我们会将一个对应的事件发布到一个Kafka主题 。 重要的是 , 我们会将聚合的ID指定为分区键 。
为什么这很重要?它确保的是对任何给定聚合的更改都会发布到同一分区 。
好吧 , 那这一点为什么会那么重要呢?当事件发布到同一分区时 , 可以保证各个事件按照它们发生的顺序进行处理 。 如果对同一聚合进行连续更改 , 并且所产生的事件发布到不同的分区 , 就可能发生争用状况 , 也就是消费者在消费第一个更改之前就消费了第二个更改 。 这会导致数据不一致 。
我们举个简单的例子 。 我们的User有界上下文提供了一个允许用户更改其名称的应用程序 。 一位用户将他的名字从Zoey更改为Zo? , 然后立即又更改为Zoiee 。 如果我们不管排序 , 则某个下游消费者(例如Login有界上下文)可能会先处理对Zoiee的更改 , 然后不久用Zo?覆盖它 。
现在 , 登录数据与我们的用户数据已经不同步了 。 更麻烦的是 , 每当Zoiee登录我们的网站时都会看到“欢迎光临 , Zo?!”的登录提示 。
这才是重试主题真正出问题的地方 。 它们让我们的消费者容易打乱处理事件的顺序 。 如果一个消费者在处理Zo?更改时受到某个临时的数据库中断的影响 , 它会把这个消息分流到一个重试主题 , 稍后再尝试 。 如果在Zoiee更改到达时数据库中断已得到纠正 , 则这条消息将先被成功处理 , 然后再由Zo?更改覆盖 。
为了说明问题 , 这里用了Zoiee/Zo?这样一个简单的示例 。 实际上 , 乱序处理事件可能导致会各种各样的数据损坏问题 。 更糟糕的是 , 这些问题很少会在一开始就被注意到 。 相反 , 它们所导致的数据损坏往往在一段时间内都不会引起注意 , 但损坏程度会随着时间的推移而增长 。 一般来说 , 当我们意识到发生了什么事情时 , 已经有大量数据受到影响了 。