复盘公司生产事故后的一些感想

6 篇文章 0 订阅
4 篇文章 0 订阅

        今年承担了一个额外工作是参加公司每个季度的生产事故复盘,并总结经验,然后在整个公司进行宣贯。今年总共复盘了近70起生产事故案例,说实话,看着很痛心,因为99%的错误都不是什么高大上的问题,都是一些低级错误。70%都是数据库sql的问题。大家都说不想做CURD码农,可就是CURD有些人也没玩明白,或者说忽略了CURD的重要性,SQL真的是非常重要,你轻视它,那么它就一定会反噬你。11月份发生了一起极其严重的事故,损失金额达到billion级别,事故原因就是一个update语句的问题。我们是金融业,一个小疏忽可能就是超大事故,相关人员要么被开除,要么就被取消绩效评优资格,很惨。

        所以,无论作为开发者,还是管理者,切不可好高骛远,眼高手低,只有脚踏实地才能够仰望星空。不要总是高谈阔论,上来动不动就谈高并发,谈分布式。大道至简是万物的理想状态。

        先分类讨论下我复盘过的生产事故案例,下面我所有列举的例子都是切切实实地在我们公司发生的案例。所以我很希望所有开发者,无论是有经验的专家,还是初入职场的新人,都能从中吸取教训,有则改之无则加勉。我写到这儿的时候,虽然立刻就想起了当年明月在《明朝那些事儿》的结尾写的那句话:“人吸取到的最大教训就是从来不吸取历史的经验教训,还是会原原本本地将前任犯过的错误再犯一遍。“。可是,聊胜于无,我还是相信写出来是有用处的。

  一、SQL问题

1、集团外系统间定时转账 - 复合条件没有指定优先级

    现象:定时任务触发转账退款,发生相同转账重复执行,导致重复退款;

    影响:多转给其他公司账款,金额达到billion,已经惊动我们最高层。好在钱都已经追回来;

    原因:有位同事写的update语句有多个判断条件,由于保密性问题,这里脱敏,如where语句

   where a and (b is null or b == "" or b =="0"),这是它本应该使用的where语句,可是它的where语句却没有了括号,即where a and b is null or b == "" or b =="0"。看到这里,大家都知道问题出在哪里了吧。

   分析:这位同事是用mybatis API写的,一是对API的使用不熟悉,二是单元测试没有写好,三是功能测试也不充分,总之每个关系都没有引起足够的重视。

2、某核心系统突发宕机 - 分库分表场景没有where,没有分片键

    现象:高峰时段交易成功率降级,系统宕机;

    影响:所有关联的外围系统受到影响,引起大量客诉;

    原因:一位同事在写数据库查询语句时,没有加任何非空判断,再加上因为某种原因,前端传过来的数据所有where条件都是空,在分库分表的场景下,进行了一次牛逼的聚合查询,也就是把每个子库的数据都查出来,进行聚合。直接导致系统出现OOM,当多次的GC依然满足不了的情况下,内存占用依然很大,Linux操作系统本身也会检测内存占用情况,如果发现某个进程内存占用过多,就会kill掉的。

   分析:我们在写where查询时一定要非常注意where的条件的非空判断,否则很容易出现慢sql的问题,在高并发场景下,慢sql影响的不仅仅是数据库本身,也影响应用,影响整个系统。尤其是分库分表,分库分表场景下,在线交易千万不要做聚合查询,一定要带上分片键。如果想做聚合查询,请采用其他的手段。可以将所有数据归档到一个聚合库,聚合库可以是关系型数据库,可以是列式存储数据库如Hbase,也可以采用目前流行的分布式数据库TiDB等等。

