大家好,我是苏三,又和大家见面了。

  • 这时有种更简单的方案浮出水面:消费者在处理消息时,先判断该订单号重试表有没有数据,如果有则直接把当前消息保存到重试表。如果没有,则进行业务处理,如果出现异常,把该消息保存到重试表

    后来我们用elastic-job建立了失败重试机制,如果重试了7次后还是失败,则将该消息的状态标记为失败,发邮件通知开发人员。

    终于由于网络不稳定,导致用户在划菜客户端有些订单和菜品一直看不到的问题被解决了。现在商户顶多偶尔延迟看到菜品,比一直看不菜品好太多。

    消息积压

    随着销售团队的市场推广,我们系统的商户越来越多。随之而来的是消息的数量越来越大,导致消费者处理不过来,经常出现消息积压的情况。对商户的影响非常直观,划菜客户端上的订单和菜品可能半个小时后才能看到。一两分钟还能忍,半个消息的延迟,对有些暴脾气的商户哪里忍得了,马上投诉过来了。我们那段时间经常接到商户投诉说订单和菜品有延迟。

    虽说,加服务器节点就能解决问题,但是按照公司为了省钱的惯例,要先做系统优化,所以我们开始了消息积压问题解决之旅。

    1. 消息体过大

    虽说kafka号称支持百万级的TPS,但从producer发送消息到broker需要一次网络IObroker写数据到磁盘需要一次磁盘IO(写操作),consumerbroker获取消息先经过一次磁盘IO(读操作),再经过一次网络IO我用kafka两年踩过的一些非比寻常的坑-LMLPHP

    一次简单的消息从生产到消费过程,需要经过2次网络IO2次磁盘IO。如果消息体过大,势必会增加IO的耗时,进而影响kafka生产和消费的速度。消费者速度太慢的结果,就会出现消息积压情况。

    除了上面的问题之外,消息体过大,还会浪费服务器的磁盘空间,稍不注意,可能会出现磁盘空间不足的情况。

    此时,我们已经到了需要优化消息体过大问题的时候。

    如何优化呢?

    我们重新梳理了一下业务,没有必要知道订单的中间状态,只需知道一个最终状态就可以了。

    如此甚好,我们就可以这样设计了:

    果然这样调整之后,消息积压问题很长一段时间都没再出现。

    2. 路由规则不合理

    还真别高兴的太早,有天中午又有商户投诉说订单和菜品有延迟。我们一查kafka的topic竟然又出现了消息积压。

    但这次有点诡异,不是所有partition上的消息都有积压,而是只有一个。

    刚开始,我以为是消费那个partition消息的节点出了什么问题导致的。但是经过排查,没有发现任何异常。

    这就奇怪了,到底哪里有问题呢?

    后来,我查日志和数据库发现,有几个商户的订单量特别大,刚好这几个商户被分到同一个partition,使得该partition的消息量比其他partition要多很多。

    这时我们才意识到,发消息时按商户编号路由partition的规则不合理,可能会导致有些partition消息太多,消费者处理不过来,而有些partition却因为消息太少,消费者出现空闲的情况。

    为了避免出现这种分配不均匀的情况,我们需要对发消息的路由规则做一下调整。

    我们思考了一下,用订单号做路由相对更均匀,不会出现单个订单发消息次数特别多的情况。除非是遇到某个人一直加菜的情况,但是加菜是需要花钱的,所以其实同一个订单的消息数量并不多。

    调整后按订单号路由到不同的partition,同一个订单号的消息,每次到发到同一个partition

    我用kafka两年踩过的一些非比寻常的坑-LMLPHP

    调整后,消息积压的问题又有很长一段时间都没有再出现。我们的商户数量在这段时间,增长的非常快,越来越多了。

    3. 批量操作引起的连锁反应

    在高并发的场景中,消息积压问题,可以说如影随形,真的没办法从根本上解决。表面上看,已经解决了,但后面不知道什么时候,就会冒出一次,比如这次:

    有天下午,产品过来说:有几个商户投诉过来了,他们说菜品有延迟,快查一下原因。

    这次问题出现得有点奇怪。

    为什么这么说?

    首先这个时间点就有点奇怪,平常出问题,不都是中午或者晚上用餐高峰期吗?怎么这次问题出现在下午?

    根据以往积累的经验,我直接看了kafkatopic的数据,果然上面消息有积压,但这次每个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小时候后也恢复正常了。我用kafka两年踩过的一些非比寻常的坑-LMLPHP

    后来,我们开了一次复盘会,得出的结论是:

    4. 表过大

    为了防止后面再次出现消息积压问题,消费者后面就一直用多线程处理消息。

    但有天中午我们还是收到很多报警邮件,提醒我们kafka的topic消息有积压。我们正在查原因,此时产品跑过来说:又有商户投诉说菜品有延迟,赶紧看看。这次她看起来有些不耐烦,确实优化了很多次,还是出现了同样的问题。

    在外行看来:为什么同一个问题一直解决不了?

    其实技术心里的苦他们是不知道的。

    表面上问题的症状是一样的,都是出现了菜品延迟,他们知道的是因为消息积压导致的。但是他们不知道深层次的原因,导致消息积压的原因其实有很多种。这也许是使用消息中间件的通病吧。

    我沉默不语,只能硬着头皮定位原因了。

    后来我查日志发现消费者消费一条消息的耗时长达2秒。以前是500毫秒,现在怎么会变成2秒呢?

    奇怪了,消费者的代码也没有做大的调整,为什么会出现这种情况呢?

    查了一下线上菜品表,单表数据量竟然到了几千万,其他的划菜表也是一样,现在单表保存的数据太多了。

    我们组梳理了一下业务,其实菜品在客户端只展示最近3天的即可。

    这就好办了,我们服务端存着多余的数据,不如把表中多余的数据归档。于是,DBA帮我们把数据做了归档,只保留最近7天的数据。

    如此调整后,消息积压问题被解决了,又恢复了往日的平静。

    主键冲突

    别高兴得太早了,还有其他的问题,比如:报警邮件经常报出数据库异常: Duplicate entry '6' for key 'PRIMARY',说主键冲突。

    出现这种问题一般是由于有两个以上相同主键的sql,同时插入数据,第一个插入成功后,第二个插入的时候会报主键冲突。表的主键是唯一的,不允许重复。

    我仔细检查了代码,发现代码逻辑会先根据主键从表中查询订单是否存在,如果存在则更新状态,不存在才插入数据,没得问题。

    这种判断在并发量不大时,是有用的。但是如果在高并发的场景下,两个请求同一时刻都查到订单不存在,一个请求先插入数据,另一个请求再插入数据时就会出现主键冲突的异常。

    解决这个问题最常规的做法是:加锁

    我刚开始也是这样想的,加数据库悲观锁肯定是不行的,太影响性能。加数据库乐观锁,基于版本号判断,一般用于更新操作,像这种插入操作基本上不会用。

    剩下的只能用分布式锁了,我们系统在用redis,可以加基于redis的分布式锁,锁定订单号。

    但后面仔细思考了一下:

    所以,我也不打算用分布式锁。

    而是选择使用mysql的INSERT INTO ...ON DUPLICATE KEY UPDATE语法:

    INSERT INTO table (column_list)
    VALUES (value_list)
    ON DUPLICATE KEY UPDATE
    c1 = v1,
    c2 = v2,
    ...;

    它会先尝试把数据插入表,如果主键冲突的话那么更新字段。

    把以前的insert语句改造之后,就没再出现过主键冲突问题。

    数据库主从延迟

    不久之后的某天,又收到商户投诉说下单后,在划菜客户端上看得到订单,但是看到的菜品不全,有时甚至订单和菜品数据都看不到。

    这个问题跟以往的都不一样,根据以往的经验先看kafkatopic中消息有没有积压,但这次并没有积压。

    再查了服务日志,发现订单系统接口返回的数据有些为空,有些只返回了订单数据,没返回菜品数据。

    这就非常奇怪了,我直接过去找订单组的同事。他们仔细排查服务,没有发现问题。这时我们不约而同的想到,会不会是数据库出问题了,一起去找DBA。果然,DBA发现数据库的主库同步数据到从库,由于网络原因偶尔有延迟,有时延迟有3秒

    如果我们的业务流程从发消息到消费消息耗时小于3秒,调用订单详情查询接口时,可能会查不到数据,或者查到的不是最新的数据。

    这个问题非常严重,会导致直接我们的数据错误。

    为了解决这个问题,我们也加了重试机制。调用接口查询数据时,如果返回数据为空,或者只返回了订单没有菜品,则加入重试表

    调整后,商户投诉的问题被解决了。

    重复消费

    kafka消费消息时支持三种模式:

    kafka默认的模式是at least onece,但这种模式可能会产生重复消费的问题,所以我们的业务逻辑必须做幂等设计。

    而我们的业务场景保存数据时使用了INSERT INTO ...ON DUPLICATE KEY UPDATE语法,不存在时插入,存在时更新,是天然支持幂等性的。

    多环境消费问题

    我们当时线上环境分为:pre(预发布环境) 和 prod(生产环境),两个环境共用同一个数据库,并且共用同一个kafka集群。

    需要注意的是,在配置kafkatopic的时候,要加前缀用于区分不同环境。pre环境的以pre_开头,比如:pre_order,生产环境以prod_开头,比如:prod_order,防止消息在不同环境中串了。

    但有次运维在pre环境切换节点,配置topic的时候,配错了,配成了prodtopic。刚好那天,我们有新功能上pre环境。结果悲剧了,prod的有些消息被pre环境的consumer消费了,而由于消息体做了调整,导致pre环境的consumer处理消息一直失败。

    其结果是生产环境丢了部分消息。不过还好,最后生产环境消费者通过重置offset,重新读取了那一部分消息解决了问题,没有造成太大损失。

    后记

    除了上述问题之外,我还遇到过:

    这两个问题说起来有些复杂,我就不一一列举了,有兴趣的朋友可以关注我的公众号,加我的微信找我私聊。

    非常感谢那两年使用消息中间件kafka的经历,虽说遇到过挺多问题,踩了很多坑,走了很多弯路,但是实打实的让我积累了很多宝贵的经验,快速成长了。

    其实kafka是一个非常优秀的消息中间件,我所遇到的绝大多数问题,都并非kafka自身的问题(除了cpu使用率100%是它的一个bug导致的之外)。

    各位亲爱的朋友,我的文章一周才更新一到两篇。很有可能在你不经意间,就发文了,导致你错过精彩内容。在公众号中扩展右上角“设为星标”能第一时间看到我的好文章喔,纯干货分享,错过真的可惜。

    我用kafka两年踩过的一些非比寻常的坑-LMLPHP


    最后说一句(求关注,别白嫖我)

    如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

    求一键三连:点赞、转发、在看。

    关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

     个人公众号

    我用kafka两年踩过的一些非比寻常的坑-LMLPHP

     个人微信

    我用kafka两年踩过的一些非比寻常的坑-LMLPHP



    本文分享自微信公众号 - 苏三说技术(gh_9f551dfec941)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    03-09 14:50
    查看更多