Nginx + Tomcat + JVM 运维速查手册 108 条

奥黛丽·逐爱者
2026-11-03 / 0 评论 / 1 阅读 / 正在检测是否收录...
108 条 · 6 章 · 建议阅读 28 分钟

系统上线的第一个小时,页面全是 502。

后端同学说:我本地 curl 是通的。运维说:Nginx 我一个字没改。

两边都没说谎。后端服务监听的是 127.0.0.1:8080,Nginx 转的是 10.0.1.12:8080。本机 curl 当然通,跨机当然不通。

这种“谁都对,系统就是不通”的场面,是应用运维每天的主旋律。

所以这 108 条按一条链路排:Nginx 进来 → Tomcat 接住 → JVM 跑起来 → 线上联调。你顺着这条链路查,就不会跳层。

* * *

先说句得罪人的话:事故不是一个大错造成的,是五个小对凑成的

每一次 502,拆开看都是一串“当时看着都对”的决定:Nginx 配了 60 秒超时、后端接口跑了 61 秒、连接池给了 20 个、线程池给了 500 个。

单看每一条都合理,凑在一起就是事故。

所以这篇的写法是:一条命令,配一个坑。

命令你抄走。坑你记住——真出事的时候,救你的是坑,不是命令。

全文 108 条,编号连续。搜“42”,就是第 42 条。

目 录

1Nginx 配置01–182Nginx 排障19–363Tomcat 部署37–544Tomcat 与线程55–725JVM 与内存73–906联调与线上91–108

* * *

1Nginx 配置:location 写错,前面全白配

Nginx 的配置难的不是语法,是顺序。同样的几行,放错层级、改个斜杠,行为就完全变了。

01检查配置语法

nginx -t

坑通过只代表语法对,逻辑错照样能启动,路径写歪也照样报 success

02看编译参数

nginx -V

坑装没装 ssl、stream 模块全在这行 configure arguments 里,没有就是没有

03平滑重载

nginx -s reload

坑reload 不断连接,但新配置里 upstream 指向不存在的地址,新 worker 会全挂

04看主进程与 worker

ps -ef | grep nginx | grep -v grep

坑只剩 worker 没有 master,说明 master 崩过,reload 也不会生效

05server_name 匹配

server_name app.xxx.gov.cn;

坑多个 server 都没匹配上时走默认 server,默认是谁取决于配置顺序,不显式写就是坑

06location 匹配优先级

location = /a {}   # 精确 > location ^~ /b/ {}  > location ~ \.php$ {}  > location / {}

坑正则 location 会盖掉普通前缀,加了 ^~ 才不再往下查正则

07root 与 alias 的区别

location /img/ { alias /data/img/; }

坑alias 结尾不加斜杠路径少一层,root 会把 location 拼到路径后面,两者结果差一个目录

08单页应用刷新 404

try_files $uri $uri/ /index.html;

坑漏了最后的 /index.html 兜底,前端路由一刷新就是 404

09proxy_pass 结尾斜杠

proxy_pass http://127.0.0.1:8080/;

坑结尾有斜杠会截掉 location 前缀,没有会原样带上,少一层多一层全靠这个

10反代 header 四件套

proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;

坑少了 X-Forwarded-Proto,后端生成的回调地址会变回 http,登录直接失败

11多级代理取真实 IP

set_real_ip_from 10.0.0.0/24; real_ip_header X-Forwarded-For;

坑不配 real_ip,日志里全是负载均衡的 IP,出事查不到来源

12上传大小限制

client_max_body_size 100m;

坑在 http 里配了、location 里又覆盖了,最终生效的是最里面那层

13超时三件套

proxy_connect_timeout 5s; proxy_read_timeout 60s; proxy_send_timeout 60s;

坑read_timeout 比接口耗时短,长任务接口必然 504,而且日志里看不出是超时

14开 gzip 压缩

gzip on; gzip_types text/css application/javascript;

坑对图片压缩纯浪费 CPU,text/html 默认就压,不用重复写

15静态资源缓存

location ~* \.(js|css|png)$ { expires 7d; }

坑把 index.html 也设了长缓存,发版后用户看到的还是旧页面

16upstream 负载均衡

upstream app { server 10.0.0.5:8080 weight=2; server 10.0.0.6:8080; }

