【Java】记录一次服务性能问题定位、调优完整过程

背景

事件中心服务新增定时消息功能:

  1. 基于 RocketMQ 5.0 的 TimerMessage 特性,作为触发定时消息的机制;
  2. 事件中心API(pubsub)提供新的定时消息接口,作为定时消息的提交入口;
  3. 定时消息元数据保存至 TiDB,RocketMQ 内只保存定时消息对应的主键ID;
  4. 定时消息执行服务消费 RocketMQ 指定 Topic ,调用事件中心回调接口,发送消息至Kafka;
  5. 为消息保证顺序性,RocketMQ Consumer 使用顺序消费。

定时消息的链路为:定时消息接口(pubsub) --> TiDB、RocketMQ --> 定时消息执行服务(Consumer) --> 定时消息回调接口 --> Kafka

本次压测及调优,只针对定时消息接口、定时消息执行服务(Consumer)、定时消息回调接口。

压测准备

压测意图

  • 测试提交定时消息接口在不同 QPS 时的 RT 情况

  • 观测pubsub服务在负载情况下的资源使用情况

  • 测试consumer的消费能力、得出消息积压与Consumer数量的比例

  • 测试定时消息回调接口的QPS峰值

场景描述

  • pubsub单实例,最终调整配置和生产环境一致

    • limit:8c-8g
    • -Xms4G -Xmx4G
  • 模拟固定QPS对提交定时消息接口进行压测:

    • QPS 1000
    • QPS 3000
    • QPS 5000
  • 堆积一定数量的定时消息:

    • 观察Consumer消费速度
    • 观察定时消息回调接口RT情况
    • 观察pubsub服务资源使用情况

观测指标

pubsub服务器:

  • 机器CPU使用情况

    • top -Hp {jvm_pid}
  • 查看线程级别 user, system 情况

    • 火焰图
  • 主机 load

  • GC 情况

    • 若没有 gc 日志,用 jstat 临时输出一下
  • 内存增长情况

  • 线程情况

  • 接口耗时(通过压测平台监控查看)

    • 99.99% RT
    • 平均 RT

定时消息执行服务:

  • consumer TPS

  • consumer 服务监控

    • jvm监控
    • httpclient监控
      • QPS
      • RT
  • consumer 主机监控

    • cpu
    • load
    • memory
  • pubsub服务器:

    • 机器CPU使用情况
    • 内存增长情况
  • 消息延迟情况

    • 对比数据库期望发送时间和实际发送时间

初步压测结论

  • pubsub服务2c-6g,单实例;Consumer服务2c-6g,单实例

  • 定时消息提交接口并发数1000,思考时间为1000ms,模拟QPS为1000的场景,平均RT在好几秒,吞吐量只有200左右

  • 单个消费者TPS只有60左右

  • pubsub服务及Consumer服务资源使用率均保持在很低水平

  • pubsub服务young GC频繁

离预期相距甚远,预期是可以承受上万条消息积压仍能保持毫秒级别的误差,所以优化目标为:

  • 提升pubsub定时消息接口单实例QPS至3000+,99.99% RT 在200ms内;
  • 提升Consumer消费TPS至10000+

排查过程记录

排除中间件及网络因素

在pubsub服务实例上ping TiDB、RocketMQ服务地址,延迟均为5ms内,先排除网络原因。

使用相同的压测脚本,尝试压了测试环境的pubsub服务,测试环境的压测结果很正常,QPS达到预期设置的1000,平均RT在10ms内,测试环境使用的是MySQL,生产环境使用的是新申请的阿里云服务器搭建的TiDB,怀疑是TiDB服务端配置或者其他原因导致影响性能。

将生产环境上pubsub使用的TiDB换为同网络环境的MySQL,压测情况并无变化,排除。

RocketMQ服务在相同网段进行过压测,压测结果正常,排除。

由于测试环境和生产环境压测结果大相径庭,很长一段时间的排查方向都指向了生产环境k8s的网络、组件间的延迟等,于是又把pubsub部署至生产环境的一台物理机上,结果任然保持不变。

客观测试了一下测试环境和生产环境的网络差异:

并发数 1,思考时间 1000ms

测试环境:

生产环境:

分析

每次接口提交,大致产生的跨进程交互如下:

