问题初现:警报突响

那是上周三的一个深夜,手机突然传来急促的警报声——生产环境某台应用服务器的CPU使用率在短短几分钟内从平时的20%飙升至95%以上。作为运维人员,这种警报总是让人心头一紧。

登录监控系统,我看到了一张触目惊心的图表:

# 当时的监控数据摘要
Timestamp        CPU%
22:15:30         25%
22:16:00         68%  
22:16:30         92%
22:17:00         97%
22:17:30         98%

排查步骤:层层深入

第一步:快速定位问题进程

我立即通过SSH连接到问题服务器,使用top命令查看实时进程状态:

top - 22:18:15 up 45 days,  8:23,  1 user,  load average: 15.32, 8.45, 4.21
Tasks: 231 total,   2 running, 229 sleeping,   0 stopped,   0 zombie
%Cpu(s): 96.8 us,  2.1 sy,  0.0 ni,  1.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  15982.4 total,    245.8 free,   9845.2 used,   5891.4 buff/cache
MiB Swap:   2048.0 total,   1024.0 free,   1024.0 used.   5123.2 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 8924 appuser   20   0   12.8g   8.4g  35688 R  88.3  53.9  12:45.67 java
 1234 mysql     20   0   3124m   1.2g  45888 S   5.2   7.8   2:34.21 mysqld

很明显,PID为8924的Java进程是罪魁祸首,占用了近90%的CPU资源。

第二步:分析Java进程状态

我使用jstack命令获取Java进程的线程堆栈信息:

jstack -l 8924 > /tmp/thread_dump.txt

分析线程堆栈时,我发现有多个线程都处于"RUNNABLE"状态,且执行栈相似,都指向同一个业务方法:OrderProcessor.handleBatchOrders()

第三步:检查应用日志

查看应用日志,发现了大量重复的错误信息:

ERROR [OrderProcessor-Thread-15] c.x.service.OrderProcessor - Failed to process order: 202408200015
java.lang.NullPointerException: null
  at com.xxx.service.OrderProcessor.calculateDiscount(OrderProcessor.java:156)
  at com.xxx.service.OrderProcessor.handleBatchOrders(OrderProcessor.java:89)

第四步:代码层面分析

找到对应的源代码,问题逐渐清晰:

// OrderProcessor.java 第89行附近
public void handleBatchOrders(List<Order> orders) {
    while (true) {  // 问题所在:死循环
        for (Order order : orders) {
            try {
                calculateDiscount(order);
                // ... 其他处理逻辑
            } catch (Exception e) {
                logger.error("Failed to process order: " + order.getId(), e);
                // 这里没有break或continue,导致循环继续
            }
        }
        // 缺少循环终止条件
    }
}

问题根源与修复

问题分析

经过深入分析,发现了几个关键问题:

  1. 死循环设计缺陷while(true)没有合适的退出条件
  2. 异常处理不当:捕获异常后没有合适的恢复或终止机制
  3. 资源泄漏:持续的错误处理消耗大量CPU资源

修复方案

我协助开发团队对代码进行了重构:

public void handleBatchOrders(List<Order> orders) {
    int maxRetries = 3;
    int processedCount = 0;
    
    for (Order order : orders) {
        boolean success = false;
        int retryCount = 0;
        
        while (!success && retryCount < maxRetries) {
            try {
                calculateDiscount(order);
                processOrder(order);
                success = true;
                processedCount++;
            } catch (Exception e) {
                retryCount++;
                logger.error("Failed to process order: {}, retry {}/{}", 
                           order.getId(), retryCount, maxRetries, e);
                
                if (retryCount >= maxRetries) {
                    logger.error("Order {} failed after {} retries, moving to DLQ", 
                               order.getId(), maxRetries);
                    sendToDeadLetterQueue(order);
                }
            }
        }
    }
    
    logger.info("Batch processing completed: {}/{} orders processed", 
               processedCount, orders.size());
}

经验总结与预防措施

监控体系建设

这次事件让我深刻认识到完善监控的重要性:

  • 应用层监控:不仅监控系统资源,还要监控应用关键指标
  • 业务指标监控:订单处理速率、成功率等业务相关指标
  • 日志聚合分析:使用ELK等工具实时分析日志模式

代码审查重点

在后续的代码审查中,我会特别关注:

  • 循环结构的终止条件
  • 异常处理的最佳实践
  • 资源管理和清理逻辑
  • 超时和重试机制

运维应急预案

  1. 快速响应流程

    • CPU异常时立即保存线程堆栈
    • 保留现场信息后再考虑重启
    • 分析问题模式,制定临时解决方案
  2. 预防性措施

    • 定期进行压力测试
    • 设置合理的资源限制
    • 建立代码质量门禁

后续思考

这次CPU异常飙升的排查经历,让我对"防御性编程"有了更深的理解。作为运维人员,我们不仅要会"救火",更要从架构和代码层面帮助团队建立"防火"机制。每一次线上事故都是宝贵的经验,记录下这些排查过程,不仅是为了解决问题,更是为了预防下一个问题的发生。

运维工作的价值,就在于将这些"血泪教训"转化为可重复、可预防的最佳实践。