未央一梦
1.21GB · 2025-09-15
作为公司核心交易系统的负责人,我始终记得前辈说过的一句话:“线上系统的稳定,从来不是‘理所当然’,而是‘如履薄冰’”。直到那个凌晨,刺耳的手机铃声划破寂静,我才真正读懂了这句话的重量——屏幕上跳动的“线上CPU告警”提示,像一道惊雷,瞬间驱散了我的睡意,后背的冷汗与加速的心跳,成了这场故障排查战役的“开幕哨”。
核心系统承载着日均百万级的用户请求,CPU持续高占用意味着什么?是用户支付时的页面卡顿、是订单状态的同步延迟、是下游依赖系统的连锁超时,甚至可能引发数据一致性问题。更现实的是,若故障持续超过1小时,不仅会触发SLA(服务等级协议)赔付条款,我的年终绩效也可能从“优秀”直接滑向“不合格”。没有时间犹豫,我迅速打开笔记本电脑,连接VPN,一场与时间赛跑的排查之旅就此展开。
线上故障排查的核心原则是“先定位范围,再深挖根源”,盲目直接查代码或日志,只会浪费宝贵的时间。我的第一步,是从Linux系统层入手,确认资源瓶颈的具体来源。
登录核心服务所在的生产服务器(通过跳板机跳转,避免直接暴露公网IP),我首先执行了最常用的系统监控命令top
。命令输出瞬间让我倒吸一口凉气:
lscpu
命令可查),负载值远超核心数,意味着大量进程在等待CPU调度top
命令虽能看到整体状态,但进程信息不够直观。我紧接着执行htop
(需提前安装,比top更友好的交互式工具),按CPU使用率排序后,结果清晰地指向了3个Java进程——它们的PID分别是1234、5678、9012,对应的服务正是我们核心系统的“订单处理服务”“支付回调服务”和“数据统计服务”,其中1234进程(订单服务)的CPU占比高达62%,是当之无愧的“罪魁祸首”。
到这里,初步诊断结论已经明确:问题出在Java应用层,且主要集中在订单处理服务,接下来需要深入JVM内部,寻找消耗CPU的具体“元凶”。
Java应用的CPU高占用,常见原因有两种:一是频繁GC导致的CPU空转,二是业务线程执行了耗时的计算逻辑。我需要逐一验证这两种可能性。
首先排除GC问题。我针对订单服务的PID(1234)执行jstat -gcutil 1234 1000 10
,该命令会每隔1秒输出一次GC统计信息,共输出10次。结果如下(关键指标节选):
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 98.56 89.23 92.15 88.76 328 4.567 18 12.345 16.912
0.00 100.00 99.12 89.23 92.15 88.76 329 4.589 18 12.345 16.934
0.00 0.00 12.34 90.56 92.15 88.76 330 4.612 19 13.567 18.179
从输出可以看出:
为了查看业务线程的执行情况,我执行jstack 1234 > thread_dump_1234.txt
,将线程栈信息导出到文件,然后通过以下步骤分析:
统计线程状态分布:用grep "java.lang.Thread.State" thread_dump_1234.txt | sort | uniq -c
命令统计线程状态,结果显示:
查找重复的栈信息:RUNNABLE线程多,说明可能存在“批量执行相同逻辑”的情况。我搜索RUNNABLE
关键字,发现有35个线程的栈信息高度相似,核心调用链如下:
"order-process-10" #123 daemon prio=5 os_prio=0 tid=0x00007f1234567890 nid=0xabc runnable [0x00007f1234567000]
java.lang.Thread.State: RUNNABLE
at com.company.order.service.impl.OrderSortServiceImpl.customSort(OrderSortServiceImpl.java:45)
at com.company.order.service.impl.OrderSortServiceImpl.sortOrderList(OrderSortServiceImpl.java:28)
at com.company.order.service.impl.OrderProcessServiceImpl.processUnpaidOrders(OrderProcessServiceImpl.java:156)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
所有这些线程都卡在OrderSortServiceImpl.java
的第45行——一个自定义排序方法customSort
上。这显然是关键线索,但仅凭线程栈,还无法确定这个方法的具体耗时占比,需要进一步用性能分析工具验证。
jstack
只能捕捉“某一时刻”的线程状态,而async-profiler
(一款轻量级Java性能分析工具,无安全点停顿,适合生产环境)可以统计“一段时间内”的方法执行耗时占比,并生成直观的火焰图。
我在服务器上执行以下命令(提前将async-profiler压缩包上传到服务器):
# -d 30:采集30秒数据;-f cpu_profile.svg:输出火焰图文件;1234:目标PID
./profiler.sh -d 30 -f cpu_profile_1234.svg 1234
30秒后,生成的cpu_profile_1234.svg
文件下载到本地,用浏览器打开后,火焰图的“峰值”瞬间锁定了问题:
com.company.order.service.impl.OrderSortServiceImpl.customSort
,其CPU时间占比高达78%,远超其他方法到这里,JVM层面的分析结论已经清晰:订单服务的CPU高占用,主因是customSort
方法执行耗时过长,Full GC频繁是老年代内存不足的副作用(后续需同步优化内存配置)。接下来,我需要深入代码层,分析这个排序方法的问题所在。
下载订单服务的源代码,定位到OrderSortServiceImpl.java
的customSort
方法,代码逻辑如下(简化后):
// 自定义排序方法:对订单列表按创建时间+金额排序
public List<OrderDTO> customSort(List<OrderDTO> orderList) {
int size = orderList.size();
// 冒泡排序实现(O(n²)时间复杂度)
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - i - 1; j++) {
OrderDTO order1 = orderList.get(j);
OrderDTO order2 = orderList.get(j + 1);
// 先比较创建时间,再比较金额
if (order1.getCreateTime().after(order2.getCreateTime())) {
Collections.swap(orderList, j, j + 1);
} else if (order1.getCreateTime().equals(order2.getCreateTime())) {
if (order1.getAmount().compareTo(order2.getAmount()) > 0) {
Collections.swap(orderList, j, j + 1);
}
}
}
}
return orderList;
}
这个方法的问题很明显:
processUnpaidOrders
(处理未支付订单)调用,而未支付订单列表每5分钟会重新加载一次,但每次加载后都要重新排序,没有缓存排序结果针对以上问题,我分两步进行优化:
Java 8的Stream API
提供了parallelStream()
(并行流),其底层基于Fork/Join框架,能自动将数据分片,利用多核CPU并行计算;同时,sorted()
方法使用的是双轴快速排序(Dual-Pivot Quicksort),时间复杂度为O(n log n),远优于冒泡排序。重构后的代码如下:
/**
* 优化后的排序方法:并行流+双轴快速排序
* @param orderList 待排序的订单列表
* @return 排序后的订单列表
*/
private List<OrderDTO> optimizeSort(List<OrderDTO> orderList) {
// 1. 并行流处理(自动利用多核CPU)
// 2. 按创建时间升序、金额升序排序(使用Comparator链式调用)
// 3. 收集结果为ArrayList(避免线程安全问题)
return orderList.parallelStream()
.sorted(
Comparator.comparing(OrderDTO::getCreateTime) // 一级排序:创建时间
.thenComparing(OrderDTO::getAmount) // 二级排序:金额
)
.collect(Collectors.toCollection(ArrayList::new));
}
由于未支付订单列表每5分钟才更新一次,排序结果可以缓存5分钟,避免每次调用都重新排序。我使用Spring的@Cacheable
注解(需提前在项目中配置缓存管理器,如Redis或Caffeine),具体代码如下:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class OrderSortServiceImpl implements OrderSortService {
// 缓存key:sortedUnpaidOrders,缓存有效期5分钟(在缓存配置中设置)
@Cacheable(value = "sortedUnpaidOrders", key = "'unpaid_order_list'")
@Override
public List<OrderDTO> getSortedUnpaidOrders() {
// 1. 从数据库加载未支付订单列表(原逻辑不变)
List<OrderDTO> unpaidOrderList = orderDAO.listUnpaidOrders();
// 2. 调用优化后的排序方法
return optimizeSort(unpaidOrderList);
}
// 优化后的排序方法(私有,仅内部调用)
private List<OrderDTO> optimizeSort(List<OrderDTO> orderList) {
// 同上,省略具体实现
}
}
将优化后的代码打包,通过灰度发布(先发布到1台测试机,验证无问题后再全量发布)部署到生产环境。5分钟后,再次用htop
查看订单服务(PID 1234)的CPU使用率:从62%骤降至15%,效果立竿见影。
在排查订单服务的过程中,我发现orderDAO.listUnpaidOrders()
(加载未支付订单列表)的查询耗时也偏高——通过arthas
(Alibaba开源的Java诊断工具)的trace
命令跟踪,发现该查询平均耗时1.2秒,这虽然不是CPU高占用的主因,但也是一个“隐性瓶颈”,需要一并优化。
listUnpaidOrders()
对应的SQL语句是:
SELECT id, order_no, user_id, amount, create_time, status
FROM t_order
WHERE status = 'UNPAID'
ORDER BY create_time DESC;
我在生产数据库(MySQL 8.0)中执行EXPLAIN
分析该SQL:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | t_order | NULL | ALL | NULL | NULL | NULL | NULL | 50000 | 10.00 | Using where; Using filesort |
从EXPLAIN
结果可以看出两个关键问题:
t_order
表共有50万条数据,全表扫描会遍历所有行,效率极低针对WHERE status = 'UNPAID'
和ORDER BY create_time DESC
的需求,创建“状态+创建时间”的复合索引是最优选择——复合索引的前缀匹配特性可以覆盖WHERE条件,同时索引的有序性可以避免文件排序。执行以下SQL创建索引:
-- 索引名称:idx_status_create_time;索引字段:status(WHERE条件)、create_time(排序字段)
CREATE INDEX idx_status_create_time ON t_order(status, create_time DESC);
创建索引后,再次执行EXPLAIN
,结果明显改善:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | t_order | NULL | ref | idx_status_create_time | idx_status_create_time | 62 | const | 5000 | 100.00 | NULL |
原orderDAO.listUnpaidOrders()
使用JPA的JPQL查询:
// 原JPQL查询
@Query("SELECT o FROM Order o WHERE o.status = :status ORDER BY o.createTime DESC")
List<OrderDTO> listUnpaidOrders(@Param("status") String status);
虽然JPQL简化了数据库操作,但在复杂查询场景下,其生成的SQL可能不够优化。为了确保完全利用新创建的复合索引,我将其改写为原生SQL:
// 优化后的原生SQL查询
@Query(value = "SELECT id, order_no, user_id, amount, create_time, status " +
"FROM t_order " +
"WHERE status = :status " +
"ORDER BY create_time DESC",
nativeQuery = true)
List<OrderDTO> listUnpaidOrders(@Param("status") String status);
原生SQL的优势在于:
优化后,listUnpaidOrders()
的平均耗时从1.2秒降至80毫秒,进一步减轻了服务的响应压力。
解决了代码和数据库的问题后,我开始思考更深层次的系统稳定性保障——如何防止单个服务故障影响整个系统?当前的部署架构是“多服务混部”,即订单服务、支付服务等核心应用部署在同一台物理机上,一旦某个服务CPU飙升,很容易“拖垮”其他服务。
我决定将所有核心服务容器化,使用Docker实现资源隔离。针对订单服务,编写的Dockerfile如下:
# 基础镜像:选择轻量级的OpenJDK 11 JRE(仅含运行时,不含开发工具)
FROM openjdk:11-jre-slim
# 设置工作目录
WORKDIR /app
# 复制应用jar包(使用CI/CD流水线构建的最新版本)
COPY target/order-service-1.0.1-SNAPSHOT.jar app.jar
# JVM参数优化:
# -Xms1g -Xmx1g:堆内存固定为1G(避免动态扩容消耗CPU)
# -XX:+UseG1GC:使用G1垃圾收集器(适合大堆内存,低延迟)
# -XX:MaxGCPauseMillis=200:最大GC停顿时间控制在200ms内
# -XX:+HeapDumpOnOutOfMemoryError:OOM时自动生成堆快照
ENV JAVA_OPTS="-Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/dump"
# 暴露服务端口(Spring Boot应用端口)
EXPOSE 8080
# 启动命令:使用exec形式,确保容器能接收到信号(如stop命令)
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
为了防止容器无限制占用宿主机资源,我使用Docker Compose进行服务编排,并设置CPU和内存上限:
# docker-compose.yml
version: '3.8' # 使用支持资源限制的新版本
services:
order-service:
build: ./order-service # 指向Dockerfile所在目录
image: order-service:1.0.1
container_name: order-service
restart: always # 服务异常退出时自动重启
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod # 启用生产环境配置
- DB_HOST=mysql-prod # 数据库连接信息(通过环境变量注入)
deploy:
resources:
limits:
cpus: '1.0' # 最多使用1个CPU核心
memory: 1536M # 最多使用1.5G内存
reservations:
cpus: '0.5' # 至少保留0.5个CPU核心
memory: 1024M # 至少保留1G内存
networks:
- app-network # 加入自定义网络,与其他服务隔离
# 其他服务(支付服务、统计服务等)配置类似...
networks:
app-network:
driver: bridge
通过资源限制,即使订单服务再次出现CPU异常,也只会占用最多1个核心,不会影响其他服务(如支付服务)的正常运行。同时,restart: always
确保服务故障后能自动恢复,减少人工干预成本。
这次故障暴露了我们监控体系的不足——虽然设置了CPU告警,但缺乏对JVM内部指标(如方法耗时、GC频率)和业务指标(如订单处理延迟)的监控,导致故障发生后只能“被动响应”。为此,我决定搭建一套更全面的监控平台。
在订单服务中集成Micrometer,添加自定义业务指标(如订单排序耗时、查询耗时):
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class OrderSortServiceImpl implements OrderSortService {
private final Timer sortTimer; // 排序耗时计时器
// 通过构造函数注入MeterRegistry
public OrderSortServiceImpl(MeterRegistry registry) {
// 定义计时器:名称+标签(用于区分不同场景)
this.sortTimer = Timer.builder("order.sort.time")
.tag("service", "order-service")
.tag("method", "optimizeSort")
.description("订单排序方法的执行耗时")
.register(registry);
}
@Override
public List<OrderDTO> getSortedUnpaidOrders() {
// 用计时器包裹排序逻辑,记录耗时
return sortTimer.record(() -> {
List<OrderDTO> unpaidOrderList = orderDAO.listUnpaidOrders();
return optimizeSort(unpaidOrderList);
});
}
}
在Prometheus中配置告警规则(prometheus.rules.yml
):
groups:
- name: order-service-rules
rules:
# 规则1:CPU使用率过高(超过80%持续5分钟)
- alert: OrderServiceHighCPU
expr: sum(rate(process_cpu_seconds_total{service="order-service"}[5m])) by (instance) * 100 > 80
for: 5m
labels:
severity: critical # 严重级别:紧急
annotations:
summary: "订单服务CPU使用率过高"
description: "实例 {{ $labels.instance }} 的CPU使用率已超过80%,持续5分钟,当前值: {{ $value | humanizePercentage }}"
# 规则2:排序方法耗时过长(超过500ms持续3分钟)
- alert: OrderSortTimeTooLong
expr: order_sort_time_seconds_sum{service="order-service"} / order_sort_time_seconds_count > 0.5
for: 3m
labels:
severity: warning # 严重级别:警告
annotations:
summary: "订单排序方法耗时过长"
description: "排序方法平均耗时超过500ms,持续3分钟,当前值: {{ $value | humanizeDuration }}"
# 规则3:Full GC频率过高(1分钟内超过2次)
- alert: OrderServiceFGCTooFrequent
expr: rate(jvm_gc_full_gc_count{service="order-service"}[1m]) > 2
for: 2m
labels:
severity: warning
annotations:
summary: "订单服务Full GC频率过高"
description: "1分钟内Full GC次数超过2次,可能导致性能下降"
通过Grafana创建可视化仪表盘,将CPU使用率、排序耗时、GC次数等指标集中展示,实现“一眼看穿”系统状态。同时,配置告警通知渠道(邮件、企业微信、短信),确保故障发生时能第一时间通知到责任人。
早上8点15分,当我在公司晨会中汇报“系统已完全恢复正常,各项指标优于故障前水平”时,紧绷了4小时的神经终于放松下来。监控面板显示:
这次凌晨的故障,与其说是一场“危机”,不如说是一次“体检”——它暴露了我们在代码质量、架构设计、监控体系上的多处短板:
作为技术负责人,我在事后推动了三项改进措施:
系统稳定性的保障没有终点——每一次故障都是一次成长的机会,每一次优化都是向“高可用”迈进的一步。作为技术人,我们能做的,就是在一次次“化险为夷”中,构建起更坚固的技术壁垒,让用户的每一次点击都安心可靠。
毕竟,线上系统的稳定,从来不是“理所当然”,而是“如履薄冰”后的“有备无患”。