-今日
-本周
-本月
-累计
项目工作台运维 个人项目与实践记录

手记 008:Tomcat 部署与 nginx 反向代理

**体积小 4.6 倍**39MB vs 180MB(流量敏感场景重要)
**官方维护**阿里云在自家 ECS 上维护的发行版
**完全兼容**Dragonwell 就是 OpenJDK + 补丁

日期: 2026-09-22 用时: 约 120 分钟 难度: ⭐⭐⭐⭐☆


1. 目标

已部署的服务里 Nginx / MySQL / Redis 都有实操记录, Tomcat 还是空白。本次补齐,并实践 nginx 反向代理(两者配合的常见架构)。


2. JDK 安装与版本选择

2.1 环境

java -version
# -bash: java: command not found

2.2 可选的 JDK 发行版

查询软件源时发现两套选择:

java-11-openjdk-headless            标准 OpenJDK,约 180 MB
java-11-alibaba-dragonwell-headless 阿里云发行版,约 39 MB
java-17-*                           更新的 LTS 版本
java-1.8.0-*                        老项目兼容

选择 Dragonwell,理由:

理由说明
体积小 4.6 倍39MB vs 180MB(流量敏感场景重要)
官方维护阿里云在自家 ECS 上维护的发行版
完全兼容Dragonwell 就是 OpenJDK + 补丁

2.3 OpenJDK 的多个发行版

这是一个值得了解的知识点:

发行商产品名
OracleOracle JDK
社区OpenJDK
Eclipse 基金会Temurin(原 AdoptOpenJDK)
阿里云Dragonwell ← 本次使用
亚马逊Corretto
AzulZulu
腾讯Kona
华为Bisheng JDK

差异点:GC 调优、长期支持承诺、许可条款、特定场景优化。

实践价值:选择版本时了解发行版差异,能避免"默认用哪个就用哪个"的盲目性。

2.4 装完后发现的问题:JDK 被"抢"了

装完 Dragonwell 后

java -version
# openjdk version "11.0.31.28"(Alibaba Dragonwell Extended Edition)

安装 Tomcat 后,再查

java -version
# openjdk version "11.0.25" 2024-10-15 LTS
# OpenJDK Runtime Environment (Red_Hat-11.0.25.0.9-1)

变回 Red Hat 的 OpenJDK 了。

原因:Tomcat 的依赖自动拉入了 java-11-openjdk-headless, 并且 alternatives 系统的软链接被切走了:

alternatives --display java
# java - status is auto.
#   link currently points to /usr/lib/jvm/java-11-openjdk-11.0.25.../bin/java
#   java-11-alibaba-dragonwell... priority -1881773657     ← 负数优先级!

Dragonwell 的优先级是负数 —— 在 auto 模式下永远排在最后。

这是 alternatives 机制的一个注意点: 安装新包时,系统会自动把软链接指向优先级更高的那个, 可能覆盖你之前的选择

验证方法

alternatives --display java        # 看当前指向和所有候选的优先级
readlink -f /usr/bin/java          # 看实际指向的路径

3. Tomcat 部署

3.1 安装方式选择

方式命令特点
yum 安装yum install -y tomcat tomcat-webapps tomcat-admin-webapps自动处理依赖、系统集成
官方 tar.gz下载解压到 /opt/tomcat目录结构清晰、版本可选

选择 yum,因为:

  1. 自动处理 servlet-api、jsp-api 等依赖
  2. 自带 systemd unit —— 可以对比自己手写的服务单元
  3. 符合 RHEL 系的目录规范

安装结果

tomcat            1:9.0.87-2.al8    96 k
tomcat-admin-webapps                77 k
tomcat-webapps                      84 k
tomcat-lib                         5.6 M
tomcat-servlet-4.0-api             290 k
tomcat-jsp-2.3-api                  75 k
tomcat-el-3.0-api                  109 k
tomcat-native                       81 k
...(共 15 个包,50 MB)

3.2 RHEL 系的目录布局(与官方文档不同)

yum 安装

/usr/share/tomcat/          程序(jar 包、脚本)
/etc/tomcat/                配置(server.xml 等)
/var/lib/tomcat/            数据(webapps)
/var/log/tomcat/            日志
/var/cache/tomcat/          缓存(工作目录)