坑默认轮询不做主动健康检查,一台挂了就是 50% 请求失败

17自定义日志格式

log_format main '$remote_addr $request_time $upstream_response_time "$request"';

坑request_time 是整个请求耗时,upstream_response_time 才是后端耗时,分不清就定位错方向

18HTTP 跳 HTTPS

return 301 https://$host$request_uri;

坑跳转写在 location 里,POST 会被转成 GET,参数全丢,用户看到的是“提交后回到首页”

2Nginx 排障:502 不是 Nginx 的错

Nginx 只是最后一环。它报的每一个错,都在替别人背锅。看懂 error.log 那一行,比查十个博客有用。

19502 先看错误日志

tail -f /var/log/nginx/error.log

坑日志里必有 connect() failed 或 upstream prematurely closed,先看这一行再猜

20502 检查后端端口

ss -lntp | grep 8080

坑后端在但只监听 127.0.0.1,Nginx 连 10.x 地址必然 502,本地 curl 却是通的

21502 后端超时被杀

grep 'upstream timed out' error.log

坑调大 proxy_read_timeout 只是掩盖,后端慢才是根因

22504 到底谁慢

对比 $request_time 与 $upstream_response_time

坑两个都大是后端慢,只有 request_time 大是客户端慢,方向完全不同

23413 上传被拒

client_max_body_size 100m;

坑改了 Nginx 还是 413,因为 Tomcat 的 maxPostSize 也在拦

24499 是什么意思

grep ' 499 ' access.log | wc -l

坑499 是客户端主动断开,多半是接口太慢用户关了页面,不是 Nginx 的错

25403 但文件存在

namei -l /data/www/index.html

坑目录缺 x 权限,文件有读权限也访问不了,这是最容易漏的一环

26证书链不完整

openssl s_client -connect app.xxx.gov.cn:443 -showcerts

坑只配站点证书不带中间证书,PC 上正常、手机上全红

27证书有没有过期

echo | openssl s_client -connect host:443 2>/dev/null | openssl x509 -noout -dates

坑证书过期当天才发现,是因为监控里根本没这一项

28跨域预检被拦

add_header Access-Control-Allow-Origin $http_origin always;

坑add_header 在 location 里会覆盖外层的,写一处漏一处

29WebSocket 连上就断

proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";

坑少了 http_version 1.1,握手成功也会立刻断开

30限流把业务限死

limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

坑burst 设 0,稍一突刺就 503,业务侧看到的是“随机失败”

31连接数不够

worker_connections 10240;

坑只调这个不调 worker_rlimit_nofile,句柄到顶照样报 too many open files

32找 TOP 访问 IP

awk '{print $1}' access.log | sort | uniq -c | sort -rn | head

坑日志格式改过之后字段位置全变,$1 不一定还是 IP

33找最慢的接口

awk '{print $NF, $7}' access.log | sort -rn | head -20

坑字段号由 log_format 决定,抄别人的命令前先确认自己的格式

34临时摘掉一台后端

sed -i 's/server 10.0.0.6:8080;/server 10.0.0.6:8080 down;/' nginx.conf && nginx -s reload

坑直接删掉 server 行,恢复时容易漏配参数

35确认 VIP 在谁身上

ip addr | grep 10.0.0.100

坑VIP 没漂,应用还连旧主,报的错看起来像网络问题

36静态文件 404 但确实在

tail -20 error.log | grep -i denied

坑SELinux 拦了 Nginx 读这个目录,日志里是 permission denied,看着像路径配错

3Tomcat 部署:war 放对地方,才算部署

Tomcat 的坑集中在三处:路径、端口、内存。三样对不上,日志里的报错都长得很像。

37看 Tomcat 版本

catalina.sh version

坑不报版本的求助基本没法答,不同大版本行为差异很大

38看关键目录

ls $CATALINA_HOME/{bin,conf,webapps,logs,work}

坑work 目录不清理,改了 JSP 不生效,看着像缓存问题

39部署 war 包

cp app.war $CATALINA_HOME/webapps/

坑war 文件名就是访问路径,叫 app-1.0.war 就得访问 /app-1.0

40指定根路径部署

mv app.war webapps/ROOT.war

