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 项目。
- 官网:http://tomcat.apache.org/
- JDK 下载地址:https://www.oracle.com/technetwork/java/javase/downloads/index.html
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 为例:
- 客户端向服务器的 8080(或 80)端口发起 TCP 连接与 HTTP 请求。
- 监听该端口的
Connector接受连接,按protocol指定的协议实现(NIO / NIO2 / APR)读取并解析 HTTP 报文,生成Request/Response对象。 Connector将 Request 交给所属Service的Engine。Engine读取请求头中的Host: www.example.com,在其下的Host列表中匹配name或Alias;匹配不到则使用defaultHost指定的主机。- 匹配到的
Host依据 URI 的第一段/shop匹配对应的Context;匹配不到则交给path=""的根上下文(ROOT)。 Context依据WEB-INF/web.xml(以及注解)中的servlet-mapping找到处理/index.jsp的Wrapper。JSP 由JspServlet处理:若work/下无对应字节码或源文件已更新,则先翻译成 Servlet 源码再编译。- 调用 Servlet 的
service()(doGet()/doPost())方法执行业务逻辑,把结果写入 Response。 - 沿原路径(Wrapper → Context → Host → Engine → Connector)返回,途经的各级
Valve依次执行(例如AccessLogValve在此写访问日志)。 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 "%r" %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&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 "%r" %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&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 "%r" %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 角色)并放开远程访问。
图形界面操作步骤
- 浏览器访问
http://IP:8080/manager/html,输入用户名密码登录。 - 页面上半部分 Applications 列表展示所有已部署应用,每行提供
Start/Stop/Reload/Undeploy按钮以及 Sessions 数量与过期设置。 - 下半部分有两个部署区域:
- Deploy directory or WAR file located on server:部署服务器本地已有的目录/WAR。
Context Path (required):填/shopXML 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)
- 点击
- Deploy directory or WAR file located on server:部署服务器本地已有的目录/WAR。
- 部署完成后页面顶部提示
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 "%r" %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 "%r" %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 "%r" %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 性能不足时,通常有三种思路:
- 微调:内存、并发数、IO、网络、内核参数等;
- 升级硬件:成本高,提升仍然有限;
- 架构扩展:由单点转为集群,避免单点故障。
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
实验前准备
- 主机名绑定(所有节点的
/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
- 时间同步(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
- 关闭 iptables / firewalld、SELinux;
- 配置好 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 共同对外提供同一应用,其站点目录内容需要保持一致,常用方案:
- 远程实时
rsync同步(配合inotify实现实时); - DRBD、共享存储或分布式存储(也可用 NFS 模拟共享存储);
- 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 "%r" %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 "%r" %s %b %D "%{Referer}i" "%{User-Agent}i" %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 基础练习
- 在 CentOS 7 上用 yum 安装 OpenJDK 8,并验证
java -version与javac -version均可用。 - 用二进制包方式安装 Oracle JDK 8 到
/usr/local/java,通过/etc/profile.d/java.sh配置JAVA_HOME、CLASSPATH、PATH,并写一个Hello.java完成编译与运行。 - 同一台机器同时装 OpenJDK 7 与 Oracle JDK 8,用
alternatives --config java在两者间切换,说明为什么切换后 Tomcat 仍可能使用旧版本。 - 解压安装 Tomcat 9 到
/usr/local/tomcat,启动后通过lsof -i:8080与浏览器验证访问,并说出catalina.out中哪一行代表启动真正完成。 - 写出 Tomcat 七个一级目录(
bin、conf、lib、logs、temp、webapps、work)各自的用途,并指出 JSP 编译产物在哪个目录。 - 用
catalina.sh run前台启动 Tomcat,说明它相比startup.sh在排错时的优势。 - 创建
tomcat系统用户,把 Tomcat 目录属主改为该用户并以其身份启动;说明为什么普通用户无法直接监听 80 端口,并列出至少两种解决办法。 - 编写
/usr/lib/systemd/system/tomcat.service,实现systemctl start/stop/status/enable tomcat;解释为什么必须写Type=forking和PIDFile。 - 在 CentOS 6 上编写
/etc/init.d/tomcat脚本并用chkconfig加入开机自启;对比 CentOS 7 用/etc/rc.local时为什么还要chmod +x /etc/rc.d/rc.local。
13.2 配置练习
- 把 Tomcat 的 HTTP 端口从 8080 改为 80,重启验证;再改回 8080。
- 说明 8080、8443、8005、8009 四个端口各自的用途。
- 演示如何用
telnet 127.0.0.1 8005发送SHUTDOWN关停 Tomcat,然后给出三种加固方案。 - 解释
maxThreads、minSpareThreads、acceptCount、maxConnections四个参数的区别与相互关系,并为一台 8 核 8G 的常规 Web 服务器给出一组建议值。 - 对比 HTTP Connector 与 AJP Connector:协议、通信对象、性能与安全风险,并写出关闭 AJP 的配置。
- 对比 BIO、NIO、NIO2、APR 四种协议实现,说明 Tomcat 8+ 默认用哪种,以及为什么建议显式写明
protocol类名。 - 为 Connector 开启 GZIP 压缩,压缩
text/html、text/css、application/javascript,最小压缩阈值 2KB,并用curl -H "Accept-Encoding: gzip" -I验证。 - 用
<Executor>配置一个共享线程池,供 HTTP 和 HTTPS 两个 Connector 共用。 - 列出 Context 的三种配置位置,说明各自优缺点;用
conf/Catalina/localhost/shop.xml方式把/data/apps/shop部署为/shop,并说明为什么该文件中不能写path属性。 - 用
RemoteAddrValve让 manager 应用只允许192.168.4.0/24访问;再用ErrorReportValve隐藏错误页中的版本信息。 - 配置
tomcat-users.xml,创建一个只有manager-gui的账号和一个只有manager-script的账号,并解释为什么官方建议不要把这两个角色给同一用户。 - 用
curl调用 manager 的文本接口,完成"列出应用 → 上传部署 sp.war → 重载 → 卸载"全过程。 - 在
conf/web.xml中把 session 超时改为 15 分钟,把欢迎页顺序改为index.jsp优先,并禁用目录列表。
13.3 部署与架构练习
- 用三种方式(放 WAR 到 webapps、Context 指向任意目录、manager 界面上传)分别部署
sp.war,并说明各自适用场景。 - 部署 JspXCMS:完成建库授权(库名
jspxcms、用户mn、密码123)、浏览器引导安装、重启后访问前台/与后台/cmscp/index.do。 - 用四种不同方式让应用通过
http://IP:8080/直接访问(去掉项目名)。 - 在一台 Tomcat 上配置
www.aaa.com与www.bbb.com两个虚拟主机,分别指向/data/webapps/aaa与/data/webapps/bbb,各自独立访问日志;用客户端hosts与curl -H "Host: ..."两种方式验证。 - 说明为什么直接用 IP 访问虚拟主机只会命中
defaultHost;如果defaultHost的值与所有<Host name>都不匹配会发生什么。 - 搭建 Nginx + 两台 Tomcat 的动静分离环境:
.html/图片/CSS/JS 由 Nginx 返回,.jsp轮询转发到后端,用1.html和1.jsp验证。 - 在上题基础上部署 Memcached 并配置 MSM,实现两台 Tomcat 的 Session 共享;用浏览器刷新
session.jsp验证SessionID不变而SessionIP变化,并在 Memcached 中用stats cachedump找到对应的 Session 记录。 - 说明为什么不能用
curl http://IP/session.jsp验证 Session 保持,以及如何用 curl 的 Cookie 罐参数正确测试。 - 对比 Session Replication、Session Sticky、集中式缓存三种会话方案的原理、优缺点与适用场景。
- 配置 Nginx 的 HTTPS 卸载,后端 Tomcat 走 HTTP;说明为什么后端 Connector 需要
scheme="https"或RemoteIpValve。 - 用 Apache 的
mod_proxy_http和mod_proxy_ajp两种方式反向代理 Tomcat,并配置stickysession=JSESSIONID。
13.4 优化与排错练习
- 在
setenv.sh中为一台 16G 内存的服务器配置 JVM 参数:堆 8G、G1 回收器、目标停顿 200ms、开启 GC 日志与 OOM 自动 dump。 - 解释
JAVA_OPTS与CATALINA_OPTS的区别,说明为什么堆参数应该放在后者。 - 说明
-XX:PermSize与-XX:MetaspaceSize的区别,以及在 JDK 8 中写PermSize会发生什么。 - 用
jps、jstat -gcutil、jmap -histo、jstack四条命令完成一次完整的性能巡检,说明jstat -gcutil输出中O、M、FGC三列的含义。 - 用"top → top -Hp → printf %x → jstack grep nid"四步法定位 CPU 占用最高的 Java 线程。
- 区分
Java heap space、Metaspace、unable to create new native thread三种 OOM 的原因与处理方式;说明如何通过 Full GC 后老年代占用率判断是"堆太小"还是"内存泄漏"。 - 模拟 8080 端口被占用(用
nc -l 8080),观察catalina.out中的报错,并给出两种解决方案;说明一机多实例时哪三个端口必须错开。 - 排查中文乱码:分别写出 GET 参数乱码、POST 表单乱码、日志与文件名乱码三种情况对应的配置项。
- 应用部署后访问 404,列出至少八个排查方向,并说明为什么"404 只是表象,必须看
localhost.<date>.log"。 - Tomcat 启动卡在
Creation of SecureRandom instance ... took 186,325 milliseconds,解释原因,并给出三种解决方案(改java.security、加-Djava.security.egd、安装 haveged);说明/dev/./urandom中/./的作用。 - 为
catalina.out配置 logrotate 每天切割、保留 30 份、压缩;解释为什么必须使用copytruncate,以及为什么绝对不能用rm删除catalina.out。 - 若已误删
catalina.out且磁盘空间未释放,如何用lsof与/proc/<pid>/fd/恢复。 - 把访问日志格式改为记录真实客户端 IP、请求耗时(毫秒)、Referer 与 User-Agent,并用 awk 统计出访问量 Top 10 的 IP、状态码分布和耗时最长的 20 个请求。
- 完成一次完整的安全加固:删除四个默认应用、关闭 AJP、修改 shutdown 端口与命令、隐藏版本信息(三种方法各做一遍)、禁止目录列表、禁用 PUT/DELETE/TRACE、以非 root 运行,并逐项写出验证方法。
- 说明为什么生产环境必须设置
reloadable="false"和autoDeploy="false"。
14. 参考答案要点
yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel;只装java-1.8.0-openjdk没有javac。alternatives只改/usr/bin/java软链接,Tomcat 读的是JAVA_HOME,两者独立,需同步修改setenv.sh。catalina.out中出现Server startup in xxxx ms;Tomcat started.仅表示脚本执行成功。- JSP 编译产物在
work/Catalina/<host>/<app>/下。 - 1024 以下为特权端口,只有 root 可绑定。解决办法:Nginx 前置代理、
setcap cap_net_bind_service、iptables/firewalld 端口重定向、jsvc。 startup.sh会 fork 后自身退出,用simple会被 systemd 误判为服务已停止;PIDFile需与CATALINA_PID一致。- 8080 HTTP、8443 HTTPS/redirectPort、8005 shutdown(仅本机)、8009 AJP。
echo "SHUTDOWN" | nc 127.0.0.1 8005;加固:改端口、改命令字符串、port="-1"禁用。maxThreads是最大工作线程(最大并发处理数);minSpareThreads是常驻空闲线程;maxConnections是同时接受并处理的连接数上限;acceptCount是连接满后的 OS backlog 队列长度。8 核 8G 参考:maxThreads=400、minSpareThreads=50、acceptCount=800、maxConnections=10000、-Xmx4096m。- Tomcat 8+ 默认 NIO;
protocol="HTTP/1.1"是自动选择(有 APR 库则用 APR),显式写类名可避免环境差异。 - 三种位置:
server.xml(需重启)、conf/[Engine]/[Host]/*.xml(推荐,热加载,文件名即路径)、应用内META-INF/context.xml。独立文件中路径由文件名决定,写path会被忽略并告警。 - 分离角色可降低 CSRF 风险:GUI 会话若被 CSRF 利用,无法调用脚本接口执行部署。
- 四种:内容放进
ROOT/、WAR 改名ROOT.war、<Context path="">、conf/Catalina/localhost/ROOT.xml(另可修改 Host 的appBase)。 - 虚拟主机按请求头
Host匹配,IP 访问时 Host 头为 IP,匹配不到任何<Host name>,落到defaultHost;defaultHost值无对应 Host 时 Tomcat 启动失败。 - curl 默认不保存回传 Cookie(
JSESSIONID),每次都是新会话;用curl -c /tmp/ck.txt -b /tmp/ck.txt。 - Nginx 卸载 SSL 后转给后端是 HTTP,Tomcat 若不知情,
request.isSecure()为 false,sendRedirect生成的绝对地址会是 http,造成混合内容或重定向死循环。 JAVA_OPTS对 start 和 stop 都生效,shutdown也会启一个小 JVM 并申请同样的大堆;CATALINA_OPTS只对 start/run 生效。- JDK 8 移除永久代,
PermSize被忽略并打印ignoring option PermSize;Metaspace 使用本地内存,MetaspaceSize是首次 Full GC 阈值而非上限。 O= 老年代使用率,M= 元空间使用率,FGC= Full GC 次数。- Full GC 后
O明显下降 → 堆偏小;Full GC 后O仍高且持续攀升 → 内存泄漏。 - 一机多实例必须错开 8080(HTTP)、8005(shutdown)、8009(AJP)。
- GET → Connector 的
URIEncoding="UTF-8";POST →SetCharacterEncodingFilter;日志/文件名 →-Dfile.encoding=UTF-8、-Dsun.jnu.encoding=UTF-8与系统 locale。 /dev/random熵不足会阻塞。JDK bug JDK-6202721 导致file:/dev/urandom仍回退到/dev/random,写file:/dev/./urandom可绕过。- Tomcat 进程持有
catalina.out的文件句柄,create方式切割后 Tomcat 仍写旧 inode,新文件永远为空;copytruncate复制后截断原文件,句柄不变。rm后空间不释放(句柄仍引用已删 inode),且日志再也看不到,必须重启才恢复。 reloadable="true"会有后台线程持续扫描WEB-INF/classes与lib,消耗 CPU 且触发意外重载中断服务;autoDeploy="true"会周期扫描appBase,误放文件即被自动部署,存在安全与稳定性风险。
14.1 课后思考题
-
如何让 Session ID 发生变化?
- 清除浏览器 Cookie / 缓存后重新访问;
- 或调用
session.invalidate()销毁当前会话后重新请求; - 调用
request.changeSessionId()(Servlet 3.1+)主动更换; - Memcached 中执行
flush_all清空服务端会话,再次访问也会生成新的 Session ID; - 重启 Tomcat(未做持久化或集中式存储时)。
-
若多台 Tomcat 家目录内容不一致,有哪些方案保证一致?
- 远程实时
rsync同步(配合inotify/sersync实现实时触发); - NFS / DRBD 等共享存储;
- 分布式存储(GlusterFS、Ceph);
- CI/CD 统一发布(Jenkins + Ansible / 容器镜像),从源头保证一致,是最推荐的做法。
- 远程实时
-
为什么 JSP 第一次访问特别慢? JSP 需要先由 Jasper 翻译成 Servlet 的
.java源码,再用javac编译成.class(产物在work/目录),之后才加载执行。后续访问直接复用已编译的字节码,因此只有首次慢。可通过预编译(jspc/precompile)消除。 -
为什么生产环境不建议保留 manager 应用? manager 具备上传 WAR 并部署的能力,等价于远程代码执行入口。一旦弱口令被爆破或存在认证绕过漏洞,攻击者可直接上传恶意 WAR 获得服务器权限。若必须保留,需强口令 +
RemoteAddrValveIP 白名单 + HTTPS + 角色分离。
此处评论已关闭