前言

复盘最近碰到的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个做法:

  • 扩展事务的规模,或许修正事务的隔离级别。
  • 保证线性一致性:监听事务提交事情,@TransactionalEventListener的运用和完成原理

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

  • 削减修正:jpa#save 这种ORM调用会从头更新悉数数据字段,多写个接口,只更新改变的部分。
  • 分布式事务: 咱们内部现在很少用分布式事务,现在主要是运用日志、事务反常记载,做人工补偿。
  • 状况机+幂等调用+重试:状况不符合,调用失败;重试k次,状况符合,更新成功。对于重要事务,上死信,不重要事务,记载成果,不做重试。

(7)异步状况同享问题

多线程比较麻烦的问题是状况同享,能够用深拷贝或许threadLocal,做对象数据分发,避免同享。还有一种是避免修正,假如是只读就不存在差异问题。

(8)this is a feature

这种需求倒逼产品修正了。请尽情battle~

三、考虑

记住一个点:

程序是一个状况机。
                - 蒋炎岩教师

不管是单体用用仍是微服务架构运用,都是一个状况机或许是n个状况机组合的状况机。而状况来自数据库(包含结构化、非结构化的)、用户输入。
状况机有初始化状况,某个时间的状况。日志、监控、数据库记载是调查状况机的手法。一个可调查或许说满足通明的体系,是能够追溯记载的改变的。微服务一般运用同一日志渠道+调用链来调查整个体系。

所以,一个完好的体系管理,在开发、测验、维护阶段,都触及状况的处理。而机器是不会骗人的,全部不正常的状况,都是超出了预期而已。咱们要敬畏代码,做好开发测验作业,防患于未然才干削减bug。

四、总结

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