内容测试环境耗时生产环境耗时
施压机到pubsub主机0.180.6
pubsub主机到tidb,开启事务0.064.9
pubsub主机到tidb,运行 sql0.064.9
pubsub主机到rocketMQ,生产消息0.34
pubsub主机到tidb,提交事务0.064.9
总计0.6619.3
本场景压测RT平均值7.9840.93

结论:

由于测试环境的物理距离问题,天然有至少5倍左右的性能差异,所以测试环境的压测结果超出预期的理想,得益于各组件的网络低延迟,但是生产环境的网络延迟属于正常范围,还需进一步排查。

借助Arthas及Skywalking

使用 Arthas 的 trace 命令,对接口的主要逻辑方法进行耗时追踪:

发现真正执行逻辑的代码只耗时13ms左右,但是执行方法之前,有一个500多ms的耗时。

使用skywalking观察耗时很长的请求,结果如下:

结论相同,在执行SQL前,有一个很长的等待时间,初步定位到问题,怀疑是数据库连接池的问题。

暴露prometheus指标

pom文件中添加:

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <scope>runtime</scope>
        </dependency>

添加配置:

# 对外暴露所有监控指标
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    prometheus:
      enabled: true # 激活普罗米修斯
    health:
      show-details: always # 健康值总是展示细节
  metrics:
    export:
      prometheus:
        enabled: true # 指标允许被导出

启动服务后,访问:/actuator/prometheus

可以看到默认连接池最大数量是10,大量并发情况下,获取连接池时,耗费了大量的时间。

修改数据库最大连接池数

场景一至场景四:并发数 500,思考时间 400 ms

场景一

pubbsub 调整内容:

  1. tomcat 线程数改为 64
  2. connection 连接池数量改为 10

分析

  1. cpu 使用率不高,execute 前执行时间较长,怀疑是获取 jdbc connection 时间较长,更改 hikari 连接池数量为 32
  2. 压测平台与 sk 查看不同,是因为存在请求积压,即等待了一段时间后,才开始被执行

场景二

pubsub 调整内容:

  1. tomcat 线程数改为 64
  2. connection 连接池数量改为 32

第一次压测:

第二次压测:

分析:

  1. 从 skywalking 看执行时间已经明显缩短,应该是增大了 connection 连接数导致
  2. 第二次执行压测时,性能已明显上升,应该是第一次压测时连接池还没有达到指定数量,都要重新建立连接,导致耗时较长
  3. cpu 使用率升高,说明线程使用 cpu 的时间多了起来,是正确的优化方向

场景三

pubsub 调整内容:

  1. tomcat 线程数改为 64
  2. connection 连接池数量改为 64

分析:

  1. 用在等待连接方面的耗时几乎已经没有
  2. cpu 还有一定空闲资源,进一步增大线程数和对应的连接数

场景四

pubsub 调整内容:

  1. tomcat 线程数改为 128
  2. connection 连接池数量改为 128

分析:

  1. cpu 已到达瓶颈,调整线程数量收益应该不太明显了
  2. 这时调整 gc 相关参数应该会有些收益

场景五

尝试提升压力,观察 RT 情况:并发数 500,思考时间 400 ms

分析:

  1. CPU 使用率下降,可能是 c2 代码优化已经完成
  2. TPS 上涨,RT 没有明显变化,继续加压

场景六

并发数 500,思考时间 300 ms

分析

  1. cpu 没有到达瓶颈,继续加压

场景七

并发数 500,思考时间 200 ms

场景八

pubsub 调整内容:

  1. tomcat 线程数改为 200
  2. connection 连接池数量改为 200

分析:

CPU已打满,继续增加线程数无提升,线程数已接近理想状态值。

结论

CPU核数、Tomcat线程数、数据库连接池数比例约为:

1:100:100

将pubsub服务CPU核数修改为:8c,Tomcat线程数、数据库连接池数修改为:800

Consumer消费能力优化

初始状态:

  • Consumer服务:2c-6g,启动一个消费者
  • RocketMQ Topic读写队列总数均为48
  • pubsub服务:8c-6g,单实例
  • 消费者TPS:60左右

增加消费者

将消费者增加至20个:

消费服务资源使用情况:

定时消息回调接口:

消费者TPS:

可以看到增加消费者后,消费能力明显提升,但是个服务资源使用率都未达到上限。

继续增加Consumer数量,TPS并没有提升。

增加Topic Queue数量

