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

手记 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;
for i in $(seq 1 30); do
  CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8083/app/)
  printf "%s " "$CODE"
done
echo

实测 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, 把 alternatives 的 java 软链接从 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 命令

可用键盘 ← → 切换上下篇