3、某票据系统-联查没有索引

    现象:交易报错,存在慢SQL;

    影响:票据业务受到影响,外联系统调用阻塞,超时;

    原因:业务涉及到多表联查,多表不是只有一两张,而是多个,且其中一张表未加任何索引。这两个问题在join联查都是致命的。

   分析:在业务量少,数据量少的情况下,你的sql写得再烂,可能感觉不明显,但一旦数据量上来,流量上来,就基本上宣告了系统的崩溃。我们都知道尽量避免在业务中使用Join联查,或者联查的表不宜过多,最好不要超过3张。另外,如果关联表字段不使用索引,笛卡尔积的出现就是梦魇。

总结:

        2023年一年复盘过的70个生产事故,有将近40个是因为SQL问题引起的。上面三个只是其中非常典型的问题,且影响比较大的。总结排名比较靠前的几个问题原因:

          1、where没有查询条件,这个是最多的,我们用orm或者半orm框架的api写时,也不做任何判空。这个发生的概率非常大,不是一般的大;

          2、where条件没有索引。尤其是新增字段时,忘了增加索引,这是最容易出现问题的。该问题时发生概率排名第二的;

          3、分库分表没有分片键;

          4、其他问题。 所有导致索引失效的问题,包括隐士转换、违背最左匹配、模糊查询等。

        实际上,写sql没有任何的技术难度(当然,建立在你的基础是OK的),只要你足够重视它,足够细心,就不会出现问题。另外,对于重要的sql,小组leader有必要做代码Review;对于使用框架API写的sql,要把sql debug时stdout或者日志中,看看是不是符合我们的预期。有时不要怕麻烦,可以手写sql,没必要一直用框架API写,尤其是复杂的sql。

二、Redis问题

        Redis由于其高效的处理能力目前可以是每个公司的必备了,官方测试,普通机器压到10W qps不成问题。可如果使用不当,依然会引起问题。本文也会列出两个我们出现的事故。  

   1、公司ToC 的核心APP - 最大的流量入口(热点key)

    现象:交易成功率低于最低告警值;

    影响:用户查询功能报错,引起客诉;

    原因:使用Redis时存在两个热点key,且这两个key都在同一个master节点上(RedisCluster集群),导致在高峰期间对应节点访问流量过大,造成了严重的访问倾斜。该master节点多难啊,妹的,5主5从,干啥都往我这儿跑,就可我一个人造啊。

    分析:热点key这个问题在很多系统上可以不用关心,因为可能多数的系统就不会有太高的并发,但稍微大点儿的系统就不可能忽视这个问题,使用缓存不是就万事大吉了,热点问题就是其中要考虑的因素,尤其是像秒杀这种超高并发场景。如果把活动库存以 key-value这种简单方式存储到Redis中,抢购中就会出现上述的问题。这个问题也是我在前东家工作时同事写的一起事故。

        对于这种问题,我们可以通过两种方式解决。1、增加一层本地缓存,减少Redis的访问,基本上多级缓存都这么干;2、考虑到一致性的问题,用不了本地缓存,像秒杀场景的活动库存这种,就可以将热点key分散存储,一个key拆成多个子key存储。比如某电商进行茅台抢购活动,首先将茅台商品活动库存放到Redis中,茅台就是一个热点key,如maotai,1000,此时就可以将热点key分散存储,茅台抢购是需要预约的,可以将所有预约用户进行分组,Redis会根据组存储活动库存,key可以是 maotai:groupId1 300,maotai:groupId2 300,maotai:groupId3 400,通过这种方式把整个流量分散到不同的节点上,减轻单节点压力。

  2、某智能系统 - 高并发大key

    现象:交易超时和成功率低预警,以及机房传输设备有端口心跳告警;

    影响:用户相应功能不可用,影响用户体验;

    原因:存在bigKey,且进行批量操作(hgetall,hsetall),在交易并发量增大时,批量查询存储操作对Redis性能影响较大,查询缓慢进而导致交易超时。

    分析:尽管Redis在Redis4.0引入了后台多线程,Redis6又引入了IO多线程,但Redis的主要命令执行依然是单线程的。如果对单个key进行批量操作,如hashKey的 hgetall,hsetall,执行的时间复杂度是O(N),当并发上来的时候,这种操作是非常低效的,很容易造成客户端阻塞。此外也可能会引发网络阻塞。因为每次获取大 key 产生的网络流量较大,举一个极端的例子,一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说比较难以承受。

  总结:

          Redis用于缓存场景非常优秀,我们开发者很少考虑性能的问题,但也要遵循相应的开发规范,这个大家可以看阿里出的开发者规范,这里主要提到对于热点key,大key,批量操作等典型的会阻塞整个主线程的问题。像其他问题也要注意,如集群环境能否容忍数据丢失的问题(Redis是AP模式)。       