Queue总数:96

(这里测试消费能力,可以只修改读队列数)

消费服务实例数单个服务消费者数量消费者总数消费TPS
1203000左右
140403000左右
220405000左右
240805000左右

以上压测过程中,pubsub服务和消费者服务的资源使用率都只维持在中等水平,单个消费者实例上消费者数量达到40时,出现个别报错:

第一次出现这个异常,刚开始没仔细看异常信息,以为是pubsub回调接口处理不过来了,没有在意,造成了一个排查疏漏,走了一些弯路。

Queue总数:192
消费服务实例数单个服务消费者数量消费者总数消费TPS
120203000左右
140403000左右
220405000左右
420808500左右
5201009500左右
6201209500左右
Queue总数:900
消费服务实例数单个服务消费者数量消费者总数消费TPS
4301209000左右
53015012000左右

由于每一轮压测成本过高,已经达到预期,没有继续进行后续压测。

消费者在30*4时,再次出现大量异常:ConnectionPoolTimeoutException

初步结论:

  • 增加消费者数量和 Topic队列数,可以有效提升消费者TPS

  • 目前单个消费服务配置20~30个消费者达到消费峰值,但是消费服务和pubsub服务的资源使用还远远没到瓶颈。

  • 消费服务内有http客户端,怀疑是瓶颈在http连接池上,而之前出现的异常:ConnectionPoolTimeoutException,正式由于http客户端使用了连接池,从连接池内获取http连接超时导致的。

调整消费程序http最大连接池数

消费程序中的http客户端使用的是apache提供的CloseableHttpClient,有两个线程池核心参数:

  • maxTotal:连接池最大连接数
  • defaultMaxPerRoute:每路由最大连接数

maxTotal默认是200,defaultMaxPerRoute默认是50,而消费程序中只会调用一个接口,所以实际只有50个连接是可用的,将defaultMaxPerRoute修改为200。

消费者TPS有提升,不再有报错信息,消费者服务CPU消耗有提升。

Consumer参数微调

对Consumer进行了几个参数的调试,包括:consumeThreadMax、consumeThreadMin、consumeMessageBatchMaxSize、pullBatchSize,其中consumeThreadMax、consumeThreadMin两个参数修改后对消费能力有较明显提升,其余两个对于顺序消费场景无明显提升。

consumeThreadMax、consumeThreadMin默认为20,修改为40:

CPU消耗有提升,加大consumeThreadMin后,消费者TPS可以瞬间达到消费峰值

继续加大consumeThreadMin后,消费TPS出现下滑。

Tips:Consumer客户端会根据消费线程的压力,动态增加消费线程:

总结

优化总结

  • pubsub服务资源为8c-6g,核数和Tomcat线程数比例为1:100、Tomcat线程数和数据库连接池数比例为1:1;
  • 基于以上配置,pubsub单实例定时消息接口QPS理想值为4500左右、回调接口QPS高于此;
  • 目前Consumer最佳配置为单实例30个消费者,每个消费者配置consumeThreadMin=40,按理说可以通过增加资源,来增加单实例上的消费者数量,减少实例数,但是通过测试发现不可行,由于时间原因没有继续探索。
  • 增加Topic 读队列数和消费者数量可以提升消费能力,本次测试过程中,消费者和队列数比例达到1:2时,获得最大消费TPS;
  • pubsub程序初始时没设置xmn,导致young GC比较频繁,后续根据观察各区域内存的变化情况,将xmn设置为总堆大小的3/4。

经验总结

  • 压测时间需要尽量长一点,需要等编译优化完成后,程序的性能才能达到一个稳定值,尤其是对刚启动的项目;
  • 优化过程中要注意小细节,对任何出现的异常都需要精确定位后再继续,避免大方向错误;
  • 借助Trace工具、prometheus指标暴露插件能更快定位问题;
  • 若性能达到瓶颈时,资源使用率没有提升,需要考虑各环节中使用到的线程池、连接池的限制;
  • 注意版本不同时,一些配置的变化,如:Tomcat线程数配置,spring 2.3版本之前:server.tomcat.max-threads,spring 2.3版本之后:server.tomcat.threads.max
  • 做优化前,需要有一个整体的规划,对整体的性能瓶颈点有一个预先的认知,包括服务本身、使用到的各中间件配置、客户端配置等,逐个进行排除
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值