参考文章:手把手带你入门火焰图——perf
关于CPU、内存、IO、网络问题处理思路整理(IO)
在项目开发中,有一处的需求是,定时触发一个同步逻辑,同步逻辑中是请求其他平台的接口返回的信息,与我们服务端库里的信息做比对,以拉取到的信息为基准,如果本地库里信息多则删除,少则插入,错则更正。
原本的实现方式是使用Quartz定时任务,任务执行时,虽然微服务有多个实例,执行该定时任务的只是其中一个实例,并未充分利用多实例的优势。决定对其进行优化,改为使用消息队列,在另一个微服务A中发送消息,在微服务B中消费消息,这时B服务的多个实例都会参与消费,应该可以达到一定的优化目的。
经过改造后,在微服务A中新增一个定时任务,该定时任务负责通过kafka发送需要同步的信息至微服务B,B消费消息进行同步逻辑。在代码修改好后,经过测试,发现微服务A和微服务B的cpu都过高,首先排查微服务A的cpu过高问题。
此次排查使用火焰图工具Async-profiler
简单介绍下火焰图:
火焰图是基于 stack 信息生成的 SVG 图片, 用来展示 CPU 的调用栈。
y 轴表示调用栈, 每一层都是一个函数. 调用栈越深, 火焰就越高, 顶部就是正在执行的函数, 下方都是它的父函数.
x 轴表示抽样数, 如果一个函数在 x 轴占据的宽度越宽, 就表示它被抽到的次数多, 即执行的时间长. 注意, x 轴不代表时间, 而是所有的调用栈合并后, 按字母顺序排列的
火焰图是 SVG 图片, 用浏览器打开可以与用户互动。
官方地址:
https://github.com/jvm-profiling-tools/async-profiler
解压工具包到需要排查问题的机器上,里面有一个执行脚本profiler.sh
执行如下命令:
./profiler.sh -d {采样时间s} {PID} -f {输出文件名称}
例如,需要抓包的JAVA应用服务进程ID是XX,要抓包的时间是20s,命令如下:
./profiler.sh -d 20 XX -f /tmp/pic.svg
抓到的火焰图,搜索包名,可以看到占用cpu比较高的语句如下图所示:
可以看到cpu均被一个名字叫XXServiceImpl.getXX的内部接口占用了。通过走读代码发现在微服务B中消费时,会调用微服务A的该接口获取必要的信息,现在微服务B消费大量消息时,所有实例都会调用该内部接口,导致微服务A的cpu升高。
代码大致如下:
使用JPA
@Repository
public interface UserRepository extends JpaRepository<User, Serializable>, JpaSpecificationExecutor<User> {
List<User> findAllByGroupId(String groupId);
}
使用原生JDBC
@Autowired
private JdbcTemplate jdbcTemplate;
public List<User> findUsers(String groupId) {
StringBuilder sql = new StringBuilder();
sql.append("select a.id, a.name name, a.age age, a.groupId groupId "
"from site_info a " +
"where a.group_id = :groupId");
NamedParameterJdbcTemplate npj = new NamedParameterJdbcTemplate(jdbcTemplate.getDataSource());
List<User> list = npj.query(sql.toString(), MapBuilder.create(new HashMap()).put("groupId", groupId).build(), (resultSet, rowNum) ->
Site.builder()
.id(resultSet.getString("id"))
.name(resultSet.getString("name"))
.age(resultSet.getInt("age"))
.groupId(resultSet.getString("groupId"))
.build());
return list;
}
微服务B的cpu高,且kafka消息有积压。因此需要控制发送速率,避免发送过快,消费者消费不过来,导致kafka积压。
通过监控消费者执行的时间,估算出消费的情况,在微服务A中修改成每隔X秒发送一条消息。以防万一,把该参数配置成可动态获取的参数,这样万一环境或网络不好,消费速率过慢,可以及时调整发送速率保证消息没有积压(不是完全没有,查看消费情况时,积压大概几条或者十几条)。修改后,cpu降了一些,没有降到理想的程度。
仍然采用火焰图工具定位cpu高的问题。cpu几乎被占满,接近100%。发现在该业务逻辑里,有两条sql几乎占满了cpu,使用之前的方法,仍然是修改为原生的JDBC进行查询。
火焰图如图:
可以看到是被findAllXXXDesc方法占据了大部分cpu。对此进行修改为原生JDBC查询。