共计 7068 个字符,预计需要花费 18 分钟才能阅读完成。
前言
我的博客
我的上家公司是做餐饮零碎的,每天中午和早晨用餐高峰期,零碎的并发量不容小觑。为了保险起见,公司规定各部门都要在吃饭的工夫轮流值班,防止出现线上问题时可能及时处理。
我过后在后厨显示零碎团队,该零碎属于订单的上游业务。用户点完菜下单后,订单零碎会通过发 kafka
音讯给咱们零碎,零碎读取音讯后,做业务逻辑解决,长久化订单和菜品数据,而后展现到划菜客户端。这样厨师就晓得哪个订单要做哪些菜,有些菜做好了,就能够通过该零碎出菜。零碎主动告诉服务员上菜,如果服务员上完菜,批改菜品上菜状态,用户就晓得哪些菜曾经上了,哪些还没有上。这个零碎能够大大提高后厨到用户的效率。
事实证明,这所有的要害是消息中间件:kafka
,如果它有问题,将会间接影响到后厨显示零碎的性能。
接下来,我跟大家一起聊聊应用 kafka
两年工夫踩过哪些坑?
程序问题
1. 为什么要保障音讯的程序?
刚开始咱们零碎的商户很少,为了疾速实现性能,咱们没想太多。既然是走消息中间件 kafka
通信,订单零碎发消息时将订单具体数据放在音讯体,咱们后厨显示零碎只有订阅topic
,就能获取相干音讯数据,而后解决本人的业务即可。
不过这套计划有个关键因素:要保障音讯的程序。
为什么呢?
订单有很多状态,比方:下单、领取、实现、撤销等,不可能 下单
的音讯都没读取到,就先读取 领取
或撤销
的音讯吧,如果真的这样,数据不是会产生错乱?
好吧,看来保障音讯程序是有必要的。
2. 如何保障音讯程序?
咱们都晓得 kafka
的topic
是无序的,然而一个 topic
蕴含多个 partition
,每个partition
外部是有序的。
![图片]()
如此一来,思路就变得清晰了:只有保障生产者写音讯时,依照肯定的规定写到同一个 partition
,不同的消费者读不同的partition
的音讯,就能保障生产和消费者音讯的程序。
咱们刚开始就是这么做的,同一个 商户编号
的音讯写到同一个 partition
,topic
中创立了 4
个partition
,而后部署了 4
个消费者节点,形成 消费者组
,一个partition
对应一个消费者节点。从实践上说,这套计划是可能保障音讯程序的。
所有布局得看似“浑然一体”,咱们就这样”顺利“上线了。
3. 出现意外
该性能上线了一段时间,刚开始还是比拟失常的。
然而,好景不长,很快就收到用户投诉,说在划菜客户端有些订单和菜品始终看不到,无奈划菜。
我定位到了起因,公司在那段时间网络常常不稳固,业务接口时不时报超时,业务申请时不时会连不上数据库。
这种状况对 程序音讯
的打击,能够说是 毁灭性
的。
为什么这么说?
假如订单零碎发了:”下单“、”领取“、”实现“三条音讯。而”下单“音讯因为网络起因咱们零碎解决失败了,而前面的两条音讯的数据是无奈入库的,因为只有”下单“音讯的数据才是残缺的数据,其余类型的音讯只会更新状态。
加上,咱们过后没有做 失败重试机制
,使得这个问题被放大了。问题变成:一旦”下单“音讯的数据入库失败,用户就永远看不到这个订单和菜品了。
那么这个紧急的问题要如何解决呢?
4. 解决过程
最开始咱们的想法是:在消费者解决音讯时,如果解决失败了,立马重试 3 - 5 次。但如果有些申请要第 6 次能力胜利怎么办?不可能始终重试呀,这种同步重试机制,会阻塞其余商户订单音讯的读取。
显然用下面的这种 同步重试机制
在出现异常的状况,会重大影响音讯消费者的生产速度,升高它的吞吐量。
如此看来,咱们不得不用 异步重试机制
了。
如果用异步重试机制,解决失败的音讯就得保留到 重试表
下来。
但有个新问题立马呈现:只存一条音讯如何保障程序?
存一条音讯确实无奈保障程序,如果:”下单“音讯失败了,还没来得及异步重试。此时,”领取“音讯被生产了,它必定是不能被失常生产的。
此时,”领取“音讯该始终等着,每隔一段时间判断一次,它后面的音讯都有没有被生产?
如果真的这么做,会呈现两个问题:
- ”领取“音讯后面只有”下单“音讯,这种状况比较简单。但如果某种类型的音讯,后面有 N 多种音讯,须要判断多少次呀,这种判断跟订单零碎的耦合性太强了,相当于要把他们零碎的逻辑搬一部分到咱们零碎。
- 影响消费者的生产速度
这时有种更简略的计划浮出水面:消费者在解决音讯时,先判断该 订单号
在重试表
有没有数据,如果有则间接把以后音讯保留到 重试表
。如果没有,则进行业务解决,如果出现异常,把该音讯保留到 重试表
。
起初咱们用 elastic-job
建设了 失败重试机制
,如果重试了7
次后还是失败,则将该音讯的状态标记为 失败
,发邮件告诉开发人员。
终于因为网络不稳固,导致用户在划菜客户端有些订单和菜品始终看不到的问题被解决了。当初商户顶多偶然提早看到菜品,比始终看不菜品好太多。
音讯积压
随着销售团队的市场推广,咱们零碎的商户越来越多。随之而来的是音讯的数量越来越大,导致消费者解决不过去,经常出现音讯积压的状况。对商户的影响十分直观,划菜客户端上的订单和菜品可能半个小时后能力看到。一两分钟还能忍,半个音讯的提早,对有些暴脾气的商户哪里忍得了,马上投诉过去了。咱们那段时间常常接到商户投诉说订单和菜品有提早。
虽说,加 服务器节点
就能解决问题,然而依照公司为了省钱的常规,要先做系统优化,所以咱们开始了 音讯积压
问题解决之旅。
1. 音讯体过大
虽说 kafka
号称反对 百万级的 TPS
,但从 producer
发送音讯到 broker
须要一次网络 IO
,broker
写数据到磁盘须要一次磁盘 IO
(写操作),consumer
从broker
获取音讯先通过一次磁盘IO
(读操作),再通过一次网络IO
。![图片]()
一次简略的音讯从生产到生产过程,须要通过 2 次网络 IO
和 2 次磁盘 IO
。如果音讯体过大,势必会减少 IO 的耗时,进而影响 kafka 生产和生产的速度。消费者速度太慢的后果,就会呈现音讯积压状况。
除了下面的问题之外,音讯体过大
,还会节约服务器的磁盘空间,稍不留神,可能会呈现磁盘空间有余的状况。
此时,咱们曾经到了须要优化音讯体过大问题的时候。
如何优化呢?
咱们从新梳理了一下业务,没有必要晓得订单的 中间状态
,只需晓得一个 最终状态
就能够了。
如此甚好,咱们就能够这样设计了:
- 订单零碎发送的音讯体只用蕴含:id 和状态等要害信息。
- 后厨显示零碎生产音讯后,通过 id 调用订单零碎的订单详情查问接口获取数据。
- 后厨显示零碎判断数据库中是否有该订单的数据,如果没有则入库,有则更新。
![图片]()
果然这样调整之后,音讯积压问题很长一段时间都没再呈现。
2. 路由规定不合理
还真别快乐的太早,有天中午又有商户投诉说订单和菜品有提早。咱们一查 kafka 的 topic 居然又呈现了音讯积压。
但这次有点诡异,不是所有 partition
上的音讯都有积压,而是只有一个。
刚开始,我认为是生产那个 partition
音讯的节点出了什么问题导致的。然而通过排查,没有发现任何异样。
这就奇怪了,到底哪里有问题呢?
起初,我查日志和数据库发现,有几个商户的订单量特地大,刚好这几个商户被分到同一个 partition
,使得该partition
的音讯量比其余 partition
要多很多。
这时咱们才意识到,发消息时按 商户编号
路由 partition
的规定不合理,可能会导致有些 partition
音讯太多,消费者解决不过去,而有些 partition
却因为音讯太少,消费者呈现闲暇的状况。
为了避免出现这种调配不平均的状况,咱们须要对发消息的路由规定做一下调整。
咱们思考了一下,用订单号做路由绝对更平均,不会呈现单个订单发消息次数特地多的状况。除非是遇到某个人始终加菜的状况,然而加菜是须要花钱的,所以其实同一个订单的音讯数量并不多。
调整后按 订单号
路由到不同的partition
,同一个订单号的音讯,每次到发到同一个partition
。
调整后,音讯积压的问题又有很长一段时间都没有再呈现。咱们的商户数量在这段时间,增长的十分快,越来越多了。
3. 批量操作引起的连锁反应
在高并发的场景中,音讯积压问题,能够说如影随形,真的没方法从根本上解决。外表上看,曾经解决了,但前面不晓得什么时候,就会冒出一次,比方这次:
有天下午,产品过去说:有几个商户投诉过去了,他们说菜品有提早,快查一下起因。
这次问题呈现得有点奇怪。
为什么这么说?
首先这个工夫点就有点奇怪,平时出问题,不都是中午或者早晨用餐高峰期吗?怎么这次问题呈现在下午?
依据以往积攒的教训,我间接看了 kafka
的topic
的数据,果然下面音讯有积压,但这次每个 partition
都积压了 十几万
的音讯没有生产,比以往加压的音讯数量减少了 几百倍
。这次音讯积压得极不寻常。
我连忙查服务监控看看消费者挂了没,还好没挂。又查服务日志没有发现异常。这时我有点迷茫,碰运气问了问订单组下午产生了什么事件没?他们说下午有个促销流动,跑了一个 JOB 批量更新过有些商户的订单信息。
这时,我一下子如梦初醒,是他们在 JOB 中批量发消息导致的问题。怎么没有告诉咱们呢?切实太坑了。
虽说晓得问题的起因了,倒是眼前积压的这 十几万
的音讯该如何解决呢?
此时,如果间接调大 partition
数量是不行的,历史音讯曾经存储到 4 个固定的partition
,只有新增的音讯才会到新的partition
。咱们重点须要解决的是已有的 partition。
间接加服务节点也不行,因为 kafka
容许同组的多个 partition
被一个 consumer
生产,但不容许一个 partition
被同组的多个 consumer
生产,可能会造成资源节约。
看来只有用多线程解决了。
为了紧急解决问题,我改成了用 线程池
解决音讯,外围线程和最大线程数都配置成了50
。
调整之后,果然,音讯积压数量一直缩小。
但此时有个更重大的问题呈现:我收到了报警邮件,有两个订单零碎的节点 down 机了。
不久,订单组的共事过去找我说,咱们零碎调用他们订单查问接口的并发量突增,超过了预计的好几倍,导致有 2 个服务节点挂了。他们把查问性能独自整成了一个服务,部署了 6 个节点,挂了 2 个节点,再不解决,另外 4 个节点也会挂。订单服务能够说是公司最外围的服务,它挂了公司损失会很大,状况万分紧急。
为了解决这个问题,只能先把线程数调小。
幸好,线程数是能够通过 zookeeper
动静调整的,我把外围线程数调成了 8
个,外围线程数改成了 10
个。
前面,运维把订单服务挂的 2 个节点重启后恢复正常了,以防万一,再多加了 2 个节点。为了确保订单服务不会呈现问题,就放弃目前的生产速度,后厨显示零碎的音讯积压问题,1 小时候后也恢复正常了。
起初,咱们开了一次复盘会,得出的论断是:
- 订单零碎的批量操作肯定提前告诉上游零碎团队。
- 上游零碎团队多线程调用订单查问接口肯定要做压测。
- 这次给订单查问服务敲响了警钟,它作为公司的外围服务,应答高并发场景做的不够好,须要做优化。
- 对音讯积压状况加监控。
顺便说一下,对于要求严格保障音讯程序的场景,能够将线程池改成多个队列,每个队列用单线程解决。
4. 表过大
为了避免前面再次出现音讯积压问题,消费者前面就始终用多线程解决音讯。
但有天中午咱们还是收到很多报警邮件,揭示咱们 kafka 的 topic 音讯有积压。咱们正在查起因,此时产品跑过来说:又有商户投诉说菜品有提早,连忙看看。这次她看起来有些不耐烦,的确优化了很屡次,还是呈现了同样的问题。
在在行看来:为什么同一个问题始终解决不了?
其实技术心里的苦他们是不晓得的。
外表上问题的症状是一样的,都是呈现了菜品提早,他们晓得的是因为音讯积压导致的。然而他们不晓得深层次的起因,导致音讯积压的起因其实有很多种。这兴许是应用消息中间件的通病吧。
我沉默不语,只能硬着头皮定位起因了。
起初我查日志发现消费者生产一条音讯的耗时长达 2 秒
。以前是500 毫秒
,当初怎么会变成 2 秒
呢?
奇怪了,消费者的代码也没有做大的调整,为什么会呈现这种状况呢?
查了一下线上菜品表,单表数据量居然到了 几千万
,其余的划菜表也是一样,当初单表保留的数据太多了。
咱们组梳理了一下业务,其实菜品在客户端只展现最近 3 天
的即可。
这就好办了,咱们服务端存着 多余的数据
,不如把表中多余的数据归档。于是,DBA 帮咱们把数据做了归档,只保留最近 7 天
的数据。
如此调整后,音讯积压问题被解决了,又复原了来日的平静。
主键抵触
别快乐得太早了,还有其余的问题,比方:报警邮件常常报出数据库异样:Duplicate entry '6' for key 'PRIMARY'
,说主键抵触。
呈现这种问题个别是因为有两个以上雷同主键的 sql,同时插入数据,第一个插入胜利后,第二个插入的时候会报主键抵触。表的主键是惟一的,不容许反复。
我仔细检查了代码,发现代码逻辑会先依据主键从表中查问订单是否存在,如果存在则更新状态,不存在才插入数据,没得问题。
这种判断在并发量不大时,是有用的。然而如果在高并发的场景下,两个申请同一时刻都查到订单不存在,一个申请先插入数据,另一个申请再插入数据时就会呈现主键抵触的异样。
解决这个问题最惯例的做法是:加锁
。
我刚开始也是这样想的,加数据库乐观锁必定是不行的,太影响性能。加数据库乐观锁,基于版本号判断,个别用于更新操作,像这种插入操作基本上不会用。
剩下的只能用分布式锁了,咱们零碎在用 redis,能够加基于 redis 的分布式锁,锁定订单号。
但前面认真思考了一下:
- 加分布式锁也可能会影响消费者的音讯处理速度。
- 消费者依赖于 redis,如果 redis 呈现网络超时,咱们的服务就喜剧了。
所以,我也不打算用分布式锁。
而是抉择应用 mysql 的 INSERT INTO ...ON DUPLICATE KEY UPDATE
语法:
INSERTINTOtable (column_list)
VALUES (value_list)
ONDUPLICATEKEYUPDATE
c1 = v1,
c2 = v2,
...;
它会先尝试把数据插入表,如果主键抵触的话那么更新字段。
把以前的 insert
语句革新之后,就没再呈现过主键抵触问题。
数据库主从提早
不久之后的某天,又收到商户投诉说下单后,在划菜客户端上看失去订单,然而看到的菜品不全,有时甚至订单和菜品数据都看不到。
这个问题跟以往的都不一样,依据以往的教训先看 kafka
的topic
中音讯有没有积压,但这次并没有积压。
再查了服务日志,发现订单零碎接口返回的数据有些为空,有些只返回了订单数据,没返回菜品数据。
这就十分奇怪了,我间接过来找订单组的共事。他们认真排查服务,没有发现问题。这时咱们不谋而合的想到,会不会是数据库出问题了,一起去找 DBA
。果然,DBA
发现数据库的主库同步数据到从库,因为网络起因偶然有提早,有时提早有 3 秒
。
如果咱们的业务流程从发消息到生产音讯耗时小于 3 秒
,调用订单详情查问接口时,可能会查不到数据,或者查到的不是最新的数据。
这个问题十分重大,会导致间接咱们的数据谬误。
为了解决这个问题,咱们也加了 重试机制
。调用接口查问数据时,如果返回数据为空,或者只返回了订单没有菜品,则退出 重试表
。
调整后,商户投诉的问题被解决了。
反复生产
kafka
生产音讯时反对三种模式:
- at most once 模式 最多一次。保障每一条音讯 commit 胜利之后,再进行生产解决。音讯可能会失落,但不会反复。
- at least once 模式 至多一次。保障每一条音讯解决胜利之后,再进行 commit。音讯不会失落,但可能会反复。
- exactly once 模式 准确传递一次。将 offset 作为惟一 id 与音讯同时解决,并且保障解决的原子性。音讯只会解决一次,不失落也不会反复。但这种形式很难做到。
kafka
默认的模式是at least once
,但这种模式可能会产生反复生产的问题,所以咱们的业务逻辑必须做幂等设计。
而咱们的业务场景保留数据时应用了 INSERT INTO ...ON DUPLICATE KEY UPDATE
语法,不存在时插入,存在时更新,是人造反对幂等性的。
多环境生产问题
咱们过后线上环境分为:pre
(预公布环境) 和 prod
(生产环境),两个环境共用同一个数据库,并且共用同一个 kafka 集群。
须要留神的是,在配置 kafka
的topic
的时候,要加前缀用于辨别不同环境。pre 环境的以 pre结尾,比方:pre_order,生产环境以 prod结尾,比方:prod_order,避免音讯在不同环境中串了。
但有次运维在 pre
环境切换节点,配置 topic
的时候,配错了,配成了 prod
的topic
。刚好那天,咱们有新性能上 pre
环境。后果喜剧了,prod
的有些音讯被 pre
环境的 consumer
生产了,而因为音讯体做了调整,导致 pre
环境的 consumer
解决音讯始终失败。
其后果是生产环境丢了局部音讯。不过还好,最初生产环境消费者通过重置offset
,从新读取了那一部分音讯解决了问题,没有造成太大损失。
后记
除了上述问题之外,我还遇到过:
kafka
的consumer
应用主动确认机制,导致cpu 使用率 100%
。kafka
集群中的一个broker
节点挂了,重启后又始终挂。
这两个问题说起来有些简单,我就不一一列举了,有趣味的敌人能够关注我的公众号,加我的微信找我私聊。
非常感谢那两年应用消息中间件 kafka
的经验,虽说遇到过挺多问题,踩了很多坑,走了很多弯路,然而实打实的让我积攒了很多贵重的教训,疾速成长了。
其实 kafka
是一个十分优良的消息中间件,我所遇到的绝大多数问题,都并非 kafka
本身的问题(除了 cpu 使用率 100% 是它的一个 bug 导致的之外)。
文章来自:苏三说技术