坑context 占了 /,其他应用全 404,而且很难想到是这里

41Connector 核心参数

<Connector port="8080" maxThreads="500" acceptCount="200" connectionTimeout="20000"/>

坑maxThreads 调到 2000 但 CPU 只有 8 核,线程全在等锁,越调越慢

42配置线程池

<Executor name="tomcatThreadPool" maxThreads="500" minSpareThreads="50"/>

坑配了 Executor 但 Connector 没写 executor 属性,配置完全不生效

43设置 JAVA_OPTS

export JAVA_OPTS="-Xms4g -Xmx4g -XX:MetaspaceSize=256m"

坑写在 /etc/profile 里,systemd 启的服务读不到,要写 bin/setenv.sh

44看启动日志

tail -100 logs/catalina.out

坑catalina.out 不切割,三个月能到几十 G,磁盘就是这么满的

45查端口冲突

ss -lntp | grep -E '8080|8005|8009'

坑shutdown 端口和 http 端口是两回事,关不掉往往是 8005 被改过

46一机多实例

cp -r tomcat tomcat2 && vi conf/server.xml

坑只改 http 端口,shutdown 和 ajp 撞了,第二个实例起不来

47部署后 404

ls webapps/app/WEB-INF/classes

坑没有 classes 目录,说明打包时编译产物没进去

48启动卡住

tail -f logs/catalina.out

坑卡在 Deploying web application 后面,多半是连接池初始化在等超时

49日志切割

vi /etc/logrotate.d/tomcat

坑用 copytruncate 切 catalina.out,正在写的那段会丢

50关不掉进程

kill -15 <pid>

坑shutdown.sh 依赖 8005,端口不对永远关不掉,最后只能 -9,而 -9 会丢数据

51关掉热部署

reloadable="false"

坑生产上开着 reloadable,反复 reload 会把 Metaspace 撑爆

52静态资源交给 Nginx

location /static/ { root /data/www; }

坑Tomcat 送静态文件慢一个数量级,但加了 Nginx 缓存后发版不生效

53上传目录独立出去

-Dfile.upload=/data/upload

坑放在 webapps 里,重新部署 war 时上传的文件全被清掉

54时区与编码

-Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8

坑只设 file.encoding 不设 timezone,日志时间还是差 8 小时

4Tomcat 与线程:假死之前,全都有征兆

Tomcat 假死最难的地方是:进程在、端口通、日志还停在一小时前。这时候唯一能问出真话的是线程栈。

55看 Java 进程与参数

jps -lvm

坑jps 默认看不到别的用户启动的进程,用 -lvm 或 ps -ef 补

56看堆与 GC 走势

jstat -gcutil <pid> 1000 10

坑看 OU(已用)而不是 O(容量),只看容量会漏掉快满的事实

57抓线程栈

jstack <pid> > /tmp/t1.txt

坑-F 强制抓会 STW,生产上先试普通模式

58找阻塞线程

grep -A 15 'BLOCKED' /tmp/t1.txt

坑只看 RUNNABLE 会漏掉真正卡住的,等锁的线程都在 BLOCKED

59数线程总数

jstack <pid> | grep -c '^"'

坑线程数持续上涨不回落就是泄漏,不是“高峰期正常波动”

60判断是否假死

curl -m 3 http://127.0.0.1:8080/

坑端口通但请求挂住,是线程池满了,不是网络问题

61看 http 线程状态

jstack <pid> | grep 'http-nio' | wc -l

坑http 线程全在跑、业务线程却在等数据库,方向就该往数据库转

62分清两层线程池

Tomcat 线程是入口,业务线程池是出口

坑堵在出口时 Tomcat 线程全在等,看起来像 Tomcat 的问题

63连接池耗尽

jstack <pid> | grep -c getConnection

坑大量线程堆在 getConnection,是池太小或连接没释放

64开 GC 日志

-Xloggc:/data/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps

坑JDK11 之后这些参数全变了,老参数直接不生效还不报错

65看 Full GC 间隔

grep 'Full GC' gc.log | head -20

坑只看次数不看间隔,一小时一次和一分钟一次完全两回事

66导出堆快照

jmap -dump:format=b,file=/tmp/heap.hprof <pid>

坑dump 会 STW 几秒到大几十秒,别在业务高峰做

