Tomcat 部署与 JVM 调优实战:从安装到生产基线
Tomcat 部署与 JVM 调优实战:从安装到生产基线
第一次独立接手 Java 应用时,开发只丢过来一个 war 包和一句”部署到服务器上”,剩下的全是我自己的事:装什么环境、配哪些参数、日志放哪、内存给多少。Tomcat 本身不难装,难的是把它调到一个”生产敢用”的状态——这套配置后来成了我部署所有 Java 应用的标准动作。
一、先弄清楚 Tomcat 在生产里的位置
Tomcat 是 Servlet 容器,Java 应用跑在它里面。类比一下:PHP 站点要 Nginx 配 PHP-FPM 才能跑,Java 应用就需要 Tomcat 这个容器。
它自己带 HTTP 服务能力(默认 8080),但静态文件处理比 Nginx 差得远,所以生产上标准姿势是:
- 用户请求 → Nginx(80/443)→ Tomcat(8080);
- 静态资源(图片、CSS、JS)Nginx 直接返回,动态请求才转发给 Tomcat;
- 8080 端口只对内网负载均衡节点开放,绝不对公网暴露。
这里有个我踩过的坑:最初图省事直接把 8080 开到公网,扫描器几天就找上门了——Tomcat 的指纹太好认,默认页面加上版本号,等于把攻击面白送出去。
二、安装与托管
1. JDK 环境
1 | # 安装 JDK 1.8(兼容性最好的版本,生产上跑老项目的首选) |
2. 安装 Tomcat
1 | cd /usr/local |
3. 交给 systemd 托管
裸跑 startup.sh 的问题是进程崩了没人拉起来,也没法开机自启。写一个 unit:
1 | vim /usr/lib/systemd/system/tomcat.service |
1 | [Unit] |
1 | systemctl daemon-reload |
访问 http://服务器IP:8080 看到 Tomcat 欢迎页就算通了。
User=root是先跑通的写法,生产要换成普通用户,见后面安全基线一节。
三、server.xml:三个端口和一个层级结构
conf/server.xml 是 Tomcat 的主配置,地位等同于 Nginx 的 nginx.conf,结构是「Server → Service → Connector + Engine → Host → Context」层层嵌套。真正需要动的是这几个:
1 | <!-- 1. 关闭端口:默认 8005,收到 SHUTDOWN 字符串就关服务。生产必须改 --> |
Host 对应 Nginx 的 server 块(一个域名站点),Context 对应 location(一个访问路径指向哪个应用):
1 | <Engine name="Catalina" defaultHost="localhost"> |
默认 8005 那个关闭端口很危险——本机任意用户都能发个字符串让 Tomcat 退出,所以生产环境把它改掉、绑到 127.0.0.1、关闭命令换成随机串。
四、war 包部署:三种方式,生产选第三种
| 方式 | 操作 | 适用 |
|---|---|---|
| 自动部署 | war 丢进 webapps/,自动解压加载 |
测试环境,图快 |
| 改 server.xml 加 Context | 路径自定义,但要重启 | 少用 |
| 独立 xml 配置文件 | conf/Catalina/域名/xxx.xml,一个项目一个文件 |
生产首选 |
第三种是我现在的标准做法:不碰主配置、热部署、回滚方便。
1 | mkdir -p /usr/local/tomcat/conf/Catalina/localhost/ |
1 | <Context docBase="/data/project/test.war" reloadable="true" /> |
文件名就是访问路径,访问 http://IP:8080/test/ 即可。想让项目直接挂根路径,把 war 命名为 ROOT.war。
发布规范上我们后来固定成一条流程:war 包先放 /data/release/ 带版本号归档 → 停实例(集群节点轮流停)→ 替换 → 起服务 → curl -I 验证 → 观察 5 分钟再动下一个节点。目的是任何一次发布都能在 3 分钟内退回上一个版本。
五、Connector 线程池:先搞清楚参数关系
Tomcat 8 之后默认就是 NIO,日常够用;追求更高并发可以上 NIO2。真正要调的是线程和连接数参数:
1 | <Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" |
有两组关系值得记住:
maxThreads + acceptCount就是这台机器能扛的瞬时请求上限,超了就是连接被拒;- 线程数不是越大越好,500 以上还压不住,通常是后端有慢逻辑,加大线程只会让上下文切换把 CPU 吃光。
我们压测过一次:4 核 8G 的机器,maxThreads 从 200 加到 500,QPS 提升不到 10%,但 Load 上升明显——线程池要按压测结果调,不要拍脑袋。
六、JVM 参数:默认值就是等着出事故
Tomcat 默认堆小得可怜,稍微有点流量就 OOM。参数写在 bin/catalina.sh 顶部:
1 | JAVA_OPTS=" |
几个关键项:
-Xms和-Xmx设成一样,避免堆动态伸缩带来的抖动;- 元空间默认太小,Spring 系应用类加载多,不调大很容易
MetaspaceOOM; -XX:+HeapDumpOnOutOfMemoryError必开——OOM 发生时现场只出现一次,有 dump 才有得分析;- GC 日志必开,Full GC 频率是判断”内存够不够”最直接的指标。
OOM 排查路径
真出事的时候,按这条线走,不要瞎猜:
1 | # 1. 先确认是什么类型的 OOM |
处理顺序也有讲究:先重启恢复业务(止血)→ 再决定调大 -Xmx(临时)→ 最后必须推动开发修代码里的泄漏(根治)。只做前两步的,过几天还会再来一次。
七、日志:两块必须配置到位
1 | # catalina.out 无限增长,不切割几个月就能占满磁盘 |
1 | /data/logs/tomcat/catalina.out { |
访问日志要加耗时字段,这是排查慢接口的依据:
1 | <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" |
另外两个约定:日志目录必须放到数据盘,别跟系统盘抢空间;日志级别生产固定 INFO,排查问题时临时开 DEBUG,查完立刻改回去——DEBUG 的日志量是 INFO 的几十倍,开一晚上能写满一块盘。
八、上线前安全基线
这份清单每次部署新实例都过一遍,一条都不能少:
1 | # 1. 删掉所有默认应用(管理页面是爆破重灾区) |
漏掉默认应用那次印象很深:manager 页面被外部扫到,一晚上几十万次爆破尝试,虽然密码复杂没被攻破,但日志量直接把磁盘告警打响了。
最后
Tomcat 的配置项不多,但要踩的坑基本都在”默认值”里:默认端口、默认堆、默认应用、默认权限。新人最容易犯的错是先跑起来再补配置——正确顺序是先按基线配好,再部署第一个应用。这份基线后来帮我省了很多次半夜被叫起来的时间。