三、高并发引起的问题

        结合2023年我们所复盘过的几十个生产事故案例来看,尽管引起线上事故的原因各不相同,但他们却都具备一个共性,即发生事故的前提或者说是导火索都是系统的并发流量很大。如果我们开发的系统访问量很低,就算写了一个慢查询,就算不用缓存,系统可能也不会有什么问题;可是当你接口或者系统流量较大时,所有的隐患都会显现出来,也许就是一个小小的疏忽,也极有可能引起系统崩掉,比如写个sql不加where条件,或者加的where条件没有索引,就是这种低级错误在实际发生的不止一起两起,而是很多起。正如墨菲定律说的那样,任何一件事情都没有表面看起来那么简单。

        说下几个我们今年出现的生产事故。

1、抢券活动报错

    现象:活动开始后,TPS峰值飙升,出现系统繁忙;

    影响:抢券活动异常,客户体验感非常差;

    原因:在上线前未对流量做好充分的预估,没有预料到可能出现的瞬时流量过高的问题。且在发券过程中,涉及到多次数据库读写,当并发量较大时,一是导致数据库连接池耗尽,二是数据库性能下降。总结其几个问题:

        1)直接操作数据库库存字段。瞬时高并发流量直接落到数据库上,去查询数据库中的活动库存字段,且每次都需要互斥锁避免超卖。导致数据库以及业务接口性能严重下降。

        2)未做任何限流措施。没有去拦截无效的流量,每次只发几千张券,每个请求都需要去查数据库库存判断是否已经消耗完。

        3)未采取降级等手段。在活动期间,一些非必要的查询接口没有进行降级,导致活动期间的一些操作直接影响到了主活动。

        4)硬件配置较差。某个系统在活动开始前横向或者纵向扩展,服务器配置较低,只有2c4G,且未做机器扩容。

        5)缺少反作弊服务。如果我们营销优惠力度比较大,就一定会引起黄牛的注意,比如各大电商的抢茅台活动,每次都会有很多黄牛来抢,他们都很专业,所以,基本上凡是涉及到秒杀的系统或者平台一定都会有一个专门的风控团队,和黄牛斗智斗勇。风控系统本身是很复杂的,他要根据线上数据去做数据模型训练,设计不同的算法制定拦击规则,开发拦截程序。

    分析:看到上面的原因,你不仅会发出感叹,这设计的系统就是个垃圾啊,完全没考虑高并发啊,没有一处是合理的,哪怕你做一样呢,也比这好啊。这个系统只是完成了功能开发,另外测试过程中也没做好压力测试。

2、Nginx代理- 请求阻塞

现象:交易涉及到三个系统,整体交易成功率低,报系统超时,交易峰值达到后,后端服务宕机。