官方 tar.gz 安装

/opt/tomcat/bin/            启动脚本
/opt/tomcat/conf/           配置
/opt/tomcat/webapps/        应用
/opt/tomcat/logs/           日志

这个差异很重要 —— 网上大部分教程按 tar.gz 布局写,照抄会找不到文件。

3.3 自带 unit 文件的分析

systemctl cat tomcat
[Unit]
Description=Apache Tomcat Web Application Container
After=syslog.target network.target

[Service]
Type=simple
EnvironmentFile=/etc/tomcat/tomcat.conf      ← 环境变量外部化
Environment="NAME="
EnvironmentFile=-/etc/sysconfig/tomcat       ← 减号 = 文件不存在也不报错
ExecStart=/usr/libexec/tomcat/server start   ← 包装脚本,不是直接跑 java
SuccessExitStatus=143                        ← 143 视为正常退出
User=tomcat                                  ← 非 root 用户

[Install]
WantedBy=multi-user.target

三个值得对比的设计(与之前手写的 quest02.service):

设计Tomcat 的做法我的 quest02 做法
环境变量EnvironmentFile 外部化硬编码在脚本里
文件缺失容错EnvironmentFile=-(减号)无此机制
信号处理SuccessExitStatus=143 在服务层声明脚本里 trap cleanup TERM

SuccessExitStatus=143 的原理

143 = 128 + 15(SIGTERM 的信号编号)

systemd 默认把非 0 退出码视为失败
但 systemctl stop 发的就是 SIGTERM
→ 不声明的话,每次正常停止都会记录为"失败"

两种解决方案对比

方案做法优劣
应用层脚本 trap + exit 0需改脚本,但更可控
服务层unit 里声明 SuccessExitStatus不动脚本,集中管理

这是"分层处理"的实例 —— 同一需求可以在不同层次解决。

3.4 缺 Restart 策略(与 nginx 同样的问题)

systemctl show tomcat -p Restart
# Restart=no

发行版的 unit 普遍不设 Restart —— 设计者认为服务崩溃应该人工介入。

之前审计发现 10 个服务存在此问题,Tomcat 是第 11 个。

修复方式(复用 nginx 的方案):

mkdir -p /etc/systemd/system/tomcat.service.d

cat > /etc/systemd/system/tomcat.service.d/override.conf <<'EOF'
[Service]
Restart=always
RestartSec=5s
EOF

systemctl daemon-reload
systemctl show tomcat -p Restart -p RestartUSec

drop-in 的价值

价值说明
升级安全yum update 更新原 unit 时,修改不丢
可追溯systemctl cat tomcat 能看到全部修改
可回滚删除 drop-in 目录即恢复原状

4. 端口冲突:服务 active 但功能不可用

4.1 现象

systemctl start tomcat
systemctl status tomcat
● tomcat.service - Apache Tomcat Web Application Container
   Active: active (running) since Tue 2026-09-22 16:44:20 CST; 5s ago
 Main PID: 221773 (java)
    Tasks: 22
   Memory: 116.7M

看起来完全正常。 但访问测试暴露了问题:

curl -I http://127.0.0.1:8080/
HTTP/1.1 200 OK
Server: nginx                     ← 返回的是 nginx!
Content-Length: 11843

4.2 排查

# Tomcat 进程监听了哪些端口
ss -tlnp | grep java
LISTEN [::ffff:127.0.0.1]:8005     ← 只有 shutdown 端口
                                   ← 没有 8080!

# 8080 是谁
ss -tlnp | grep ':8080'
LISTEN 0.0.0.0:8080  users:(("nginx",...))

4.3 根因

日志里有关键错误

journalctl -u tomcat | grep -iE 'severe|bind'
SEVERE [main] Failed to initialize component [Connector["http-nio-8080"]]
Caused by: java.net.BindException: Address already in use

完整链条

Tomcat 启动
    ↓
尝试绑定 8080 → 被 nginx 占用 → BindException
    ↓
HTTP 连接器(Connector)初始化失败
    ↓
但 shutdown 端口 8005 绑定成功
    ↓
