1. 问题现象与初步诊断
那天凌晨2点15分,我正在睡梦中被刺耳的告警铃声惊醒。监控系统显示生产环境的Nginx服务器突然开始大量返回502 Bad Gateway错误。作为运维人员,我立即打开终端连上服务器,在错误日志中看到了这样的记录:
code复制2023/03/15 02:13:47 [error] 14257#0: *3286256 connect() failed (111: Connection refused)
while connecting to upstream, client: 192.168.1.100, server: api.example.com,
request: "GET /v1/user/profile HTTP/1.1", upstream: "http://127.0.0.1:8080/v1/user/profile",
host: "api.example.com"
502错误本质上是一个网关错误,意味着Nginx作为反向代理时,无法与上游(upstream)服务建立有效连接。从日志中可以明确三点关键信息:
- 连接被拒绝(Connection refused)
- 上游服务地址是127.0.0.1:8080
- 错误集中发生在凌晨2点后
提示:Nginx的error日志默认位于/var/log/nginx/error.log,日志级别需要设置为warn或更低才能记录连接问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 上游服务状态检查
我立即检查了上游服务的状态。这是一个运行在8080端口的Java应用,通过systemctl status命令发现服务处于active(running)状态。但netstat验证却显示异常:
bash复制$ netstat -tulnp | grep 8080
# 无任何输出
这表明虽然服务进程存在,但实际并未监听端口。进一步检查发现应用日志中有大量OOM错误:
code复制java.lang.OutOfMemoryError: Java heap space
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
显然,JVM因为内存泄漏导致崩溃,虽然systemd保持进程ID存在,但实际服务已不可用。这也是为什么Nginx会收到Connection refused——TCP栈明确拒绝了连接请求。
3. 临时解决方案与验证
为快速恢复服务,我采取了以下步骤:
-
重启上游Java服务
bash复制sudo systemctl restart my-java-app -
验证端口监听
bash复制
$ netstat -tulnp | grep 8080 tcp6 0 0 :::8080 :::* LISTEN 24567/java -
配置Nginx健康检查
在nginx.conf中添加:nginx复制upstream backend { server 127.0.0.1:8080 max_fails=3 fail_timeout=30s; } -
测试请求
bash复制
curl -I http://localhost/api/test HTTP/1.1 200 OK
注意:这种直接重启的方式只是临时解决方案,必须后续排查根本原因。
4. 内存泄漏根因分析
通过分析heap dump文件,发现是缓存组件使用不当导致的内存泄漏。具体表现为:
java复制// 错误代码示例
public class CacheManager {
private static final Map<String, Object> CACHE = new HashMap<>();
public void put(String key, Object value) {
CACHE.put(key, value); // 从未清理
}
}
这个静态Map会持续增长,最终耗尽JVM内存。更严重的是,当OOM发生时,JVM的崩溃方式会导致端口未正确释放,出现"僵尸监听"现象。
5. 长期解决方案实施
5.1 应用层修复
-
引入Guava Cache替代原始Map:
java复制Cache<String, Object> cache = CacheBuilder.newBuilder() .maximumSize(10000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); -
添加JVM参数监控:
bash复制
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/my-app/heapdump.hprof
5.2 Nginx层优化
-
配置多节点备份:
nginx复制upstream backend { server 127.0.0.1:8080 max_fails=3 fail_timeout=30s; server 127.0.0.1:8081 backup; } -
设置更精细的超时控制:
nginx复制location /api { proxy_connect_timeout 2s; proxy_read_timeout 5s; proxy_next_upstream error timeout invalid_header; }
5.3 监控增强
-
添加Prometheus监控:
yaml复制- job_name: 'java_app' metrics_path: '/actuator/prometheus' static_configs: - targets: ['127.0.0.1:8080'] -
配置Grafana看板监控:
- JVM内存使用率
- 线程池状态
- Nginx upstream响应时间
6. 深度防御措施
为防止类似问题再次发生,我们实施了以下防御措施:
-
引入Circuit Breaker模式:
java复制@CircuitBreaker(failureThreshold=3, delay=30000) public ResponseEntity<?> sensitiveOperation() { // 业务逻辑 } -
部署Sidecar容器:
dockerfile复制version: '3' services: my-app: image: my-java-app nginx-sidecar: image: nginx ports: ["80:80"] -
压力测试验证:
bash复制
wrk -t4 -c1000 -d60s http://localhost/api/stress-test
7. 经验总结与建议
在这次故障排查过程中,有几个关键经验值得分享:
-
不要相信systemctl status的表面状态:一定要通过netstat或ss验证端口实际监听情况。我们后来编写了自动化检查脚本:
bash复制#!/bin/bash PORT=8080 if ! ss -tuln | grep -q ":${PORT} "; then systemctl restart my-app fi -
Nginx超时配置的黄金组合:
nginx复制proxy_connect_timeout 2s; proxy_send_timeout 5s; proxy_read_timeout 10s; proxy_next_upstream error timeout http_500 http_502 http_503; -
JVM内存设置的注意事项:
- Xmx不要超过物理内存的70%
- 预留足够的空间给操作系统的page cache
- 容器环境下要设置-XX:MaxRAMPercentage
-
日志分析的技巧:
bash复制# 统计502错误频率 awk '$9=="502"{print $7}' access.log | sort | uniq -c | sort -nr # 跟踪上游响应时间 awk '{print $1,$(NF-1)}' access.log | grep -v "-" | sort
这次故障给我们的最大教训是:网关错误从来不是网关本身的问题,而是整个系统健康状况的晴雨表。一个好的运维工程师应该像老中医一样,通过502这个"症状"把出整个系统的"脉象"。
