接口性能调优

66 阅读3分钟

性能问题排查顺序:

客户端并发量是否足够大->tomcat线程池是否占满->cpu利用率是否急剧增高->GC频率是否急剧增高->是否存在慢SQL->数据库连接池是否占满

工具:

  • Jmeter
  • VisualVM
  • IntelliJ Profiler
  • Driud Monitor

1 客户端并发量是否足够大

A1jnzEGaIwxiE3Cn3McwViG-7WryVd5NnOpFoPL7Uo8.png

如果 Number of Threads 设置的太小或者 Ramp-up period 设置的太大都会导致每秒只有很少的线程真正开始发送,表现为tomcat线程池无法充分利用且吞吐率低

2 Tomcat 线程池是否占满

java线程池配置

Hbg3U_kGpnGiPG84FSsMTrF37zcCVO4Dd-Gy7TOcI30.png 场景1:

Nk1-uxjCpggHzWULwWu2KH6kXYaimHksyl9yHWdRTc8.png

dLb6b0-2rpd0thpbSEr4i8QejKePQxybUBRf9CRQpTM.png

处理请求时业务线程(exec-n结尾的)始终保持10个没有增长,说明线程池未充分利用

visualvm默认每隔200ms的采样中未采样到业务线程的running状态,说明每次请求执行时间都很短未被采样到

场景2:

B54VILiwgQUwZMr-Fsl8wJBqyaO_wMe08hFt1MYnZs0.png

Q1_KfWKEfu4Lh0d31iLFxA0VyTHEMW8129hPwGz16qs.png

处理请求时业务线程(exec-n结尾的)由10个增长到200个,说明线程池被充分利用了

visualvm默认每隔200ms的采样中采样到了业务线程的running状态,说明有任务执行时间大约在 200ms 甚至更长

场景3:

  • JMeter 配置:2s 内起 5000 并发(等于瞬间洪峰)。
  • 线程池:exec 从 10 涨到 200。
    • 160 个线程同时 Running(业务逻辑正在跑)。
    • 30 个线程同时 Monitor(说明在等锁/竞争共享资源)。
  • 5 秒后 exec 又涨到 205(说明线程池仍在扩容、任务没消化完)。
  • 错误全是 java.net.SocketException: Socket closed

scvB_04IF1swPD_uPiaa8rijfVEby7yA2qp37VU1hQw.png

T8M_XdVQEGdyBe9E8iYhhcvQ_p6mHZVuBF-nXJgEOqI.png

CULuGanTZl1Y1k4nd_GK97u-N4p0oZCJv9ZlvCiiCvk.png

Tomcat有连接池和最大并发保护,达到上限后,新的连接请求要么进入 acceptCount 队列,要么直接被拒绝,此时需调大server.tomcat.threads.max

出现 Monitor 状态可能是:容器队列 server.tomcat.accept-count(默认 100)也满了。

出现Monitor也可能是因为业务逻辑里有全局锁(比如 synchronized、数据库连接池锁、缓存锁),在高并发下大量线程卡在锁上。

场景4:

  • JMeter 配置: 5s内启动 10000 个线程。
  • 后端线程池:从 10 涨到 200(说明后端在扩容处理请求)。
  • 线程状态:出现 Running(执行)。
  • 错误率:16%

ty04jQkF1oXqy9Wun2xU02dCwyg1msXU6_s0eFJUNmE.png

bOEBR-dd53SCfjbrc3sMa9n8y1Q1dPh_Fb0hz_ZQ3is.png

EuVwzfoyArnIhACsqpYta1lCySLZZ_tGO9pzUc2U9Lo.png

Zln_m-5Ec63MmqQqXoEthiWrQjd6Odf_747fvWF7x08.png

服务端直接拒绝某些请求,说明服务端进程可用的文件描述符不够(ulimit -n)或服务端 backlog(半连接队列)太小

也可能是客户端单机 JMeter 发 1w 并发本身就到极限了

3 CPU 利用率是否急剧增高,GC 频率是否急剧增高

TQuAId4fdU-FBoFnH0VtZVpncHzOK0cRjRIlomD0fXY.png

I8xNwKdpulnDxSgWwFgLwxJxygmUZUWjRRAGR_b_Ti8.png

DiW1sFfOOIGWCWcm1Kc-bU3lLFQhT6xYVZAVxe8pSeQ.png

cpu急剧增高和GC频繁的原因是每次登录成功都将yaml文件加载到内存一次(导致GC频繁),并进行解析(导致CPU增高),将初始化代码放到全局参数中(private final Parser uaParser = new Parser();)后效果如下:

mFd546z5QWpfedCZltMkOIGinNiBzaoZY1T5D-7kP9k.png

4 是否存在慢 SQL

配置参数

# 打印执行超过 10ms 的 SQL(可根据情况调整,比如 50ms)
spring.datasource.dynamic.druid.stat.slow-sql-millis=10
# 是否将慢 SQL 打印到日志中
spring.datasource.dynamic.druid.stat.log-slow-sql=true

40dqVmKDOmvrIngzSGDC2hjw1aPzELbh-AbqphX9Oq8.png

如果出现慢SQl则进行相应调优即可,如分区、增加索引命中率等

p2727RJYMyhhw3Hlm8bHukng6AkWWRmuga4uuPmZPo8.png

5 数据库连接池是否占满

CL4bWptQNz15aTHhPfvg0-QYtqpsqboeisvdOjmgF7w.png

上图未充分利用数据库连接池

WjZ1sAvcxgQGsG8aejAogXlhxByJ1kdfgizYBVvcnL8.png

上图充分利用了线程池

当数据库是瓶颈时,可适当调大数据最大连接数

show max_connections;
ALTER SYSTEM SET max_connections = 500;