排查CPU过高问题记录

参考文章:手把手带你入门火焰图——perf
关于CPU、内存、IO、网络问题处理思路整理(IO)

在项目开发中,有一处的需求是,定时触发一个同步逻辑,同步逻辑中是请求其他平台的接口返回的信息,与我们服务端库里的信息做比对,以拉取到的信息为基准,如果本地库里信息多则删除,少则插入,错则更正。
原本的实现方式是使用Quartz定时任务,任务执行时,虽然微服务有多个实例,执行该定时任务的只是其中一个实例,并未充分利用多实例的优势。决定对其进行优化,改为使用消息队列,在另一个微服务A中发送消息,在微服务B中消费消息,这时B服务的多个实例都会参与消费,应该可以达到一定的优化目的。

微服务A的cpu过高

经过改造后,在微服务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

Async-profiler工具的使用

  1. 解压工具包到需要排查问题的机器上,里面有一个执行脚本profiler.sh

  2. 执行如下命令:

    ./profiler.sh -d {采样时间s} {PID} -f {输出文件名称}
    

例如,需要抓包的JAVA应用服务进程ID是XX,要抓包的时间是20s,命令如下:

./profiler.sh -d 20 XX -f /tmp/pic.svg
  1. 执行后,20s后会生成一个pic.svg的图片(火焰图),用浏览器打开分析。

抓到的火焰图,搜索包名,可以看到占用cpu比较高的语句如下图所示:

排查CPU过高问题记录_第1张图片

问题原因

可以看到cpu均被一个名字叫XXServiceImpl.getXX的内部接口占用了。通过走读代码发现在微服务B中消费时,会调用微服务A的该接口获取必要的信息,现在微服务B消费大量消息时,所有实例都会调用该内部接口,导致微服务A的cpu升高。

解决

  1. 在微服务A中直接把需要的信息从库里查出来,通过kafka发给微服务B,避免微服务B中再反过来调用内部接口获取数据。修改后再经测试,cpu降低很多。
  2. 此外,有一条从数据库查询数据的语句,使用了JPA查询,JPA通过orm框架转换成对象时会耗费比较多的时间(疑似,是同事讲的),而改为使用原生的JDBC查询(见JPA 使用过程中问题汇总)。修改此后经过测试,cpu又下降了,基本上恢复到正常的水平,使用情况最高值的25%左右。

代码大致如下:
使用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过高

微服务B的cpu高,且kafka消息有积压。因此需要控制发送速率,避免发送过快,消费者消费不过来,导致kafka积压。

  1. 通过监控消费者执行的时间,估算出消费的情况,在微服务A中修改成每隔X秒发送一条消息。以防万一,把该参数配置成可动态获取的参数,这样万一环境或网络不好,消费速率过慢,可以及时调整发送速率保证消息没有积压(不是完全没有,查看消费情况时,积压大概几条或者十几条)。修改后,cpu降了一些,没有降到理想的程度。

  2. 仍然采用火焰图工具定位cpu高的问题。cpu几乎被占满,接近100%。发现在该业务逻辑里,有两条sql几乎占满了cpu,使用之前的方法,仍然是修改为原生的JDBC进行查询。
    火焰图如图:
    排查CPU过高问题记录_第2张图片

可以看到是被findAllXXXDesc方法占据了大部分cpu。对此进行修改为原生JDBC查询。

  1. 在微服务B消费的逻辑中,有一处方法是调用feign从其他平台获取token,进而再使用token从该平台获取更多的数据信息。发现由于代码逻辑写的有问题,导致所有的获取token的接口直接失败了。据此推测cpu高的可能情况是:消费者里计算后调用第一个网络IO时就失败了,从而立刻处理下一个消息,导致线程池一直在处理消息,进行计算,所以cpu较高。修改逻辑错误后,消费的逻辑里既有计算,又有网络io,又有数据库读写io,io阻塞时,cpu不会一直计算,会让cpu下降,不再一直进行计算。修改后cpu基本达到理想的程度,也是使用情况最高值的25%左右。

小结

  1. 使用火焰图定位CPU过高问题
  2. 解决kafka消息积压时,应先估算消费能力,再控制发送速率
  3. 写代码时要认真仔细,写原生JDBC查询组装返回的内容时,一定要将字段对应上,避免简单的错误。

你可能感兴趣的:(解决问题记录,kafka,java)