生产bug记录和思考

前言

复盘最近碰到的bug,记录下日常处理异常的方法。

一、定位异常手段

1.看日志

(1)单个服务日志

每个服务的日志,可以通过配置日志格式+日志滚动存在服务器上,运维小伙伴指定保存在对应路径。登录堡垒机,用tail\less\awk\sed等linux命令查看,常用:

cd log-path
tail -f -n 200 xxx.log

less xxx.log | grep key-word 

参考:Linux 文本处理三剑客:grep、sed 和 awk

(2)统一日志平台

微服务架构,不同服务都有日志,各个日志文件散落在不同目录,所以出现了统一日志平台。
其次,基于调用链跟踪,每个日志记录会标记调用的traceId和spanId,可以用traceId来找到一次调用涉及的全部日志。
开源的日志架构有如 elk(Elasticsearch+Logstash+Kibana),我们内部用的是阿里云日志平台。所以在日志平台中查询异常:

  • 时间+服务+error

2.看接口

有些异常比较好重现,接口重新调用之后,日志里面可以看到和请求相关的traceId。用traceId在日志平台直接搜索,可以看到完整的调用上下文。
当然,服务可能没有打印日志或者打印的日志不够用,这时候有几种处理方式:

  • (1)看bug是否本地、测试环境、预生产能复现,能复现的话可以单步调试或者加日志,这种方式需要为加日志发版;
  • (2)不能复现的,又想加日志,可以试下使用 arthas

3.看数据

上面接口会在数据库、缓存、搜索库有对应记录,还有对应记录的操作历史,可以在数据工具查看,也可以在管理后台观察对应数据。

二、bug分类

这里讨论下常见的一些bug.

(1)NPE、空数组、空集合

除了臭名昭著的NPE,日常还会碰到空数组和空集合被访问的问题,一般是数据出问题了,预期有值但是产生了空,访问[0]或者get(0)就会有问题。

修复:

  • 数据修复:有些是数据问题,这种可以试下数据修复;
  • 代码修复:加个if-null判断,用Optional,或者用断言返回业务异常。

注意:可以用arthas线上修复,不过在生产上有点危险,其次要记得合并代码。

预防:

  • 静态检测spotBug找bug
  • chatGpt 单元测试

(2)OOM

gc行为目前是通过grafana看,不过这块一般很少动。出现OOM,目前我的一般做法是用MAT看dump文件(使用MAT定位OOM异常代码)。下面是导致OOM的几次总结:

大查询

一次数据查询加载数据太大。处理:

  • 减少join查询,改成多次查询,在java里map处理;
  • limit分页;

条件丢失

ORM 框架会处理查询条件,但是因为条件数据为null,条件就没了,如果全部条件都没了,会导致查询条件为空,加载全表。结果就是数据库io升高,程序的内存也被耗尽。

select * from user where age=?1 and name=?2; 

-- 代码可能会将age=null和name=''的处理成
select * from user;

修复方式:

  • 异常数据处理,但是风险还在;
  • null 不查询,提前终止;

预防:

  • 查询加上limit分页
  • 单元测试+边界测试

反序列化

有个接口返回了一个大的json,其实也不大,但是下游OOM了,因为部分服务给的内存不多。分析接口的作用:数据{后台用户信息,用户全部权限信息},下游只用到前面部分,目前是提供2个接口,新加一个只返回用户数据的接口。

(3)慢查询

慢查询不算异常,但是对使用方来说有点不能用了。优化手段:

  • 合适的index:这个可以观察或者用explain看查询sql找到要加的index,我们内部用的阿里云数据库,阿里云数据库有优化建议,不过一般不直接使用,会稍作调整。
  • 避免回表:日常使用select * 会返回全部字段,如果接口只返回用到的字段,可以直接使用合适的索引命中数据。
  • 避免索引失效:in、order by、like 、范围查询等可能会导致索引失效,limit 很大也可能有问题。这种可以试下修改sql的写法优化。
  • 架构优化:反范式,冗余部分字段,还有一些计算结果存入库。
  • 缓存:jvm caffine缓存+redis缓存

(4)大量数据操作

有些操作是很耗时的,比如调用数据库、调用外部接口服务,因为有网络调用,在循环里面操作就更慢了。优化方式:

  • 数据库交互改成批量操作,比如查询,用可以索引的查询条件in,再map。多条update可以改成批量。insert into 可以改成批量,或者合成一个sql。
  • 接口查询,如果没有依赖,可以用线程池+future/CompletableFuture,控制返回可以用join/future.get/countdownlatch,比较粗暴的手段是parallelStream。
  • 架构调整:有些查询是没必要的,或者说应该下沉到数据中台,而不是在某个服务里面去做一些累死人的操作。
  • 批量改成增量:部分操作是在设计的时候采用了拉取数据的模式,其实可以通过事件(主动mq、canal之类的解析binlog)推送出去,变成分散的增量式的操作。

(5)事务丢失

spring声明式事务是会丢失的,根本原因是spring的代理行为是产生了一个代理类,而方法直接调用不会使用代理方法。这种可以改成bean调用来恢复代理:

service A{

    @事务
    methodA(){
        methodB(); // 这里会丢失methodB事务
    }

    @事务
    methodB(){

    }
}
service A{

    serviceB   
    @事务
    methodA(){
        serviceB.methodB();
    }
}

service B{
    @事务
    methodB(){

    }
}
当然,这样改有点大,可以手动管理事务。

(6)时序和数据一致性问题

部分事务还没提交,但是状态被使用了。这种有2个做法:

还有一种是跨应用或者mq消息先发出来了,这种涉及数据一致性了。

  • 减少修改:jpa#save 这种ORM调用会重新更新全部数据字段,多写个接口,只更新变化的部分。
  • 分布式事务: 我们内部目前很少用分布式事务,目前主要是利用日志、业务异常记录,做人工补偿。
  • 状态机+幂等调用+重试:状态不符合,调用失败;重试k次,状态符合,更新成功。对于重要业务,上死信,不重要业务,记录结果,不做重试。

(7)异步状态共享问题

多线程比较麻烦的问题是状态共享,可以用深拷贝或者threadLocal,做对象数据分发,避免共享。还有一种是避免修改,如果是只读就不存在差异问题。

(8)this is a feature

这种需要倒逼产品修改了。请尽情battle~

三、思考

记住一个点:

程序是一个状态机。
                - 蒋炎岩老师

不管是单体用用还是微服务架构应用,都是一个状态机或者是n个状态机组合的状态机。而状态来自数据库(包括结构化、非结构化的)、用户输入。
状态机有初始化状态,某个时刻的状态。日志、监控、数据库记录是观察状态机的手段。一个可观察或者说足够透明的系统,是可以追溯记录的变化的。微服务一般使用同一日志平台+调用链来观察整个系统。

所以,一个完整的系统治理,在开发、测试、维护阶段,都涉及状态的处理。而机器是不会骗人的,一切不正常的状态,都是超出了预期而已。我们要敬畏代码,做好开发测试工作,防患于未然才能减少bug。

四、总结

少写代码,bug就少了。
怎么可以少写代码,又能把事做成?
修炼自己!

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYFuVmzc' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片