面试官问 kafka 重试机制原理.docx
- 文档编号:9869309
- 上传时间:2023-02-07
- 格式:DOCX
- 页数:14
- 大小:223.22KB
面试官问 kafka 重试机制原理.docx
《面试官问 kafka 重试机制原理.docx》由会员分享,可在线阅读,更多相关《面试官问 kafka 重试机制原理.docx(14页珍藏版)》请在冰豆网上搜索。
面试官问kafka重试机制原理
面试官问:
kafka重试机制原理
阅读本文的读者应当都对Kafka有所了解。
网上也有一些引见Kafka及其使用方法的深度文章。
话虽如此,我们这里还是先简要回顾一下对我们的争辩很重要的一些概念。
大事日志、发布者和消费者
Kafka是用来处理数据流的系统。
从概念上讲,我们可以认为Kafka包含三个基本组件:
∙一个大事日志(EventLog),消息会发布到它这里
∙发布者(Publisher),将消息发布到大事日志
∙消费者(Consumer),消费(也就是使用)大事日志中的消息
与RabbitMQ之类的传统消息队列不同,Kafka由消费者来打算何时读取消息(也就是说,Kafka接受了拉取而非推送模式)。
每条消息都有一个偏移量(offset),每个消费者都跟踪(或提交)其最近消费消息的偏移量。
这样,消费者就可以通过这条消息的偏移量恳求下一条消息。
主题
大事日志分为几个主题(topic),每个主题都定义了要发布给它的消息类型。
定义主题是我们这些工程师的责任,所以我们应当记住一些阅历法则:
∙每个主题都应描述一个其他服务可能需要了解的大事。
∙每个主题都应定义每条消息都将遵照的一个独一模式(schema)。
分区和分区键
主题被进一步细分为多个分区(partition)。
分区使消息可以被并行消费。
Kafka允许通过一个**分区键(partitionkey)**来确定性地将消息安排给各个分区。
分区键是一段数据(通常是消息本身的某些属性,例如ID),其上会应用一个算法以确定分区。
这里,我们将消息的UUID字段安排为分区键。
生产者应用一种算法(例如依据分区数修改每个UUID值)来将每条消息安排给一个分区。
以这种方式使用分区键,使我们能够确保与给定ID关联的每条消息都会发布到单个分区上。
还需要留意的是,可以将一个消费者的多个实例部署为一个消费者组。
Kafka将确保给定分区中的任何消息将一直由组中的同一消费者实例读取。
在微服务中使用Kafka
Kafka格外强大。
所以它可用于多种环境中,涵盖众多用例。
在这里,我们将重点引见微服务架构中最常见的用法。
跨有界上下文传递消息
当我们刚开头构建微服务时,我们很多人一开头接受的是某种中心化模式。
每条数据都有一个驻留的单一微服务(即单一真实来源)。
假如其他任何微服务需要访问这份数据,它将发起一个同步调用以检索它。
这种方法导致了很多问题,包括同步调用链较长、单点毛病、团队自主权下降等。
最终我们找到了更好的方法。
在今日的成熟架构中,我们将通信分为命令处理和大事处理。
命令处理通常在单个有界上下文中执行,并且往往还是会包含同步通信。
另一方面,大事通常由一个有界上下文中的服务发出,并异步发布到Kafka,以供其他有界上下文中的服务消费。
左侧是我们以前设计微服务通信的方式:
一个有界上下文(由虚线框表示)中的服务从其他有界上下文中的服务接收同步调用。
左边是我们如今的做法:
一个有界上下文中的服务发布大事,其他有界上下文中的服务在本人空闲时消费它们。
例如,以一个User有界上下文为例。
我们的User团队会构建担任启用新用户、更新现有用户帐户等任务的应用程序和服务。
创建或修改用户帐户后,UserAccount服务会将一个相应的大事发布到Kafka。
其他感爱好的有界上下文可以消费该大事,将其存储在本地,使用其他数据添加它,等等。
例如,我们的Login有界上下文可能想晓得用户的当前名称,以便在登录时向他们致意。
我们将这种用例称为跨边界大事发布。
在执行跨边界大事发布时,我们应当发布聚合(Aggregate)。
聚合是自包含的实体组,每个实体都被视为一个单独的原子实体。
每个聚合都有一个“根”实体,以及一些供应附加数据的从失实体。
当管理聚合的服务发布一条消息时,该消息的负载将是一个聚合的某种表示方式(例如JSON或Avro)。
重要的是,该服务将指定聚合的独一标识符作为分区键。
这将确保对任何给定聚合实体的更改都将发布到同一分区。
出问题的时候怎样办?
虽然Kafka的跨边界大事发布机制显得相当优雅,但到底这是一个分布式系统,因而系统可能会有很多错误。
我们将关注或许是最常见的恼人问题:
消费者可能无法成功处理其消费的消息。
图片
我们现在该怎样办?
确定这是一个问题
团队做错的第一件事就是根本没无意识到这是一个潜在的问题。
消息失败时有发生,我们需要制定一种策略来处理它……要未雨绸缪,而非亡羊补牢。
因而,了解这是一种迟早会发生的问题并设计针对性的处理方案是我们要做的第一步。
假如我们做到了这一点,就应当向本人表示一点庆祝。
现在最大的问题仍旧存在:
我们该如何处理这种情况?
我们不能一直重试那条消息吗?
默认情况下,假如消费者没有成功消费一条消息(也就是说消费者无法提交当前偏移量),它将重试同一条消息。
那么,莫非我们不能简约地让这种默认行为接管一切,然后重试消息直到成功吗?
问题是这条消息可能永久不会成功。
至少,没有某种方式的手动干涉它是不会成功的。
于是乎,消费者就永久不会连续处理后续的任何消息,并且我们的消息处理将陷入顺境。
好吧,我们不能简约地跳过那条消息吗?
我们通常允许同步恳求失败。
例如,对我们的UserAccount服务所做的一个“create-user”POST可能包含错误或丢失的数据。
在这种情况下,我们可以简约地前往一个错误代码(例如HTTP400),然后要求调用方重试。
虽然这种方法并不不抱负,但这不会对我们的数据完整性形成任何长期问题。
那个POST代表一条命令,是还没有发生的事情。
即便我们让它失败,我们的数据也将保持全都形态。
当我们丢弃消息时情况并非如此。
消息表示已经发生的大事。
任何忽视这些大事的消费者都将与生成大事的上游服务不再同步。
全部这些都表明,我们不想丢弃消息。
那么我们如何处理这个问题呢?
对我们来说这不是什么简约处理的问题。
因而,一旦我们生疏到它需要处理,就可以向互联网询问处理方案。
但这引出了我们的其次个问题:
网上有一些我们可能不应当遵照的建议。
重试主题:
流行的处理方案
你会发觉最受欢迎的一种处理方案就是重试主题(retrytopics)的概念。
具体细节因实现而异,但总体概念是这样的:
∙消费者尝试消费次要主题中的一条消息。
∙假如未能正确消费该消息,则消费者将消息发布到第一个重试主题,然后提交消息的偏移量,以便连续处理下一条消息。
∙订阅重试主题的是重试消费者,它包含与主消费者相同的规律。
该消费者在消息消费尝试之间引入了短暂的延迟。
假如这个消费者也无法消费该消息,则会将该消息发布到另一个重试主题,并提交该消息的偏移量。
∙这一过程连续,并添加了一些重试主题和重试消费者,每个重试的延迟越来越多(用作退避策略)。
最终,在最终重试消费者无法处理某条消息后,该消息将发布到一个死信队列(DeadLetterQueue,DLQ)中,工程团队将在该队列中对其进行手动分类。
概念上讲,重试主题模式定义了失败的消息将被分流到的多个主题。
假如次要主题的消费者消费了它无法处理的消息,它会将该消息发布到重试主题1并提交当前偏移量,从而将本身释放给下一条消息。
重试主题的消费者将是主消费者的副本,但假如它无法处理该消息,它将发布到一个新的重试主题。
最终,假如最终一个重试消费者也无法处理该消息,它将把该消息发布到一个死信队列(DLQ)。
问题出在哪里?
看起来这种方法好像很合理。
实际上,它在很多用例中都能正常工作。
问题在于它不能充当一种通用处理方案。
现实中存在一些特殊用例(例如我们的跨边界大事发布),对于这些用例来说,这种方法实际上是危急的。
它忽视了不同类型的错误
第一个问题是,它没有考虑到导致大事消费失败的两大缘由:
可恢复错误和不行恢复错误。
可恢复错误指的是,假如我们多次重试,这些错误最终将得以处理。
一个简约的示例是将数据保存到数据库的消费者。
假如数据库临时不行用,那么当下一条消息通过时,消费者将失败。
一旦数据库再次变得可用,消费者就能够再次处理该消息。
从另一个角度来看:
可恢复错误指的是那些根源在消息和消费者外部的错误。
处理这种错误后,我们的消费者将连续前进,好像无事发生一样。
(很多人在这里被弄糊涂了。
“可恢复”一词并不意味着应用程序本身——在我们的示例中为消费者——可以恢复。
相反,它指的是某些外部资源——在此示例中为数据库——会失败并最终恢复。
)
关于可恢复错误需要留意的是,它们将困扰主题中的几乎每一条消息。
回想一下,主题中的全部消息都应遵照相同的架构,并代表相同类型的数据。
同样,我们的消费者将针对该主题的每个大事执行相同的操作。
因而,假如消息A由于数据库中缀而失败,那么消息B、消息C等也将失败。
不行恢复错误指的是无论我们重试多少次都将失败的错误。
例如,消息中缺少字段可能会导致一个NullPointerException,或者包含特殊字符的字段可能会使消息无法解析。
与可恢复错误不同,不行恢复错误通常会影响单个孤立消息。
例如,假如只要消息A包含不行解析的特殊字符,则消息B将成功,消息C等也将成功。
与可恢复错误不同,处理不行恢复错误意味着我们必需修复消费者本身(永久不要“修复”消息本身——它们是不行变的记录!
)例如,我们可能会修复消费者以便正确处理空值,然后重新部署它。
那么,这与重试主题处理方案有什么关系?
对于初学者来说,它对可恢复错误不是特殊有用。
请记住,在处理外部问题之前,可恢复错误将影响每一条消息,而不只仅是当前的一条消息。
因而可以确定的是,将失败的消息分流到重试主题将为下一条消息清理出通道。
但接下来的消息也将失败,下一条以及再下一条也将失败。
我们最好还是让消费者本人重试,直到问题处理为止。
不行恢复的错误呢?
重试队列可以在这些情况下供应挂念。
假如一条麻烦的消息阻挠了全部后续消息的消费,那么毫无疑问,分流该消息确定会为我们的用户消费清除妨碍(当然,多个重试主题是没必要的)。
但是,虽然重试队列可以挂念受不行恢复错误困扰的消息消费者连续前进,但它也可能带来更多隐患。
下面我们就进一步分析背后的缘由。
它会忽视排序
我们简要回顾一下跨边界大事发布的一些重要环节。
在有界上下文中处理一条命令后,我们会将一个对应的大事发布到一个Kafka主题。
重要的是,我们会将聚合的ID指定为分区键。
为什么这很重要?
它确保的是对任何给定聚合的更改都会发布到同一分区。
好吧,那这一点为什么会那么重要呢?
当大事发布到同一分区时,可以保证各个大事依据它们发生的挨次进行处理。
假如对同一聚合进行连续更改,并且所产生的大事发布到不同的分区,就可能发生争用情况,也就是消费者在消费第一个更改之前就消费了其次个更改。
这会导致数据不全都。
我们举个简约的例子。
我们的User有界上下文供应了一个允许用户更改其名称的应用程序。
一位用户将他的名字从Zoey更改为Zoë,然后马上又更改为Zoiee。
假如我们不管排序,则某个下游消费者(例如Login有界上下文)可能会先处理对Zoiee的更改,然后不久用Zoë掩盖它。
现在,登录数据与我们的用户数据已经不同步了。
更麻烦的是,每当Zoiee登录我们的网站时都会看到“欢迎光临,Zoë!
”的登录提示。
这才是重试主题真正出问题的地方。
它们让我们的消费者简约打乱处理大事的挨次。
假如一个消费者在处理Zoë更改时遭到某个临时的数据库中缀的影响,它会把这个消息分流到一个重试主题,稍后再尝试。
假如在Zoiee更改到达时数据库中缀已得到订正,则这条消息将先被成功处理,然后再由Zoë更改掩盖。
为了说明问题,这里用了Zoiee/Zoë这样一个简约的示例。
实际上,乱序处理大事可能导致会各种各样的数据损坏问题。
更蹩脚的是,这些问题很少会在一开头就被留意到。
相反,它们所导致的数据损坏往往在一段时间内都不会引起留意,但损坏程度会随着时间的推移而增长。
一般来说,当我们意识到发生了什么事情时,已经有大量数据遭到影响了。
重试主题什么时候可行?
需要明确的是,重试主题并非一直都是错误的模式。
当然,它也存在一些合适的用例。
具体来说,当消费者的工作是收集不行修改的记录时,这种模式就很不错。
这样的例子可能包括:
∙处理网站活动流以生成报告的消费者
∙将买卖添加到分类账的消费者(只需这些买卖用不着按特定挨次跟踪)
∙正在从另一个数据源ETL数据的消费者
这类消费者可能会从重试主题模式中受益,同时没有数据损坏的风险。
不过,请留意
即便存在这种用例,我们仍应谨慎行事。
构建这样的处理方案既简单又耗时。
因而,作为一个组织,我们不想为每个新的消费者编写一个新的处理方案。
相反,我们要创建一个统一的处理方案,比如一个库或一个容器等,可以在各种服务之间反复使用。
还存在另一个问题。
我们可能会为相关消费者构建一个重试主题的处理方案。
不幸的是,不久之后,这个处理方案就会进入跨边界大事发布消费者的领域了。
拥有这些消费者的团队可能没无意识到风险的存在。
正如我们前面所争辩的那样,在发生严重数据损坏之前,他们可能不会意识到任何问题。
因而,在实现重试主题处理方案之前,我们应100%确定:
∙我们的业务中永久不会有消费者来更新现有数据,或者
∙我们拥有严格的把握措施,以确保我们的重试主题处理方案不会在此类消费者中实现
我们如何改善这种模式?
鉴于重试主题模式可能不是跨边界大事发布消费者的可接受处理方案,我们能否可以对其做一些调整来改善它呢?
一开头,本文想要供应一种完整的处理方案。
但之后我意识到,并不存在什么万能的路径。
因而,我们将只争辩一些在制定合适处理方案时需要考虑的事项。
衰退错误类型
假如我们能够在可恢复错误和不行恢复错误之间衰退歧义,生活就会变得轻松很多。
例如,假如我们的消费者开头遇到可恢复错误,那么重试主题就变得多余了。
因而,我们可以尝试确定所遇到的错误类型:
void processMessage(KafkaMessage km) {
try {
Message m = km.getMessage();
transformAndSave(m);
} catch (Throwable t) {
if (isRecoverable(t)) {
// ...
} else {
// ...
}
}
}
在上面的Java伪代码示例中,isRecoverable()将接受一种白名单方法来确定t能否表示可恢复错误。
换句话说,它检查t以确定它能否与任何已知的可恢复错误(例如SQL连接错误或ReST客户端超时)相婚配,假如婚配则前往true,否则前往false。
这样就能防止我们的消费者被不行恢复错误一直堵塞下去。
诚然,要在可恢复错误和不行恢复错误之间衰退歧义可能很困难。
例如,一个SQLException可能指的是一次数据库毛病(可恢复)或一次约束违反情况(不行恢复)。
如有疑问,我们可能应当假设错误是不行恢复的——为此要冒的风险是将其他好的消息发送给隐蔽主题,从而延迟它们的处理……但这也能避开我们无意间陷入泥潭,无休止地尝试处理不行恢复错误。
在消费者内重试可恢复错误
正如我们所争辩的那样,存在可恢复错误时,将消息发布到重试主题毫无意义。
我们只会为下一条消息的失败扫清道路。
相反,消费者可以简约地重试,直到条件恢复。
当然,消灭可恢复错误意味着外部资源存在问题。
我们不断对这块资源发送恳求是无济于事的。
因而,我们期望对重试应用一个退避策略。
我们的伪Java代码现在可能看起来像这样:
void processMessage(KafkaMessage km) {
try {
Message m = km.getMessage();
transformAndSave(m);
} catch (Throwable t) {
if (isRecoverable(t)) {
doWithRetry(m, Backoff.EXPONENTIAL, this:
:
transformAndSave);
} else {
// ...
}
}
}
(留意:
我们使用的任何退避机制都应配置为在达到某个阈值时向我们发出警报,并通知我们潜在的严峻错误)
遇到不行恢复错误时,将消息直接发送到最终一个主题
另一方面,当我们的消费者遇到不行恢复错误时,我们可能期望马上隐蔽(stash)该消息,以释放后续消息。
但在这里使用多个重试主题会有用吗?
答案能否定的。
在转到DLQ之前,我们的消息只会经受n次消费失败而已。
那么,为什么不从一开头就将消息粘贴在那里呢?
与重试主题一样,这个主题(在这里,我们将其称为隐蔽主题)将拥有本人的消费者,其与主消费者保持全都。
但就像DLQ一样,这个消费者并不总是在消费消息;它只要在我们明确需要时才会这么做。
考虑排序
来看看排序的情况。
我们在这里重用之前的“用户/登录”示例。
尝试处理Zoë名称中的ë字符时,Login消费者可能会遇到错误。
消费者将其识别为一个不行恢复错误,将消息放在一边,然后连续处理后续消息。
不久之后,消费者将获得Zoiee消息并成功处理它。
Zoë消息已隐蔽,并且Zoiee消息现在已成功处理完毕。
目前,两个有界上下文之间的数据是全都的。
晚些时候,我们的团队会修复消费者,以便其可以正确处理特殊字符并重新部署它。
然后,我们将Zoë消息重新发布给消费者,消费者现在可以正确处理该消息了。
当更新的消费者随后处理隐蔽的Zoë消息后,两个有界上下文之间的数据将变得不全都。
因而,当User有界上下文将用户视为Zoiee时,Login有界上下文会将她称为Zoë。
明显,我们没有保持排序;Zoë是在Zoiee之前由Login消费者处理的,但正确的挨次是倒过来的。
隐蔽一条消息后,我们可以开头隐蔽全部消息,但在那种情况下我们实际上会陷入顺境。
侥幸的是,我们不需要保持全部消息的挨次,只需考虑与单个聚合相关联的消息即可。
因而,假如我们的消费者可以跟踪已隐蔽的特定聚合,它就可以确保属于同一聚合的后续消息也被隐蔽。
收到隐蔽主题中消息的警报后,我们可以取消部署消费者并修复其代码(请留意:
切勿修改消息本身;消息代表不行变的大事!
)在修复并测试了我们的消费者之后,我们可以重新部署它。
当然,在连续使用次要主题之前,我们将需要特殊留意先处理隐蔽主题中的全部记录。
这样,我们将连续保持正确的排序形态。
出于这个缘由,我们将首先部署隐蔽消费者,并且只要在其完成时(这意味着消费者组中的全部实例都完成,假如我们使用了多个消费者),我们才会取消部署它并部署主消费者。
我们还应当考虑以下现实:
固定的消费者处理了隐蔽消息后,它仍可能会遇到其他错误。
在这种情况下,其错误处理行为应像我们之前描述的那样:
∙假如错误是可恢复的,则使用退避策略重试;
∙假如错误是不行恢复的,它将隐蔽消息并连续下一条消息。
为此,我们可以考虑使用其次个隐蔽主题。
可以接受一些数据不全都?
这样的系统构建起来可能会变得相当简单。
它们可能很难构建、测试和维护。
因而,某些组织可能会想要确定出数据不全都的可能性,并推断他们能否可以承受这种风险。
在很多情况下,这些组织可能会接受数据协调机制,以使他们的数据最终(是相对较长的“最终”)变得全都。
为此也存在很多策略(超出了本文的范围)。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 面试官问 kafka 重试机制原理 面试 重试 机制 原理
![提示](https://static.bdocx.com/images/bang_tan.gif)