面试必问-kafka可靠性
前言
常规系统的架构设计过程中,必然是少不了会使用到消息中间件,作为中间件,其最直观的收益即是解耦、异步、削峰。本文仅关注Kafka。
Kafka是最初由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx日志、访问日志,消息服务等等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。
一、消息生产者
作为消息中间件,自然会有消息的生产和消费处理。下节以员工(E)向经理(M)提交工作汇报为例类比相关使用场景
1.最多一次(我发给你了啊)
当员工E有事需要向经理M汇报工作,于是他写好周报并通过邮件发给了经理,然后就下班回家了。而经理下班后,又找到E,询问周报为何没给我?员工满脸疑惑问道:我发给你了啊?
以上场景,周报丢了,也就是消息发送模式中的最多一次的概念。即消息者最多只会收到一次消息(特指同一消息)。
该逻辑适用于消息本身重要性可靠性不高场景,比如普通的日志信息,丢这么一两条无伤大雅。
在Kafka编码实践中,通过如下代码实现:
producer.send(msg)
2.最少一次(不回我就一直发)
经理提前交代M周报很重要,下班前一定要发给我。M机制地在周报邮件里加了句收到请回复,这样确保经理能收到他的邮件。可第一次发过去,经理说没收到,M又发了一次,这一次隔了好久没回复,没办法M又发一次,如些循环,终于收到了经理回复说收到了。M这才下班回家。这是消息发送模式中的最少一次的概念,即消息者保证会最少会收到一次消息(特指同一消息),也是常说的不丢消息。
该逻辑适用于消息本身重要性和可靠性很高且可以接受重复消息的场景。
而重复消息的消费处理,又引来接口幂等的控制与保证,思考一下,如果是银行扣费消息,在该模式下被扣了多次钱,除了银行谁也不愿意啊…
在Kafka编码实践中,通过如下代码实现,详细可以参考:
kafka重试机制解读
acks = all
设置retries为一个较大的值
producer.send(msg,callback)
当然重试多次仍可能失败,在callback回调中进行处理。
3.一次就好(你烦不烦)
在上节中,经理回复消息,可能由于网络或服务邮件服务器甚至发送端自己问题,导致没有收到该回复,重复发送了周报,为此收到多次周报的经理,不耐烦地回复,你烦不烦,你发一次给我就行了。这是消息发送模式中的exactly-once的概念,即消息者保证收到一次,不多不少(特指同一消息)。M想到既然如此,那周报我先发给经理小秘,再由小秘转交经理即可。即使重复发送也是发给小秘,她不会笨到相同文件不停转交经理的。(假设经理小秘间交流是完全可靠的)
在Kafka编码实践中,通过如下代码实现:
enable.idempotence
enable.idempotence设置成true后,Producer自动升级成幂等性Producer。Kafka会自动去重。Broker会多保存一些字段。当Producer发送了相同字段值的消息后,Broker能够自动知晓这些消息已经重复了。
设置该值后,会自动设置acks = all,retries为整型最大值。
3.保序问题
在消息的导步发送中,如果对于消息的顺序特别敏感,如两个消息
- 将一条记录id为1的数据的改成vlaueB
- 将一条记录id为1的数据的改成vlaueC
这两条消息是否顺序消费处理会直接影响这条记录最后的值。
enable.idempotence设置成true后,Producer自动升级成幂等性Producer
注意:在Kafka中无法保证全局有序,但可以保序分区有序,可以通过参数控制:
max.in.flight.requests.per.connection=1
即客户端在单个连接上能够发送的未响应请求的个数来解决乱序,但降低了系统吞吐量。简单来说即消息你不确定收到,我就不再发送。
ps.在这个模式下,如仍要保证全局保序,一种策略即不要对topic设置多个分区。
3.发送端参数
- max.in.flight.requests.per.connection
该参数指定kafka一次发送请求在得到服务器回应之前,可发送的消息数量。它的值越高,就会占用越多的内存,不过也会提升吞吐量。把它设为 1 可以保证消息是按照发送的顺序写入服务器的,即使发生了重试。新版本kafka设置enable.idempotence=true后能够动态调整max.in.flight.requests.per.connection且也能保证幂等有序,但值要求小于等于 5 。主要原因是:Server 端的 ProducerStateManager 实例会缓存每个 PID 在每个 Topic-Partition 上发送的最近 5 个batch 数据(这个5是写死的),这样一旦发送消息重发,会根据消息的seq,在ProducerStateManager 仍然可以找到消息正确的顺序插入,保证顺序。并把max.in.flight.requests.per.connection设为1。
二、消息消费者
1.自动提交
Kafka默认使用自动提交
2.手动提交
如上所述,consumer拉取到消息后,把消息交给线程池workers,workers对message的handle可能包含异步操作,又会出现以下情况:
- 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失
- 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行
- 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失
ps.业务的保序处理原则上不是kafka需要关心的事情
对于业务可靠性要求较高场景是不允许丢消息的,可采用
- 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行
对于可能造成的重复消费问题,我们常用的方法是works取到消息后在业务端进行去重:
if(cache.contain(msgId)){
// cache中包含msgId,已经处理过
continue;
}else {
lock.lock();
cache.put(msgId,timeout);
commitSync();
lock.unLock();
}
当然在业务端提交后正好down机,down机后cache数据则丢失,好提交正好失败,这样还是会造成重复消费。
业务手动提交场景下的并行处理:
消息并发异步处理过程后,会造成offset提交乱序,如果不做任何处理,可能造成丢消息或重复消费问题。
eg.针对1,2,3,4,5消息,异步处理先提交了5,而1,2,3,4对应业务逻辑还未处理完,此时down机,重启时,1,2,3,4消息就丢失了。同样如果先提交了4(3正常处理完),又提交了2,这样down机重启后,3,4又要重复拉取消费。
但业务处理过程中,上文说过丢消息往往是不能接受的,所以在消费端需要维护一个自增变量,在即使先提交了4,又提交2这类场景,由于1还未提交,4和2都不会真正提交,专业术语高低水位的控制;实际上是允许重复消费存在。
考虑另一极端场景,如果1w条消息,仅第二条消费提交失败,其余正常消费提交,此时重启仍会从第二条消息重新消费,如何避免这样大规模的浪费?
借鉴参数 max.in.flight.requests.per.connection思路,也设置一个最大确认消息的窗口,使用滑动窗口思路:
如图窗口大小为6,橙色代表未正常提交,主要是业务处理失败,故意不提交
避免上述极端情况,在2未提交情况下,进行阻塞,API中可以通过设置回调,供业务方进行该种场景的处理:
- 丢弃
- 内存记录
- 本地持久化记录
- 业务重新处理(时序不敏感)
如是可以处理的,则2等同提交完成,窗口滑动到8。
三、高性能
- 顺序读写
- 零拷贝
- 批量和压缩传输