67看对象分布

jmap -histo:live <pid> | head -20

坑:live 会触发一次 Full GC,线上慎用

68分清 OOM 类型

Java heap space / Metaspace / Direct buffer 三者处理方式完全不同

坑都叫 OutOfMemoryError,方向错了会白查一天

69元空间上限

-XX:MaxMetaspaceSize=512m

坑动态生成类会让 Metaspace 只涨不降,不设上限最后吃光物理内存

70堆外内存

-XX:MaxDirectMemorySize=512m

坑堆外泄漏 jmap 看不出,要看 pmap 和 NMT

71确认线程泄漏

间隔 5 分钟各抓一次 jstack 对比

坑单次快照看不出泄漏,必须有对比

72容器里限制堆

-XX:+UseContainerSupport -XX:MaxRAMPercentage=70

坑老 JDK 不认 cgroup,按宿主机内存算堆,容器直接被 OOM Killer 干掉

5JVM 与内存:OOM 之前,日志里早有答案

没有人是被 OOM 突然杀死的。在它之前,GC 日志已经喊了三天。

73基础参数模板

-Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

坑Xms 小于 Xmx 会动态扩堆,扩容时抖动明显,还查不出原因

74为什么 Xms 要等于 Xmx

-Xms4g -Xmx4g

坑不相等就会反复扩缩容,GC 频率上去,监控上看是“内存曲线像锯齿”

75新生代比例

-XX:NewRatio=2 或 -Xmn2g

坑Xmn 和 NewRatio 同时写,以 Xmn 为准,另一个静默失效

76选垃圾回收器

-XX:+UseG1GC

坑JDK8 默认 Parallel、JDK9+ 默认 G1,参数没改但行为全变了

77G1 关键参数

-XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=8m

坑暂停目标设太小,G1 会疯狂 Young GC,吞吐反而下降

78栈大小

-Xss512k

坑栈小深递归直接 StackOverflow,栈大线程一多就吃光内存

79OOM 自动 dump

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump

坑路径没权限,dump 失败只在日志写一行,事后什么都拿不到

80容器里堆别超过 limit

-Xmx 建议 ≤ 容器内存 70%

坑堆设满,加上元空间和堆外,容器必然被 kill,看着像“莫名重启”

81用 jcmd 替代一堆工具

jcmd <pid> VM.flags; jcmd <pid> GC.heap_info

坑JDK8 早期版本 jcmd 不全,还得回去用 jmap/jstat

82Arthas 快速定位

java -jar arthas-boot.jar → dashboard → thread -n 3

坑attach 会短暂 STW,生产上先确认业务能接受

83类加载冲突

jcmd <pid> VM.classloader_stats

坑同一个包多个版本共存,行为随机,堆栈看起来却完全正常

84YGC 太频繁

jstat -gcutil <pid> 1000 20

坑YGC 频繁不一定是内存小,可能是对象创建速率太高

85内存泄漏定位流程

两次 dump → MAT 对比 → 找增长最多的类 → 回溯引用链

坑只 dump 一次,看到的都是“正常占用”,没有对比就没有结论

86最常见的泄漏写法

静态集合、ThreadLocal、监听器未注销、无上限缓存

坑缓存没有淘汰策略,本质就是内存泄漏,只是看起来像“业务需要”

87确认参数真的生效

jcmd <pid> VM.flags | tr ' ' ' ' | grep -E 'Xmx|Metaspace'

坑启动脚本里 JAVA_OPTS 被覆盖,写了等于没写

88CPU 飙高定位

top -Hp <pid> → printf '%x' 线程号 → jstack 里搜

坑不做 16 进制转换,在 jstack 里永远搜不到那个线程

89动态调日志级别

Arthas logger 或 Spring Boot Actuator /loggers

坑调完忘了改回去,DEBUG 日志几天就把磁盘写满

90参数变更流程

灰度一台 → 观察 24 小时 → 全量

坑一次性改全量,出问题连对照组都没有

6联调与线上:接口报错,先分清是谁的

线上问题最贵的不是修复,是定位。定位的关键只有一句:把链路切成段,一段一段排除。

91接口 500 但日志没异常

在 access.log 里按时间和 URI 找

