Tomcat 与 Java Web 部署

本文合并自:01-Tomcat完全指南.md、3-1-Tomcat与JavaWeb部署.md

适用环境:CentOS 6 / CentOS 7 + JDK 8 + Tomcat 8 / Tomcat 9。文中凡涉及服务管理、开机自启等与系统版本相关的差异,均分别标注 CentOS 6 与 CentOS 7 两套写法。


1. Tomcat 概述与 Java Web 技术基础

1.1 主流 Web 架构

架构组合 适用语言 代表组件
Windows Server + IIS + SQL Server + .NET .NET IIS、SQL Server
LAMP / LNMP PHP Apache / Nginx + MySQL + PHP
Linux + Java 应用服务器 + 数据库 Java Tomcat / JBoss / WebLogic / WebSphere / Resin + MySQL / Oracle / DB2

1.2 名词解释

缩写 全称 说明
JDK Java Development Kit Java 开发工具包,是整个 Java 的核心,包含 JRE、Java 工具和基础类库。运行任何 Java 程序都必须先安装 JDK
JRE Java Runtime Environment Java 运行时环境,包含 JVM 与运行 Java 程序所需的核心类库,只能运行不能编译
JVM Java Virtual Machine Java 虚拟机,负责将字节码解释/编译为机器码并执行,是 Java 跨平台的基础
J2SE / Java SE Standard Edition Java 标准版本
J2EE / Java EE Enterprise Edition Java 企业级版本,由一整套服务、API 和协议规范组成
J2ME / Java ME Micro Edition Java 微型版本,用于手机等运算能力、电量有限的设备

三者关系可以概括为:JDK ⊃ JRE ⊃ JVM。

1.3 Servlet 与 JSP

技术 说明
Servlet 运行在服务器端的 Java 类,实现 javax.servlet.Servlet 接口,负责接收 HTTP 请求、处理业务逻辑并生成响应。是 Java Web 的底层核心
JSP(JavaServer Pages) 在 HTML 中嵌入 Java 代码的页面模板。JSP 在第一次被访问时由容器翻译成 Servlet 源码,再编译为 .class 字节码执行,编译产物保存在 Tomcat 的 work/ 目录

因此 "JSP 首次访问慢" 的本质原因就是需要先翻译再编译,后续访问直接使用已编译好的 Servlet。

一个最简单的 JSP 示例(输出服务器当前时间):

<html>
<body>
<center>
<H1><%=new java.util.Date()%></H1>
</center>
</body>
</html>

1.4 WAR 包与标准 Web 应用目录结构

WAR(Web Application Archive)本质是一个 ZIP 压缩包,内部遵循 Java EE 规定的标准目录结构:

myapp.war
├── index.jsp                 # 页面文件,可直接被访问
├── static/                   # 静态资源:css、js、图片等
├── META-INF/
│   └── MANIFEST.MF           # 打包元信息
└── WEB-INF/                  # 受保护目录,客户端无法直接访问
    ├── web.xml               # 应用部署描述符(Servlet 映射、欢迎页、session 超时等)
    ├── classes/              # 编译后的 .class 文件、配置文件(properties/xml)
    └── lib/                  # 应用私有依赖 JAR 包

关键点:

  • WEB-INF/ 下的内容浏览器无法直接访问,是放置配置与类文件的安全位置。
  • WEB-INF/lib/ 是应用私有 JAR,$CATALINA_HOME/lib/ 是所有应用共享 JAR。
  • JAR(Java Archive)包的作用是对 Class 文件进行打包管理,内部包含编译后的 .class 文件。

1.5 Servlet 容器与应用服务器的区别

类型 实现的规范 代表产品
Servlet 容器(Web 容器) 仅实现 Servlet / JSP 规范,提供 HTTP 请求处理、会话管理、类加载 Tomcat、Jetty、Resin
完整的 Java EE 应用服务器 除 Servlet/JSP 外还实现 EJB、JTA 分布式事务、JMS 消息、JCA、JPA 等完整 Java EE 规范 JBoss/WildFly、WebLogic、WebSphere、GlassFish

Tomcat 属于 轻量级应用服务器(严格说是 Servlet 容器 + JSP 引擎),不支持 EJB 等重量级规范;如果应用需要 EJB、分布式事务,则要选用 JBoss、WebLogic 或 WebSphere。

1.6 常见 Java 应用服务器定位对比

产品 厂商 授权 定位说明
Tomcat Apache 软件基金会 开源免费(Apache License) 轻量级 Servlet 容器,互联网公司主流选择,配置简单、启动快、社区活跃
JBoss / WildFly Red Hat 开源(社区版免费,EAP 收费订阅) 完整 Java EE 实现,内部早期使用过 Tomcat 作为 Web 容器,适合企业级中间件
WebLogic Oracle 商业收费 大型商业中间件,功能全面、管理控制台强大,金融/电信行业常见,价格昂贵
WebSphere IBM 商业收费 与 IBM 生态(DB2、MQ、AIX)整合紧密,重量级、资源占用大
Resin CAUCHO 商业 + 开源版 以性能与低内存占用著称,国内使用相对较少
Jetty Eclipse 开源免费 极轻量,常作为嵌入式容器被其他项目内置

1.7 Tomcat 概述

Tomcat 是 Apache 软件基金会 Jakarta 项目中的核心项目,由 Apache、Sun 及其他公司/个人共同开发,是一款免费、开源的 Web 应用服务器,属于轻量级应用服务器,也是一款常用的 Java 中间件,普遍用于运行 Java Web 项目。

Tomcat 9.0.21 下载示例:

wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-9/v9.0.21/bin/apache-tomcat-9.0.21.tar.gz

版本与 Servlet 规范、JDK 的对应关系(选版本时的重要依据):

Tomcat 版本 Servlet 规范 最低 JDK 要求
Tomcat 6.x Servlet 2.5 JDK 5
Tomcat 7.x Servlet 3.0 JDK 6
Tomcat 8.0.x Servlet 3.1 JDK 7
Tomcat 8.5.x Servlet 3.1 JDK 7
Tomcat 9.x Servlet 4.0 JDK 8

1.8 Tomcat 体系结构

conf/server.xml 描述的组件层级自上而下为:

Server(整个 Tomcat 实例,监听 8005 关闭端口)
 └─ Service(把若干 Connector 绑定到同一个 Engine)
     ├─ Connector(HTTP/1.1 : 8080)
     ├─ Connector(AJP/1.3 : 8009)
     └─ Engine(Servlet 引擎,name="Catalina")
         ├─ Host(虚拟主机 localhost,appBase="webapps")
         │   ├─ Context(应用上下文 /,docBase=ROOT)
         │   └─ Context(应用上下文 /sp,docBase=sp)
         └─ Host(虚拟主机 www.example.com)
             └─ Context(...)

各组件职责:

组件 职责说明
Server 代表整个 Tomcat 容器(一个 JVM 进程),是最顶层元素,只能有一个。监听 8005 端口接收 SHUTDOWN 命令。可包含多个 Service
Service 中间层容器,将一个或多个 Connector 与一个 Engine 组合成对外提供服务的整体。一个 Service 只能有一个 Engine
Connector 连接器,负责在指定端口监听、接收客户端连接,解析请求协议(HTTP、AJP),把请求封装成 Request 对象交给 Engine,并把 Response 写回客户端
Engine Servlet 引擎(Catalina 引擎),接收 Connector 传来的所有请求,根据请求头中的 Host 字段在多个 Host 之间路由;找不到匹配主机则交给 defaultHost
Host 虚拟主机,按域名区分站点,对应一个 appBase 应用基础目录。一个 Engine 下可有多个 Host
Context 具体的 Web 应用上下文,对应一个 Web 应用(一个 WAR 或一个目录),拥有独立的类加载器与 Session 空间。path 即访问路径
Wrapper 最内层容器,代表一个具体的 Servlet,通常不需要手工配置
Valve 阀门(拦截器),可挂在 Engine / Host / Context 上,用于访问日志、IP 过滤、单点登录等
Realm 认证域,提供用户名/密码/角色的存储与校验(如 UserDatabaseRealm 读取 tomcat-users.xml)
Listener 生命周期监听器,在组件启动/停止时执行特定动作
GlobalNamingResources 全局 JNDI 资源定义,如全局数据源、UserDatabase

1.9 请求处理流程

以浏览器访问 http://www.example.com/shop/index.jsp 为例:

  1. 客户端向服务器的 8080(或 80)端口发起 TCP 连接与 HTTP 请求。
  2. 监听该端口的 Connector 接受连接,按 protocol 指定的协议实现(NIO / NIO2 / APR)读取并解析 HTTP 报文,生成 Request / Response 对象。
  3. Connector 将 Request 交给所属 Service 的 Engine。
  4. Engine 读取请求头中的 Host: www.example.com,在其下的 Host 列表中匹配 name 或 Alias;匹配不到则使用 defaultHost 指定的主机。
  5. 匹配到的 Host 依据 URI 的第一段 /shop 匹配对应的 Context;匹配不到则交给 path="" 的根上下文(ROOT)。
  6. Context 依据 WEB-INF/web.xml(以及注解)中的 servlet-mapping 找到处理 /index.jsp 的 Wrapper。JSP 由 JspServlet 处理:若 work/ 下无对应字节码或源文件已更新,则先翻译成 Servlet 源码再编译。
  7. 调用 Servlet 的 service()(doGet() / doPost())方法执行业务逻辑,把结果写入 Response。
  8. 沿原路径(Wrapper → Context → Host → Engine → Connector)返回,途经的各级 Valve 依次执行(例如 AccessLogValve 在此写访问日志)。
  9. Connector 将 Response 序列化为 HTTP 响应报文发回客户端。

2. JDK 环境安装与配置

Tomcat 由 Java 编写,运行前必须先具备 JDK 环境。以下给出 yum 安装 OpenJDK 与 Oracle JDK 二进制解压安装 两种方式,二者选其一即可。

2.1 方式一:yum 安装 OpenJDK(简单、推荐用于测试环境)

CentOS 7:

# 查询可用的 JDK 包
yum list | grep -i openjdk
yum search java | grep -i jdk

# 只装运行环境(只能运行,不能编译)
yum install -y java-1.8.0-openjdk

# 装开发包(包含 javac 编译器,编译 JSP 需要)
yum install -y java-1.8.0-openjdk-devel

# 可选:源码与文档包
yum install -y java-1.8.0-openjdk-src

CentOS 6:

yum install -y java-1.7.0-openjdk java-1.7.0-openjdk-devel
# CentOS 6 官方源亦提供 java-1.8.0-openjdk(6.6 之后)
yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel

yum 安装的 JDK 默认路径在 /usr/lib/jvm/ 下,命令通过 /usr/bin/java 软链接暴露,通常无需手工配置 PATH。查看真实路径:

ls -l /usr/bin/java
# /usr/bin/java -> /etc/alternatives/java
ls -l /etc/alternatives/java
# /etc/alternatives/java -> /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64/jre/bin/java
ls /usr/lib/jvm/

注意:Tomcat 需要 JAVA_HOME。对于 yum 安装的 OpenJDK,JAVA_HOME 应指向 /usr/lib/jvm/java-1.8.0-openjdk(去掉末尾的 /jre/bin/java),而不是 /usr/bin。

2.2 方式二:Oracle JDK 二进制解压安装(推荐用于生产环境)

从 Oracle 官网下载 jdk-8u45-linux-x64.tar.gz(需登录接受许可),上传到服务器后解压:

# 解压到 /usr/local
tar xf jdk-8u45-linux-x64.tar.gz -C /usr/local/

# 确认目录
ls /usr/local/jdk1.8.0_45/

确认输出类似:

bin        javafx-src.zip  man          THIRDPARTYLICENSEREADME-JAVAFX.txt
COPYRIGHT  jre             README.html  THIRDPARTYLICENSEREADME.txt
db         lib             release
include    LICENSE         src.zip

JDK 目录内容说明:

子目录 说明
bin/ 可执行程序:java(运行)、javac(编译)、jar(打包)、jps / jstat / jmap / jstack(诊断工具)等
jre/ Java 运行时环境,其中 jre/lib/security/java.security 是安全策略配置(后文排错会用到)
lib/ 工具类库,lib/tools.jar 提供编译等工具支持
include/ C/C++ 头文件,供 JNI 使用
src.zip JDK 类库源码

建议:把带版本号的目录改名或做软链接为固定名,便于后续升级维护(升级时只需改软链接指向,不必改环境变量):

# 做法一:直接改名(讲义做法)
mv /usr/local/jdk1.8.0_45 /usr/local/java

# 做法二:保留版本号目录,另建软链接(升级更友好,推荐)
ln -s /usr/local/jdk1.8.0_45 /usr/local/java

2.3 配置 JAVA_HOME / CLASSPATH / PATH 环境变量

环境变量的作用:

变量 作用
JAVA_HOME 指向 JDK 安装根目录。Tomcat 的 catalina.sh 会读取它来定位 java 命令,必须正确设置
JRE_HOME 指向 JRE 目录,通常为 $JAVA_HOME/jre,可不显式设置
CLASSPATH Java 类搜索路径,JVM 据此查找 .class 与 JAR。JDK 5 之后一般无需手工设置(默认含当前目录),但讲义中习惯显式声明
PATH 命令搜索路径,加入 $JAVA_HOME/bin 后即可在任意位置执行 java、javac
CATALINA_HOME Tomcat 安装根目录
CATALINA_BASE Tomcat 实例目录,单实例时与 CATALINA_HOME 相同;一机多实例时各实例不同

做法一:写入 /etc/profile(全局生效,所有用户)

vim /etc/profile

在文件末尾追加:

export JAVA_HOME=/usr/local/java
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
export PATH=$PATH:$JAVA_HOME/bin

做法二:写入 /etc/profile.d/java.sh(推荐,便于单独管理与卸载)

cat > /etc/profile.d/java.sh <<'EOF'
export JAVA_HOME=/usr/local/java
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
export PATH=$PATH:$JAVA_HOME/bin
EOF

chmod 755 /etc/profile.d/java.sh

使配置立即生效:

source /etc/profile
# 或
. /etc/profile.d/java.sh

echo $JAVA_HOME
echo $PATH

做法三:只写入 Tomcat 的启停脚本(仅对该 Tomcat 实例生效)

