千阶智能技术博客 | 2026-07-14
前言
千阶智能(qjchat.com)的 AI 智能外呼平台每天处理海量通话请求,网关层的稳定性和低延迟直接关系到外呼接通率。我们在生产环境中长期被一个问题困扰:JDK8 + CMS 组合下,每 3 周出现一次长 GC,STW 暂停长达 10 秒,期间所有外呼请求超时失败。
经过评估,千阶智能技术团队决定将核心网关从 JDK8 升级到 JDK17,同时将垃圾回收器从 CMS 切换为 ZGC。本文完整记录迁移的预期收益、验证方法、实测数据与 GC 分析,供千阶智能内部其他服务以及面临类似问题的团队参考。
一、JDK8+CMS VS JDK17+ZGC
1.1 性能优化点
与性能相关的升级分为两部分:一部分性能优化需要使用新特征才能有收益,另一部分性能优化升级完成便会有收益。
改进的垃圾回收器:本次使用 ZGC。
改进的 JIT 编译器:本次并未使用,后续可考虑启用 Graal 编译器。
1.2 推演预期收益
延迟:ZGC < 1ms 最大暂停时间(JDK < 16 是 10ms,JDK >= 16 是 < 1ms),延迟降低来源于长 GC 的解决,解决了现有的 JDK8+CMS GC 在连续大内存分配时导致内存碎片(如大字符串、大数组)的问题。
吞吐量:吞吐量预期提升 10%,来源于新的 ZGC 垃圾收集器在内存分配、内存回收时 CPU 使用会降低 10%。
1.3 验证方式
收益在延迟、吞吐量两个性能指标上,延迟可以直接统计,但真实在线流量下的吞吐量很难测定,所以以资源占用来判定。
1.3.1 延迟统计
通过以下命令统计响应时间,包含平响、分位响应时间:
grep -v 'monitor/health' /home/app/qj-gateway/var/log/logbk/20230410/access* \
| awk -F ' ' '{print $(NF-1)}' \
| sort -n | uniq -c | awk -f cal.awk
cal.awk 脚本如下:
#! /usr/bin/awk -f
{variance=0;sumCount+=$1;sumCost+=($2*$1);count[NR]=$1;cost[NR]=$2}
END { staticTotal[0]=50;
staticTotal[1]=66;
staticTotal[2]=80;
staticTotal[3]=85;
staticTotal[4]=90;
staticTotal[5]=95;
staticTotal[6]=98;
staticTotal[7]=99;
staticTotal[8]=99.9;
staticTotal[9]=99.99;
staticTotal[10]=100;
staticFlag[0]=1;
staticFlag[1]=1;
staticFlag[2]=1;
staticFlag[3]=1;
staticFlag[4]=1;
staticFlag[5]=1;
staticFlag[6]=1;
staticFlag[7]=1;
staticFlag[8]=1;
staticFlag[9]=1;
staticFlag[10]=1;
printf "%3s %10s %15s %15s\n", "static", "cost", "count", "diffPre";
averageCost = sumCost/sumCount;
for(i=1; i <=length(count); i++) {
diff = (cost[i] - averageCost);
variance += (diff*diff*count[i]/(sumCount-1));
countTotal += count[i];
for (j=0; j <length(staticTotal); j++) {
if (countTotal >= sumCount*staticTotal[j]/100) if (staticFlag[j]==1) {
staticFlag[j]=sprintf("P%-3s %10s %15s %15s", staticTotal[j],cost[i],countTotal, countTotal - countTotalPre);
countTotalPre = countTotal;
}
}
};
for( i=0;i<length(staticFlag);i++) print staticFlag[i];
printf "count total: %s\n", sumCount, countTotal;
printf "average cost: %s \n", averageCost;
printf "variance cost: %s \n", variance;
}
1.3.2 资源占用统计
为提高置信度、降低其他影响(如监控 agent 等进程的影响),通过监控 Java 进程的 CPU,得到 CPU 平均使用核与使用核的方差。
每秒获取一次 CPU 使用核:
top -b -d 1 | grep -A 10 ^top
采集输出如下:
top - 15:42:28 up 1102 days, 3:20, 0 users, load average: 21.56, 21.58, 22.54
Tasks: 15 total, 1 running, 14 sleeping, 0 stopped, 0 zombie
Cpu(s): 21.7%us, 5.4%sy, 2.7%ni, 67.4%id, 1.5%wa, 0.0%hi, 1.2%si, 0.0%st
Mem: 196314484k total, 191484656k used, 4829828k free, 201756k buffers
Swap: 0k total, 0k used, 0k free, 42174472k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2083 app 20 0 46.2g 16g 3176 S 151.6 8.6 18903:08 java
42 app 39 19 5092m 147m 664 S 19.9 0.1 2193:45 monitor-agent
1 root 20 0 16160 1648 1240 S 0.0 0.0 0:00.33 init-pro
3 root 20 0 16160 1284 876 S 0.0 0.0 12:54.70 init-pro
--
top - 15:42:29 up 1102 days, 3:20, 0 users, load average: 21.56, 21.58, 22.54
Tasks: 15 total, 1 running, 14 sleeping, 0 stopped, 0 zombie
Cpu(s): 29.1%us, 7.6%sy, 3.8%ni, 55.0%id, 3.5%wa, 0.0%hi, 1.0%si, 0.0%st
Mem: 196314484k total, 190620124k used, 5694360k free, 201304k buffers
Swap: 0k total, 0k used, 0k free, 41193524k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2083 app 20 0 46.2g 16g 3176 S 231.7 8.6 18903:10 java
42 app 39 19 5092m 144m 664 S 23.0 0.1 2193:45 monitor-agent
3 root 20 0 16160 1284 876 S 0.9 0.0 12:54.71 init-pro
1 root 20 0 16160 1648 1240 S 0.0 0.0 0:00.33 init-pro
--
top - 15:42:30 up 1102 days, 3:20, 0 users, load average: 21.56, 21.58, 22.54
Tasks: 15 total, 1 running, 14 sleeping, 0 stopped, 0 zombie
Cpu(s): 27.4%us, 7.3%sy, 2.8%ni, 58.0%id, 3.5%wa, 0.0%hi, 1.0%si, 0.0%st
Mem: 196314484k total, 190696200k used, 5618284k free, 201324k buffers
Swap: 0k total, 0k used, 0k free, 41289680k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2083 app 20 0 46.2g 16g 3176 S 151.7 8.6 18903:12 java
42 app 39 19 5092m 144m 664 S 21.0 0.1 2193:45 monitor-agent
227 app 20 0 5598m 140m 2020 S 1.0 0.1 28:15.91 java
1 root 20 0 16160 1648 1240 S 0.0 0.0 0:00.33 init-pro
对采集数据计算平均值和标准差:
awk '{for(i=1;i<=NF;i++) {sum[i] += $i; sumsq[i] += ($i)^2}}
END {for (i=1;i<=NF;i++) {
printf "%f %f \n", sum[i]/NR, sqrt((sumsq[i]-sum[i]^2/NR)/NR)}
}' dc-a_cpu >> aver-std.dc-a
1.3.3 GC 暂停时间对比
JDK8+CMS real 暂停时间:
从 GC 日志中提取 real= 暂停时间,典型数据如下:
# 单日超过 100ms 的 STW 共 206 次
# 单日超过 200ms 的 STW 共 5 次
# 单日超过 300ms 的 STW 共 1 次
# 最长一次 real 暂停约 10 秒(长 GC)
JDK17+ZGC 暂停时间:
红色为全部线程 STW,黄色为部分线程 STW,典型数据如下:
# 全暂停(Pause)全部在 1ms 以内
Phase: Pause Mark End min=0.023 avg=0.108 max=0.574 ms
Phase: Pause Mark Start min=0.024 avg=0.053 max=0.266 ms
Phase: Pause Relocate Start min=0.019 avg=0.024 max=0.250 ms
# 部分暂停(Stall)超过 100ms 的仅 4 次,只影响个别 reactor 线程
Allocation Stall (reactor-http-epoll-6) 149.101ms
Allocation Stall (taskScheduler-3) 369.472ms
1.4 验证结果
延迟:在没有长 GC 的 Java 应用中,延迟几乎无变化。在有内存碎片导致的长 GC 的 Java 应用中,延迟的降低来源于长 GC 的解决(内存碎片生成速率越快,前后对比效果越好)。
吞吐量:CPU 利用率降低 10%,等价于吞吐量提升 10%。
二、千阶智能外呼网关迁移案例
2.1 系统背景
千阶智能外呼网关属于 QPS 非常高的应用,每实例 1000 QPS。当前平均每个实例 3 周一次长 GC,每次 GC STW 10 秒左右。
2.2 延迟统计
延迟结论:千阶智能外呼网关 3 周一次长 GC,在没有长 GC 的天数里,延迟没有明显的变化。有长 GC 的时候,效果就是长 GC 影响的时间,长 GC 越多的业务收益越大。
2.2.1 JDK8 延迟
命令如下,每个机房抽样一个:
grep -v 'monitor/health' /home/app/qj-gateway/var/log/logbk/20230411/access* \
| awk -F ' ' '{print $(NF-1)}' \
| sort -n | uniq -c | awk -f cal.awk
dc-a 机房:
static cost count diffPre
P50 11 11403426 11403426
P66 15 14439925 3036499
P80 25 17527536 3087611
P85 29 18525690 998154
P90 35 19540998 1015308
P95 47 20637999 1097001
P98 69 21233024 595025
P99 91 21445391 212367
P99.9 366 21639350 193959
P99.99 1761 21658843 19493
P100 59931 21661002 2159
count total: 21661002
average cost: 18.772
variance cost: 4120.08
dc-b 机房:
static cost count diffPre
P50 12 11621245 11621245
P66 16 14616507 2995262
P80 25 17587200 2970693
P85 29 18553862 966662
P90 35 19536799 982937
P95 47 20643596 1106797
P98 71 21248034 604438
P99 93 21457365 209331
P99.9 367 21650504 193139
P99.99 1766 21670005 19501
P100 59917 21672166 2161
count total: 21672166
average cost: 19.133
variance cost: 4392.93
dc-c 机房:
static cost count diffPre
P50 12 12185826 12185826
P66 16 14706159 2520333
P80 23 17417915 2711756
P85 27 18470322 1052407
P90 34 19504124 1033802
P95 46 20596734 1092610
P98 68 21244459 647725
P99 87 21452284 207825
P99.9 405 21647241 194957
P99.99 1804 21666679 19438
P100 59903 21668845 2166
count total: 21668845
average cost: 18.4702
variance cost: 4056.16
2.2.2 JDK17 延迟
命令如下,每个机房抽样一个:
grep -v 'monitor/health' /home/app/qj-gateway/log/logbk/20230411/access* \
| awk -F ' ' '{print $(NF)}' \
| sort -n | uniq -c | awk -f cal.awk
dc-a 机房:
static cost count diffPre
P50 12 11812377 11812377
P66 16 15481138 3668761
P80 24 18717435 3236297
P85 28 19878215 1160780
P90 35 21139589 1261374
P95 46 22212004 1072415
P98 68 22896082 684078
P99 88 23123364 227282
P99.9 396 23332797 209433
P99.99 1821 23353794 20997
P100 41225 23356124 2330
count total: 23356124
average cost: 19.2942
variance cost: 3638.57
dc-c 机房:
static cost count diffPre
P50 12 13081178 13081178
P66 16 15824476 2743298
P80 23 18707620 2883144
P85 27 19959368 1251748
P90 34 21105022 1145654
P95 47 22252895 1147873
P98 70 22893706 640811
P99 90 23124273 230567
P99.9 441 23331965 207692
P99.99 1934 23352984 21019
P100 30169 23355318 2334
count total: 23355318
average cost: 18.6652
variance cost: 3966.02
2.3 资源占用
统计口径:
- 真实流量均衡
- 每个物理机房挑了一个
- 通过 top 统计 java 进程,排除容器、监控 agent 等带来的其他影响
统计置信度分析:
- 样本:当前 JDK8 随机挑选了 3 个实例,JDK17 只部署了 2 个实例,样本不足
- 线程模型:对于千阶智能外呼网关,由于 JDK8、JDK17 基础参数(逻辑核数不同)导致两个线程模型 reactor 线程池大小不同,从 60 -> 6
- Spring Boot 大版本升级:Spring Boot 大版本升级会导致前后框架代码不同,会有性能差异
- CPU 限制规则:JDK8 为 CPU 软限制,JDK17 为 CPU 硬限制
- 新老版本日志变化(业务自身问题):日志优化降低了 8% 的 CPU 使用,做修订(后面会上线个等同版本,再刷新此数据)
资源占用结论(当前):
- CPU 突刺:幅度降 30%
- CPU 利用:降低 10%,吞吐量提升 10%
2.3.1 CPU 使用统计
| 实例 | 平均使用核数 | 使用核数标准差 | 采样时间、次数 |
|---|---|---|---|
| JDK8 - dc-b | 151.46 | 80.99 | 单实例 3600 秒 x 24 小时 |
| JDK8 - dc-a | 163.29 | 81.88 | 单实例 3600 秒 x 24 小时 |
| JDK8 - dc-c | 139.43 | 53.71 | 单实例 3600 秒 x 24 小时 |
| JDK17 - dc-c | 124.65 | 42.85 | 单实例 3600 秒 x 24 小时 |
| JDK17 - dc-a | 120.02 | 38.68 | 单实例 3600 秒 x 24 小时 |
2.3.2 CPU 分位统计
JDK17 CPU 分位统计:
# dc-c 机房
static cost count diffPre
P50 121.0 45370 45370
P66 141.0 60362 14992
P80 156.0 72938 12576
P85 162.0 77230 4292
P90 171.0 81588 4358
P95 194.0 86125 4537
P98 237.0 88851 2726
P99 265.0 89732 881
P99.9 363.0 90541 809
P99.99 417.0 90619 78
P100 442.0 90628 9
count total: 90628
average cost: 124.653
variance cost: 1835.96
# dc-a 机房
static cost count diffPre
P50 118.0 45534 45534
P66 137.0 60588 15054
P80 149.0 73251 12663
P85 154.0 77315 4064
P90 162.0 81687 4372
P95 181.0 86123 4436
P98 215.0 88842 2719
P99 245.0 89733 891
P99.9 325.0 90543 810
P99.99 380.0 90620 77
P100 407.0 90629 9
count total: 90629
average cost: 120.02
variance cost: 1496.07
JDK8 CPU 分位统计:
# dc-a 机房
static cost count diffPre
P50 149.6 44850 44850
P66 174.5 59009 14159
P80 204.5 71546 12537
P85 230.5 76029 4483
P90 262.5 80466 4437
P95 319.4 84926 4460
P98 398.2 87587 2661
P99 462.1 88481 894
P99.9 683.1 89284 803
P99.99 937.2 89365 81
P100 1190.4 89373 8
count total: 89373
average cost: 163.292
variance cost: 6704.04
# dc-c 机房
static cost count diffPre
P50 134.7 44952 44952
P66 154.8 59087 14135
P80 177.5 71625 12538
P85 195.4 76095 4470
P90 213.6 80634 4539
P95 239.4 85050 4416
P98 272.5 87756 2706
P99 295.4 88629 873
P99.9 357.3 89430 801
P99.99 452.1 89509 79
P100 507.0 89517 8
count total: 89517
average cost: 139.427
variance cost: 2884.34
# dc-b 机房
static cost count diffPre
P50 129.7 44801 44801
P66 151.7 59084 14283
P80 217.6 71526 12442
P85 243.6 75931 4405
P90 275.5 80409 4478
P95 323.4 84868 4459
P98 371.3 87557 2689
P99 400.2 88435 878
P99.9 484.8 89239 804
P99.99 569.3 89319 80
P100 740.8 89327 8
count total: 89327
average cost: 151.461
variance cost: 6559.96
2.3.3 原因分析
原因 1:内存管理能力提升,从抽样统计上看降低了如下:
| GC 方案 | GC 线程 CPU 占用 |
|---|---|
| JDK17 + ZGC | 2.3% |
| JDK8 + CMS | 13.2% |
JDK17 + ZGC 的 GC 线程 CPU 占用仅 2.3%,而 JDK8 + CMS GC 线程 CPU 占用高达 13.2%,降低了约 11 个百分点。
原因 2:待进一步分析(Spring Boot 大版本升级带来的框架层优化等)。
2.4 GC 分析
JDK8 + CMS
- 长 GC:当前平均每个实例 3 周一次长 GC,每次 GC STW 10 秒左右
- 超过 100ms STW 的 GC,统计如下:
# 统计单日超过 100ms 的 STW 次数
$ grep '2023-04-12' /home/app/qj-gateway/var/log/gc.log \
| grep -v 'CMS-concurrent' | grep 'Allocation Failure' \
| awk -F ', real=| secs] $' '$2>0.1{print $0}' | wc -l
206
# 统计超过 200ms 的 STW 次数
$ grep '2023-04-12' /home/app/qj-gateway/var/log/gc.log \
| grep -v 'CMS-concurrent' | grep 'Allocation Failure' \
| awk -F ', real=| secs] $' '$2>0.2{print $0}' | wc -l
5
# 统计超过 300ms 的 STW 次数
$ grep '2023-04-12' /home/app/qj-gateway/var/log/gc.log \
| grep -v 'CMS-concurrent' | grep 'Allocation Failure' \
| awk -F ', real=| secs] $' '$2>0.3{print $0}' | wc -l
1
JDK17 + ZGC
全暂停(Pause):全部在 1ms 内,符合预期
[2023-04-12T04:24:53.960+0000][info][gc,stats] Phase: Pause Mark End 0.023 / 0.023 0.108 / 0.314 0.079 / 0.574 0.053 / 0.594 ms
[2023-04-12T04:24:53.960+0000][info][gc,stats] Phase: Pause Mark Start 0.024 / 0.024 0.053 / 0.190 0.049 / 0.266 0.047 / 0.442 ms
[2023-04-12T04:24:53.960+0000][info][gc,stats] Phase: Pause Relocate Start 0.019 / 0.019 0.024 / 0.038 0.023 / 0.063 0.023 / 0.250 ms
[2023-04-12T04:24:53.960+0000][info][gc,stats] Subphase: Pause Mark Try Complete 0.000 / 0.000 0.136 / 0.231 0.150 / 0.264 0.145 / 0.291 ms
部分暂停(Stall):超过 100ms 的暂停次数为 4 次,reactor 会影响响应时间
[2023-04-12T01:32:31.879+0000][info][gc] Allocation Stall (reactor-http-epoll-5) 23.300ms
[2023-04-12T01:32:31.879+0000][info][gc] Allocation Stall (taskScheduler-2) 47.838ms
[2023-04-12T01:32:31.879+0000][info][gc] Allocation Stall (reactor-http-epoll-3) 16.713ms
[2023-04-12T01:32:32.560+0000][info][gc,start] GC(32888) Garbage Collection (Allocation Stall)
[2023-04-12T01:32:33.945+0000][info][gc] GC(32888) Garbage Collection (Allocation Stall) 1820M(13%)->862M(6%)
[2023-04-12T02:00:50.569+0000][info][gc] Allocation Stall (reactor-http-epoll-6) 149.101ms
[2023-04-12T02:00:50.569+0000][info][gc] Allocation Stall (reactor-http-epoll-2) 148.719ms
[2023-04-12T02:00:50.569+0000][info][gc] Allocation Stall (reactor-http-epoll-3) 149.214ms
[2023-04-12T02:00:52.360+0000][info][gc,start] GC(32929) Garbage Collection (Allocation Stall)
[2023-04-12T02:00:53.430+0000][info][gc] GC(32929) Garbage Collection (Allocation Stall) 3292M(23%)->1380M(10%)
[2023-04-12T02:12:24.840+0000][info][gc] Allocation Stall (taskScheduler-3) 369.472ms
[2023-04-12T02:12:24.840+0000][info][gc] Allocation Stall (reactor-http-epoll-2) 368.839ms
[2023-04-12T02:12:24.840+0000][info][gc] Allocation Stall (taskScheduler-2) 95.198ms
[2023-04-12T02:12:25.860+0000][info][gc,start] GC(32950) Garbage Collection (Allocation Stall)
[2023-04-12T02:12:26.460+0000][info][gc] GC(32950) Garbage Collection (Allocation Stall) 2526M(18%)->700M(5%)
[2023-04-12T03:04:26.571+0000][info][gc] Allocation Stall (taskScheduler-3) 69.843ms
[2023-04-12T03:04:26.571+0000][info][gc] Allocation Stall (reactor-http-epoll-2) 67.273ms
[2023-04-12T03:04:26.571+0000][info][gc] Allocation Stall (reactor-http-epoll-1) 20.436ms
[2023-04-12T03:04:27.460+0000][info][gc,start] GC(33029) Garbage Collection (Allocation Stall)
[2023-04-12T03:04:28.355+0000][info][gc] GC(33029) Garbage Collection (Allocation Stall) 2108M(15%)->808M(6%)
[2023-04-12T03:11:21.253+0000][info][gc] Allocation Stall (taskScheduler-2) 220.134ms
[2023-04-12T03:11:21.253+0000][info][gc] Allocation Stall (reactor-http-epoll-3) 218.501ms
[2023-04-12T03:11:21.253+0000][info][gc] Allocation Stall (AsyncAppender-Worker-asyncApiAppender) 69.338ms
[2023-04-12T03:11:21.253+0000][info][gc] Allocation Stall (reactor-http-epoll-1) 204.948ms
[2023-04-12T03:11:21.253+0000][info][gc] Allocation Stall (reactor-http-epoll-2) 203.588ms
[2023-04-12T03:11:21.253+0000][info][gc] Allocation Stall (reactor-http-epoll-6) 217.505ms
[2023-04-12T03:11:22.060+0000][info][gc,start] GC(33041) Garbage Collection (Allocation Stall)
[2023-04-12T03:11:22.707+0000][info][gc] GC(33041) Garbage Collection (Allocation Stall) 2172M(15%)->736M(5%)
[2023-04-12T03:33:53.306+0000][info][gc] Allocation Stall (reactor-http-epoll-1) 142.936ms
[2023-04-12T03:33:53.306+0000][info][gc] Allocation Stall (reactor-http-epoll-6) 117.235ms
[2023-04-12T03:33:53.306+0000][info][gc] Allocation Stall (reactor-http-epoll-3) 134.536ms
[2023-04-12T03:33:53.306+0000][info][gc] Allocation Stall (reactor-http-epoll-5) 155.286ms
[2023-04-12T03:33:53.306+0000][info][gc] Allocation Stall (reactor-http-epoll-4) 159.730ms
[2023-04-12T03:33:53.306+0000][info][gc] Allocation Stall (reactor-http-epoll-2) 115.295ms
[2023-04-12T03:33:54.160+0000][info][gc,start] GC(33074) Garbage Collection (Allocation Stall)
[2023-04-12T03:33:54.817+0000][info][gc] GC(33074) Garbage Collection (Allocation Stall) 1934M(14%)->738M(5%)
三、总结
千阶智能外呼网关从 JDK8+CMS 迁移到 JDK17+ZGC 后:
- 长 GC 彻底消除:全暂停 STW 全部在 1ms 以内,彻底解决了每 3 周一次、每次 10 秒的长 GC 问题
- CPU 突刺降低 30%:CPU 使用核数标准差大幅下降,系统更平稳
- CPU 利用率降低 10%:GC 线程 CPU 从 13.2% 降至 2.3%,释放的 CPU 全部用于业务处理
- 延迟基本持平:在无长 GC 的时段,JDK8 和 JDK17 的 P50-P99 延迟基本一致,迁移不引入额外延迟
千阶智能建议:其他高频服务(如 AI 外呼调度服务、实时通话服务等)如果存在类似的长 GC 问题,建议尽快推进 JDK17+ZGC 迁移。
更多千阶智能技术实践文章,请访问 qjchat.com。