坑异常被全局处理器吞了,日志里只剩一句“系统繁忙”

92超时三层定位

浏览器 → Nginx($upstream_response_time) → 应用日志 → 数据库

坑只看应用日志会漏掉 Nginx 的 504,两边时间对不上就查不下去

93前端报跨域,其实是网关

curl -I -X OPTIONS http://host/api

坑预检被网关直接 403,浏览器只会显示成跨域

94登录状态丢 Cookie

看 Set-Cookie 的 Domain / Path / SameSite / Secure

坑http 环境下带了 Secure,Cookie 根本存不下

95上传失败要三层一起改

Nginx client_max_body_size + Tomcat maxPostSize + 应用限制

坑只改一层就以为好了,另外两层还在拦

96大报文 400

maxHttpHeaderSize="8192"

坑JWT 放 header 里,token 一大就 400,还看不出原因

97中文乱码整条链路

浏览器 → Nginx → Tomcat(URIEncoding) → 数据库字符集

坑只在应用里设 UTF-8,Tomcat 没设 URIEncoding,GET 参数照样乱

98时间差 8 小时

date; jcmd <pid> VM.system_properties | grep timezone

坑容器基础镜像默认 UTC,代码里取的时间全错

99内部调用握手失败

openssl s_client -connect host:443 | openssl x509 -noout -dates

坑自签证书过期没人管,报错只有 handshake_failure

100单点登录回调失败

核对回调域名、端口、协议与注册信息

坑Nginx 转 https 后回调地址还是 http,页面直接白屏

101灰度分流

Nginx 按 header 或 IP 分流

坑灰度机与正式机共用 session,用户状态来回跳

102发布后内存持续上涨

看是新版本对象增长还是缓存预热

坑把正常预热当泄漏去回滚,白折腾一晚上

103多实例会话不共享

检查负载均衡是否做了会话粘滞

坑请求落到不同机器,用户每点一次就要重新登录

104线上问题最小复现

固定时间、固定参数、固定数据

坑不能复现就改代码,等于赌运气

105变更检查清单

备份 + 回滚脚本 + 验证点 + 通知

坑只写“已备份”,却没验证能不能恢复,等于没备份

106回滚要比发布快

保留上一个版本的包和配置,回滚脚本先写好

坑出事时才找旧包,找的过程比故障本身还长

107发布后必看的三个数

错误率、P99 耗时、线程数

坑只看错误率,接口没报错但慢了三倍,等于没看

108故障时间线复盘

现象时间 → 发现时间 → 定位时间 → 恢复时间

坑不记时间线,复盘一定变成互相甩锅

* * *

页面打不开,先分清是 Nginx、Tomcat 还是 JVM,按这个顺序切
# 1. Nginx 这一层通不通 curl -I -m 3 http://127.0.0.1/ ; tail -20 /var/log/nginx/error.log # 2. 后端端口在不在 ss -lntp | grep 8080 # 3. 绕过 Nginx 直连后端 curl -I -m 3 http://127.0.0.1:8080/ # 4. 后端活着但没响应,看线程 jstack $(jps -l | grep tomcat | awk '{print $1}') | grep -c http-nio # 5. JVM 是不是在疯狂 GC jstat -gcutil <pid> 1000 5

为什么第 3 步必须绕过 Nginx:它能一刀切开“代理层的问题”和“应用层的问题”,省掉一半的排查时间。

第 3 步通、第 1 步不通,问题在 Nginx;两步都不通,问题在 Tomcat 或 JVM。这个判断只花 3 秒。

如果只能记一个,记哪个?

curl -I -m 3 http://127.0.0.1:8080/

不是因为它能解决问题,是因为它能一句话把链路切成两段。

我见过太多“页面 502”的报障,一半时间花在争论是 Nginx 的问题还是应用的问题。绕过代理直连一次,争论立刻结束。

先切开链路,再谈原因。这个顺序比记住一百条命令都值钱。

最后,我是运维行者,在政府项目里做信创运维第 8 年。以前做医疗 HIS 实施,现在主要和达梦、麒麟、国产中间件打交道。

这个号只写两件事:现场踩过的坑,和一个 32 岁技术人的选择。

每周三 20:00 技术,周日 21:00 聊聊人。

0

评论 (0)

取消