原因:服务端和Nginx都出现了大量的TIME_WAIT状态的TCP连接,是因为频繁进行连接的建立和释放导致TIME_WAIT堆积。由于调用链路经过nginx,因为是nginx未做keepAlive相关配置导致。TIME_WAIT在nginx和服务端都有出现的话,主要是因为:

        1)导致 nginx端出现大量TIME_WAIT的情况有两种:

          keepalive_requests设置比较小,高并发下超过此值后nginx会强制关闭和客户端保持的keepalive长连接;(主动关闭连接后导致nginx出现TIME_WAIT)

          keepalive设置的比较小(空闲数太小),导致高并发下nginx会频繁出现连接数震荡(超过该值会关闭连接),不停的关闭、开启和后端server保持的keepalive长连接;

        2)导致后端server端出现大量TIME_WAIT的情况:

          nginx没有打开和后端的长连接,即没有设置proxy_http_version 1.1;和proxy_set_header

  Connection “”;从而导致后端server每次关闭连接,高并发下就会出现server端出现大量TIME_WAIT。

分析:Toc客户端发起大量请求,导致服务端负载较高,服务本身承载并发能力较弱,tps约100,原本涉及其他场景调用(如近1000万客户地理信息处理)负载就高,客户端调用上升过大,导致超出接口本身负载能力,出现了大量TIMEAWAIT套接字,接口本身耗时也收到tcp链接的释放和创建导致等待,进而引发超时;达到交易峰值,后端服务宕机。

3、logback写入阻塞

    现象:整个服务接口都出现超时、阻塞的现象;

    原因:日志采用logback框架,该框架默认同步写入,当并发较高时,涉及到非常频繁的日志写入,同步写入出现了阻塞,hang住了线程。

    分析:logback框架默认是同步写入,可以配置异步写入,不过Logback的异步写也是有问题的,它使用定长的队列,如果队列满了,且没有配置丢弃策略,还是会出现阻塞,没完全解决问题。所以,考虑到高性能,可以使用log4j2框架,纯异步日志框架。

 总结

       高并发是一个极其复杂的问题,不是一两句就能说得清的,因为我们所开发的系统不是独立的系统,涉及到很多的上下游。所以,整个服务链路的任何一个环节都可能影响到性能。你开发的应用、机器配置、网络、出入口带宽等等所有方面都是影响性能的因素,任何一点都不能忽视。

        这个地方我后续会专门写一篇高并发相关的经验总结。敬请期待!

四、其他需要注意的问题

   1、无关性验证

        我们现在都喜欢敏捷开发,快速迭代,快速上线。可在追求快的同时有可能损失质量。比如今年有个案例,前端新上线的一个功能,改了一个地方,但加了后没有走整个业务的无关性验证,导致上线后影响其他模块了,某个地方添加数据出现异常。所以我们无论是要上线多么小的功能,都要把全流程测试一下,就算是你认为不会影响的地方,这就是无关性验证。像我们金融业,系统错综复杂,如果不是对整个公司的上百个系统烂熟于心,你都不知道谁依赖谁,很容易出错。

  2、APM

         APM是系统运行期间最重要的故障定位、排查、修复的手段。现在大厂的基建都很完善了,基本上都有完备的APM监控系统。可有些小公司,或者大厂里有些系统觉得麻烦就不加监控。永远记住,日志,链路追踪,指标收集,三者缺一不可,工欲善其事必先利其器,千万不能少了它。我们所排查的案例中有些是发现问题了,迟迟查不到问题存在的原因,少则几十分钟,多则几个小时。你可能会问,我们金融业没有监控吗?有,但我们公司太大,全国各地的金融业务,没有企业级的APM,企业级监控不合适。所以,很多系统都是各自为政。

3、代码Review

        这步真的很关键,我自己的习惯是首先确保自己要merge到主分支的代码是没问题的,在gitlab上我一遍遍看自己提交的代码,看我加的代码改动了哪些逻辑,可能影响啥。实际上,除了自查,还需要组内其他同事或者leader再一次Review。有时,我们经常会因为时间原因忽略Review代码阶段,殊不知是错过了一次发现bug的最重要的一步。

        追求简单,追求稳妥,脚踏实地,用心对待每一行代码,专心做好每一个任务,做好充足的准备,生产事故就不会向你走来。

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值