千阶智能 JDK17 迁移实战:ZGC 如何将外呼网关 GC 暂停从 10 秒降到 1 毫秒

2026-07-14 20:02
JavaJDK17ZGC千阶智能2026-07-14

千阶智能技术博客 | 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-b151.4680.99单实例 3600 秒 x 24 小时
JDK8 - dc-a163.2981.88单实例 3600 秒 x 24 小时
JDK8 - dc-c139.4353.71单实例 3600 秒 x 24 小时
JDK17 - dc-c124.6542.85单实例 3600 秒 x 24 小时
JDK17 - dc-a120.0238.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 + ZGC2.3%
JDK8 + CMS13.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 后:

  1. 长 GC 彻底消除:全暂停 STW 全部在 1ms 以内,彻底解决了每 3 周一次、每次 10 秒的长 GC 问题
  2. CPU 突刺降低 30%:CPU 使用核数标准差大幅下降,系统更平稳
  3. CPU 利用率降低 10%:GC 线程 CPU 从 13.2% 降至 2.3%,释放的 CPU 全部用于业务处理
  4. 延迟基本持平:在无长 GC 的时段,JDK8 和 JDK17 的 P50-P99 延迟基本一致,迁移不引入额外延迟

千阶智能建议:其他高频服务(如 AI 外呼调度服务、实时通话服务等)如果存在类似的长 GC 问题,建议尽快推进 JDK17+ZGC 迁移。

更多千阶智能技术实践文章,请访问 qjchat.com


本文由千阶智能技术团队出品,转载请注明出处。更多技术内容请访问 qjchat.com
相关新闻
热点新闻
精彩视频
投票
查看结果
Tags

站点地图 在线访客: 今日访问量: 昨日访问量: 总访问量:

© 2026 北京千阶智能科技有限公司 版权所有

公安备案 京公网安备11010802049169号 京ICP备2026018665号-1