JVM 主进程继续运行
    ↓
systemd 看到主进程活着 → 报告 active (running)
    ↓
【服务状态正常,但功能完全不可用】

4.4 为什么这个故障特别隐蔽

检查项结果能否发现问题
systemctl is-activeactive
systemctl statusrunning, 116MB 内存
进程存在java PID 221773
8005 端口监听
8080 端口监听
curl 8080200,但 Server 头是 nginx

curl 返回 200 反而是误导 —— 因为它访问到的是 nginx 的静态站点。

判断"访问到的到底是谁",要看 Server 响应头

Server: nginx                    → 走到了 nginx
无 Server 头 + Transfer-Encoding: chunked → 走到了 Tomcat

4.5 修复

# 1. 备份
cp /etc/tomcat/server.xml /etc/tomcat/server.xml.bak-$(date +%s)

# 2. 改端口(用行号精确定位,避免误改注释中的同名配置)
sed -i '69s/port="8080"/port="8082"/' /etc/tomcat/server.xml

# 3. 验证
sed -n '69p' /etc/tomcat/server.xml

为什么用行号而不用全局替换

server.xml 里有两处 port="8080",其中一处被注释掉(备选配置示例)。 全局替换虽然无害,但不够精确。

4.6 修复结果

修复前修复后
8082 监听*:8082(java)
curl 响应Server: nginx无 Server 头 + chunked
日志BindException无错误,只有 Started
Restartnoalways(5s)

这是第三次遇到"服务状态正常但功能不可用"

场景假正常的现象真正的验证方法
心跳服务(手记 003)is-active 说 active,日志不增长观察产出是否变化
Docker 端口(手记 006)容器 Up,端口未映射从外部实测可达性
Tomcat(本次)active + 116MB 内存,但端口没监听ss 看端口 + curl 看 Server 头

共同规律

systemctl is-active 只能证明进程存在,不能证明服务在工作。

5. 崩溃自愈验证

配好 Restart=always 后用 SIGKILL 验证

PID_BEFORE=$(systemctl show tomcat -p MainPID --value)   # 222298
kill -9 $PID_BEFORE
sleep 8
PID_AFTER=$(systemctl show tomcat -p MainPID --value)    # 222671

结果

修复前 PID: 222298
修复后 PID: 222671     ← 变化了,说明重启成功
端口 8082 仍在监听(pid=222671)

与 nginx 的验证方式一致 —— kill -9 模拟 OOM Killer 的行为 (不可捕获的信号,trap 无法处理)。


6. nginx 反向代理 Tomcat

6.1 架构

客户端
   ↓