把环境变量直接定义在 Tomcat 启停脚本中(位于 #!/bin/bash 之后),这样不污染全局环境,一台机器跑多个不同 JDK 版本的 Tomcat 时很实用:

vim /usr/local/tomcat/bin/startup.sh
vim /usr/local/tomcat/bin/shutdown.sh

在两个脚本顶部(#!/bin/bash 下方)加入:

export JAVA_HOME=/usr/local/java/
export TOMCAT_HOME=/usr/local/tomcat
export CATALINA_HOME=/usr/local/tomcat
export CLASS_PATH=$JAVA_HOME/bin/lib:$JAVA_HOME/jre/lib:$JAVA_HOME/lib/tool.jar
export PATH=$PATH:$JAVA_HOME/bin:$TOMCAT_HOME/bin

若 JDK 未改名,仍使用原始带版本号的目录,则写作:

export JAVA_HOME=/usr/local/jdk1.8.0_45/
export TOMCAT_HOME=/usr/local/tomcat
export CATALINA_HOME=/usr/local/tomcat
export CLASS_PATH=$JAVA_HOME/bin/lib:$JAVA_HOME/jre/lib:$JAVA_HOME/lib/tool.jar
export PATH=$PATH:/usr/local/jdk1.8.0_45/bin:/usr/local/tomcat/bin

说明:讲义中曾出现 /$TOMCAT_HOME/bin(多一个斜杠)、tool.jar(正确名为 tools.jar)以及 CLASS_PATH(标准变量名为 CLASSPATH)等笔误。上面保留了讲义原文写法以便对照,实际生产中建议使用 CLASSPATH 与 tools.jar 的规范写法。

做法四(更规范):使用 bin/setenv.sh

catalina.sh 启动时会自动 source $CATALINA_BASE/bin/setenv.sh(该文件默认不存在,需手工创建),这是官方推荐的注入环境变量与 JVM 参数的位置,升级 Tomcat 时不会被覆盖:

cat > /usr/local/tomcat/bin/setenv.sh <<'EOF'
export JAVA_HOME=/usr/local/java
export JRE_HOME=$JAVA_HOME/jre
export CATALINA_PID=/usr/local/tomcat/logs/tomcat.pid
EOF

chmod 755 /usr/local/tomcat/bin/setenv.sh

2.4 验证 JDK 安装

# 查看运行环境版本
java -version

# 查看编译器版本(若提示 command not found,说明只装了 JRE,需装 devel 包或使用完整 JDK)
javac -version

# 查看 java 命令实际来源
which java
readlink -f $(which java)

正常输出示例:

java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

编写一个测试程序,完整验证编译与运行链路:

mkdir -p /tmp/javatest && cd /tmp/javatest
cat > Hello.java <<'EOF'
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}
EOF

javac Hello.java      # 生成 Hello.class
ls                    # Hello.java  Hello.class
java Hello            # 输出 Hello, Java!

2.5 多版本 JDK 用 alternatives 切换

一台机器上同时存在多个 JDK(例如 yum 装的 OpenJDK 7 与手工解压的 Oracle JDK 8)时,用 alternatives(CentOS 6/7 通用;Debian 系为 update-alternatives)统一管理默认版本。

查看当前已注册的候选项

alternatives --display java
alternatives --display javac
alternatives --list | grep -i java

注册新的 JDK(把手工解压的 Oracle JDK 加入候选,优先级 200)

alternatives --install /usr/bin/java   java   /usr/local/java/bin/java   200
alternatives --install /usr/bin/javac  javac  /usr/local/java/bin/javac  200
alternatives --install /usr/bin/jar    jar    /usr/local/java/bin/jar    200
alternatives --install /usr/bin/javaws javaws /usr/local/java/bin/javaws 200

优先级数字越大越优先,自动模式下会选择优先级最高的。

交互式切换默认版本

alternatives --config java

输出示例:

There are 2 programs which provide 'java'.

  Selection    Command
-----------------------------------------------
*+ 1           /usr/lib/jvm/java-1.7.0-openjdk-1.7.0.261/jre/bin/java
   2           /usr/local/java/bin/java

Enter to keep the current selection[+], or type selection number: 2

* 表示当前生效项,+ 表示当前选中项。输入编号后回车即完成切换,再用 java -version 验证。

非交互方式设置 / 恢复自动模式 / 移除候选

# 直接指定使用某个版本(进入手动模式)
alternatives --set java /usr/local/java/bin/java

# 恢复为按优先级自动选择
alternatives --auto java

# 移除某个候选项
alternatives --remove java /usr/local/java/bin/java

提示:alternatives 只改变 /usr/bin/java 等命令的指向,不会改变 JAVA_HOME。Tomcat 依赖 JAVA_HOME,因此切换 JDK 后还需同步修改 /etc/profile.d/java.sh 或 setenv.sh 中的 JAVA_HOME,否则 Tomcat 仍会使用旧版本。


3. Tomcat 安装与目录结构

3.1 二进制解压安装完整步骤

Tomcat 是纯 Java 程序,官方二进制包解压即可运行,无需编译。

第一步:准备工作

# 关闭 SELinux(CentOS 6/7 通用)
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

# 放行 8080 端口 —— CentOS 7(firewalld)
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --reload
firewall-cmd --list-all

# 放行 8080 端口 —— CentOS 6(iptables)
iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
service iptables save
service iptables restart

# 课堂环境也可直接关闭防火墙
# CentOS 7
systemctl stop firewalld; systemctl disable firewalld
# CentOS 6
service iptables stop; chkconfig iptables off

第二步:解压并改名

Tomcat 9:

tar xf apache-tomcat-9.0.21.tar.gz -C /usr/local/
mv /usr/local/apache-tomcat-9.0.21/ /usr/local/tomcat

Tomcat 8:

tar xf apache-tomcat-8.0.20.tar.gz -C /usr/local/
mv /usr/local/apache-tomcat-8.0.20/ /usr/local/tomcat

与 JDK 同理,也可以保留版本号目录并建立软链接 ln -s /usr/local/apache-tomcat-9.0.21 /usr/local/tomcat,便于版本回退。

第三步:赋予脚本执行权限(若 tar 包权限异常)

chmod +x /usr/local/tomcat/bin/*.sh

第四步:配置环境变量(见 2.3 节,可写入 /etc/profile.d/、启停脚本或 setenv.sh)

3.2 目录结构逐项说明

ls /usr/local/tomcat/
# bin  conf  lib  LICENSE  logs  NOTICE  RELEASE-NOTES  RUNNING.txt  temp  webapps  work
目录 作用
bin/ 启停等可执行脚本与核心引导 JAR
conf/ 配置文件目录,核心文件有 server.xml、web.xml、context.xml、tomcat-users.xml、catalina.properties 等
lib/ Tomcat 自身及所有 Web 应用共享的 JAR 包(如 JDBC 驱动、MSM 相关 JAR 放这里)
logs/ 日志目录,包含 catalina.out、访问日志、管理日志等
temp/ JVM / Tomcat 运行时临时文件目录(对应 CATALINA_TMPDIR),可安全清空
webapps/ Web 应用部署目录,每个子目录即一个应用上下文,ROOT 为根应用
work/ 工作目录,存放 JSP 翻译生成的 Servlet 源码与编译后的 .class 字节码,以及序列化的 Session

bin/ 关键文件

文件 说明
startup.sh / startup.bat 启动脚本,内部调用 catalina.sh start
shutdown.sh / shutdown.bat 关闭脚本,内部调用 catalina.sh stop
catalina.sh 核心脚本,真正负责组装 JVM 参数并启动/停止 Tomcat,支持 start / stop / run / version / configtest 等参数
setenv.sh 默认不存在,需手工创建。用于自定义 JAVA_HOME、JAVA_OPTS、CATALINA_OPTS 等,升级不覆盖
bootstrap.jar Tomcat 引导类,main() 入口
tomcat-juli.jar Tomcat 自身的日志实现(基于 JULI)
version.sh 打印 Tomcat 与 JVM 版本信息
configtest.sh 检查配置文件语法(Tomcat 7+),改完 server.xml 建议先跑一遍
digest.sh 生成密码摘要,用于 tomcat-users.xml 中的密码加密存储
jsvc / daemon.sh 通过 commons-daemon 以 root 绑定特权端口、再降权为普通用户运行

conf/ 关键文件

文件 说明
server.xml 主配置文件,定义 Server / Service / Connector / Engine / Host / Context / Valve
web.xml 全局部署描述符,对所有应用生效(欢迎页、MIME 类型、DefaultServlet、JspServlet、session 超时)
context.xml 全局上下文配置,对所有应用生效(如 Session Manager、JNDI 资源)
tomcat-users.xml manager / host-manager 应用的用户、密码与角色
catalina.properties Catalina 的属性配置(类加载路径、包访问保护、字符串缓存等)
catalina.policy Security Manager 的安全策略文件
logging.properties JULI 日志配置(日志级别、输出文件、Handler)
jaspic-providers.xml JASPIC 认证提供者配置(Tomcat 8.5+)

logs/ 关键文件

文件 说明
catalina.out 标准输出与标准错误的汇总,System.out.println 与异常堆栈都在这里。排错第一现场,默认不切割会无限增长
catalina.<date>.log Catalina 容器自身的日志(按天切割)
localhost.<date>.log 应用内部(localhost 主机)的日志,常含应用启动失败的详细堆栈
localhost_access_log.<date>.txt 访问日志,由 AccessLogValve 产生
manager.<date>.log manager 应用日志
host-manager.<date>.log host-manager 应用日志

webapps/ 默认应用

应用 说明
ROOT 根应用(默认站点家目录 /usr/local/tomcat/webapps/ROOT/),访问 http://IP:8080/ 即为它
docs 官方文档,生产环境应删除
examples Servlet / JSP 示例,含可被利用的代码,生产环境必须删除
manager 应用管理器,可在线部署/停止/重载应用,生产环境建议删除或严格限权
host-manager 虚拟主机管理器,可在线增删 Host,生产环境建议删除

3.3 启动与关闭

启动

/usr/local/tomcat/bin/startup.sh

正常输出:

Using CATALINA_BASE:   /usr/local/tomcat
Using CATALINA_HOME:   /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME:        /usr/local/jdk1.8.0_45/
Using CLASSPATH:       /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
Tomcat started.

注意:Tomcat started. 只表示启动脚本执行成功(JVM 进程已拉起),不代表应用部署成功。必须查看 catalina.out 中出现 Server startup in xxxx ms 才算真正启动完成。

关闭

/usr/local/tomcat/bin/shutdown.sh

使用 catalina.sh 的多种模式

/usr/local/tomcat/bin/catalina.sh start      # 后台启动,等价于 startup.sh
/usr/local/tomcat/bin/catalina.sh stop       # 停止,等价于 shutdown.sh
/usr/local/tomcat/bin/catalina.sh stop 30    # 等待 30 秒后仍未停止则放弃
/usr/local/tomcat/bin/catalina.sh stop 30 -force  # 等待 30 秒后强制 kill
/usr/local/tomcat/bin/catalina.sh run        # 前台运行,日志直接刷屏,Ctrl+C 停止
/usr/local/tomcat/bin/catalina.sh version    # 查看版本
/usr/local/tomcat/bin/catalina.sh configtest # 校验配置文件语法
/usr/local/tomcat/bin/catalina.sh jpda start # 以调试模式启动(默认 8000 端口)

catalina.sh run 的价值:前台运行、日志直接输出到终端,在排查 "启动脚本报 started 但服务实际没起来"、"改完 server.xml 后启动失败" 这类问题时最直观,无需另开窗口 tail 日志。调试确认无误后再改回 start 后台运行。

强制停止(仅在正常停止失败时使用)

pkill -9 -f tomcat
# 或
kill -9 $(pgrep -f tomcat)
# 或先定位再杀
ps -ef | grep tomcat
kill -9 <PID>

kill -9 会跳过 Tomcat 的优雅关闭流程(不执行 Servlet 的 destroy()、不持久化 Session),可能造成 Session 丢失或临时文件残留,务必优先使用 shutdown.sh。

查看运行状态

lsof -i:8080                 # 查看端口监听
netstat -antup | grep 8080   # 同上(CentOS 6 常用)
ss -antpl | grep 8080        # CentOS 7 推荐
ps -ef | grep tomcat         # 查看进程
jps -l                       # 查看 Java 进程,输出中 org.apache.catalina.startup.Bootstrap 即 Tomcat

lsof -i:8080 正常输出:

COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    4224 root   44u  IPv6  40596      0t0  TCP *:webcache (LISTEN)

webcache 是 /etc/services 中 8080 端口的服务名,属正常显示;加 -P 参数(lsof -Pi:8080)可显示为数字端口。

3.4 nohup 与后台运行

startup.sh 内部已经通过 catalina.sh start 实现了后台运行(末尾带 & 并把输出重定向到 catalina.out),因此正常情况下不需要额外加 nohup,退出 SSH 也不会导致 Tomcat 退出。

但在以下场景下需要手工使用 nohup:

# 场景:想以前台模式运行但又要脱离终端(例如自定义了日志输出)
cd /usr/local/tomcat
nohup ./bin/catalina.sh run > /usr/local/tomcat/logs/catalina.out 2>&1 &

# 查看后台作业
jobs -l
# 记录 PID
echo $! > /usr/local/tomcat/logs/tomcat.pid
要点 说明
nohup 忽略 SIGHUP 信号,使进程在终端关闭后继续运行
& 放入后台执行
> file 2>&1 把标准输出和标准错误都重定向到文件,否则默认写入当前目录的 nohup.out
CATALINA_PID 在 setenv.sh 中设置该变量后,shutdown.sh 才支持 -force 强制停止,强烈建议设置

设置 PID 文件:

echo 'export CATALINA_PID=/usr/local/tomcat/logs/tomcat.pid' >> /usr/local/tomcat/bin/setenv.sh

3.5 开机自启

CentOS 6(/etc/rc.local)

echo "/usr/local/tomcat/bin/startup.sh" >> /etc/rc.local

CentOS 7(/etc/rc.local 需要额外授予执行权限)

echo "/usr/local/tomcat/bin/startup.sh" >> /etc/rc.local
chmod +x /etc/rc.d/rc.local

CentOS 7 中 /etc/rc.local 是指向 /etc/rc.d/rc.local 的软链接,且默认没有执行权限,不加 chmod +x 则不会生效。另外通过 rc.local 启动时不会加载登录 shell 的环境变量,因此 JAVA_HOME 必须写在 Tomcat 的 setenv.sh 或启停脚本内。

3.6 纳入 systemd 管理(CentOS 7 推荐)

生产环境更推荐用 systemd 统一管理,可获得 systemctl start/stop/restart/status、开机自启、自动拉起等能力。

创建 unit 文件:

vim /usr/lib/systemd/system/tomcat.service

完整内容:

[Unit]
Description=Apache Tomcat 9 Web Application Container
Documentation=http://tomcat.apache.org/
After=syslog.target network.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking

# 环境变量:systemd 不加载 /etc/profile,必须在此显式声明
Environment="JAVA_HOME=/usr/local/java"
Environment="JRE_HOME=/usr/local/java/jre"
Environment="CATALINA_HOME=/usr/local/tomcat"
Environment="CATALINA_BASE=/usr/local/tomcat"
Environment="CATALINA_PID=/usr/local/tomcat/logs/tomcat.pid"
Environment="CATALINA_TMPDIR=/usr/local/tomcat/temp"
Environment="JAVA_OPTS=-Xms1024m -Xmx1024m -Djava.security.egd=file:/dev/./urandom"

PIDFile=/usr/local/tomcat/logs/tomcat.pid

ExecStart=/usr/local/tomcat/bin/startup.sh
ExecStop=/usr/local/tomcat/bin/shutdown.sh
ExecReload=/bin/kill -s HUP $MAINPID

# 以非 root 用户运行
User=tomcat
Group=tomcat

# 异常退出自动重启
Restart=on-failure
RestartSec=10s

# 允许打开的文件数
LimitNOFILE=65535
LimitNPROC=65535

SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

关于 Type=forking 的说明:

Type 适用场景
simple(默认) ExecStart 启动的进程就是主进程,一直在前台运行。若用 simple,ExecStart 应写 catalina.sh run
forking ExecStart 启动的进程会 fork 出子进程后自身退出,真正的服务进程是那个子进程。startup.sh 正是这种行为(脚本执行完就退出,Java 进程在后台),因此必须用 Type=forking,并配合 PIDFile 让 systemd 知道要跟踪哪个 PID
notify 服务启动完成后主动通过 sd_notify 通知 systemd,Tomcat 不支持

若不设置 Type=forking,systemd 会认为 startup.sh 退出即服务已停止,从而杀掉 Tomcat 进程或状态显示异常。 PIDFile 必须与 CATALINA_PID 指向同一路径,否则 systemd 无法准确跟踪主进程。 SuccessExitStatus=143 是因为 Tomcat 被 SIGTERM 正常终止时退出码为 143(128+15),不声明会被 systemd 判定为失败。

管理命令:

systemctl daemon-reload
systemctl start tomcat
systemctl status tomcat
systemctl stop tomcat
systemctl restart tomcat
systemctl enable tomcat        # 开机自启
systemctl disable tomcat
journalctl -u tomcat -f        # 查看 systemd 记录的日志

CentOS 6 的 SysV init 脚本方式

CentOS 6 没有 systemd,可编写 /etc/init.d/tomcat 脚本:

vim /etc/init.d/tomcat
#!/bin/bash
# chkconfig: 2345 80 20
# description: Apache Tomcat Web Application Container

export JAVA_HOME=/usr/local/java
export CATALINA_HOME=/usr/local/tomcat
export CATALINA_PID=/usr/local/tomcat/logs/tomcat.pid

case "$1" in
    start)
        echo "Starting Tomcat..."
        $CATALINA_HOME/bin/startup.sh
        ;;
    stop)
        echo "Stopping Tomcat..."
        $CATALINA_HOME/bin/shutdown.sh
        ;;
    restart)
        $0 stop
        sleep 5
        $0 start
        ;;
    status)
        pgrep -f catalina > /dev/null && echo "Tomcat is running" || echo "Tomcat is stopped"
        ;;
    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac
exit 0
chmod +x /etc/init.d/tomcat
chkconfig --add tomcat
chkconfig tomcat on
service tomcat start
service tomcat status

3.7 验证访问

lsof -i:8080
curl -I http://127.0.0.1:8080/
tail -f /usr/local/tomcat/logs/catalina.out

浏览器访问 http://IP:8080,看到 Tomcat 默认的 "Apache Tomcat" 猫头首页即表示成功。

首次访问因 JVM 预热与 JSP 编译可能较慢,属正常现象;若长时间无响应,参见后文 SecureRandom 熵不足的排错。

站点家目录与日志位置:

  • 默认站点家目录:/usr/local/tomcat/webapps/ROOT/
  • 运行时日志:/usr/local/tomcat/logs/catalina.out
tail -f /usr/local/tomcat/logs/catalina.out

启动、关闭及部署应用时,均应通过该日志确认是否正常。

3.8 创建独立 tomcat 用户运行(安全加固)

默认以 root 运行 Tomcat 风险极高:一旦 Web 应用存在漏洞(如任意文件上传、反序列化 RCE),攻击者将直接获得 root 权限。生产环境务必以专用普通用户运行。

# 1. 创建不可登录的系统用户与同名组
groupadd -r tomcat
useradd -r -g tomcat -s /sbin/nologin -d /usr/local/tomcat -M tomcat
# 或简化写法
useradd -r -s /sbin/nologin tomcat

# 2. 修改 Tomcat 目录属主属组
chown -R tomcat:tomcat /usr/local/tomcat/
# 若使用了软链接,需加 -h 或对真实目录操作
chown -R tomcat:tomcat /usr/local/apache-tomcat-9.0.21/

# 3. 收紧权限:配置文件仅属主可读
chmod 750 /usr/local/tomcat/conf
chmod 640 /usr/local/tomcat/conf/*
chmod 750 /usr/local/tomcat/bin/*.sh

# 4. 以 tomcat 用户启动
su - tomcat -s /bin/bash -c "/usr/local/tomcat/bin/startup.sh"

# 5. 验证运行用户
ps -ef | grep tomcat | grep -v grep

普通用户无法绑定 1024 以下特权端口,需要监听 80/443 时有三种解决办法:

# 方案一(推荐):Tomcat 仍听 8080,由 Nginx/Apache 在 80 端口反向代理

# 方案二:给 java 可执行文件授予 CAP_NET_BIND_SERVICE 能力
setcap 'cap_net_bind_service=+ep' /usr/local/java/jre/bin/java
getcap /usr/local/java/jre/bin/java

# 方案三:iptables 端口转发,把 80 重定向到 8080
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
# CentOS 7 firewalld 写法
firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
firewall-cmd --reload

# 方案四:使用 jsvc(commons-daemon),以 root 绑定端口后自动降权
/usr/local/tomcat/bin/daemon.sh start

4. 配置文件详解

4.1 server.xml 完整结构

vim /usr/local/tomcat/conf/server.xml

一份精简后的完整骨架,标注了各部分作用:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Server:整个 Tomcat 实例;port 为关闭端口,shutdown 为关闭命令 -->
<Server port="8005" shutdown="SHUTDOWN">

  <!-- 生命周期监听器 -->
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- 全局 JNDI 资源:manager 应用的用户库就来自这里 -->
  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <!-- Service:把 Connector 绑定到 Engine -->
  <Service name="Catalina">

    <!-- HTTP 连接器 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    <!-- AJP 连接器(与前端 Apache/Nginx 通信,不用则注释掉) -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

    <!-- Engine:Servlet 引擎 -->
    <Engine name="Catalina" defaultHost="localhost">

      <!-- 认证域 -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <!-- Host:虚拟主机 -->
      <Host name="localhost" appBase="webapps"
            unpackWARs="true" autoDeploy="true">

        <!-- 访问日志阀 -->
        <Valve className="org.apache.catalina.valves.AccessLogValve"
               directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

4.2 <Server> 元素

<Server port="8005" shutdown="SHUTDOWN">
属性 说明
port 关闭端口,默认 8005,只监听 127.0.0.1
shutdown 关闭命令字符串,默认 SHUTDOWN

8005 关闭端口的安全风险

shutdown.sh 的工作原理就是向 localhost:8005 发送明文字符串 SHUTDOWN。这意味着任何能连上 8005 端口的人,只要知道命令字符串就能关停 Tomcat:

# 验证:手工发送关闭命令即可停掉 Tomcat
telnet 127.0.0.1 8005
SHUTDOWN

# 或
echo "SHUTDOWN" | nc 127.0.0.1 8005

加固措施:

<!-- 1. 修改端口为非默认的高位端口,并改掉命令字符串 -->
<Server port="8765" shutdown="Xk9_sTOP@2024_pLs">

<!-- 2. 更彻底:把 port 设为 -1,完全禁用关闭端口 -->
<Server port="-1" shutdown="SHUTDOWN">

设为 -1 后 shutdown.sh 将失效,只能通过 kill、systemd 或 catalina.sh stop -force(需配置 CATALINA_PID)停止服务。若使用 systemd 管理并配置了 PIDFile,推荐这种做法。 若保留 8005,务必确认它只绑定在 127.0.0.1,可用 lsof -i:8005 验证。

4.3 <Connector> 连接器详解

修改 HTTP 监听端口

vim /usr/local/tomcat/conf/server.xml

将默认的 8080 改为 80:

<Connector port="80" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />

重启 Tomcat 后即监听 80 端口。课堂测试完后建议改回 8080(生产环境由前端 Nginx 占用 80 端口)。

生产环境常用的完整 Connector 配置

<Connector port="8080"
           protocol="org.apache.coyote.http11.Http11NioProtocol"
           connectionTimeout="20000"
           keepAliveTimeout="15000"
           maxKeepAliveRequests="100"
           redirectPort="8443"
           maxThreads="800"
           minSpareThreads="100"
           acceptCount="1000"
           maxConnections="10000"
           maxHttpHeaderSize="8192"
           maxPostSize="10485760"
           enableLookups="false"
           disableUploadTimeout="true"
           URIEncoding="UTF-8"
           useBodyEncodingForURI="true"
           compression="on"
           compressionMinSize="2048"
           noCompressionUserAgents="gozilla, traviata"
           compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json"
           server="Server" />

属性逐项说明:

属性 默认值 说明
port 8080 监听端口
protocol HTTP/1.1 协议实现,见 4.3.3 节
address 全部地址 绑定的 IP,设为 127.0.0.1 可只允许本机(配合前端代理时更安全)
connectionTimeout 60000(server.xml 中设为 20000) 连接建立后等待收到完整请求行/请求头的超时时间(毫秒)。设置过大会占用线程,过小会误杀慢速客户端
keepAliveTimeout 同 connectionTimeout 长连接空闲后保持的时间
maxKeepAliveRequests 100 一个长连接最多处理的请求数,设为 1 即禁用 keep-alive,-1 为不限制
redirectPort 8443 当请求要求 SSL 传输(<transport-guarantee>CONFIDENTIAL</transport-guarantee>)时,自动重定向到的 HTTPS 端口
maxThreads 200 最重要的并发参数,处理请求的工作线程池最大线程数,即最大并发处理请求数。经验值 200~1000,不宜盲目调大(线程上下文切换开销、内存占用)
minSpareThreads 10 最小空闲(常驻)线程数,保证突发流量到来时无需临时创建线程
maxSpareThreads — 最大空闲线程数,超出会被回收(Tomcat 6 属性,7+ 已废弃)
acceptCount 100 当所有工作线程都繁忙时,OS 层面 TCP 连接请求队列(backlog)的长度。队列满后新连接会被直接拒绝(Connection refused)
maxConnections NIO 为 10000、APR 为 8192、BIO 等于 maxThreads 容器在同一时间接受并处理的最大连接数。达到上限后新连接进入 acceptCount 队列
maxHttpHeaderSize 8192 请求/响应 HTTP 头的最大字节数,Cookie 或 Header 过大时需调高
maxPostSize 2097152(2MB) POST 表单最大字节数,-1 为不限制。文件上传报错时需调整
enableLookups false 是否对客户端 IP 做 DNS 反解(request.getRemoteHost())。开启会显著拖慢性能,务必保持 false
disableUploadTimeout true 上传数据时是否禁用超时,大文件上传时应为 true
URIEncoding Tomcat 8 前为 ISO-8859-1,Tomcat 8+ 为 UTF-8 URL 中参数的解码字符集,GET 请求中文乱码的关键
useBodyEncodingForURI false 为 true 时,用请求体的 contentType 中指定的字符集(即 request.setCharacterEncoding() 的值)来解码 URL 查询字符串。为兼容老应用常与 URIEncoding 同时配置
compression off 是否启用 GZIP 压缩:off / on / force / 数值(大于该字节数才压缩)
compressionMinSize 2048 启用压缩的响应最小字节数,太小的响应压缩反而不划算
compressableMimeType text/html,text/xml,text/plain 参与压缩的 MIME 类型。注意 Tomcat 7 及之前拼写为 compressableMimeType,Tomcat 8+ 改为 compressibleMimeType(旧名仍兼容)
noCompressionUserAgents — 正则匹配的 UA 不做压缩(照顾不支持 gzip 的老浏览器)
server Apache-Coyote/1.1 覆盖响应头中的 Server 字段,用于隐藏版本信息
SSLEnabled / scheme / secure false / http / false HTTPS 相关;前端 Nginx 卸载 SSL 时后端需设 scheme="https" 才能让应用正确生成 https 链接
executor — 引用共享线程池 <Executor>,多个 Connector 可共用一个线程池

共享线程池配置示例(多个 Connector 共用,避免线程总量翻倍):

<Executor name="tomcatThreadPool"
          namePrefix="catalina-exec-"
          maxThreads="800"
          minSpareThreads="100"
          maxIdleTime="60000"
          prestartminSpareThreads="true"/>

<Connector executor="tomcatThreadPool"
           port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="UTF-8"/>

4.3.1 常用端口一览

端口 用途
8080 默认 HTTP 端口,可改为 80
8443 HTTPS / SSL 端口,也是 redirectPort 的默认值
8005 Server shutdown 端口,仅监听本机
8009 AJP 端口,用于与 Apache / Nginx 等反向代理通信(按需开启)
8000 JPDA 远程调试端口(catalina.sh jpda start)

4.3.2 HTTP 连接器与 AJP 连接器的区别

<!-- HTTP 连接器:直接对浏览器提供服务 -->
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000" redirectPort="8443" />

<!-- AJP 连接器:与前端 Web 服务器通信 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"
           address="127.0.0.1" secretRequired="false" />
对比项 HTTP Connector(8080) AJP Connector(8009)
协议 标准 HTTP/1.1,文本协议 AJP(Apache JServ Protocol),二进制协议
通信对象 浏览器或任何 HTTP 客户端 Apache 的 mod_jk / mod_proxy_ajp、Nginx 的第三方 ajp 模块
性能 文本解析开销略高 二进制、复用长连接,包头更紧凑,转发效率更高
适用场景 直接对外提供服务;Nginx 通过 proxy_pass HTTP 反向代理 Apache + Tomcat 经典组合,Apache 处理静态、AJP 转发动态
安全风险 常规 CVE-2020-1938(Ghostcat / 幽灵猫) 可通过 AJP 任意文件读取甚至 RCE,影响 Tomcat 6/7/8/9 全系列

AJP 加固建议

<!-- 1. 不使用 AJP 时直接注释掉整行(推荐) -->
<!--
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
-->
<!-- 2. 必须使用时:绑定内网地址 + 设置 secret(Tomcat 7.0.100/8.5.51/9.0.31 及以上支持) -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"
           address="127.0.0.1"
           secretRequired="true"
           secret="Your_Strong_AJP_Secret"
           maxThreads="500" />

同时用防火墙限制 8009 端口只允许前端 Web 服务器访问:

# CentOS 7
firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=192.168.224.10/32 port port=8009 protocol=tcp accept'
firewall-cmd --permanent --remove-port=8009/tcp
firewall-cmd --reload

# CentOS 6
iptables -A INPUT -p tcp --dport 8009 -s 192.168.224.10 -j ACCEPT
iptables -A INPUT -p tcp --dport 8009 -j DROP
service iptables save

4.3.3 BIO / NIO / NIO2 / APR 协议对比与选择

实现 protocol 写法 原理 特点与适用场景
BIO(阻塞 IO) HTTP/1.1(Tomcat 6/7 的默认) 一个连接独占一个线程,socket.accept() 阻塞 实现简单、稳定,但并发能力受 maxThreads 硬限制。Tomcat 8.5 起已被移除
NIO(非阻塞 IO) org.apache.coyote.http11.Http11NioProtocol 基于 Java NIO 的 Selector 多路复用,少量线程管理大量连接 Tomcat 8+ 默认,并发能力强,长连接场景优势明显,无需额外依赖。首选
NIO2(异步 IO) org.apache.coyote.http11.Http11Nio2Protocol 基于 Java 7 的 AIO(异步 Channel + 回调) 理论上更优,实际提升有限,成熟度不如 NIO,一般不选
APR / native org.apache.coyote.http11.Http11AprProtocol 通过 JNI 调用 Apache Portable Runtime 的本地 C 库(epoll + OpenSSL) 静态文件读写与 SSL 握手性能最好(SSL 由 OpenSSL 处理而非 JSSE),但需额外安装 apr、apr-util、tomcat-native,环境依赖重

protocol="HTTP/1.1" 是自动选择:Tomcat 8+ 若检测到 APR 本地库可用则用 APR,否则退回 NIO。因此显式写明协议类名可以避免环境差异导致的行为不一致。

安装 APR 支持(可选):

# 1. 安装依赖
yum install -y apr apr-devel apr-util apr-util-devel openssl-devel gcc make

# 2. 编译 tomcat-native(源码包在 Tomcat 的 bin 目录下)
cd /usr/local/tomcat/bin/
tar xf tomcat-native.tar.gz
cd tomcat-native-*/native/
./configure --with-apr=/usr/bin/apr-1-config --with-java-home=$JAVA_HOME
make && make install

# 3. 在 setenv.sh 中声明本地库路径
echo 'export LD_LIBRARY_PATH=/usr/local/apr/lib:$LD_LIBRARY_PATH' >> /usr/local/tomcat/bin/setenv.sh

# 4. 重启后在 catalina.out 中确认
grep -i apr /usr/local/tomcat/logs/catalina.out
# 出现 "APR/OpenSSL configured" 或 Http11AprProtocol 即生效

APR 需要 server.xml 中保留 AprLifecycleListener;若未安装本地库,日志中会出现 The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found,这只是提示信息,不影响运行。

4.4 <Engine> 元素

<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat1">
属性 说明
name Engine 名称,用于日志标识,默认 Catalina。它同时决定 conf/ 下应用配置的子目录名(conf/Catalina/localhost/)
defaultHost 默认虚拟主机。当请求的 Host 头与所有 <Host name>、<Alias> 都不匹配时,交由该主机处理。其值必须等于某个 <Host> 的 name,否则 Tomcat 启动失败
jvmRoute 集群环境下的节点标识,会被追加到 SessionID 后(如 ABC123.tomcat1),配合 Nginx/Apache 实现粘性会话。集群内各节点必须唯一
startStopThreads 并行启动 Host 的线程数,0 表示等于 CPU 核数,应用多时可加快启动

4.5 <Host> 虚拟主机

在 Engine 内通过多个 Host 节点配置基于域名的虚拟主机:

<Engine name="Catalina" defaultHost="localhost">

  <Host name="localhost" appBase="webapps"
        unpackWARs="true" autoDeploy="true">
  </Host>

  <Host name="www.example.com" appBase="/data/webapps/example"
        unpackWARs="true" autoDeploy="true">
    <Alias>example.com</Alias>
    <Context path="" docBase="ROOT" reloadable="false" />
  </Host>

</Engine>
属性 默认值 说明
name — 虚拟主机域名,必须与浏览器请求的 Host 头完全一致(不区分大小写)
appBase webapps 该主机的应用基础目录,其中的 WAR 包和目录会被自动部署。可用相对路径(相对 $CATALINA_BASE)或绝对路径
unpackWARs true 是否把 appBase 下的 WAR 包自动解压成目录。设为 false 则直接从 WAR 内运行(省磁盘但性能略低、JSP 无法热改)
autoDeploy true Tomcat 运行期间是否周期性扫描 appBase,自动部署新增/更新的应用。生产环境建议设为 false,避免误放文件导致意外部署和额外扫描开销
deployOnStartup true 启动时是否部署 appBase 下的应用
autoDeploy 与 deployOnStartup 均为 false — 则只部署 server.xml 或 conf/<Engine>/<Host>/ 中显式声明的 Context
Alias — 主机别名,可写多个,用于让同一主机响应多个域名
workDir work/\<Engine>/\<Host> 该主机的工作目录
errorReportValveClass — 自定义错误页阀,用于隐藏堆栈信息

4.6 <Context> 应用上下文

Context 用于将某个 URL 路径映射到具体应用:

<Context path="/shop" docBase="/data/apps/shop"
         reloadable="false" crossContext="true">
  <!-- 可在此嵌入 Manager、Realm、Resource、Valve、WatchedResource 等配置 -->
</Context>
属性 默认值 说明
path — 访问上下文路径。根应用为 ""(空字符串),子路径写 /shop。在 server.xml 外的位置配置时不允许写 path(由文件名决定)
docBase — 应用的物理路径,可以是目录或 WAR 包的绝对/相对路径。相对路径相对于所属 Host 的 appBase
reloadable false 监控 WEB-INF/classes 与 WEB-INF/lib 的变化并自动重载应用。开发方便,生产必须 false(后台线程持续扫描,消耗 CPU 且重载会中断服务)
crossContext false 是否允许通过 ServletContext.getContext() 访问同一 Host 下其他应用的上下文
sessionCookiePath 同 path Session Cookie 的作用路径
cookies true 是否用 Cookie 传递 SessionID;false 则用 URL 重写(;jsessionid=)
useHttpOnly true Session Cookie 加 HttpOnly 标记,防 XSS 窃取
privileged false 是否允许访问容器内部 Servlet(manager 应用需要为 true)
antiResourceLocking false Windows 下防止 JAR 文件被锁定
allowLinking false 是否允许 docBase 内使用符号链接

Context 的三种配置位置(重要)

方式 位置 说明与优缺点
方式一:写在 conf/server.xml 的 <Host> 内 conf/server.xml 讲义常用写法,直观集中。缺点:修改后必须重启 Tomcat 才生效,且 Tomcat 官方不推荐(server.xml 无法热加载,一处语法错误全站起不来)
方式二:独立文件 conf/[Engine]/[Host]/[应用名].xml(推荐) 如 conf/Catalina/localhost/shop.xml 文件名即上下文路径(shop.xml → /shop,ROOT.xml → /,多级路径用 # 分隔如 a#b.xml → /a/b)。文件内 <Context> 不要写 path 属性。修改后自动重新加载该应用,无需重启 Tomcat,且互不影响
方式三:应用内的 META-INF/context.xml webapps/shop/META-INF/context.xml 随应用一起打包分发,部署时 Tomcat 自动复制到 conf/Catalina/localhost/shop.xml。适合应用自带数据源等配置

方式二示例:

mkdir -p /usr/local/tomcat/conf/Catalina/localhost
vim /usr/local/tomcat/conf/Catalina/localhost/shop.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 注意:这里不能写 path,路径由文件名 shop.xml 决定,即 /shop -->
<Context docBase="/data/apps/shop" reloadable="false">
    <!-- 监控这些文件的变化,变化时重载应用 -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <!-- 应用私有数据源示例 -->
    <Resource name="jdbc/shopDB" auth="Container"
              type="javax.sql.DataSource"
              maxTotal="100" maxIdle="30" maxWaitMillis="10000"
              username="shop" password="123456"
              driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&amp;characterEncoding=UTF-8"/>
</Context>

优先级:server.xml 中的 Context > conf/[Engine]/[Host]/*.xml > 应用内 META-INF/context.xml > conf/context.xml(全局默认)。

4.7 <Valve> 阀门

Valve 是请求处理链上的拦截器,可配置在 Engine / Host / Context 三个层级。

访问日志阀 AccessLogValve

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
  <Valve className="org.apache.catalina.valves.AccessLogValve"
         directory="logs"
         prefix="localhost_access_log"
         suffix=".txt"
         pattern="%h %l %u %t &quot;%r&quot; %s %b"
         rotatable="true"
         fileDateFormat=".yyyy-MM-dd"
         resolveHosts="false" />
</Host>

远程地址过滤阀 RemoteAddrValve(按 IP 允许/拒绝访问)

<!-- 只允许 192.168.4.0/24 与本机访问,其他一律拒绝 -->
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
       allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1|192\.168\.4\.\d+" />

<!-- 或者黑名单模式:拒绝指定 IP -->
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
       deny="192\.168\.100\.\d+" />

远程主机名过滤阀 RemoteHostValve

<Valve className="org.apache.catalina.valves.RemoteHostValve"
       allow=".*\.example\.com" />

真实客户端 IP 阀 RemoteIpValve(前端有 Nginx 时必配)

<Valve className="org.apache.catalina.valves.RemoteIpValve"
       remoteIpHeader="X-Forwarded-For"
       protocolHeader="X-Forwarded-Proto"
       internalProxies="192\.168\.224\.\d+" />

配置后,request.getRemoteAddr() 与访问日志中的 %h 才会记录真实客户端 IP,而不是 Nginx 的地址。

其他常用 Valve

Valve 作用
ErrorReportValve 错误页展示,可通过 showReport="false" showServerInfo="false" 隐藏堆栈与版本
SingleSignOn 同一 Host 下多个应用间的单点登录(配置在 Host 级别)
StuckThreadDetectionValve 检测长时间未完成的请求线程,输出堆栈便于定位卡死
SemaphoreValve 限制并发请求数
CrawlerSessionManagerValve 让爬虫共用一个 Session,避免产生大量无用会话

隐藏错误页版本信息示例:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
  <Valve className="org.apache.catalina.valves.ErrorReportValve"
         showReport="false"
         showServerInfo="false" />
</Host>

5. web.xml 与其他配置文件

5.1 conf/web.xml 全局部署描述符

conf/web.xml 对所有应用生效;应用自己的 WEB-INF/web.xml 只对该应用生效,且优先级更高。

欢迎页 welcome-file-list

<welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>

访问目录(如 http://IP:8080/shop/)时,Tomcat 按列表顺序查找存在的文件返回;全都不存在时,若 listings=true 则列出目录,否则返回 404。

Servlet 定义与映射

<!-- 定义 Servlet -->
<servlet>
    <servlet-name>helloServlet</servlet-name>
    <servlet-class>com.example.web.HelloServlet</servlet-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <!-- 数值越小越先加载;不写则首次访问时才加载 -->
    <load-on-startup>1</load-on-startup>
</servlet>

<!-- 映射访问路径 -->
<servlet-mapping>
    <servlet-name>helloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>helloServlet</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

url-pattern 匹配规则优先级:精确匹配(/hello)> 路径匹配(/api/*,最长优先)> 扩展名匹配(*.do)> 默认匹配(/,即 DefaultServlet)。

Tomcat 内置的两个关键 Servlet 都在 conf/web.xml 中定义:

<!-- DefaultServlet:处理所有静态资源,listings 控制目录列表 -->
<servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param>
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param>
    <init-param>
        <param-name>listings</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<!-- JspServlet:负责把 .jsp / .jspx 翻译编译并执行 -->
<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
        <param-name>fork</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>xpoweredBy</param-name>
        <param-value>false</param-value>
    </init-param>
    <!-- 生产环境设为 false,不检查 JSP 是否被修改,可提升性能 -->
    <init-param>
        <param-name>development</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

Session 超时时间

<session-config>
    <!-- 单位:分钟。默认 30;设为 0 或负数表示永不过期 -->
    <session-timeout>30</session-timeout>
    <cookie-config>
        <http-only>true</http-only>
        <secure>false</secure>
    </cookie-config>
    <tracking-mode>COOKIE</tracking-mode>
</session-config>

自定义错误页(防止堆栈信息外泄)

<error-page>
    <error-code>404</error-code>
    <location>/404.html</location>
</error-page>
<error-page>
    <error-code>500</error-code>
    <location>/500.html</location>
</error-page>
<error-page>
    <exception-type>java.lang.Throwable</exception-type>
    <location>/error.html</location>
</error-page>

MIME 类型映射

<mime-mapping>
    <extension>json</extension>
    <mime-type>application/json</mime-type>
</mime-mapping>

5.2 conf/tomcat-users.xml 与 manager 应用

manager 应用可在浏览器中查看/启动/停止/重载/卸载应用,以及上传 WAR 部署,非常适合教学与测试环境。

第一步:配置用户与角色

vim /usr/local/tomcat/conf/tomcat-users.xml

在 <tomcat-users> 与 </tomcat-users> 之间加入:

<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">

  <!-- 定义角色 -->
  <role rolename="manager-gui"/>
  <role rolename="manager-script"/>
  <role rolename="manager-jmx"/>
  <role rolename="manager-status"/>
  <role rolename="admin-gui"/>
  <role rolename="admin-script"/>

  <!-- 定义用户,一个用户可拥有多个角色,逗号分隔 -->
  <user username="admin" password="Adm1n@Tomcat_2024"
        roles="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script"/>

  <!-- 只用于脚本化部署的账号 -->
  <user username="deployer" password="Dep1oy@2024" roles="manager-script"/>

</tomcat-users>

角色说明:

角色 权限说明
manager-gui 访问 manager 的 HTML 图形界面(/manager/html),可点击部署、启停、重载应用
manager-script 访问文本接口(/manager/text/*),供 curl、Ant、Maven、CI 脚本调用
manager-jmx 访问 JMX 代理接口(/manager/jmxproxy),可读取/修改 MBean
manager-status 只能查看服务器状态页(/manager/status),权限最小
admin-gui 访问 host-manager 的图形界面(/host-manager/html),可在线增删虚拟主机
admin-script 访问 host-manager 的文本接口

安全要点:manager-gui 与 manager-script 不要授予同一个用户(官方安全建议,可降低 CSRF 风险)。密码必须使用强口令。修改 tomcat-users.xml 后需重启 Tomcat 或重载 manager 应用。

第二步:开放远程访问(修改 manager 的 context.xml)

manager 与 host-manager 默认只允许 localhost 访问,远程访问会返回 403 Access Denied。原因是其 context.xml 中配置了 RemoteAddrValve:

vim /usr/local/tomcat/webapps/manager/META-INF/context.xml

默认内容:

<Context antiResourceLocking="false" privileged="true" >
  <Valve className="org.apache.catalina.valves.RemoteAddrValve"
         allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />
  <Manager sessionAppName="/manager" />
</Context>

做法 A(推荐):在正则中追加允许的运维网段

<Context antiResourceLocking="false" privileged="true" >
  <Valve className="org.apache.catalina.valves.RemoteAddrValve"
         allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1|192\.168\.4\.\d+|192\.168\.224\.\d+" />
  <Manager sessionAppName="/manager" />
</Context>

做法 B(仅限内网实验,不推荐用于生产):注释掉整个 Valve,放开所有来源

<Context antiResourceLocking="false" privileged="true" >
  <!--
  <Valve className="org.apache.catalina.valves.RemoteAddrValve"
         allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />
  -->
  <Manager sessionAppName="/manager" />
</Context>

host-manager 同理:

vim /usr/local/tomcat/webapps/host-manager/META-INF/context.xml

修改后无需重启整个 Tomcat,Tomcat 会自动重载该应用(也可手动重启确保生效):

/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh

第三步:访问验证

http://IP:8080/manager/html          # 应用管理器(图形)
http://IP:8080/manager/status        # 服务器状态:JVM 内存、线程、连接器统计
http://IP:8080/manager/text/list     # 文本接口:列出所有应用
http://IP:8080/host-manager/html     # 虚拟主机管理器

用 tomcat-users.xml 中的账号进行 HTTP Basic 认证登录。

命令行调用文本接口示例:

# 列出所有应用及状态
curl -u deployer:Dep1oy@2024 "http://127.0.0.1:8080/manager/text/list"

# 停止 / 启动 / 重载应用
curl -u deployer:Dep1oy@2024 "http://127.0.0.1:8080/manager/text/stop?path=/sp"
curl -u deployer:Dep1oy@2024 "http://127.0.0.1:8080/manager/text/start?path=/sp"
curl -u deployer:Dep1oy@2024 "http://127.0.0.1:8080/manager/text/reload?path=/sp"

# 上传 WAR 并部署为 /sp
curl -u deployer:Dep1oy@2024 -T sp.war "http://127.0.0.1:8080/manager/text/deploy?path=/sp&update=true"

# 卸载应用(会删除已部署的目录/WAR)
curl -u deployer:Dep1oy@2024 "http://127.0.0.1:8080/manager/text/undeploy?path=/sp"

# 查看 JVM 与连接器状态
curl -u admin:'Adm1n@Tomcat_2024' "http://127.0.0.1:8080/manager/status?XML=true"

密码加密存储(可选)

# 生成 SHA-256 摘要
/usr/local/tomcat/bin/digest.sh -a sha-256 mypassword
# 输出:mypassword:xxxxxxxx...

在 server.xml 的 Realm 中声明摘要算法,并把 tomcat-users.xml 中的密码替换为摘要值:

<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase">
  <CredentialHandler className="org.apache.catalina.realm.MessageDigestCredentialHandler"
                     algorithm="sha-256" />
</Realm>

5.3 conf/catalina.properties

Catalina 类加载与运行属性的集中配置文件,常用项:

# 公共类加载器扫描路径,可追加外部 jar 目录
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar","/data/extlib/*.jar"

# 服务器类加载器路径
server.loader=
shared.loader=

# 禁止 Web 应用访问的包(安全保护)
package.access=sun.,org.apache.catalina.,org.apache.coyote.,org.apache.jasper.,org.apache.tomcat.
package.definition=sun.,java.,org.apache.catalina.,org.apache.coyote.

# 不做 TLD 与注解扫描的 JAR(可显著加快启动)
tomcat.util.scan.StandardJarScanFilter.jarsToSkip=\
bootstrap.jar,commons-daemon.jar,tomcat-juli.jar,\
annotations-api.jar,ant-junit*.jar,ant-launcher.jar,ant.jar,\
jsp-api.jar,servlet-api.jar,tomcat-api.jar,tomcat-jdbc.jar

# 字符串缓存(提升性能)
tomcat.util.buf.StringCache.byte.enabled=true

# 解决启动缓慢:指定熵源(也可在 JAVA_OPTS 中设置)
# -Djava.security.egd=file:/dev/./urandom

5.4 conf/context.xml

全局上下文配置,对所有应用生效。常见用途:

<?xml version="1.0" encoding="UTF-8"?>
<Context>

    <!-- 监控 web.xml 变化,变化时重载应用 -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- 关闭 Session 持久化:默认关闭时会把 Session 序列化到 work/.../SESSIONS.ser
         应用类未实现 Serializable 时会报错,可显式禁用 -->
    <!--
    <Manager pathname="" />
    -->

    <!-- 全局数据源示例 -->
    <!--
    <Resource name="jdbc/globalDB" auth="Container" type="javax.sql.DataSource"
              maxTotal="200" maxIdle="30" maxWaitMillis="10000"
              username="app" password="app123"
              driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://127.0.0.1:3306/appdb?useUnicode=true&amp;characterEncoding=UTF-8"/>
    -->

    <!-- 静态资源缓存(Tomcat 8+) -->
    <Resources cachingAllowed="true" cacheMaxSize="102400" cacheTtl="60000" />

</Context>

5.5 conf/logging.properties

控制 Tomcat 自身(JULI)的日志输出:

handlers = 1catalina.org.apache.juli.AsyncFileHandler, \
           2localhost.org.apache.juli.AsyncFileHandler, \
           java.util.logging.ConsoleHandler

# catalina 日志:级别、保留天数、目录、前缀
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
# 保留 30 天(Tomcat 8.5+ 支持,0 为不删除)
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 30

# 关闭控制台输出,可避免日志在 catalina.out 中重复一份
java.util.logging.ConsoleHandler.level = INFO

# 调整根日志级别:SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST > ALL
.level = INFO

6. Java Web 应用部署

6.1 方式一:直接放到 webapps 自动部署

6.1.1 目录部署与 JSP 测试

在 webapps 下创建应用目录(与 ROOT 同级):

mkdir /usr/local/tomcat/webapps/abc
vim /usr/local/tomcat/webapps/abc/time.jsp

time.jsp 内容:

<html>
<body>
<center>
<H1><%=new java.util.Date()%></H1>
</center>
</body>
</html>

访问 http://IP:8080/abc/time.jsp 即可看到服务器时间。

也可直接将 time.jsp 放在 webapps/ROOT/ 下,通过 http://IP:8080/time.jsp(或改端口为 80 后 http://IP/time.jsp)访问。

目录名即上下文路径:webapps/abc/ → 访问 /abc;webapps/ROOT/ → 访问 /。

6.1.2 WAR 包部署(自动解包)

cp sp.war /usr/local/tomcat/webapps/

Tomcat 检测到新 WAR 后(autoDeploy="true" 时无需重启),会自动解压成同名目录 webapps/sp/ 并部署,访问路径为:

http://IP:8080/sp

观察部署过程:

ls /usr/local/tomcat/webapps/
tail -f /usr/local/tomcat/logs/catalina.out
# 出现 Deployment of web application archive [...sp.war] has finished in [xxx] ms 即成功

注意:WAR 已解压后,若只删除 webapps/sp/ 目录而不删 sp.war,Tomcat 会再次自动解压出来;反之只删 WAR 而留下目录,Tomcat 会认为是 "WAR 被删除" 从而把解压目录也一并删掉。正确的卸载方式是同时删除 WAR 与解压目录,或使用 manager 的 undeploy。

6.1.3 部署 shop 商城项目(sp.war + sp.sql)

shop 目录下包含 sp.war(应用包)与 sp.sql(数据库脚本)。完整步骤:

# 1. 清空默认家目录(避免默认首页干扰)
rm -rf /usr/local/tomcat/webapps/ROOT/*

# 2. 复制 WAR 包到 webapps,Tomcat 会自动解压为 sp 应用
cp sp.war /usr/local/tomcat/webapps/

# 3. 安装数据库
yum install -y mariadb mariadb-server
systemctl restart mariadb        # CentOS 7
systemctl enable mariadb
# CentOS 6:
# service mysqld start; chkconfig mysqld on

# 4. 导入数据库(需提前创建库并授权)
mysql < sp.sql
# 或在已进入 mysql 客户端时使用:
# source /path/to/sp.sql;

访问:

http://IP:8080/sp

6.1.4 部署 JspXCMS(Java 开源 CMS 完整实验)

开源 Java CMS,讲义中演示了 5.2.4 与 6.5.4 两个版本,步骤一致。下面以 6.5.4 为准,并附 5.2.4 的命令。

下载:

wget http://www.jspxcms.com/uploads/jspxcms-6.5.4-release.zip
# 或课堂镜像源
wget http://192.168.4.254/stageThree/tomcat/soft/jspxcms-6.5.4-release.zip

第一步:部署应用

方式 A(6.5.4,先解压到临时目录再复制 ROOT):

rm -rf /usr/local/tomcat/webapps/ROOT
mkdir /test
unzip jspxcms-6.5.4-release.zip -d /test/
cp -r /test/ROOT /usr/local/tomcat/webapps/

方式 B(5.2.4,直接解压到 webapps):

rm /usr/local/tomcat/webapps/ROOT/* -rf
unzip jspxcms-5.2.4-release.zip -d /usr/local/tomcat/webapps/

第二步:安装并启动 MariaDB

yum install mariadb mariadb-server -y
systemctl restart mariadb.service
systemctl status mariadb.service
systemctl enable mariadb.service

CentOS 6 对应命令:

yum install mysql mysql-server -y
service mysqld restart
service mysqld status
chkconfig mysqld on

第三步:建库与授权

进入 MySQL 客户端执行:

mysql
CREATE DATABASE jspxcms;
GRANT ALL ON jspxcms.* TO 'mn'@'localhost' IDENTIFIED BY '123';
FLUSH PRIVILEGES;

也可用一行命令方式(讲义另一种写法,用户名 li):

mysql -e "create database jspxcms;"
mysql -e "grant all on jspxcms.* to 'li'@'localhost' identified by '123';"
mysql -e "flush privileges;"

第四步:浏览器引导安装

访问 http://IP:8080/,按页面提示填写:

项 值
数据库名 jspxcms
数据库用户名 mn(或 li)
数据库密码 123
是否创建数据库 否
管理员密码 123

第五步:重启 Tomcat 并访问

/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh
  • 前台:http://IP/
  • 后台:http://IP/cmscp/index.do(使用 admin 用户登录)

6.2 方式二:配置 Context 指向任意目录

应用不一定要放在 webapps 下,可以放在数据盘的任意路径(如 /data/apps/),通过 <Context> 的 docBase 指向。这在生产环境很常见:应用与 Tomcat 分离,升级 Tomcat 不影响应用数据。

做法一:写在 server.xml 的 Host 内(需重启)

mkdir -p /data/apps/shop
cp -r /path/to/shop/* /data/apps/shop/
vim /usr/local/tomcat/conf/server.xml
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">

  <Context path="/shop" docBase="/data/apps/shop" reloadable="false" />

  <Valve className="org.apache.catalina.valves.AccessLogValve"
         directory="logs" prefix="localhost_access_log" suffix=".txt"
         pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>
/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh

访问 http://IP:8080/shop。

做法二:独立 XML 文件(推荐,无需重启)

mkdir -p /usr/local/tomcat/conf/Catalina/localhost
cat > /usr/local/tomcat/conf/Catalina/localhost/shop.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<Context docBase="/data/apps/shop" reloadable="false">
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>
EOF

文件名 shop.xml 决定访问路径为 /shop。Tomcat 会在几秒内自动加载。

多级路径示例:文件名 api#v1.xml 对应访问路径 /api/v1。

做法三:直接指向 WAR 包

<Context path="/shop" docBase="/data/apps/sp.war" reloadable="false" />

6.3 方式三:通过 manager 界面上传部署

前置条件:已按 5.2 节配置 tomcat-users.xml(需 manager-gui 角色)并放开远程访问。

图形界面操作步骤

  1. 浏览器访问 http://IP:8080/manager/html,输入用户名密码登录。
  2. 页面上半部分 Applications 列表展示所有已部署应用,每行提供 Start / Stop / Reload / Undeploy 按钮以及 Sessions 数量与过期设置。
  3. 下半部分有两个部署区域:
    • Deploy directory or WAR file located on server:部署服务器本地已有的目录/WAR。
      • Context Path (required):填 /shop
      • XML Configuration file URL:可选,填 Context 配置 XML 的路径
      • WAR or Directory URL:填 /data/apps/shop 或 /data/apps/sp.war
      • 点击 Deploy
    • WAR file to deploy:从本机上传 WAR 文件。
      • 点击 选择文件(Browse)选中本地的 sp.war
      • 点击 Deploy,上传完成后自动以 WAR 文件名作为上下文路径部署(sp.war → /sp)
  4. 部署完成后页面顶部提示 OK,列表中出现新应用,点击应用名即可访问。

上传部署有默认大小限制(manager 应用的 web.xml 中 max-file-size / max-request-size 默认约 50MB),超大 WAR 需修改 webapps/manager/WEB-INF/web.xml 中的 <multipart-config>:

<multipart-config>
    <max-file-size>209715200</max-file-size>
    <max-request-size>209715200</max-request-size>
    <file-size-threshold>0</file-size-threshold>
</multipart-config>

服务器状态页

访问 http://IP:8080/manager/status,可以看到:

  • JVM:free / total / max memory,以及各内存池(Eden、Survivor、Old Gen、Metaspace)的使用情况
  • 各 Connector 的线程数(Max threads、Current thread count、Current thread busy)
  • 请求统计(Max processing time、Request count、Error count、Bytes received/sent)

这是不装任何监控工具就能快速判断 Tomcat 是否内存/线程吃紧的最便捷手段。

6.4 ROOT 应用与去掉访问路径中的项目名

需求:让用户访问 http://IP:8080/ 直接进入应用,而不是 http://IP:8080/sp。共有四种做法。

做法一:把应用内容放进 ROOT 目录

rm -rf /usr/local/tomcat/webapps/ROOT/*
cd /usr/local/tomcat/webapps/ROOT/
jar xf /path/to/sp.war      # 或 unzip /path/to/sp.war
# 也可直接:unzip sp.war -d /usr/local/tomcat/webapps/ROOT/

做法二:把 WAR 改名为 ROOT.war

rm -rf /usr/local/tomcat/webapps/ROOT
cp sp.war /usr/local/tomcat/webapps/ROOT.war

Tomcat 会解压出 webapps/ROOT/,访问路径即 /。

做法三:配置 path="" 的 Context(应用可放在任意目录)

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
  <!-- path 为空字符串即根路径;docBase 指向真实应用目录 -->
  <Context path="" docBase="/data/apps/shop" reloadable="false" />
</Host>

若同时 appBase="webapps" 且 webapps/ROOT 存在,会产生冲突(日志出现重复部署告警),应先删除或改名 webapps/ROOT。

做法四:用 conf/Catalina/localhost/ROOT.xml(推荐)

# 先移走原来的 ROOT,避免冲突
mv /usr/local/tomcat/webapps/ROOT /usr/local/tomcat/webapps/ROOT.bak

cat > /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<Context docBase="/data/apps/shop" reloadable="false">
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>
EOF

文件名 ROOT.xml 就代表上下文路径 /。

做法五:修改 Host 的 appBase 指向应用父目录

<Host name="www.example.com" appBase="/data/webapps/example"
      unpackWARs="true" autoDeploy="true">
  <Context path="" docBase="ROOT" reloadable="false" />
</Host>

此时 /data/webapps/example/ROOT/ 即为该站点根目录。

6.5 配置多虚拟主机

需求:在一台 Tomcat 上通过不同域名提供三个独立站点。

域名 站点目录 说明
www.aaa.com / aaa.com /data/webapps/aaa 站点 A
www.bbb.com / bbb.com /data/webapps/bbb 站点 B
localhost webapps 默认主机,保留 manager 等应用

第一步:创建站点目录与测试页

mkdir -p /data/webapps/aaa/ROOT
mkdir -p /data/webapps/bbb/ROOT

cat > /data/webapps/aaa/ROOT/index.jsp <<'EOF'
<html><body>
<h1>This is Site AAA</h1>
<p>ServerName: <%=request.getServerName()%></p>
<p>ServerPort: <%=request.getServerPort()%></p>
<p>Time: <%=new java.util.Date()%></p>
</body></html>
EOF

cat > /data/webapps/bbb/ROOT/index.jsp <<'EOF'
<html><body>
<h1>This is Site BBB</h1>
<p>ServerName: <%=request.getServerName()%></p>
<p>Time: <%=new java.util.Date()%></p>
</body></html>
EOF

# 若以 tomcat 用户运行,需授权
chown -R tomcat:tomcat /data/webapps/

第二步:编辑 server.xml,在 Engine 内增加 Host

vim /usr/local/tomcat/conf/server.xml
<Engine name="Catalina" defaultHost="localhost">

  <Realm className="org.apache.catalina.realm.LockOutRealm">
    <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
           resourceName="UserDatabase"/>
  </Realm>

  <!-- 默认主机:保留 manager / host-manager -->
  <Host name="localhost" appBase="webapps"
        unpackWARs="true" autoDeploy="true">
    <Valve className="org.apache.catalina.valves.AccessLogValve"
           directory="logs" prefix="localhost_access_log" suffix=".txt"
           pattern="%h %l %u %t &quot;%r&quot; %s %b" />
  </Host>

  <!-- 虚拟主机 A -->
  <Host name="www.aaa.com" appBase="/data/webapps/aaa"
        unpackWARs="true" autoDeploy="true">
    <Alias>aaa.com</Alias>
    <Context path="" docBase="ROOT" reloadable="false" />
    <Valve className="org.apache.catalina.valves.AccessLogValve"
           directory="logs" prefix="aaa_access_log" suffix=".txt"
           pattern="%h %l %u %t &quot;%r&quot; %s %b %D" />
  </Host>

  <!-- 虚拟主机 B -->
  <Host name="www.bbb.com" appBase="/data/webapps/bbb"
        unpackWARs="true" autoDeploy="true">
    <Alias>bbb.com</Alias>
    <Context path="" docBase="ROOT" reloadable="false" />
    <Valve className="org.apache.catalina.valves.AccessLogValve"
           directory="logs" prefix="bbb_access_log" suffix=".txt"
           pattern="%h %l %u %t &quot;%r&quot; %s %b %D" />
  </Host>

  <!-- 示例:使用通用域名 -->
  <Host name="www.example.com" appBase="/data/webapps/example"
        unpackWARs="true" autoDeploy="true">
    <Alias>example.com</Alias>
    <Context path="" docBase="ROOT" reloadable="false" />
  </Host>

</Engine>

第三步:检查语法并重启

/usr/local/tomcat/bin/configtest.sh
/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh
tail -f /usr/local/tomcat/logs/catalina.out

正常日志中会出现每个 Host 的部署记录,且 logs/ 下自动生成 aaa_access_log.*.txt、bbb_access_log.*.txt。

第四步:客户端域名解析(DNS 或 hosts)

虚拟主机依据请求的 Host 头匹配,因此必须用域名访问,直接用 IP 访问只会命中 defaultHost。

在客户端(发起访问的机器)配置解析:

Linux / macOS 客户端:

vim /etc/hosts
192.168.4.10   www.aaa.com aaa.com
192.168.4.10   www.bbb.com bbb.com
192.168.4.10   www.example.com example.com

Windows 客户端:编辑 C:\Windows\System32\drivers\etc\hosts,内容同上(需管理员权限)。

若有内网 DNS,则在正向区域中添加 A 记录:

www.aaa.com.    IN  A   192.168.4.10
aaa.com.        IN  A   192.168.4.10
www.bbb.com.    IN  A   192.168.4.10

第五步:验证

# 浏览器分别访问
# http://www.aaa.com:8080/   -> This is Site AAA
# http://www.bbb.com:8080/   -> This is Site BBB
# http://192.168.4.10:8080/  -> 命中 defaultHost=localhost,显示 Tomcat 默认页

# 命令行验证(无需改 hosts,用 -H 手工指定 Host 头)
curl -H "Host: www.aaa.com" http://192.168.4.10:8080/
curl -H "Host: www.bbb.com" http://192.168.4.10:8080/
curl -H "Host: aaa.com"     http://192.168.4.10:8080/    # 验证 Alias
curl http://192.168.4.10:8080/                            # 验证 defaultHost

# 查看各站点独立访问日志
tail -f /usr/local/tomcat/logs/aaa_access_log.*.txt

常见错误:<Engine defaultHost="xxx"> 中的 xxx 必须与某个 <Host name> 完全一致,否则 Tomcat 启动直接失败,catalina.out 中报 No Host matches server name 或引擎初始化异常。

通过 host-manager 在线添加虚拟主机(无需重启)

访问 http://IP:8080/host-manager/html(需 admin-gui 角色),在 Add Virtual Host 表单中填写 Host name、Aliases、App base 等,点击 Add。注意此方式添加的 Host 不会写入 server.xml,Tomcat 重启后丢失,仅适合临时使用。


7. JVM 参数调优与性能诊断

7.1 性能不足时的三种思路

当一台 Tomcat 性能不足时,通常有三种思路:

  1. 微调:内存、并发数、IO、网络、内核参数等;
  2. 升级硬件:成本高,提升仍然有限;
  3. 架构扩展:由单点转为集群,避免单点故障。

7.2 JAVA_OPTS 与 CATALINA_OPTS 的区别

变量 生效范围
JAVA_OPTS 对 catalina.sh 的所有操作生效,包括 start 和 stop。因为 shutdown 也会启动一个小 JVM,若把大堆参数放这里,关闭时也会申请大内存
CATALINA_OPTS 只对启动(start / run)生效,不影响 stop。堆内存等参数推荐放这里

配置位置有两处(二者选其一,推荐后者):

位置一:bin/catalina.sh(讲义做法,升级 Tomcat 会被覆盖)

vim /usr/local/tomcat/bin/catalina.sh

在文件开头注释块之后(约 # OS specific support 之前)加入:

JAVA_OPTS="-server -Xms2048m -Xmx2048m \
-Xmn768m -XX:PermSize=256m -XX:MaxPermSize=512m \
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC \
-XX:+PrintGCDetails -Xloggc:$CATALINA_HOME/logs/gc.log \
-Djava.awt.headless=true -Dfile.encoding=UTF-8"

位置二:bin/setenv.sh(推荐,升级不覆盖)

vim /usr/local/tomcat/bin/setenv.sh

JDK 7 及以前(使用 PermGen 永久代):

#!/bin/bash
export JAVA_HOME=/usr/local/java
export CATALINA_PID=/usr/local/tomcat/logs/tomcat.pid

CATALINA_OPTS="-server \
-Xms2048m -Xmx2048m -Xmn768m \
-Xss256k \
-XX:PermSize=256m -XX:MaxPermSize=512m \
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC \
-XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection \
-XX:CMSFullGCsBeforeCompaction=0 \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps \
-Xloggc:/usr/local/tomcat/logs/gc.log \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/usr/local/tomcat/logs/heapdump.hprof \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Djava.security.egd=file:/dev/./urandom"
export CATALINA_OPTS

JDK 8+(永久代已被 Metaspace 元空间取代,推荐 G1 回收器):

#!/bin/bash
export JAVA_HOME=/usr/local/java
export CATALINA_PID=/usr/local/tomcat/logs/tomcat.pid

CATALINA_OPTS="-server \
-Xms4096m -Xmx4096m \
-Xmn1536m \
-Xss512k \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:+ParallelRefProcEnabled \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps \
-Xloggc:/usr/local/tomcat/logs/gc-%t.log \
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=20M \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/usr/local/tomcat/logs/heapdump.hprof \
-XX:+DisableExplicitGC \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Dsun.jnu.encoding=UTF-8 \
-Djava.security.egd=file:/dev/./urandom"
export CATALINA_OPTS

使用 G1 时不要同时设置 -Xmn(G1 会自适应调整新生代大小,显式指定会削弱其自动调优能力)。上面同时列出仅为展示参数含义。

7.3 常用 JVM 参数详解

参数 说明
-server 以 Server 模式运行 JVM,启用 C2 编译器与更激进的 JIT 优化,启动稍慢但长期运行性能更好。64 位 JVM 默认即 server 模式
-Xms 初始堆内存大小
-Xmx 最大堆内存大小。生产建议与 -Xms 设置为相同值,避免运行中动态扩容/收缩带来的 Full GC 与性能抖动
-Xmn 新生代(Young Generation) 大小。经验值为整个堆的 1/3 ~ 1/2
-XX:NewRatio=2 老年代 : 新生代 = 2 : 1(与 -Xmn 二者选一)
-XX:SurvivorRatio=8 Eden : 单个 Survivor = 8 : 1
-Xss 每个线程的栈大小,默认 1M。调小(如 256k)可支持更多线程;递归过深会 StackOverflowError
-XX:PermSize / -XX:MaxPermSize 永久代初始/最大大小,存放类元数据、常量池。仅 JDK 7 及以前有效,JDK 8 已移除(写了会告警 ignoring option PermSize)
-XX:MetaspaceSize / -XX:MaxMetaspaceSize 元空间(JDK 8+ 取代永久代)。元空间使用本地内存而非堆内存,MetaspaceSize 是触发首次 Full GC 的阈值而非上限。不设 MaxMetaspaceSize 时可无限增长直至耗尽物理内存
-XX:+UseSerialGC 串行回收器,单线程,适合小内存、单核
-XX:+UseParallelGC / -XX:+UseParallelOldGC 并行(吞吐量优先)回收器,JDK 8 默认
-XX:+UseConcMarkSweepGC CMS 并发标记清除回收器(老年代),停顿短,适合响应时间敏感的 Web 应用。JDK 9 起废弃,JDK 14 移除
-XX:+UseParNewGC 新生代并行回收,通常与 CMS 搭配
-XX:CMSInitiatingOccupancyFraction=70 老年代占用达 70% 时触发 CMS 回收
-XX:+UseCMSCompactAtFullCollection Full GC 后进行内存压缩,减少碎片
-XX:+UseG1GC G1 回收器,JDK 9+ 默认。把堆分成多个 Region,可预测停顿时间,适合大堆(4G 以上)。JDK 8 中大内存场景推荐
-XX:MaxGCPauseMillis=200 G1 的目标最大停顿时间(软目标)
-XX:InitiatingHeapOccupancyPercent=45 G1 触发并发标记周期的堆占用阈值
-XX:+PrintGCDetails 打印 GC 详情,便于定位问题
-XX:+PrintGCDateStamps GC 日志中打印绝对日期时间(而非 JVM 启动后的秒数)
-Xloggc:<file> GC 日志输出文件路径
-XX:+UseGCLogFileRotation 开启 GC 日志滚动,配合 NumberOfGCLogFiles、GCLogFileSize
-XX:+HeapDumpOnOutOfMemoryError 发生 OOM 时自动导出堆快照,是事后分析内存泄漏的关键,强烈建议开启
-XX:HeapDumpPath 堆快照输出路径(注意留足磁盘空间,快照大小约等于堆大小)
-XX:+DisableExplicitGC 忽略代码中的 System.gc() 调用,防止应用触发不必要的 Full GC
-XX:+UseCompressedOops 开启指针压缩(堆小于 32G 时默认开启,可节省内存)
-Djava.awt.headless=true 无图形环境模式,服务器上必设,否则某些图形/报表操作会抛异常
-Dfile.encoding=UTF-8 JVM 默认字符集,中文乱码排查的关键参数
-Dsun.jnu.encoding=UTF-8 文件名编码,含中文文件名时需要
-Djava.security.egd=file:/dev/./urandom 指定随机数熵源为非阻塞的 urandom,解决启动缓慢
-Dcom.sun.management.jmxremote... 开启 JMX 远程监控(供 JConsole / VisualVM 连接)

7.4 堆内存与线程数的经验值

堆内存(-Xmx)

物理内存 建议 -Xms / -Xmx 说明
2 GB 1024m 留足内存给 OS 与元空间
4 GB 2048m 约物理内存的 50%
8 GB 4096m
16 GB 8192m ~ 10240m 超过 8G 建议改用 G1
32 GB 以上 不建议单实例超过 31g 超过 32G 会失去指针压缩优势,反而更占内存;建议一机多实例(每个 8~16G)而非单个超大堆

核心原则:

  • -Xms = -Xmx,避免动态扩容抖动。
  • JVM 总内存占用 ≈ 堆(Xmx)+ 元空间 + 线程栈(Xss × 线程数)+ 直接内存 + JVM 自身开销,不要把 -Xmx 设成等于物理内存,否则会触发 OS swap 甚至 OOM Killer 杀进程。
  • 一般 -Xmx 取物理内存的 50%~70%。

线程数(maxThreads)

应用类型 建议 maxThreads 说明
纯 CPU 密集型计算 CPU 核数 × 2 线程多了只是徒增上下文切换
常规 Web 应用(有 DB / 远程调用等 IO 等待) 200 ~ 500 Tomcat 默认 200 已能应对多数场景
IO 等待明显、长连接多 500 ~ 1000(NIO) 需同时调大 maxConnections
— 不建议超过 1000 每线程占用 -Xss 内存,1000 线程 × 512k ≈ 500M

配套参数关系:minSpareThreads(常驻)≈ maxThreads 的 10%~25%;acceptCount 一般 500~1000;maxConnections(NIO)可设 10000。

同时需调整 OS 层面的限制:

# 文件描述符上限
vim /etc/security/limits.conf
tomcat  soft  nofile  65535
tomcat  hard  nofile  65535
tomcat  soft  nproc   65535
tomcat  hard  nproc   65535
*       soft  nofile  65535
*       hard  nofile  65535
# 内核网络参数
vim /etc/sysctl.conf
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 32768
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.ip_local_port_range = 1024 65000
sysctl -p
ulimit -n

注意:acceptCount 实际受 net.core.somaxconn 限制,OS 值小于 Tomcat 配置时以 OS 为准。 systemd 管理时需在 unit 文件中设置 LimitNOFILE=65535(limits.conf 对 systemd 启动的服务不生效)。

具体数值需结合物理内存、应用规模和压测结果调整,切忌照抄。

7.5 用 JDK 自带工具排查

jps —— 列出 Java 进程

jps                # 简单列表
jps -l             # 显示主类全名
jps -v             # 显示传给 JVM 的参数(可用来验证 CATALINA_OPTS 是否生效)
jps -lvm

输出示例:

4224 org.apache.catalina.startup.Bootstrap
4501 sun.tools.jps.Jps

jstat —— 查看 GC 与内存统计(最常用)

# 每 1000 毫秒采样一次,共 10 次,查看 GC 统计
jstat -gc 4224 1000 10

# 以百分比显示各区使用率(最直观)
jstat -gcutil 4224 1000

# 查看内存容量
jstat -gccapacity 4224

# 新生代 / 老年代 / 元空间 GC 详情
jstat -gcnew 4224
jstat -gcold 4224
jstat -gcmetacapacity 4224

# 类加载统计
jstat -class 4224
# JIT 编译统计
jstat -compiler 4224

jstat -gcutil 输出列的含义:

列 含义
S0 / S1 两个 Survivor 区使用率(%)
E Eden 区使用率(%)
O Old(老年代)使用率(%)。持续 90%+ 且 Full GC 后不下降 → 内存泄漏
M Metaspace 元空间使用率(%)
CCS 压缩类空间使用率(%)
YGC / YGCT Young GC 次数 / 累计耗时(秒)
FGC / FGCT Full GC 次数 / 累计耗时。FGC 频繁(如每分钟数次)是严重信号
GCT GC 总耗时

jmap —— 查看/导出堆内存

# 查看堆配置与各区使用情况
jmap -heap 4224

# 查看对象直方图(按占用内存排序,快速定位大对象)
jmap -histo 4224 | head -30
# 只统计存活对象(会触发一次 Full GC,生产慎用)
jmap -histo:live 4224 | head -30

# 导出堆快照(dump),用 MAT / VisualVM / jhat 分析
jmap -dump:format=b,file=/tmp/heap.hprof 4224
jmap -dump:live,format=b,file=/tmp/heap-live.hprof 4224

# 分析 dump 文件
jhat /tmp/heap.hprof        # 启动后访问 http://IP:7000

jmap -dump 会暂停应用(Stop The World),堆越大暂停越久(10G 堆可能数十秒),生产环境请在低峰期操作,或依赖 -XX:+HeapDumpOnOutOfMemoryError 自动生成。 必须用与 Tomcat 相同的用户执行(su - tomcat -s /bin/bash -c "jmap ..."),否则报 Unable to open socket file。

jstack —— 导出线程栈(排查卡死、CPU 高、死锁)

# 打印所有线程栈
jstack 4224
# 保存到文件(建议连续采集 3 次,间隔 5 秒,便于对比)
jstack 4224 > /tmp/thread1.txt; sleep 5
jstack 4224 > /tmp/thread2.txt; sleep 5
jstack 4224 > /tmp/thread3.txt

# 强制导出(进程无响应时)
jstack -F 4224

# 统计线程状态分布
jstack 4224 | grep "java.lang.Thread.State" | sort | uniq -c

定位 CPU 占用最高的线程(经典四步法)

# 1. 找出占用 CPU 最高的 Java 进程 PID
top -c
# 假设 PID = 4224

# 2. 找出该进程内占用 CPU 最高的线程 TID
top -Hp 4224
# 或
ps -mp 4224 -o THREAD,tid,time | sort -rn | head -10
# 假设 TID = 4256

# 3. 把线程 ID 转成 16 进制(jstack 中的 nid 是 16 进制)
printf "%x\n" 4256
# 输出 10a0

# 4. 在 jstack 输出中搜索该 nid,向下看 30 行即为热点代码
jstack 4224 | grep -A 30 "nid=0x10a0"

其他工具

jinfo 4224                              # 查看 JVM 参数与系统属性
jinfo -flag MaxHeapSize 4224            # 查看单个参数
jinfo -flag +PrintGCDetails 4224        # 运行时动态开启部分参数
jcmd 4224 help                          # JDK 8+ 综合工具
jcmd 4224 VM.flags                      # 查看所有生效的 JVM 参数
jcmd 4224 GC.heap_info                  # 堆信息
jcmd 4224 Thread.print                  # 等价于 jstack
jcmd 4224 GC.heap_dump /tmp/h.hprof     # 等价于 jmap -dump
jconsole                                # 图形化监控(需 X 转发或远程 JMX)
jvisualvm                               # 图形化监控与 Profiling

开启 JMX 远程监控(在 setenv.sh 中):

CATALINA_OPTS="$CATALINA_OPTS \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9999 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-Djava.rmi.server.hostname=192.168.4.10"

生产环境务必开启认证(authenticate=true + jmxremote.password 文件)并用防火墙限制来源,未认证的 JMX 端口可被直接利用执行代码。


8. Tomcat 集群与会话共享

8.1 架构演进

  • 早期方案:Apache + Tomcat + mod_jk,由 mod_jk 通过 AJP 协议负责 Apache 与 Tomcat 之间的通信与负载均衡。
  • 当前方案:Nginx 替代 Apache,无需 mod_jk,通过 HTTP 反向代理将动态请求转发给后端 Tomcat,Nginx 处理静态资源。

基础架构:

            client
              |
         DNS 解析
              |
       +------+------+
       |             |
     nginx1       nginx2      (处理静态)
              ip_hash
       +------+------+
       |             |
    tomcat1      tomcat2      (处理动态 JSP,RR 轮询)

完整分层架构(可选引入 Squid 缓存、Memcached 会话、MariaDB 主从):

client
  |
DNS
  |
squid1 / squid2          (静态图片缓存)
  |
nginx1 / nginx2          (处理静态)
  |
tomcat1 / tomcat2        (处理动态 1.jsp,RR)
  |
memcached1 / memcached2  (会话集中存储)
  |
mariadb1 / mariadb2      (数据库)

简化视图:

        Client
          |
        Nginx
       /     \
  Tomcat1   Tomcat2
       \     /
      Memcached

前端负载均衡器本身也需要高可用与更强的调度能力,相关内容详见:

详见 12-01-LVS负载均衡.md

详见 12-03-Keepalived高可用.md

8.2 Session 与 Cookie

HTTP 是无状态协议,仅靠 HTTP 本身无法确定两次访问是否来自同一用户,因此需要 Cookie 或 Session 跟踪用户。

  • Cookie:服务器发给客户端、保存在客户端本地的小文件,包含用户访问信息(如账号标识等),可手动删除或设置有效期,下次访问时由浏览器自动回传给服务器。Cookie 可被浏览器禁用。
  • Session:服务器端保存用户状态的机制,通过 Session ID 关联用户,通常借助 Cookie(名为 JSESSIONID)传递 Session ID;Cookie 被禁用时可退化为 URL 重写(;jsessionid=xxx)。
  • 类比:去商场办会员卡,卡可以自己保存(Cookie),也可以由商场保管、自己只记卡号(Session ID)。

集群下的 Session 问题:Session 默认保存在单台 Tomcat 的 JVM 内存中。用户第一次请求落到 tomcat1 并登录,第二次被轮询到 tomcat2,tomcat2 上没有这个 Session,用户就被要求重新登录——这就是必须解决会话共享的原因。

8.3 集群 Session 管理三种方式

方式 简介 使用场景 优点 缺点
Session Replication(Session 复制) 将一台机器的 Session 通过组播(multicast)复制到集群其他机器 机器较少(一般不超过 4~6 台)、网络流量较小 实现简单、配置少,某台机器宕机不影响用户访问 广播复制有延迟,有网络开销,节点越多开销呈指数增长
Session Sticky(粘性会话) 用户首次访问某台后,后续所有请求强制落到该机器(Nginx 的 ip_hash、sticky 模块,或 Apache 的 jvmRoute) 机器数适中、稳定性要求不苛刻 实现简单、无额外网络开销 该机器宕机会导致用户 Session 丢失,存在单点风险;负载可能不均
缓存集中式管理(如 MSM + Memcached、Spring Session + Redis) Session 存入分布式缓存,访问任意节点时都从缓存读取 机器数多、网络环境复杂、大规模集群 可靠性好、扩容方便、Tomcat 无状态化 实现复杂,稳定性依赖缓存,写入策略需合理设计

Tomcat 内置的 Session 复制配置(方式一,在 server.xml 的 <Engine> 或 <Host> 内加入 <Cluster>):

<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
         channelSendOptions="8">

  <Manager className="org.apache.catalina.ha.session.DeltaManager"
           expireSessionsOnShutdown="false"
           notifyListenersOnReplication="true"/>

  <Channel className="org.apache.catalina.tribes.group.GroupChannel">
    <Membership className="org.apache.catalina.tribes.membership.McastService"
                address="228.0.0.4"
                port="45564"
                frequency="500"
                dropTime="3000"/>
    <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
              address="auto" port="4000" autoBind="100"
              selectorTimeout="5000" maxThreads="6"/>
    <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
      <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
    </Sender>
    <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
    <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
  </Channel>

  <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
  <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
  <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>

同时应用的 WEB-INF/web.xml 中必须声明可分发:

<web-app ...>
    <distributable/>
</web-app>

且 Session 中存放的所有对象都必须实现 java.io.Serializable。

Nginx 粘性会话配置(方式二):

upstream tomcat {
    ip_hash;                              # 按客户端 IP 哈希,同一 IP 固定后端
    server 192.168.224.11:8080 weight=1;
    server 192.168.224.12:8080 weight=1;
}

配合 <Engine jvmRoute="tomcat1"> 后,SessionID 形如 A1B2C3....tomcat1,可用 nginx-sticky-module 或 upstream 的 sticky 指令按 route 精确调度。

8.4 实战:Nginx + Tomcat + MSM 集中式 Session

实验拓扑

                client 192.168.2.x
                   |
                   |  192.168.2.51
                 nginx
                   |  192.168.224.10
          |--------+--------|
       tomcat1           tomcat2
   192.168.224.11    192.168.224.12
          |--------+--------|
                   |
          192.168.224.13  memcached
  • 192.168.2.0/24 模拟外网(KVM 桥接)
  • 192.168.224.0/24 模拟内网(KVM virbr1)
  • Nginx 处理静态,并将动态请求负载均衡到 Tomcat
  • MSM(memcached-session-manager)让两台 Tomcat 共享 Session

实验前准备

  1. 主机名绑定(所有节点的 /etc/hosts):
192.168.224.10  nginx.cluster.com
192.168.224.11  tomcat1.cluster.com
192.168.224.12  tomcat2.cluster.com
192.168.224.13  memcached.cluster.com
  1. 时间同步(Session 过期判断依赖时间):
# CentOS 7
yum install -y chrony
systemctl start chronyd && systemctl enable chronyd
chronyc sources -v
# CentOS 6
yum install -y ntp
ntpdate cn.pool.ntp.org
service ntpd start && chkconfig ntpd on
  1. 关闭 iptables / firewalld、SELinux;
  2. 配置好 yum 源。

第一步:安装并配置 Nginx(192.168.224.10 / 192.168.2.51)

yum install epel-release -y
yum install nginx -y

修改 /etc/nginx/nginx.conf,完整内容:

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    include /etc/nginx/conf.d/*.conf;

    upstream tomcat {
        server 192.168.224.11:8080 weight=1;
        server 192.168.224.12:8080 weight=1;
    }

    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  192.168.2.51;
        root         /usr/share/nginx/html;

        include /etc/nginx/default.d/*.conf;

        location ~ .*\.jsp$ {
            proxy_pass   http://tomcat;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $remote_addr;
        }

        error_page 404 /404.html;
        location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }
    }
}

启动并设置开机自启:

systemctl start nginx
systemctl status nginx
systemctl enable nginx
lsof -i:80
nginx -t          # 语法检查

CentOS 6 对应命令:

service nginx start
chkconfig nginx on

第二步:在 Tomcat1 / Tomcat2 安装 MSM JAR 包

在 192.168.224.11、192.168.224.12 上分别安装干净的 Tomcat(若之前部署过 JspXCMS 等应用,建议重装以免影响测试)。

将 MSM 相关 JAR 包复制到 /usr/local/tomcat/lib/(以下 JAR 针对 Tomcat 8,Tomcat 6/7/9 需下载对应的 -tc6 / -tc7 / -tc9 版本):

asm-3.2.jar
kryo-1.04.jar
kryo-serializers-0.11.jar
memcached-session-manager-1.8.1.jar
memcached-session-manager-tc8-1.8.1.jar
minlog-1.2.jar
msm-kryo-serializer-1.8.1.jar
reflectasm-1.01.jar
serializers-0.11.jar
spymemcached-2.11.1.jar
cp *.jar /usr/local/tomcat/lib/
ls /usr/local/tomcat/lib/ | grep -Ei "memcached|kryo|asm|minlog|serial"

JAR 包的作用是对 Class 文件进行打包管理,内部包含编译后的 .class 文件。 关键点:memcached-session-manager-tcX-*.jar 的 tcX 必须与 Tomcat 大版本一致,否则启动时报 ClassNotFoundException 或 NoSuchMethodError。

第三步:配置 MSM

在两台 Tomcat 上编辑 /usr/local/tomcat/conf/context.xml,在 <Context> 与 </Context> 之间加入:

<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
  memcachedNodes="n1:192.168.224.13:11211"
  lockingMode="auto"
  sticky="false"
  requestUriIgnorePattern=".*\.(png|gif|jpg|css|js)$"
  sessionBackupAsync="false"
  sessionBackupTimeout="100"
  copyCollectionsForSerialization="true"
  transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory" />

参数说明:

参数 说明
memcachedNodes Memcached 节点地址,格式 节点名:IP:端口,多个节点用逗号分隔
lockingMode 非粘性模式下的加锁策略:none / all / auto / uriPattern:<pattern>
sticky false 表示非粘性会话(任意节点都能读取),true 表示粘性会话(Memcached 只作备份)
requestUriIgnorePattern 匹配该正则的请求(静态资源)不进行 Session 备份,减少无用开销
sessionBackupAsync 是否异步备份。非粘性模式必须为 false(同步),否则可能读到旧数据
sessionBackupTimeout 备份超时时间(毫秒)
copyCollectionsForSerialization 序列化集合时是否复制,避免并发修改异常
transcoderFactoryClass 序列化实现。Kryo 性能最好;也可用 JavaSerializationTranscoderFactory(无需额外 JAR 但较慢)

多节点写法:

memcachedNodes="n1:192.168.224.13:11211,n2:192.168.224.14:11211"

重启两台 Tomcat:

/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh
lsof -i:8080
grep -i memcached /usr/local/tomcat/logs/catalina.out
# 出现 "MemcachedBackupSessionManager finished initialization" 即配置成功

第四步:安装并启动 Memcached(192.168.224.13)

yum install memcached -y
systemctl start memcached.service
systemctl status memcached.service
systemctl enable memcached.service
lsof -i:11211

CentOS 6:

yum install memcached -y
service memcached start
chkconfig memcached on

调整 Memcached 参数(内存、连接数、监听地址):

vim /etc/sysconfig/memcached
PORT="11211"
USER="memcached"
MAXCONN="2048"
CACHESIZE="1024"
OPTIONS="-l 0.0.0.0"
systemctl restart memcached

-l 0.0.0.0 让 Memcached 监听所有网卡(默认只监听本机)。Memcached 无任何认证机制,必须用防火墙严格限制来源 IP,仅允许 Tomcat 节点访问,否则数据可被任意读写。

第五步:保证应用内容一致

Nginx、Tomcat1、Tomcat2 共同对外提供同一应用,其站点目录内容需要保持一致,常用方案:

  1. 远程实时 rsync 同步(配合 inotify 实现实时);
  2. DRBD、共享存储或分布式存储(也可用 NFS 模拟共享存储);
  3. CI/CD 统一发布(Jenkins + Ansible)。

在 Nginx 家目录 /usr/share/nginx/html 以及两台 Tomcat 家目录 /usr/local/tomcat/webapps/ROOT/ 分别放置测试文件(若使用 NFS 共享存储,只需要建立一次)。

session.jsp(用于观察 SessionID 是否保持):

web1
SessionID:<%=session.getId()%> <BR>
SessionIP:<%=request.getServerName()%> <BR>
SessionPort:<%=request.getServerPort()%>

第六步:测试

确认 Nginx、Tomcat1、Tomcat2、Memcached 均正常启动。

测试一:静态资源由 Nginx 处理

# 在 Nginx 节点
echo "nginx" > /usr/share/nginx/html/1.html
# 在 tomcat1 节点
echo "tomcat1" > /usr/local/tomcat/webapps/ROOT/1.html
# 在 tomcat2 节点
echo "tomcat2" > /usr/local/tomcat/webapps/ROOT/1.html

访问 http://192.168.2.51/1.html,浏览器显示 nginx,因为 Nginx 配置中只有 .jsp 结尾的请求才会转发到后端。

测试二:JSP 请求轮询到 Tomcat

# 在 tomcat1 节点
echo "tomcat1" > /usr/local/tomcat/webapps/ROOT/1.jsp
# 在 tomcat2 节点
echo "tomcat2" > /usr/local/tomcat/webapps/ROOT/1.jsp

访问 http://192.168.2.51/1.jsp,tomcat1 与 tomcat2 交替显示,说明 JSP 请求经 Nginx 轮询到后端。

# 命令行验证轮询
for i in $(seq 1 6); do curl -s http://192.168.2.51/1.jsp; done

测试三:Session 保持一致

访问 http://192.168.2.51/session.jsp,不断按 F5 刷新,页面上的 SessionIP 在两台 Tomcat 间切换,但 SessionID 保持不变,说明会话由 Memcached 集中保存,共享成功。

不要使用 curl http://192.168.2.51/session.jsp 测试,因为 curl 默认不会像浏览器那样保存并回传 Session ID(Cookie),每次都会生成新的 SessionID,但数据仍会写入 Memcached。若要用 curl 测试,需带上 Cookie 罐:

curl -c /tmp/ck.txt -b /tmp/ck.txt http://192.168.2.51/session.jsp
curl -c /tmp/ck.txt -b /tmp/ck.txt http://192.168.2.51/session.jsp

在 Memcached 服务器上查看缓存内容:

echo "stats cachedump 3 0" | nc 192.168.224.13 11211 > /tmp/session.txt
head -1 /tmp/session.txt
ITEM validity:A8F853F36B9931DD5ADF51DFB806A0CF-n1 [20 b; 1442225217 s]

导出的 Session ID 与页面刷新时看到的 Session ID 一致,即说明会话已存入 Memcached。字段含义:

字段 说明
validity: Session 有效性键前缀
B671A8EAB6A358CD3FB73DA58C76685B-n1 Session ID 及所在节点(-n1 即 memcachedNodes 中定义的节点名)
20 b Session 数据大小 / 过期时间标识
1545374604 s 访问时间戳

其他常用 Memcached 查看命令:

echo "stats" | nc 192.168.224.13 11211              # 总体统计
echo "stats items" | nc 192.168.224.13 11211        # 各 slab 的条目数
echo "stats slabs" | nc 192.168.224.13 11211        # slab 分配情况
echo "get <sessionid>" | nc 192.168.224.13 11211    # 查看指定 key
memcached-tool 192.168.224.13:11211 display         # 图表化展示

测试四:清除浏览器缓存 / Cookie 后,Session ID 会发生变化(说明 SessionID 是通过 Cookie 中的 JSESSIONID 传递的)。

清空 Memcached 缓存:

# 方法一:交互式
nc 192.168.224.13 11211
flush_all
quit
# 方法二:命令行
echo "flush_all" | nc localhost 11211
echo "flush_all" | nc 192.168.224.13 11211

关于 Redis 替代方案:现代项目更常用 Spring Session + Redis,或直接使用 JWT 等无状态令牌方案,从架构上消除 Session 共享需求;MSM 亦有 Redis 后端(memcached-session-manager 的 redis 支持)。


9. 反向代理 Tomcat 实现动静分离

Tomcat 处理静态资源(图片、CSS、JS)的效率远低于 Nginx / Apache,因此生产环境标准做法是:前端 Nginx 处理静态资源与 SSL,后端 Tomcat 只处理动态请求。

详见 10-02-Nginx服务.md

9.1 Nginx 反向代理完整配置

vim /etc/nginx/conf.d/tomcat.conf
# 后端 Tomcat 集群
upstream tomcat_backend {
    # 调度算法:默认 rr 轮询;ip_hash 为源地址哈希(粘性会话);least_conn 为最少连接
    # ip_hash;
    least_conn;

    server 192.168.224.11:8080 weight=1 max_fails=3 fail_timeout=30s;
    server 192.168.224.12:8080 weight=1 max_fails=3 fail_timeout=30s;
    server 192.168.224.13:8080 backup;              # 备用节点,主节点全挂时才启用

    # 与后端保持长连接,显著降低 TIME_WAIT 与握手开销(需 Nginx 1.1.4+)
    keepalive 64;
}

server {
    listen       80;
    server_name  www.example.com example.com;

    # 静态资源根目录(与 Tomcat 的应用目录内容需保持一致,可用 rsync/NFS)
    root         /data/webapps/example/ROOT;
    index        index.html index.htm index.jsp;

    access_log  /var/log/nginx/example_access.log  main;
    error_log   /var/log/nginx/example_error.log;

    # 上传大小限制
    client_max_body_size 100m;
    client_body_buffer_size 256k;

    # ---------- 静态资源:由 Nginx 直接返回 ----------
    location ~* \.(html|htm|gif|jpg|jpeg|png|bmp|ico|swf|css|js|woff|woff2|ttf|svg|eot|mp4|pdf)$ {
        root      /data/webapps/example/ROOT;
        expires   30d;                    # 浏览器缓存 30 天
        access_log off;                   # 静态资源不记日志,减少 IO
        add_header Cache-Control "public, max-age=2592000";
    }

    # 静态目录整体交给 Nginx
    location ^~ /static/ {
        root      /data/webapps/example/ROOT;
        expires   30d;
    }

    # ---------- 动态请求:转发给 Tomcat ----------
    # 方式 A:按扩展名分离(讲义方式,适合传统 JSP 项目)
    location ~ \.(jsp|jspx|do|action)$ {
        proxy_pass http://tomcat_backend;
        include    /etc/nginx/proxy_params.conf;
    }

    # 方式 B:按路径分离(适合前后端分离项目,推荐)
    location /api/ {
        proxy_pass http://tomcat_backend;
        include    /etc/nginx/proxy_params.conf;
    }

    # 兜底:其余请求全部交给 Tomcat(放在最后)
    location / {
        # 先尝试本地静态文件,找不到再转发给 Tomcat
        try_files $uri @tomcat;
    }

    location @tomcat {
        proxy_pass http://tomcat_backend;
        include    /etc/nginx/proxy_params.conf;
    }

    # 禁止访问敏感目录
    location ~ ^/(WEB-INF|META-INF)/ {
        deny all;
        return 404;
    }

    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;
}

抽取公共代理参数(/etc/nginx/proxy_params.conf):

proxy_http_version 1.1;
proxy_set_header Connection "";                    # 配合 upstream keepalive 使用

# 传递真实客户端信息(Tomcat 侧需配 RemoteIpValve 才能识别)
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_set_header X-Forwarded-Port  $server_port;

# 超时设置
proxy_connect_timeout 30s;
proxy_send_timeout    60s;
proxy_read_timeout    60s;

# 缓冲设置
proxy_buffering       on;
proxy_buffer_size     8k;
proxy_buffers         8 16k;
proxy_busy_buffers_size 32k;

# 后端故障时自动切换到下一台
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;

# 不重写响应中的重定向 Location(视业务需要)
proxy_redirect off;

启用与验证:

nginx -t                       # 检查语法
systemctl reload nginx         # 平滑重载
curl -I http://www.example.com/index.html    # 应由 Nginx 返回,响应头含 Server: nginx
curl -I http://www.example.com/index.jsp     # 应转发到 Tomcat
tail -f /var/log/nginx/example_access.log

Nginx 侧的 HTTPS 卸载(SSL 终止)

server {
    listen 443 ssl http2;
    server_name www.example.com;

    ssl_certificate     /etc/nginx/ssl/example.crt;
    ssl_certificate_key /etc/nginx/ssl/example.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 10m;

    location / {
        proxy_pass http://tomcat_backend;
        include    /etc/nginx/proxy_params.conf;
    }
}

# HTTP 强制跳转 HTTPS
server {
    listen 80;
    server_name www.example.com;
    return 301 https://$host$request_uri;
}

此时后端 Tomcat 的 Connector 需声明 scheme / secure,否则应用生成的绝对链接会退化成 http:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           scheme="https"
           secure="true"
           proxyName="www.example.com"
           proxyPort="443"
           URIEncoding="UTF-8" />

或使用 RemoteIpValve 自动依据 X-Forwarded-Proto 识别(更灵活):

<Valve className="org.apache.catalina.valves.RemoteIpValve"
       remoteIpHeader="X-Forwarded-For"
       protocolHeader="X-Forwarded-Proto"
       protocolHeaderHttpsValue="https"
       internalProxies="192\.168\.224\.\d+" />

9.2 Apache 反向代理 Tomcat

方式一:mod_proxy_http(推荐,无需 AJP)

yum install -y httpd
vim /etc/httpd/conf.d/tomcat.conf
# 确保模块已加载
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
LoadModule slotmem_shm_module modules/mod_slotmem_shm.so

<VirtualHost *:80>
    ServerName www.example.com
    DocumentRoot /data/webapps/example/ROOT

    ProxyRequests Off
    ProxyPreserveHost On

    # 静态资源不转发,由 Apache 处理
    ProxyPass /static/ !
    ProxyPass /images/ !

    # 负载均衡集群
    <Proxy balancer://tomcatcluster>
        BalancerMember http://192.168.224.11:8080 loadfactor=1 route=tomcat1
        BalancerMember http://192.168.224.12:8080 loadfactor=1 route=tomcat2
        # 粘性会话:依据 JSESSIONID 中的 route 后缀
        ProxySet stickysession=JSESSIONID|jsessionid
        ProxySet lbmethod=byrequests
    </Proxy>

    # 只转发动态请求
    ProxyPassMatch ^/(.*\.(jsp|do|action))$ balancer://tomcatcluster/$1
    ProxyPass        /api/  balancer://tomcatcluster/api/
    ProxyPassReverse /api/  balancer://tomcatcluster/api/

    # 负载均衡管理页面
    <Location /balancer-manager>
        SetHandler balancer-manager
        Require ip 192.168.4
    </Location>

    <Directory "/data/webapps/example/ROOT">
        Options -Indexes
        AllowOverride None
        Require all granted
    </Directory>

    ErrorLog  logs/example_error.log
    CustomLog logs/example_access.log combined
</VirtualHost>

方式二:mod_proxy_ajp(走 8009 AJP 端口,性能略优)

LoadModule proxy_ajp_module modules/mod_proxy_ajp.so

<VirtualHost *:80>
    ServerName www.example.com

    ProxyPass        /static/ !
    ProxyPassMatch ^/(.*\.(jsp|do))$ ajp://192.168.224.11:8009/$1
    ProxyPass        /api/  ajp://192.168.224.11:8009/api/
    ProxyPassReverse /api/  ajp://192.168.224.11:8009/api/
</VirtualHost>

使用 AJP 时后端必须启用 8009 Connector 并做好加固(见 4.3.2 节)。

方式三:mod_jk(早期经典方案,已逐步淘汰)

LoadModule jk_module modules/mod_jk.so
JkWorkersFile /etc/httpd/conf/workers.properties
JkLogFile     /var/log/httpd/mod_jk.log
JkLogLevel    info
JkMount /*.jsp  loadbalancer
JkMount /*.do   loadbalancer

/etc/httpd/conf/workers.properties:

worker.list=loadbalancer,jkstatus

worker.tomcat1.type=ajp13
worker.tomcat1.host=192.168.224.11
worker.tomcat1.port=8009
worker.tomcat1.lbfactor=1

worker.tomcat2.type=ajp13
worker.tomcat2.host=192.168.224.12
worker.tomcat2.port=8009
worker.tomcat2.lbfactor=1

worker.loadbalancer.type=lb
worker.loadbalancer.balance_workers=tomcat1,tomcat2
worker.loadbalancer.sticky_session=1

worker.jkstatus.type=status

mod_jk 的 worker.tomcatN 名称必须与各 Tomcat 的 <Engine jvmRoute="tomcatN"> 一致,粘性会话才能正常工作。

systemctl restart httpd
systemctl enable httpd

9.3 Nginx 与 Apache 代理方案对比

对比项 Nginx + HTTP 代理 Apache + mod_proxy_ajp Apache + mod_jk
配置复杂度 低 中 高(需额外 workers.properties)
静态资源性能 最优(epoll + sendfile) 良好 良好
并发能力 极高(异步事件驱动) 中(prefork/worker 进程模型) 中
后端协议 HTTP AJP(二进制) AJP(二进制)
安全风险 无需开 8009 需开 8009,注意 Ghostcat 需开 8009
粘性会话 ip_hash / sticky 模块 原生支持 stickysession 原生支持 sticky_session
现状 主流推荐 仍在使用 已基本淘汰

10. 日志详解

10.1 日志文件一览

ls -lh /usr/local/tomcat/logs/
文件 产生者 内容
catalina.out shell 重定向(catalina.sh 中 >> "$CATALINA_OUT" 2>&1) 标准输出 + 标准错误:Tomcat 启动过程、应用 System.out.println、未捕获异常堆栈。排错第一现场
catalina.2026-08-22.log JULI FileHandler Catalina 容器自身日志,按天切割
localhost.2026-08-22.log JULI 应用级别日志,应用启动失败的详细原因常在此
localhost_access_log.2026-08-22.txt AccessLogValve HTTP 访问日志
manager.2026-08-22.log JULI manager 应用操作记录
host-manager.2026-08-22.log JULI host-manager 操作记录
gc.log JVM(-Xloggc) GC 日志

10.2 catalina.out 详解

启动、关闭及部署应用时,均应通过该日志确认是否正常:

tail -f /usr/local/tomcat/logs/catalina.out

# 只看错误
grep -Ei "error|exception|severe|caused by" /usr/local/tomcat/logs/catalina.out | tail -50

# 查看启动耗时(确认真正启动成功)
grep "Server startup in" /usr/local/tomcat/logs/catalina.out

# 查看部署结果
grep -Ei "Deploy|Deployment" /usr/local/tomcat/logs/catalina.out | tail -20

关键日志行示例:

信息 [main] org.apache.catalina.startup.Catalina.start Server startup in 5321 ms
信息 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployWAR
      Deployment of web application archive [/usr/local/tomcat/webapps/sp.war] has finished in [3,412] ms

catalina.out 的致命问题:默认不切割,会无限增长,最终撑满磁盘导致服务不可用。必须做切割。

10.3 catalina.out 切割方案

方案一:logrotate(推荐,系统原生)

vim /etc/logrotate.d/tomcat
/usr/local/tomcat/logs/catalina.out {
    daily                 # 每天切割
    rotate 30             # 保留 30 份
    copytruncate          # 关键:复制后清空原文件,无需重启 Tomcat
    compress              # 压缩旧日志
    delaycompress         # 延迟一轮再压缩(便于查看最近一份)
    missingok             # 文件不存在时不报错
    notifempty            # 空文件不切割
    dateext               # 用日期作后缀而非序号
    dateformat -%Y-%m-%d
    size 100M             # 也可按大小触发(与 daily 同时存在则任一满足即切)
    su tomcat tomcat      # 以 tomcat 用户操作
}

/usr/local/tomcat/logs/*.log /usr/local/tomcat/logs/*.txt {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    dateext
    su tomcat tomcat
}
# 手工测试(-d 为演练不实际执行,-f 为强制执行)
logrotate -d /etc/logrotate.d/tomcat
logrotate -vf /etc/logrotate.d/tomcat
ls -lh /usr/local/tomcat/logs/

copytruncate 是关键。因为 Tomcat 进程一直持有 catalina.out 的文件句柄,如果用默认的 create 方式(重命名+新建),Tomcat 仍会往已改名的旧文件写入,新文件永远是空的。copytruncate 先复制内容再把原文件截断为 0,句柄不变,日志继续写入。缺点是复制瞬间可能丢失极少量日志。

方案二:cronolog(切割精度高,需修改 catalina.sh)

yum install -y gcc make
wget http://cronolog.org/download/cronolog-1.6.2.tar.gz
tar xf cronolog-1.6.2.tar.gz
cd cronolog-1.6.2
./configure && make && make install
which cronolog        # /usr/local/sbin/cronolog

修改 /usr/local/tomcat/bin/catalina.sh,把两处输出重定向改为管道给 cronolog:

# 原始(约 400 行附近):
#   org.apache.catalina.startup.Bootstrap "$@" start >> "$CATALINA_OUT" 2>&1 "&"
# 改为:
org.apache.catalina.startup.Bootstrap "$@" start 2>&1 \
  | /usr/local/sbin/cronolog "$CATALINA_BASE"/logs/catalina.%Y-%m-%d.out >> /dev/null "&"

方案三:交给 JULI,彻底不用 catalina.out

修改 conf/logging.properties,关闭 ConsoleHandler(不再向标准输出打印),日志只写入 catalina.<date>.log(JULI 自带按天切割与 maxDays 保留策略):

handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler
# 移除 java.util.logging.ConsoleHandler

.handlers = 1catalina.org.apache.juli.AsyncFileHandler
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 30
2localhost.org.apache.juli.AsyncFileHandler.maxDays = 30

此时 catalina.out 仅剩极少量启动前的输出。注意:应用中直接 System.out.println 的内容仍会进入 catalina.out。

方案四:临时救急,直接清空(不要用 rm)

# 正确:清空内容,保留 inode 与文件句柄
> /usr/local/tomcat/logs/catalina.out
# 或
cat /dev/null > /usr/local/tomcat/logs/catalina.out
truncate -s 0 /usr/local/tomcat/logs/catalina.out

# 错误示范:rm 后 Tomcat 句柄仍指向已删除的 inode,
# 磁盘空间不会释放,且日志再也看不到,必须重启 Tomcat 才能恢复
# rm -f /usr/local/tomcat/logs/catalina.out   # 不要这样做

若已误删,可用 lsof | grep deleted 确认,并从 /proc/<pid>/fd/ 中恢复:

lsof -p $(pgrep -f catalina) | grep deleted
cp /proc/$(pgrep -f catalina)/fd/1 /usr/local/tomcat/logs/catalina.out

10.4 访问日志 AccessLogValve

Tomcat 的访问日志在 conf/server.xml 中通过 AccessLogValve 配置(默认位于 Host 节点下):

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
  <Valve className="org.apache.catalina.valves.AccessLogValve"
         directory="logs"
         prefix="localhost_access_log"
         suffix=".txt"
         pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>

生产环境增强版(记录耗时、来源、UA、真实 IP、Session):

<Valve className="org.apache.catalina.valves.AccessLogValve"
       directory="logs"
       prefix="localhost_access_log"
       suffix=".txt"
       pattern="%{X-Forwarded-For}i %h %l %u %t &quot;%r&quot; %s %b %D &quot;%{Referer}i&quot; &quot;%{User-Agent}i&quot; %S"
       rotatable="true"
       fileDateFormat=".yyyy-MM-dd"
       resolveHosts="false"
       renameOnRotate="true"
       buffered="true"
       maxDays="30" />
属性 说明
directory 日志目录,相对路径相对于 $CATALINA_BASE
prefix / suffix 日志文件名前缀 / 后缀
pattern 日志格式,可用内置别名 common、combined,也可自定义
rotatable 是否自动切割,false 则单文件不断增长(交给 logrotate 时可设 false)
fileDateFormat 切割后缀格式。改为 .yyyy-MM-dd.HH 即按小时切割
resolveHosts 是否将 IP 反解为主机名,开启会严重拖慢性能,保持 false
buffered 是否缓冲写入,true 性能更好但日志有延迟
maxDays 保留天数(Tomcat 8.5+),超期自动删除
condition / conditionIf 满足条件时才记录,可用于过滤健康检查请求

常用 pattern 字段:

字段 含义
%h 客户端 IP(resolveHosts=true 时为主机名)
%l 远程逻辑用户名(通常为 -)
%u 认证用户名
%t 请求时间
%r 请求行(如 GET /index.jsp HTTP/1.1)
%s 响应状态码
%b 响应字节数(不含头,无内容时为 -)
%B 响应字节数(无内容时为 0)
%D 请求处理耗时(毫秒),性能排查必备
%T 请求处理耗时(秒)
%F 提交响应头的耗时(毫秒)
%S 用户 Session ID
%m 请求方法(GET / POST)
%U 请求 URL 路径(不含查询串)
%q 查询字符串(含 ?)
%p 服务端端口
%v 服务端名称(虚拟主机名)
%A 本地 IP
%a 远程 IP
%I 处理该请求的线程名
%{XXX}i 请求头 XXX,如 %{Referer}i、%{User-Agent}i、%{X-Forwarded-For}i
%{XXX}o 响应头 XXX
%{XXX}c Cookie XXX
%{XXX}r ServletRequest 属性 XXX
%{XXX}s HttpSession 属性 XXX

内置别名:

common   = %h %l %u %t "%r" %s %b
combined = %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"

如需记录真实客户端 IP(前端有 Nginx),除了在 pattern 中直接取 %{X-Forwarded-For}i,更推荐在 server.xml 中配置 RemoteIpValve,让 Tomcat 全局识别 X-Forwarded-For,这样 %h 与应用中的 request.getRemoteAddr() 都能得到真实 IP(配置见 4.7 与 9.1 节)。

访问日志分析常用命令

LOG=/usr/local/tomcat/logs/localhost_access_log.2026-08-22.txt

# 访问量 Top 10 IP
awk '{print $1}' $LOG | sort | uniq -c | sort -rn | head -10

# 访问量 Top 10 URL
awk '{print $7}' $LOG | sort | uniq -c | sort -rn | head -10

# 状态码分布
awk '{print $9}' $LOG | sort | uniq -c | sort -rn

# 所有 5xx 错误请求
awk '$9 ~ /^5/ {print $0}' $LOG

# 慢请求 Top 20(假设 %D 在最后一列,单位毫秒)
awk '{print $NF, $7}' $LOG | sort -rn | head -20

# 每分钟请求数(QPS 趋势)
awk -F'[' '{print $2}' $LOG | awk -F':' '{print $1":"$2":"$3}' | uniq -c

# 总请求数
wc -l $LOG

11. 安全加固

11.1 加固清单总览

项目 建议
删除默认应用 生产环境删除 webapps/ 下的 docs、examples、manager、host-manager、ROOT(或替换为自有首页)
以非 root 运行 创建普通用户(如 tomcat),将 Tomcat 目录属主改为该用户并以其启动;80/443 端口可通过 Nginx 转发或 setcap 实现
关闭 AJP 不需要时注释掉 8009 AJP Connector,防止 Ghostcat(CVE-2020-1938)等 CVE 被利用
修改 shutdown 端口与命令 改掉默认 8005 与 SHUTDOWN 字符串,或直接 port="-1" 禁用
隐藏版本信息 修改 lib/catalina.jar 中 ServerInfo.properties,Connector 加 server 属性,ErrorReportValve 设 showServerInfo="false"
禁止目录列表 在 conf/web.xml 中将 DefaultServlet 的 listings 设置为 false
自定义错误页 配置 404、500 等错误页面,避免堆栈信息与版本外泄
禁用不安全的 HTTP 方法 禁止 PUT / DELETE / TRACE / OPTIONS
HTTPS 配置 SSL/TLS Connector,或在前置 Nginx/Apache 上卸载证书
文件权限 conf/ 仅授权给运行用户;限制 JSP/上传目录的写入与执行权限
manager 强口令 + IP 限制 tomcat-users.xml 设置强口令,RemoteAddrValve 限制来源 IP
关闭 autoDeploy 生产环境 autoDeploy="false"、deployOnStartup="false",防止误放文件被自动部署
生产关闭 reloadable reloadable="false",避免后台扫描消耗与非预期重载
JVM 安全 按需开启 Security Manager;及时升级 JDK 与 Tomcat 补丁版本
应用部署 不部署来历不明的 WAR 包;对上传目录禁止解析 JSP
Session 安全 useHttpOnly="true"、HTTPS 下设 secure、登录后调用 changeSessionId() 防会话固定
日志与审计 记录访问日志与 manager 操作日志,定期切割与归档

11.2 删除默认应用

cd /usr/local/tomcat/webapps/
rm -rf docs examples manager host-manager
# ROOT 通常替换为自有应用或一个空白/自定义首页
rm -rf ROOT/*
echo "<html><body>It works.</body></html>" > ROOT/index.html

ls -l /usr/local/tomcat/webapps/

examples 危害最大:其中包含可用于会话操作、Cookie 读写的示例 Servlet,历史上多次被作为攻击入口。必须删除。

11.3 关闭 AJP 连接器

vim /usr/local/tomcat/conf/server.xml
<!--
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
-->
/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh
lsof -i:8009      # 应无输出

11.4 修改 shutdown 端口与命令

<!-- 改端口 + 改命令 -->
<Server port="8765" shutdown="Xk9_sTOP@2024_pLs">

<!-- 或彻底禁用(配合 systemd / CATALINA_PID 管理进程) -->
<Server port="-1" shutdown="SHUTDOWN">

验证只绑定本机:

lsof -i:8765
# 应显示 TCP localhost:8765 (LISTEN),而非 *:8765

11.5 隐藏版本信息

做法一:Connector 加 server 属性(最简单,改响应头)

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           server="WebServer"
           xpoweredBy="false" />

做法二:ErrorReportValve 隐藏错误页中的版本与堆栈

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
  <Valve className="org.apache.catalina.valves.ErrorReportValve"
         showReport="false"
         showServerInfo="false" />
</Host>

做法三:修改 catalina.jar 中的 ServerInfo.properties(最彻底)

cd /tmp
mkdir -p org/apache/catalina/util
cd /usr/local/tomcat/lib/

# 1. 从 jar 中提取该文件
unzip -o catalina.jar org/apache/catalina/util/ServerInfo.properties -d /tmp/

# 2. 编辑
vim /tmp/org/apache/catalina/util/ServerInfo.properties
server.info=WebServer
server.number=0.0.0.0
server.built=Unknown
# 3. 打回 jar 包
cd /tmp
jar uvf /usr/local/tomcat/lib/catalina.jar org/apache/catalina/util/ServerInfo.properties
# 或
zip -r /usr/local/tomcat/lib/catalina.jar org/apache/catalina/util/ServerInfo.properties

# 4. 重启并验证
/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh
curl -I http://127.0.0.1:8080/
/usr/local/tomcat/bin/version.sh

做法四:JSP 响应头去掉 X-Powered-By

在 conf/web.xml 的 JspServlet 中:

<init-param>
    <param-name>xpoweredBy</param-name>
    <param-value>false</param-value>
</init-param>

11.6 禁止目录列表

修改 conf/web.xml,把 DefaultServlet 的 listings 设为 false(默认已是 false,需确认未被改动):

<servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param>
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param>
    <init-param>
        <param-name>listings</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

验证:访问一个没有 index.* 文件的目录,应返回 404 而不是文件列表。

11.7 禁用不安全的 HTTP 方法

在应用的 WEB-INF/web.xml(或 conf/web.xml)中加入安全约束:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>restricted methods</web-resource-name>
        <url-pattern>/*</url-pattern>
        <http-method>PUT</http-method>
        <http-method>DELETE</http-method>
        <http-method>HEAD</http-method>
        <http-method>OPTIONS</http-method>
        <http-method>TRACE</http-method>
    </web-resource-collection>
    <!-- 空的 auth-constraint 表示拒绝所有人 -->
    <auth-constraint/>
</security-constraint>

同时确认 DefaultServlet 的 readonly 为 true(默认值),禁止通过 PUT/DELETE 写文件:

<init-param>
    <param-name>readonly</param-name>
    <param-value>true</param-value>
</init-param>

验证:

curl -X PUT -d "test" http://127.0.0.1:8080/shell.jsp   # 应返回 403 或 405
curl -X TRACE http://127.0.0.1:8080/                    # 应返回 403 或 405
curl -X OPTIONS -i http://127.0.0.1:8080/

11.8 配置 HTTPS(Tomcat 自身卸载 SSL)

# 生成自签名证书(生产请使用 CA 签发证书)
keytool -genkeypair -alias tomcat -keyalg RSA -keysize 2048 -validity 3650 \
        -keystore /usr/local/tomcat/conf/keystore.jks \
        -storepass changeit -keypass changeit \
        -dname "CN=www.example.com, OU=IT, O=Example, L=Beijing, ST=Beijing, C=CN"

keytool -list -v -keystore /usr/local/tomcat/conf/keystore.jks -storepass changeit
chown tomcat:tomcat /usr/local/tomcat/conf/keystore.jks
chmod 600 /usr/local/tomcat/conf/keystore.jks

server.xml 中启用 HTTPS Connector(Tomcat 8.5 / 9 语法):

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
           maxThreads="500" SSLEnabled="true"
           scheme="https" secure="true"
           URIEncoding="UTF-8">
    <SSLHostConfig protocols="TLSv1.2,TLSv1.3"
                   ciphers="HIGH:!aNULL:!MD5:!RC4"
                   honorCipherOrder="true">
        <Certificate certificateKeystoreFile="conf/keystore.jks"
                     certificateKeystorePassword="changeit"
                     type="RSA" />
    </SSLHostConfig>
</Connector>

Tomcat 7 / 8.0 的旧语法:

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
           maxThreads="150" scheme="https" secure="true"
           keystoreFile="conf/keystore.jks" keystorePass="changeit"
           clientAuth="false" sslProtocol="TLS" />

强制 HTTP 跳转 HTTPS(conf/web.xml 末尾):

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Protected Context</web-resource-name>
        <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <user-data-constraint>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
</security-constraint>

此时 HTTP Connector 的 redirectPort 必须指向 8443。

11.9 上传目录禁止解析 JSP

若应用有文件上传功能,攻击者可能上传 shell.jsp。在应用的 WEB-INF/web.xml 中把上传目录的所有请求交给 DefaultServlet(只当静态文件返回,不执行):

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/upload/*</url-pattern>
</servlet-mapping>

或在前置 Nginx 中拦截:

location ~* ^/upload/.*\.(jsp|jspx|java|class|sh|php)$ {
    deny all;
    return 403;
}

同时对上传目录去掉执行权限:

chmod -R 750 /data/webapps/example/ROOT/upload
find /data/webapps/example/ROOT/upload -type f -exec chmod 640 {} \;

11.10 Session 安全

<!-- conf/context.xml 或应用的 context.xml -->
<Context useHttpOnly="true" sessionCookieName="SESSIONID">
    ...
</Context>
<!-- conf/web.xml -->
<session-config>
    <session-timeout>30</session-timeout>
    <cookie-config>
        <http-only>true</http-only>
        <!-- 全站 HTTPS 时开启 -->
        <secure>true</secure>
    </cookie-config>
    <!-- 只用 Cookie 传递 SessionID,禁止 URL 重写(防 SessionID 泄漏到日志/Referer) -->
    <tracking-mode>COOKIE</tracking-mode>
</session-config>

11.11 关闭 autoDeploy 与 reloadable

<Host name="localhost" appBase="webapps"
      unpackWARs="true"
      autoDeploy="false"
      deployOnStartup="true">
  <Context path="" docBase="ROOT" reloadable="false" />
</Host>

11.12 开启 Security Manager(按需)

/usr/local/tomcat/bin/catalina.sh start -security

策略文件为 conf/catalina.policy,可精细限制应用的文件读写、网络访问、反射等权限。代价是性能下降约 5%~10%,且需要仔细调试策略,通常只在多租户共享 Tomcat 的场景使用。


12. 常见排错清单

12.1 8080 端口被占用

现象:启动后 catalina.out 中出现:

严重 [main] org.apache.catalina.core.StandardService.initInternal
      Failed to initialize connector [Connector[HTTP/1.1-8080]]
Caused by: java.net.BindException: Address already in use
      at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:210)

排查与解决:

# 1. 找出占用端口的进程
lsof -i:8080
netstat -antup | grep 8080
ss -antpl | grep 8080
fuser -n tcp 8080

# 2. 判断是不是上一次没关干净的 Tomcat 残留
ps -ef | grep -i tomcat
jps -l

# 3a. 若是残留的 Tomcat,杀掉后重启
kill -9 <PID>
# 或
pkill -9 -f catalina
/usr/local/tomcat/bin/startup.sh

# 3b. 若是其他程序占用,则修改 Tomcat 端口
vim /usr/local/tomcat/conf/server.xml
# <Connector port="8081" .../>

常见诱因:shutdown.sh 执行后没等进程真正退出就立刻 startup.sh。正确做法是等待或检查:

/usr/local/tomcat/bin/shutdown.sh
while pgrep -f catalina > /dev/null; do echo waiting; sleep 1; done
/usr/local/tomcat/bin/startup.sh

一机多实例时,8080、8005、8009 三个端口都必须错开,否则第二个实例起不来(8005 冲突尤其容易被忽略,因为它不影响对外访问但会导致启动失败)。

12.2 内存溢出 OutOfMemoryError

必须先区分是哪一类 OOM,堆和元空间的处理方式完全不同:

错误信息 区域 原因 解决
java.lang.OutOfMemoryError: Java heap space 堆内存 对象过多/过大,或存在内存泄漏(集合类、静态 Map 持续增长、连接未关闭、ThreadLocal 未清理) 调大 -Xmx;用 jmap -histo / 堆 dump 定位泄漏点并修复代码
java.lang.OutOfMemoryError: PermGen space 永久代(JDK 7 及以前) 加载的类过多;反复热部署导致 ClassLoader 泄漏;动态生成大量代理类 调大 -XX:MaxPermSize=512m;减少热部署次数,改为重启发布
java.lang.OutOfMemoryError: Metaspace 元空间(JDK 8+) 同上,但元空间使用本地内存不占堆 调大 -XX:MaxMetaspaceSize=512m;排查 ClassLoader 泄漏
java.lang.OutOfMemoryError: unable to create new native thread 线程/本地内存 线程数超过 OS 限制(ulimit -u)或内存不足以分配线程栈 调大 ulimit -u / LimitNPROC;减小 -Xss;降低 maxThreads;检查线程池泄漏
java.lang.OutOfMemoryError: Direct buffer memory 直接内存 NIO DirectByteBuffer 未释放 设置 -XX:MaxDirectMemorySize;检查 NIO 使用
java.lang.OutOfMemoryError: GC overhead limit exceeded 堆 GC 占用超过 98% 时间却只回收不到 2% 内存,接近 OOM 的前兆 与 heap space 同处理
java.lang.StackOverflowError 线程栈 递归过深 修复递归逻辑;必要时调大 -Xss

排查步骤

# 1. 确认 OOM 类型
grep -i "OutOfMemoryError" /usr/local/tomcat/logs/catalina.out

# 2. 观察内存趋势:重点看 O(老年代)与 M(元空间),以及 FGC 次数
jstat -gcutil $(pgrep -f catalina) 2000 30

# 3. 查看堆配置与实际使用
jmap -heap $(pgrep -f catalina)

# 4. 查看占用内存最多的对象
jmap -histo:live $(pgrep -f catalina) | head -30

# 5. 导出堆快照,用 MAT(Eclipse Memory Analyzer)分析支配树与泄漏嫌疑
jmap -dump:format=b,file=/tmp/heap.hprof $(pgrep -f catalina)

预防配置(务必提前加上,否则 OOM 后无据可查):

CATALINA_OPTS="$CATALINA_OPTS \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/usr/local/tomcat/logs/heapdump.hprof \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps \
-Xloggc:/usr/local/tomcat/logs/gc.log"

判断是"配置太小"还是"内存泄漏"的关键:观察 Full GC 之后老年代占用率。若 Full GC 后 O 能明显下降 → 只是堆偏小;若 Full GC 后 O 仍居高不下且持续攀升 → 内存泄漏,加内存只是拖延时间,必须修代码。

12.3 中文乱码

乱码分三个层次,需分别处理:

(1)GET 请求 URL 参数乱码 → URIEncoding

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="UTF-8"
           useBodyEncodingForURI="true" />
Tomcat 版本 URIEncoding 默认值
Tomcat 7 及之前 ISO-8859-1 → 必须显式设为 UTF-8
Tomcat 8 及之后 UTF-8 → 通常无需修改

useBodyEncodingForURI="true" 的作用:让查询字符串使用 request.setCharacterEncoding() 设定的编码来解码,用于兼容老应用。

(2)POST 请求体乱码 → 应用侧字符编码过滤器

<!-- WEB-INF/web.xml,使用 Tomcat 自带的过滤器 -->
<filter>
    <filter-name>setCharacterEncodingFilter</filter-name>
    <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>ignore</param-name>
        <param-value>false</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>setCharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

(3)JVM / 系统层面乱码(日志乱码、读写文件名乱码)→ file.encoding

# 查看 JVM 当前默认字符集
jinfo -sysprops $(pgrep -f catalina) | grep -Ei "file.encoding|sun.jnu"
java -XshowSettings:properties -version 2>&1 | grep -i encoding

在 setenv.sh 中显式指定:

CATALINA_OPTS="$CATALINA_OPTS -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8"
# 若日志中中文乱码,还可指定 JULI 的编码
CATALINA_OPTS="$CATALINA_OPTS -Dorg.apache.catalina.startup.EXIT_ON_INIT_FAILURE=true"

同时确认 OS 与数据库层面一致:

# 系统 locale
locale
echo $LANG
# CentOS 7 设置系统语言
localectl set-locale LANG=zh_CN.UTF-8
# CentOS 6
vim /etc/sysconfig/i18n     # LANG="zh_CN.UTF-8"

# MySQL/MariaDB 字符集
mysql -e "show variables like 'character%';"

MySQL 连接串也需带编码:

jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8

(4)JSP 页面自身编码

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

(5)Nginx 前端也需声明

charset utf-8;

排查思路总结:GET 乱码查 URIEncoding;POST 乱码查过滤器;页面整体乱码查 contentType/meta;日志与文件名乱码查 file.encoding 与系统 locale;数据库存进去就乱查连接串与库表字符集。

12.4 应用部署后访问 404

按以下顺序逐项排查:

# 1. 确认应用是否真的部署成功(而不是启动时静默失败)
grep -Ei "Deploy|Error|Exception" /usr/local/tomcat/logs/catalina.out | tail -40
cat /usr/local/tomcat/logs/localhost.$(date +%F).log

# 2. 确认解压目录存在,且内部结构正确
ls -l /usr/local/tomcat/webapps/
ls -l /usr/local/tomcat/webapps/sp/WEB-INF/

# 3. 确认访问路径与目录名/Context path 一致(大小写敏感!)
#    webapps/sp     -> http://IP:8080/sp
#    webapps/ROOT   -> http://IP:8080/
#    <Context path="/shop"> -> http://IP:8080/shop

# 4. 确认文件属主与权限(以 tomcat 用户运行时最常见的坑)
ls -ld /usr/local/tomcat/webapps/sp
chown -R tomcat:tomcat /usr/local/tomcat/webapps/

# 5. 确认欢迎页存在
ls /usr/local/tomcat/webapps/sp/index.*
grep -A5 welcome-file-list /usr/local/tomcat/conf/web.xml

# 6. 用 manager 查看应用状态(是否处于 stopped)
curl -u admin:pass "http://127.0.0.1:8080/manager/text/list"
# 输出示例:/sp:running:0:sp   或   /sp:stopped:0:sp

常见原因归纳:

原因 说明与解决
WAR 未自动解压 unpackWARs="true" 未开启,或 autoDeploy="false" 且未重启
应用启动失败 Spring 容器初始化异常、缺 JAR、数据库连不上。日志中有堆栈,404 只是表象,必须看 localhost.<date>.log
路径大小写错误 Linux 下 webapps/Sp 与 /sp 不匹配
缺少 WEB-INF/web.xml Servlet 3.0+ 可无 web.xml,但老容器要求必须有
权限不足 tomcat 用户对 webapps 或 docBase 无读权限
Context 冲突 同一 path 在 server.xml 和 conf/Catalina/localhost/*.xml 中重复定义
docBase 路径写错 相对路径是相对于 Host 的 appBase,容易搞错
前端代理路径不匹配 Nginx proxy_pass http://tomcat/ 末尾斜杠会截断路径前缀
访问了 WEB-INF 内的资源 WEB-INF 下内容禁止直接访问,只能通过转发
work/ 缓存脏数据 停 Tomcat 后 rm -rf work/* temp/* 再启动

12.5 启动卡住 / 首次访问极慢 / shutdown 报错(SecureRandom 熵不足)

现象一:catalina.out 停在这一行长时间不动(30 秒到数分钟):

信息 [localhost-startStop-1] org.apache.catalina.util.SessionIdGeneratorBase.createSecureRandom
      Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [186,325] milliseconds.

现象二:执行 shutdown.sh 时报错:

Jul 28, 2017 11:56:26 AM org.apache.catalina.startup.Catalina stopServer
SEVERE: Could not contact localhost:8005. Tomcat may not be running.
Jul 28, 2017 11:56:26 AM org.apache.catalina.startup.Catalina stopServer
SEVERE: Catalina.stop:

原因:Tomcat 生成 SessionID 需要安全随机数,默认熵源是 /dev/random。它依赖外部中断事件(鼠标、键盘、磁盘 IO、网络中断)积累熵,在虚拟机、云主机、无人操作的服务器上熵极少,导致读取时阻塞(hang 住),进而启动缓慢或卡死;因为 8005 端口还没监听,shutdown.sh 自然联系不上。

设备 说明
/dev/random 依赖外部中断事件(鼠标、键盘等)生成随机数,熵池耗尽时会阻塞等待,产生大量随机数时可能 hang 住
/dev/urandom 不依赖外部中断,熵不足时用伪随机算法继续输出,不会阻塞,生成大量随机数速度更快,安全性对 Web 会话场景完全足够

解决方案一:修改 JDK 的 java.security(讲义方法,全局生效)

# JDK 改名为 /usr/local/java 后
vim /usr/local/java/jre/lib/security/java.security
# 或未改名时
vim /usr/local/jdk1.8.0_45/jre/lib/security/java.security

找到第 117 行左右:

securerandom.source=file:/dev/random

将 random 改为 urandom:

securerandom.source=file:/dev/urandom

然后 kill 残留进程并重启 Tomcat:

pkill -9 -f catalina
/usr/local/tomcat/bin/startup.sh
tail -f /usr/local/tomcat/logs/catalina.out

解决方案二:JVM 参数指定(推荐,不改 JDK 文件)

在 bin/setenv.sh 中加入:

CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom"

注意 /dev/./urandom 中间的 /./ 不是笔误。这是 JDK 的一个历史 bug(JDK-6202721):直接写 file:/dev/urandom 会被内部逻辑识别为特殊值而仍然回退到 /dev/random,加上 /./ 可绕过该判断。

解决方案三:安装熵服务补充熵池

# CentOS 7
yum install -y rng-tools
systemctl start rngd && systemctl enable rngd
# 或 haveged(虚拟机效果更好)
yum install -y haveged
systemctl start haveged && systemctl enable haveged

# CentOS 6
yum install -y rng-tools
service rngd start && chkconfig rngd on

# 查看当前可用熵值(低于 200 即偏低,正常应在 1000+)
cat /proc/sys/kernel/random/entropy_avail

shutdown 报 Could not contact localhost:8005 的其他可能原因

原因 排查
Tomcat 本来就没运行 ps -ef \| grep tomcat、lsof -i:8005
server.xml 中改了 shutdown 端口或命令,但用旧脚本关闭 确认 <Server port> 与 shutdown 值
port="-1" 已禁用关闭端口 改用 kill、systemd 或 catalina.sh stop -force
8005 被防火墙/本机策略拦截 iptables -L -n \| grep 8005
JAVA_HOME 未设置,脚本执行失败 catalina.sh 会报 Neither the JAVA_HOME nor the JRE_HOME environment variable is defined

12.6 catalina.out 无限增长撑满磁盘

现象:df -h 显示 / 或 /usr 使用率 100%,Tomcat 报 No space left on device,应用无法写文件甚至请求全部失败。

# 1. 定位大文件
df -h
du -sh /usr/local/tomcat/logs/*
ls -lh /usr/local/tomcat/logs/catalina.out

# 2. 应急处理:清空(切记不要 rm)
> /usr/local/tomcat/logs/catalina.out

# 3. 检查是否有已删除但仍占空间的文件
lsof | grep deleted
lsof -p $(pgrep -f catalina) | grep deleted

# 4. 长期方案:配置 logrotate(见 10.3 节)
vim /etc/logrotate.d/tomcat

根因排查:日志暴涨往往说明应用在疯狂打印异常或 DEBUG 日志。

# 统计最高频的异常
grep -Eo "([a-zA-Z]+\.)+[A-Za-z]*Exception" /usr/local/tomcat/logs/catalina.out \
  | sort | uniq -c | sort -rn | head -20

# 查看日志增长速率
ls -l --time-style=full-iso /usr/local/tomcat/logs/catalina.out; sleep 60
ls -l --time-style=full-iso /usr/local/tomcat/logs/catalina.out

同时把应用的日志级别从 DEBUG 调回 INFO/WARN(logging.properties、log4j.properties 或 logback.xml)。

12.7 其他常见问题

(1)浏览器无法访问

  • 使用 lsof -i:8080 确认 Tomcat 已监听;
  • 检查防火墙与 SELinux 状态:
# CentOS 7
firewall-cmd --list-all
systemctl status firewalld
# CentOS 6
iptables -L -n
service iptables status
# SELinux
getenforce
setenforce 0
  • 确认 server.xml 中 Connector 端口配置正确,且未绑定到 127.0.0.1;
  • 从服务器本机先测通:curl -I http://127.0.0.1:8080/,本机通而远程不通 → 网络/防火墙问题;本机也不通 → Tomcat 或配置问题;
  • 若 Tomcat 启动后浏览器仍无法正常访问,重启服务器后再试(讲义中记录的现象,通常与残留进程或端口占用有关)。

(2)应用部署不生效 / 修改代码不更新

  • 检查 webapps/ 目录权限和属主;
  • 查看 logs/catalina.out 是否有应用启动异常;
  • 生产环境关闭 reloadable,避免频繁扫描;
  • WAR 包未自动解压时检查 unpackWARs="true";
  • 修改 JSP 后不生效:清理编译缓存后重启。
/usr/local/tomcat/bin/shutdown.sh
rm -rf /usr/local/tomcat/work/Catalina/localhost/*
rm -rf /usr/local/tomcat/temp/*
/usr/local/tomcat/bin/startup.sh

(3)Tomcat 启动慢(非熵问题)

# 关闭不必要的 TLD / 注解扫描(conf/catalina.properties)
tomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar

# 并行启动多个应用(server.xml)
# <Host ... startStopThreads="0">
# <Engine ... startStopThreads="0">

也可关闭 metadata-complete 注解扫描(应用的 web.xml):

<web-app metadata-complete="true" ...>

(4)线程池耗尽 / 请求排队

现象:请求响应变慢或超时,manager/status 中 Current thread busy 接近 Max threads。

# 观察线程状态分布
jstack $(pgrep -f catalina) | grep -c "http-nio-8080-exec"
jstack $(pgrep -f catalina) | grep "java.lang.Thread.State" | sort | uniq -c

# 找出阻塞在数据库或锁上的线程
jstack $(pgrep -f catalina) | grep -B5 -A20 "BLOCKED"
jstack $(pgrep -f catalina) | grep -A20 "waiting to lock"

处理:调大 maxThreads 只是缓解;根因通常是慢 SQL、外部接口超时或死锁,需从 %D 访问日志与 jstack 定位。可配置 StuckThreadDetectionValve 主动告警:

<Valve className="org.apache.catalina.valves.StuckThreadDetectionValve"
       threshold="60" />

(5)Session 突然全部失效

  • Tomcat 重启(Session 保存在内存中)→ 用集中式 Session 或开启 Session 持久化;
  • session-timeout 过短;
  • 集群下未做会话共享,请求被轮询到其他节点;
  • context.xml 中 <Manager pathname="" /> 关闭了持久化,重启即丢。

(6)JAVA_HOME 未设置

Neither the JAVA_HOME nor the JRE_HOME environment variable is defined
At least one of these environment variable is needed to run this program

解决:按 2.3 节在 /etc/profile.d/java.sh、启停脚本或 setenv.sh 中设置。注意 systemd、cron、rc.local 启动时都不加载 /etc/profile,必须写进 setenv.sh 或 unit 文件的 Environment=。

(7)Permission denied 无法执行脚本

chmod +x /usr/local/tomcat/bin/*.sh

(8)server.xml 语法错误导致全站起不来

# 改动前先备份
cp /usr/local/tomcat/conf/server.xml /usr/local/tomcat/conf/server.xml.$(date +%F)
# 改动后先校验
/usr/local/tomcat/bin/configtest.sh
xmllint --noout /usr/local/tomcat/conf/server.xml

日志中会明确指出行号:

严重 [main] org.apache.tomcat.util.digester.Digester.fatalError
      Parse Fatal Error at line 45 column 3: The element type "Host" must be terminated by the matching end-tag "</Host>".

12.8 排错通用流程

1. 看日志:catalina.out → localhost.<date>.log → localhost_access_log
2. 看进程:ps -ef | grep tomcat  /  jps -l
3. 看端口:lsof -i:8080  /  ss -antpl
4. 看资源:top、free -m、df -h、jstat -gcutil
5. 看配置:configtest.sh、xmllint、对比备份
6. 看链路:curl 本机 → curl 内网 → 浏览器外网,逐段缩小范围
7. 前台跑:catalina.sh run,让所有输出直接可见

13. 练习

13.1 基础练习

  1. 在 CentOS 7 上用 yum 安装 OpenJDK 8,并验证 java -version 与 javac -version 均可用。
  2. 用二进制包方式安装 Oracle JDK 8 到 /usr/local/java,通过 /etc/profile.d/java.sh 配置 JAVA_HOME、CLASSPATH、PATH,并写一个 Hello.java 完成编译与运行。
  3. 同一台机器同时装 OpenJDK 7 与 Oracle JDK 8,用 alternatives --config java 在两者间切换,说明为什么切换后 Tomcat 仍可能使用旧版本。
  4. 解压安装 Tomcat 9 到 /usr/local/tomcat,启动后通过 lsof -i:8080 与浏览器验证访问,并说出 catalina.out 中哪一行代表启动真正完成。
  5. 写出 Tomcat 七个一级目录(bin、conf、lib、logs、temp、webapps、work)各自的用途,并指出 JSP 编译产物在哪个目录。
  6. 用 catalina.sh run 前台启动 Tomcat,说明它相比 startup.sh 在排错时的优势。
  7. 创建 tomcat 系统用户,把 Tomcat 目录属主改为该用户并以其身份启动;说明为什么普通用户无法直接监听 80 端口,并列出至少两种解决办法。
  8. 编写 /usr/lib/systemd/system/tomcat.service,实现 systemctl start/stop/status/enable tomcat;解释为什么必须写 Type=forking 和 PIDFile。
  9. 在 CentOS 6 上编写 /etc/init.d/tomcat 脚本并用 chkconfig 加入开机自启;对比 CentOS 7 用 /etc/rc.local 时为什么还要 chmod +x /etc/rc.d/rc.local。

13.2 配置练习

  1. 把 Tomcat 的 HTTP 端口从 8080 改为 80,重启验证;再改回 8080。
  2. 说明 8080、8443、8005、8009 四个端口各自的用途。
  3. 演示如何用 telnet 127.0.0.1 8005 发送 SHUTDOWN 关停 Tomcat,然后给出三种加固方案。
  4. 解释 maxThreads、minSpareThreads、acceptCount、maxConnections 四个参数的区别与相互关系,并为一台 8 核 8G 的常规 Web 服务器给出一组建议值。
  5. 对比 HTTP Connector 与 AJP Connector:协议、通信对象、性能与安全风险,并写出关闭 AJP 的配置。
  6. 对比 BIO、NIO、NIO2、APR 四种协议实现,说明 Tomcat 8+ 默认用哪种,以及为什么建议显式写明 protocol 类名。
  7. 为 Connector 开启 GZIP 压缩,压缩 text/html、text/css、application/javascript,最小压缩阈值 2KB,并用 curl -H "Accept-Encoding: gzip" -I 验证。
  8. 用 <Executor> 配置一个共享线程池,供 HTTP 和 HTTPS 两个 Connector 共用。
  9. 列出 Context 的三种配置位置,说明各自优缺点;用 conf/Catalina/localhost/shop.xml 方式把 /data/apps/shop 部署为 /shop,并说明为什么该文件中不能写 path 属性。
  10. 用 RemoteAddrValve 让 manager 应用只允许 192.168.4.0/24 访问;再用 ErrorReportValve 隐藏错误页中的版本信息。
  11. 配置 tomcat-users.xml,创建一个只有 manager-gui 的账号和一个只有 manager-script 的账号,并解释为什么官方建议不要把这两个角色给同一用户。
  12. 用 curl 调用 manager 的文本接口,完成"列出应用 → 上传部署 sp.war → 重载 → 卸载"全过程。
  13. 在 conf/web.xml 中把 session 超时改为 15 分钟,把欢迎页顺序改为 index.jsp 优先,并禁用目录列表。

13.3 部署与架构练习

  1. 用三种方式(放 WAR 到 webapps、Context 指向任意目录、manager 界面上传)分别部署 sp.war,并说明各自适用场景。
  2. 部署 JspXCMS:完成建库授权(库名 jspxcms、用户 mn、密码 123)、浏览器引导安装、重启后访问前台 / 与后台 /cmscp/index.do。
  3. 用四种不同方式让应用通过 http://IP:8080/ 直接访问(去掉项目名)。
  4. 在一台 Tomcat 上配置 www.aaa.com 与 www.bbb.com 两个虚拟主机,分别指向 /data/webapps/aaa 与 /data/webapps/bbb,各自独立访问日志;用客户端 hosts 与 curl -H "Host: ..." 两种方式验证。
  5. 说明为什么直接用 IP 访问虚拟主机只会命中 defaultHost;如果 defaultHost 的值与所有 <Host name> 都不匹配会发生什么。
  6. 搭建 Nginx + 两台 Tomcat 的动静分离环境:.html/图片/CSS/JS 由 Nginx 返回,.jsp 轮询转发到后端,用 1.html 和 1.jsp 验证。
  7. 在上题基础上部署 Memcached 并配置 MSM,实现两台 Tomcat 的 Session 共享;用浏览器刷新 session.jsp 验证 SessionID 不变而 SessionIP 变化,并在 Memcached 中用 stats cachedump 找到对应的 Session 记录。
  8. 说明为什么不能用 curl http://IP/session.jsp 验证 Session 保持,以及如何用 curl 的 Cookie 罐参数正确测试。
  9. 对比 Session Replication、Session Sticky、集中式缓存三种会话方案的原理、优缺点与适用场景。
  10. 配置 Nginx 的 HTTPS 卸载,后端 Tomcat 走 HTTP;说明为什么后端 Connector 需要 scheme="https" 或 RemoteIpValve。
  11. 用 Apache 的 mod_proxy_http 和 mod_proxy_ajp 两种方式反向代理 Tomcat,并配置 stickysession=JSESSIONID。

13.4 优化与排错练习

  1. 在 setenv.sh 中为一台 16G 内存的服务器配置 JVM 参数:堆 8G、G1 回收器、目标停顿 200ms、开启 GC 日志与 OOM 自动 dump。
  2. 解释 JAVA_OPTS 与 CATALINA_OPTS 的区别,说明为什么堆参数应该放在后者。
  3. 说明 -XX:PermSize 与 -XX:MetaspaceSize 的区别,以及在 JDK 8 中写 PermSize 会发生什么。
  4. 用 jps、jstat -gcutil、jmap -histo、jstack 四条命令完成一次完整的性能巡检,说明 jstat -gcutil 输出中 O、M、FGC 三列的含义。
  5. 用"top → top -Hp → printf %x → jstack grep nid"四步法定位 CPU 占用最高的 Java 线程。
  6. 区分 Java heap space、Metaspace、unable to create new native thread 三种 OOM 的原因与处理方式;说明如何通过 Full GC 后老年代占用率判断是"堆太小"还是"内存泄漏"。
  7. 模拟 8080 端口被占用(用 nc -l 8080),观察 catalina.out 中的报错,并给出两种解决方案;说明一机多实例时哪三个端口必须错开。
  8. 排查中文乱码:分别写出 GET 参数乱码、POST 表单乱码、日志与文件名乱码三种情况对应的配置项。
  9. 应用部署后访问 404,列出至少八个排查方向,并说明为什么"404 只是表象,必须看 localhost.<date>.log"。
  10. Tomcat 启动卡在 Creation of SecureRandom instance ... took 186,325 milliseconds,解释原因,并给出三种解决方案(改 java.security、加 -Djava.security.egd、安装 haveged);说明 /dev/./urandom 中 /./ 的作用。
  11. 为 catalina.out 配置 logrotate 每天切割、保留 30 份、压缩;解释为什么必须使用 copytruncate,以及为什么绝对不能用 rm 删除 catalina.out。
  12. 若已误删 catalina.out 且磁盘空间未释放,如何用 lsof 与 /proc/<pid>/fd/ 恢复。
  13. 把访问日志格式改为记录真实客户端 IP、请求耗时(毫秒)、Referer 与 User-Agent,并用 awk 统计出访问量 Top 10 的 IP、状态码分布和耗时最长的 20 个请求。
  14. 完成一次完整的安全加固:删除四个默认应用、关闭 AJP、修改 shutdown 端口与命令、隐藏版本信息(三种方法各做一遍)、禁止目录列表、禁用 PUT/DELETE/TRACE、以非 root 运行,并逐项写出验证方法。
  15. 说明为什么生产环境必须设置 reloadable="false" 和 autoDeploy="false"。

14. 参考答案要点

  1. yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel;只装 java-1.8.0-openjdk 没有 javac。
  2. alternatives 只改 /usr/bin/java 软链接,Tomcat 读的是 JAVA_HOME,两者独立,需同步修改 setenv.sh。
  3. catalina.out 中出现 Server startup in xxxx ms;Tomcat started. 仅表示脚本执行成功。
  4. JSP 编译产物在 work/Catalina/<host>/<app>/ 下。
  5. 1024 以下为特权端口,只有 root 可绑定。解决办法:Nginx 前置代理、setcap cap_net_bind_service、iptables/firewalld 端口重定向、jsvc。
  6. startup.sh 会 fork 后自身退出,用 simple 会被 systemd 误判为服务已停止;PIDFile 需与 CATALINA_PID 一致。
  7. 8080 HTTP、8443 HTTPS/redirectPort、8005 shutdown(仅本机)、8009 AJP。
  8. echo "SHUTDOWN" | nc 127.0.0.1 8005;加固:改端口、改命令字符串、port="-1" 禁用。
  9. maxThreads 是最大工作线程(最大并发处理数);minSpareThreads 是常驻空闲线程;maxConnections 是同时接受并处理的连接数上限;acceptCount 是连接满后的 OS backlog 队列长度。8 核 8G 参考:maxThreads=400、minSpareThreads=50、acceptCount=800、maxConnections=10000、-Xmx4096m。
  10. Tomcat 8+ 默认 NIO;protocol="HTTP/1.1" 是自动选择(有 APR 库则用 APR),显式写类名可避免环境差异。
  11. 三种位置:server.xml(需重启)、conf/[Engine]/[Host]/*.xml(推荐,热加载,文件名即路径)、应用内 META-INF/context.xml。独立文件中路径由文件名决定,写 path 会被忽略并告警。
  12. 分离角色可降低 CSRF 风险:GUI 会话若被 CSRF 利用,无法调用脚本接口执行部署。
  13. 四种:内容放进 ROOT/、WAR 改名 ROOT.war、<Context path="">、conf/Catalina/localhost/ROOT.xml(另可修改 Host 的 appBase)。
  14. 虚拟主机按请求头 Host 匹配,IP 访问时 Host 头为 IP,匹配不到任何 <Host name>,落到 defaultHost;defaultHost 值无对应 Host 时 Tomcat 启动失败。
  15. curl 默认不保存回传 Cookie(JSESSIONID),每次都是新会话;用 curl -c /tmp/ck.txt -b /tmp/ck.txt。
  16. Nginx 卸载 SSL 后转给后端是 HTTP,Tomcat 若不知情,request.isSecure() 为 false,sendRedirect 生成的绝对地址会是 http,造成混合内容或重定向死循环。
  17. JAVA_OPTS 对 start 和 stop 都生效,shutdown 也会启一个小 JVM 并申请同样的大堆;CATALINA_OPTS 只对 start/run 生效。
  18. JDK 8 移除永久代,PermSize 被忽略并打印 ignoring option PermSize;Metaspace 使用本地内存,MetaspaceSize 是首次 Full GC 阈值而非上限。
  19. O = 老年代使用率,M = 元空间使用率,FGC = Full GC 次数。
  20. Full GC 后 O 明显下降 → 堆偏小;Full GC 后 O 仍高且持续攀升 → 内存泄漏。
  21. 一机多实例必须错开 8080(HTTP)、8005(shutdown)、8009(AJP)。
  22. GET → Connector 的 URIEncoding="UTF-8";POST → SetCharacterEncodingFilter;日志/文件名 → -Dfile.encoding=UTF-8、-Dsun.jnu.encoding=UTF-8 与系统 locale。
  23. /dev/random 熵不足会阻塞。JDK bug JDK-6202721 导致 file:/dev/urandom 仍回退到 /dev/random,写 file:/dev/./urandom 可绕过。
  24. Tomcat 进程持有 catalina.out 的文件句柄,create 方式切割后 Tomcat 仍写旧 inode,新文件永远为空;copytruncate 复制后截断原文件,句柄不变。rm 后空间不释放(句柄仍引用已删 inode),且日志再也看不到,必须重启才恢复。
  25. reloadable="true" 会有后台线程持续扫描 WEB-INF/classes 与 lib,消耗 CPU 且触发意外重载中断服务;autoDeploy="true" 会周期扫描 appBase,误放文件即被自动部署,存在安全与稳定性风险。

14.1 课后思考题

  1. 如何让 Session ID 发生变化?

    • 清除浏览器 Cookie / 缓存后重新访问;
    • 或调用 session.invalidate() 销毁当前会话后重新请求;
    • 调用 request.changeSessionId()(Servlet 3.1+)主动更换;
    • Memcached 中执行 flush_all 清空服务端会话,再次访问也会生成新的 Session ID;
    • 重启 Tomcat(未做持久化或集中式存储时)。
  2. 若多台 Tomcat 家目录内容不一致,有哪些方案保证一致?

    • 远程实时 rsync 同步(配合 inotify / sersync 实现实时触发);
    • NFS / DRBD 等共享存储;
    • 分布式存储(GlusterFS、Ceph);
    • CI/CD 统一发布(Jenkins + Ansible / 容器镜像),从源头保证一致,是最推荐的做法。
  3. 为什么 JSP 第一次访问特别慢? JSP 需要先由 Jasper 翻译成 Servlet 的 .java 源码,再用 javac 编译成 .class(产物在 work/ 目录),之后才加载执行。后续访问直接复用已编译的字节码,因此只有首次慢。可通过预编译(jspc / precompile)消除。

  4. 为什么生产环境不建议保留 manager 应用? manager 具备上传 WAR 并部署的能力,等价于远程代码执行入口。一旦弱口令被爆破或存在认证绕过漏洞,攻击者可直接上传恶意 WAR 获得服务器权限。若必须保留,需强口令 + RemoteAddrValve IP 白名单 + HTTPS + 角色分离。

最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