nginx (8083)
   ├─ /app/  → Tomcat (127.0.0.1:8082)  ← 动态应用
   └─ /*     → 静态文件

6.2 配置

/etc/nginx/conf.d/tomcat-proxy.conf

upstream tomcat_backend {
    server 127.0.0.1:8082;
    keepalive 16;
}

server {
    listen       8083;
    listen       [::]:8083;
    server_name  _;

    access_log  /var/log/nginx/tomcat-proxy.access.log;
    error_log   /var/log/nginx/tomcat-proxy.error.log;

    location /app/ {
        limit_req  zone=general burst=20 nodelay;
        limit_conn perip 10;

        proxy_pass http://tomcat_backend/;

        proxy_http_version 1.1;
        proxy_set_header Connection "";

        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;

        proxy_connect_timeout 5s;
        proxy_send_timeout    30s;
        proxy_read_timeout    30s;

        proxy_buffering off;
    }

    location / {
        root  /var/www/personal-site;
        index index.html;
    }
}

6.3 三个关键配置点

proxy_pass 末尾的斜杠

proxy_pass http://tomcat_backend/;     # 有斜杠
proxy_pass http://tomcat_backend;      # 无斜杠
写法请求 /app/foo 转发为
有斜杠/foo(替换掉 location 匹配的部分)
无斜杠/app/foo(原样拼接)

这个差异容易踩坑,写错会导致后端的路径不对。

proxy_http_version 1.1 + Connection "" 必须成对

proxy_http_version 1.1;
proxy_set_header Connection "";

为什么两个都要写

nginx 默认对后端使用 HTTP/1.0,并带 Connection: close
    ↓
upstream 里配的 keepalive 完全不生效
    ↓
要启用长连接,必须:
  1. 声明使用 HTTP/1.1
  2. 清空 Connection 头(不能是 close)

实测验证(Tomcat 访问日志):

修复前:  "GET / HTTP/1.0" 200 11044     ← HTTP/1.0
修复后:  "GET / HTTP/1.1" 200 11064     ← HTTP/1.1,且响应大了 20 字节

四个传递头的作用

头部作用不传的后果
Host原始主机名Tomcat 看到的是 127.0.0.1:8082
X-Real-IP客户端真实 IPTomcat 日志里全是 nginx 的 IP
X-Forwarded-For代理链(多个代理时追加)无法追溯真实来源
X-Forwarded-Proto原始协议应用生成的绝对 URL 会用错协议

6.4 验证

验证一:内容一致性

curl -s http://127.0.0.1:8083/app/ | wc -c
# 11044 —— 与直连 Tomcat 的响应大小一致

验证二:响应头对比

# 直连 Tomcat
curl -I http://127.0.0.1:8082/
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
                              ← 无 Server 头

# 经 nginx 代理
curl -I http://127.0.0.1:8083/app/
HTTP/1.1 200
Server: nginx/1.24.0          ← nginx 加了 Server 头
Content-Type: text/html;charset=UTF-8

验证三:404 页面的归属

curl -I http://127.0.0.1:8082/nonexistent      # 直连
curl -I http://127.0.0.1:8083/app/nonexistent  # 代理

两个响应都有 Content-Language: en

8082:  404, Content-Type: text/html;charset=utf-8, Content-Language: en
8083:  404, Content-Type: text/html;charset=utf-8, Content-Language: en

Content-Language: en 是 Tomcat 错误页的特征 —— nginx 自己的 404 页面不会有这个头。

这证明了

  1. 请求确实转发到了 Tomcat
  2. 路径重写生效(/app/nonexistent/nonexistent
  3. 404 是 Tomcat 生成的,不是 nginx

验证四:Tomcat 访问日志

tail -2 /var/log/tomcat/localhost_access_log.$(date +%Y-%m-%d).txt
127.0.0.1 - - [17:10:35] "GET / HTTP/1.0" 200 11044     ← 修复前
127.0.0.1 - - [17:19:08] "GET / HTTP/1.1" 200 11064     ← 修复后(HTTP/1.1)

6.5 限流验证

/app/ 加了限流(复用 00-limit-zones.conf 定义的 zone):

limit_req  zone=general burst=20 nodelay;
limit_conn perip 10;

实测 30 次连续请求

200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 429 429 429 429 429 429 429
└──────────────────── 23 个 200 ────────────────────┘└────── 7 个 429 ──────┘

数字与配置精确匹配

rate=10r/s   → 平均每秒 10 个
burst=20     → 令牌桶容量 20
               初始 20 + 1 秒内补充的约 3-10 个 ≈ 23 个通过
               之后令牌耗尽 → 429

关于 nodelay

配置突发请求的行为
burst=20超出的请求排队等待逐个放行(变慢但不拒绝)
burst=20 nodelay超出的请求立即处理或拒绝,不排队

实测结果是"23 个通过后直接 429",说明 nodelay 生效(没有排队延迟)。


7. 遇到的一次配置事故

现象

修改配置后 nginx -t 报错:

nginx: [emerg] duplicate location "/app/" in /etc/nginx/conf.d/tomcat-proxy.conf:58

原因

编辑时用粘贴代替了替换 —— 文件里出现了两个 location /app/

location /app/ {        ← 原有的
    ...
}

location /app/ {        ← 新加的(造成重复)
    ...
}

nginx -t 拦住了这个错误

如果跳过检查直接 reload

nginx 会拒绝加载新配置,继续使用旧配置
    ↓
配置文件的改动"看起来生效了"(因为文件确实改了)
    ↓
但实际运行的是旧配置 —— 磁盘与内存分叉

这正是之前学到的原则的又一次验证

改配置 → nginx -t → reload → 检查 error.log
         └──┬──┘
      这一步拦住了一个错误

修复

因为改动只有几行,直接重写整个文件比在 vim 里删改更可靠。

学到的方法

编辑配置文件时,如果改动超过 3 行,重写整个文件往往比局部编辑更安全 —— 尤其在使用 cat > file <<'EOF' 这种能完整替换的方式时。


8. 最终状态

┌─────────────────────────────────────────────────────────┐
│  JDK: Dragonwell 11.0.31(alternatives 可切换)          │
├─────────────────────────────────────────────────────────┤
│  Tomcat 9.0.87                                          │
│    · 监听 8082(HTTP)+ 8005(shutdown)                 │
│    · User=tomcat(非 root)                              │
│    · Restart=always(drop-in 配置)                      │
│    · enabled(开机自启)                                 │
│    · 崩溃自愈已验证(kill -9 后 PID 变化)                │
├─────────────────────────────────────────────────────────┤
│  nginx 反向代理                                          │
│    · 8083/app/ → 127.0.0.1:8082                         │
│    · HTTP/1.1 + upstream keepalive                      │
│    · 限流:23 个 200 + 7 个 429(实测)                   │
│    · 传递 Host/X-Real-IP/X-Forwarded-For/Proto           │
├─────────────────────────────────────────────────────────┤
│  日常巡检扩展到 13 项(新增 Tomcat 代理链路检查)          │
└─────────────────────────────────────────────────────────┘

端口全景

22     SSH
80     nginx(生产站点)
8080   nginx(测试站点)
8081   nginx(练习 lab01)
8082   Tomcat(HTTP)           ← 本次新增
8083   nginx(Tomcat 代理)      ← 本次新增
8090   nginx(练习 quest01)
9090   nginx(练习 lab02)
3306   MariaDB(仅本机)
6379   Redis(仅本机)
8005   Tomcat(shutdown,仅本机)

共 11 个监听端口 —— 端口规划开始变得重要。


9. 结论

软件包管理与已装版本可能冲突

安装 Tomcat 时,依赖自动拉入了 java-11-openjdk-headless, 把 alternativesjava 软链接从 Dragonwell 切换走了。

这类问题的特点

排查方法alternatives --display java 看当前指向和优先级。

"服务 active" 与 "功能可用" 是两件事(第三次遇到)

Tomcat 的 8080 端口绑定失败,但主进程因为 8005 绑定成功而继续运行, systemd 报告 active (running)

更麻烦的是 curl 返回 200 —— 因为访问到的是 nginx 的静态站点。

判断"请求走到了谁",要看 Server 响应头Server: nginx 说明走到了 nginx;Tomcat 默认不发 Server 头。

反向代理的配置细节决定行为

三个必须理解的点:

配置影响
proxy_pass 末尾的斜杠路径重写方式
proxy_http_version 1.1 + Connection ""长连接是否生效
四个 proxy_set_header后端能否看到真实客户端信息

nginx -t 是配置修改的必要环节

本次 duplicate location 的错误被它拦住。 如果直接 reload,文件改了但运行配置没变,会造成"磁盘与内存分叉" (与之前遇到的 limit_req_zone 问题同类)。

配置应该纳入版本控制

/etc/ 下的配置不在 Git 里,改坏了无法回滚、也无法追溯。 做法:在项目里维护一份副本(conf/ 目录),每次修改后同步并提交。


10. 遗留


11. 协作说明

部署命令和配置内容由 AI 提供,执行和排查由我做。

两个地方走了弯路:

一是 Tomcat 装完后 systemctl status 显示正常,但 8080 端口实际没监听 —— curl 拿到的是 nginx 的响应。要发现这个问题,得先想到 "服务状态"和"功能状态"是两件事。

二是验证代理是否生效。curl 返回 200 不足以说明问题, 因为返回的可能是 nginx 自己的响应。最后用 Content-Language: en 这个 Tomcat 错误页特有的响应头,确认 404 确实来自 Tomcat。


12. 后续计划

  1. 部署一个真实的 JSP 应用,验证头部传递
  2. 整理端口规划(11 个端口的用途归类)
  3. 配置 HTTPS(备案通过后)
  4. 继续系统练习 Linux 命令

可用键盘 ← → 切换上下篇