avatar

青青子衿的拾枝杂谈

A text-focused Halo theme

  • 首页
  • linux基础
  • Linux系统
  • Linux高级
  • nginx
  • k8s
  • 网络
Home Nginx与Tomcat
文章

Nginx与Tomcat

Posted recently Updated recently
By 青青子衿
130~168 min read

Nginx 与 Tomcat 完全指南


目录

  • 第一部分:Nginx 篇
    • 1. Nginx 介绍 -- 你的网站为什么需要一位"前台接待员"?
    • 2. Nginx 基本功能 -- 从零搭建你的第一台 Web 服务器
    • 3. Nginx 模块 -- 给服务器装上"瑞士军刀"
    • 4. Rewrite 匹配 -- URL 的"变形术"
    • 5. 日志管理 -- 服务器的"行车记录仪"
    • 6. 反向代理 -- 你的私人"代购"
    • 7. 负载均衡 -- 网站的"交通警察"
    • 8. LNMP 架构 -- 搭建完整的网站"生态链"
  • 第二部分:Tomcat 篇
    • 9. Tomcat 介绍 -- Java 世界的"咖啡师"
    • 10. Tomcat 部署 -- 亲手搭建 Java Web 服务器
    • 11. 虚拟主机 -- 一台服务器托管多个网站
    • 12. 多实例 -- 让 Tomcat "影分身"
    • 13. 负载均衡 -- Nginx + Tomcat 集群作战

第一部分:Nginx 篇

1. Nginx 介绍 -- 你的网站为什么需要一位"前台接待员"?

生活中的问题

想象一下:你是学校图书馆的管理员。每天下课铃一响,200 个学生同时涌向前台,只有你一个人在服务台。你既要找书、借书、还书、回答问题,忙得不可开交——这就是没有 Nginx 的 Web 服务器面临的困境。

生活类比:Nginx 就像学校图书馆的"前台接待员",它负责接待每一位"来访者"(HTTP 请求),根据需求把访客引到正确的"书架"(后端服务),还能同时处理成千上万的请求而不手忙脚乱。

Nginx 是什么?

Nginx(读作 "engine x")是一款高性能的 HTTP 服务器和反向代理服务器。它由俄罗斯程序员 Igor Sysoev 于 2004 年开发,最初是为了解决俄罗斯第二大网站 Rambler.ru 的高并发问题。

核心特征:

  • 占用内存极少
  • 并发能力极强(单机可支持 50,000+ 并发连接)
  • 事件驱动的非阻塞架构
  • 模块化设计,功能可扩展

谁在用? 百度、新浪、网易、腾讯、淘宝、京东......中国最大的互联网公司几乎都在使用。

事件驱动模型 -- Nginx 为什么能"一心多用"?

让我们用数学来理解。假设你有 100 个学生同时来借书:

模型类比资源消耗
多进程模型(Apache)每个借书请求都叫一个新老师来处理100 个老师 = 100 倍资源
事件驱动模型(Nginx)一个老师拿着对讲机,轮流响应每个人1 个老师 = 极低资源

这就是 Nginx 的事件驱动 + 非阻塞 I/O 模型:一个 worker 进程可以同时处理成千上万个连接,而不需要为每个连接创建新的进程或线程。

传统模型(Apache/prefork):
  连接1 → 进程1 → 等待响应 → 完成
  连接2 → 进程2 → 等待响应 → 完成
  连接3 → 进程3 → 等待响应 → 完成
  (100 个连接 = 100 个进程)

Nginx 模型(事件驱动):
  Worker 进程
    ├─ 连接1 → 注册事件 → 处理 → 完成
    ├─ 连接2 → 注册事件 → 处理 → 完成
    ├─ 连接3 → 注册事件 → 处理 → 完成
    └─ ... 连接N
  (100 个连接 = 1 个进程)

Nginx vs Apache

对比维度NginxApache
架构模型事件驱动(epoll/kqueue)多进程/多线程(prefork/worker)
静态资源处理极快(sendfile 零拷贝)较慢
内存占用低高
动态内容需搭配 PHP-FPM 等可内置模块处理
配置文件简洁直观功能强大但复杂
高并发优势明显连接数过多时性能下降
动态加载模块不支持(需重新编译)支持(DSO 机制)

结论:Nginx 适合做"前端入口"(静态资源 + 反向代理),Apache 适合做"后端处理"(动态内容)。在实际生产中,两者经常搭配使用。

一次 Web 请求的完整过程

当你在浏览器输入 http://www.example.com/index.html 时,服务器做了什么?

1. 建立连接(TCP 三次握手)
2. 接受请求(解析 HTTP 报文)
3. 处理请求(路由匹配、权限检查)
4. 访问资源(读取文件、执行脚本)
5. 构建响应(组装 HTTP 响应报文)
6. 发送响应(返回 HTML 内容)
7. 记录日志(写入 access_log)

HTTP 状态码速查表

状态码含义生活类比
200成功找到了你要的书
301永久重定向这本书换了新书架,以后去新位置找
302临时重定向这本书暂时放在别处,下次还来这找
304未修改这本书和上次看到的一样,不用重借
403禁止访问你没有权限进入这个阅览室
404未找到图书馆没有这本书
500服务器内部错误图书馆系统崩溃了
502网关错误前台找不到后台的工作人员
503服务不可用图书馆正在装修,暂时不开放

互动问题:如果你打开一个网页看到 "502 Bad Gateway",你能判断问题出在 Nginx 还是后端服务器吗?

答案:问题出在后端服务器。502 表示 Nginx(网关/代理)尝试从上游服务器获取数据但失败了,说明 Nginx 本身正常工作,但后端服务不可用或返回了无效响应。


2. Nginx 基本功能 -- 从零搭建你的第一台 Web 服务器

问题:我想在学校机房搭建一个网站,第一步该做什么?

2.1 安装 Nginx

前置条件:安装编译所需的依赖

# 安装编译依赖
yum install -y pcre-devel openssl-devel gcc make

# 创建运行 Nginx 的专用用户(不允许登录,不创建家目录)
useradd -s /sbin/nologin -M nginx

# 下载并解压源码
tar -zxvf nginx-1.24.0.tar.gz -C /usr/local/src
cd /usr/local/src/nginx-1.24.0

# 编译安装(--prefix 指定安装目录)
./configure \
  --user=nginx \
  --group=nginx \
  --prefix=/usr/local/nginx \
  --with-http_stub_status_module \
  --with-http_ssl_module

make && make install

编译参数解释:

参数作用
--user=nginx指定 worker 进程的运行用户
--prefix=/usr/local/nginx安装目录
--with-http_stub_status_module启用状态监控页面
--with-http_ssl_module启用 HTTPS(SSL)支持

2.2 目录结构

/usr/local/nginx/
├── conf/           # 配置文件目录
│   ├── nginx.conf  # 主配置文件(最重要!)
│   ├── mime.types  # MIME 类型映射
│   └── fastcgi_params
├── html/           # 默认网站根目录
│   ├── index.html
│   └── 50x.html
├── logs/           # 日志目录
│   ├── access.log  # 访问日志
│   ├── error.log   # 错误日志
│   └── nginx.pid   # 主进程 PID
└── sbin/           # 可执行文件
    └── nginx       # Nginx 主程序

2.3 常用命令

# 启动 Nginx
/usr/local/nginx/sbin/nginx

# 测试配置文件语法(修改配置后必做!)
/usr/local/nginx/sbin/nginx -t

# 平滑重载配置(不中断服务)
/usr/local/nginx/sbin/nginx -s reload

# 优雅停止
/usr/local/nginx/sbin/nginx -s quit

# 强制停止
/usr/local/nginx/sbin/nginx -s stop

# 查看版本
/usr/local/nginx/sbin/nginx -v

# 查看编译参数
/usr/local/nginx/sbin/nginx -V

# 指定配置文件启动
/usr/local/nginx/sbin/nginx -c /path/to/custom.conf

信号与操作对照表:

信号操作类比
HUP重载配置给接待员一张新的工作安排表
QUIT从容停止等所有客人离开后再关门
TERM/INT快速停止立刻关门下班
USR1重新打开日志换一本新的记录本
USR2平滑升级新老交接,无缝过渡

2.4 配置 Systemd 服务(推荐)

为了让 Nginx 像系统服务一样管理(systemctl start/stop/restart),创建服务文件:

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

写入以下内容:

[Unit]
Description=Nginx HTTP Server
After=network.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s quit
PrivateTmp=true

[Install]
WantedBy=multi-user.target
# 重载 systemd 并启用开机自启
systemctl daemon-reload
systemctl enable nginx
systemctl start nginx
systemctl status nginx

2.5 虚拟主机 -- 一台服务器跑多个网站

生活类比:虚拟主机就像一栋写字楼里开了多家公司,每家公司有自己的门牌号(域名)和办公室(网站目录),但共用同一个地址(服务器 IP)。

三种虚拟主机类型:

方式一:基于域名(最常用)

# www.youku.com 和 www.aiqiyi.com 共用同一个 IP
server {
    listen 80;
    server_name www.youku.com;
    location / {
        root /srv/youku;
        index index.html;
    }
}

server {
    listen 80;
    server_name www.aiqiyi.com;
    location / {
        root /srv/aiqiyi;
        index index.html;
    }
}

方式二:基于端口

server {
    listen 80;
    location / {
        root /srv/site1;
        index index.html;
    }
}

server {
    listen 8080;
    location / {
        root /srv/site2;
        index index.html;
    }
}

方式三:基于 IP

server {
    listen 192.168.1.100:80;
    location / {
        root /srv/site1;
        index index.html;
    }
}

server {
    listen 192.168.1.101:80;
    location / {
        root /srv/site2;
        index index.html;
    }
}

2.6 访问控制

IP 黑白名单:

# 只允许特定 IP 访问管理页面
location /admin {
    allow 192.168.1.100;   # 管理员 IP
    allow 10.0.0.0/24;     # 内网段
    deny all;              # 拒绝其他所有人
}

用户认证(Basic Auth):

# 创建密码文件
htpasswd -c -b /usr/local/nginx/passwd.db admin 123456
# 追加用户
htpasswd -b /usr/local/nginx/passwd.db teacher 654321
location /admin {
    auth_basic "Please Login";
    auth_basic_user_file /usr/local/nginx/passwd.db;
}

互动问题:如果你同时配置了 allow 和 auth_basic,Nginx 会先检查哪个?

答案:Nginx 按配置顺序依次检查。如果 allow/deny 先拒绝了请求,就不会再弹出密码输入框。建议先做 IP 过滤,再做密码认证,这样可以减少暴力破解攻击。


3. Nginx 模块 -- 给服务器装上"瑞士军刀"

问题:Nginx 功能不够用怎么办?

Nginx 的核心程序很小,但通过模块可以扩展出丰富的功能——就像给一把基础刀装上开瓶器、螺丝刀、剪刀等附件。

3.1 模块分类

类别说明示例
核心模块Nginx 基础功能events, http, mail
标准 HTTP 模块编译时可选的内置模块ssl, stub_status, realip
第三方模块社区开发的外部模块pagespeed, cache_purge

3.2 常用内置模块详解

SSL 模块 -- 让网站支持 HTTPS

HTTPS 就像给你的信件加了一个密封信封,即使在路上被拦截,别人也看不到内容。

# 首先生成自签名证书(测试用)
# openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
#   -keyout /etc/pki/tls/certs/server.key \
#   -out /etc/pki/tls/certs/server.crt

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

    ssl_certificate     /etc/pki/tls/certs/server.crt;
    ssl_certificate_key /etc/pki/tls/certs/server.key;

    ssl_session_timeout 5m;
    ssl_session_cache shared:SSL:1m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        root html;
        index index.html;
    }
}

Status 模块 -- 查看服务器实时状态

location /nginx_status {
    stub_status on;
    access_log off;
    allow 192.168.1.100;   # 只允许管理员查看
    deny all;
}

访问该页面会看到:

Active connections: 291
server accepts handled requests
 16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106
指标含义
Active connections当前活跃连接数
accepts已接受的总连接数
handled已处理的总连接数
requests总请求数
Reading正在读取请求头的连接数
Writing正在发送响应的连接数
Waiting空闲的 keep-alive 连接数

autoindex 模块 -- 显示目录列表

location /files {
    alias /data/shared/;
    autoindex on;
    autoindex_exact_size off;    # 显示文件大小为 KB/MB
    autoindex_localtime on;      # 显示本地时间
}

limit_rate 模块 -- 限速

location /download {
    limit_rate_after 10m;  # 前 10MB 不限速
    limit_rate 100k;       # 之后限速 100KB/s
}

# 针对特定浏览器限速
if ($http_user_agent ~* Chrome) {
    limit_rate 20k;
}

limit_conn 模块 -- 限制并发连接数

http {
    # 在 http 段定义共享内存区域
    limit_conn_zone $binary_remote_addr zone=addr:10m;
}

server {
    location /download {
        limit_conn addr 1;   # 每个 IP 最多 1 个并发连接
        limit_rate 512k;     # 限速 512KB/s
    }
}

3.3 安装第三方模块

注意:Nginx 不支持动态加载模块,安装第三方模块需要重新编译。

场景一:全新安装时加入模块

./configure \
  --prefix=/usr/local/nginx \
  --with-http_stub_status_module \
  --with-http_ssl_module \
  --with-http_realip_module \
  --add-module=/path/to/third-party-module

make && make install

场景二:已安装 Nginx 后追加模块

# 1. 进入源码目录重新 configure(加上原来的参数和新模块)
./configure \
  --prefix=/usr/local/nginx \
  --with-http_stub_status_module \
  --with-http_ssl_module \
  --add-module=/path/to/new-module

# 2. 只编译,不要 make install(否则会覆盖配置文件!)
make

# 3. 备份旧程序
cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak

# 4. 用新编译的程序替换旧程序
cp objs/nginx /usr/local/nginx/sbin/nginx

# 5. 重启 Nginx
/usr/local/nginx/sbin/nginx -s stop
/usr/local/nginx/sbin/nginx

server_tokens -- 隐藏版本信息

# 隐藏 Nginx 版本号,提高安全性
http {
    server_tokens off;
}

开启前响应头:Server: nginx/1.24.0
开启后响应头:Server: nginx


4. Rewrite 匹配 -- URL 的"变形术"

问题:网站改版了,旧链接全部失效怎么办?

生活类比:Rewrite 就像邮局的"邮件转寄"服务。你家搬家了,但只要在邮局登记新地址,寄到旧地址的信件都会自动转寄到新家。

4.1 Rewrite 是什么?

Rewrite(URL 重写)是将用户请求的 URL 在服务端自动转换为另一个 URL 的技术。常见用途:

  1. URL 伪静态化:将 news.php?id=123 伪装成 news/123.html,更美观也更安全
  2. 网站迁移跳转:旧域名的链接自动跳转到新域名
  3. 防盗链:阻止其他网站直接引用你的图片/文件

4.2 正则表达式基础

Nginx 的 rewrite 使用正则表达式来匹配 URL,以下是常用语法:

语法含义示例
.匹配任意单个字符a.c 匹配 abc, axc
*前一个字符重复 0 次或多次ab* 匹配 a, ab, abb
+前一个字符重复 1 次或多次ab+ 匹配 ab, abb
?前一个字符重复 0 次或 1 次ab? 匹配 a, ab
^匹配行首^/test 匹配以 /test 开头的路径
$匹配行尾\.html$ 匹配以 .html 结尾的路径
()分组捕获(\d+) 捕获数字
\d匹配数字\d{3} 匹配三位数字
.*匹配任意字符任意次数相当于"万能匹配"

4.3 四种 Flag 标记

rewrite 正则表达式 替换目标 [flag];
FlagHTTP 状态码效果适用场景
last内部重写完成当前规则后,重新匹配 location服务器内部 URL 重写
break内部重写完成当前规则后,停止后续匹配在 location 块内使用
redirect302 临时重定向浏览器地址栏变为新 URL临时跳转
permanent301 永久重定向浏览器地址栏变为新 URL永久跳转(SEO 友好)

301 vs 302 的区别:301 告诉搜索引擎"这个页面永久搬家了",搜索引擎会把权重转移到新地址。302 是临时跳转,搜索引擎仍会保留旧地址的索引。对 SEO 来说,选错状态码可能导致搜索排名下降。

4.4 实战案例

案例 1:整站域名迁移

需求:www.old.com 的所有请求跳转到 www.new.com
server {
    listen 80;
    server_name www.old.com;

    location / {
        rewrite ^/(.*)$ http://www.new.com/$1 permanent;
    }
}

案例 2:URL 路径重写

需求:/2015/xxx.html --> /2024/xxx.html
location /2015 {
    rewrite ^/2015/(.*)$ /2024/$1 permanent;
}

案例 3:提取参数重写

需求:/login/robin.html --> /reg/login.php?user=robin
location /login {
    rewrite ^/login/(.*)\.html$ /reg/login.php?user=$1 permanent;
}

案例 4:多段数字重组

需求:/uplook/11-22-33.html --> /uplook/11/22/33.html
location /uplook {
    rewrite ^/uplook/(\d+)-(\d+)-(\d+)\.html$ /uplook/$1/$2/$3.html permanent;
}

案例 5:二级域名泛解析跳转

需求:robin.example.com --> www.example.com/robin
      zorro.example.com --> www.example.com/zorro
# DNS 泛解析:*.example.com  IN  A  服务器IP
server {
    listen 80;
    server_name *.example.com;

    if ($host ~* "^www\.example\.com$") {
        break;
    }
    if ($host ~* "^(.*)\.example\.com$") {
        set $user $1;
        rewrite ^.*$ http://www.example.com/$user permanent;
    }
}

案例 6:防盗链

# 只允许自己网站的页面引用 .txt 文件
location ~* \.(txt|jpg|png|gif)$ {
    valid_referers none blocked www.example.com;
    if ($invalid_referer) {
        rewrite ^/ http://www.example.com/forbidden.html;
        # 或者直接返回 403
        # return 403;
    }
}

案例 7:return 指令 -- 直接返回状态码

# 禁止访问 .sh 结尾的文件
location ~* \.sh$ {
    return 403;
}

# 禁止访问 .env 文件
location ~* \.env$ {
    return 403;
}

4.5 if 条件判断

# 判断文件是否存在
if (!-f $request_filename) {
    rewrite ^/(.*)$ /404.html last;
}

# 判断目录是否存在
if (!-d $request_filename) {
    # 自动补全目录末尾的斜杠
    rewrite ^(.*)([^/])$ http://$host$1$2/ permanent;
}

常用 Nginx 变量:

变量含义
$host请求的主机名
$uri当前请求的 URI(不含参数)
$request_uri完整的请求 URI(含参数)
$remote_addr客户端 IP
$request_method请求方法(GET/POST)
$args请求参数
$document_root当前请求的根目录

互动问题:为什么 last 和 break 不会改变浏览器地址栏的 URL,而 redirect 和 permanent 会?

答案:last 和 break 是服务器内部的 URL 重写,客户端完全不知道服务器"偷偷"换了文件。而 redirect(302)和 permanent(301)会返回一个重定向响应给浏览器,浏览器收到后会主动发起新请求,所以地址栏会变化。


5. 日志管理 -- 服务器的"行车记录仪"

问题:网站被攻击了,怎么追查是谁干的?

生活类比:日志就像学校的"来访登记簿",记录了每个来访者的姓名(IP)、来访时间、去了哪个房间(URL)、待了多久(响应时间)。出了问题,翻登记簿就能查到线索。

5.1 两种日志类型

日志类型记录内容文件位置
access_log每一次成功的访问请求/usr/local/nginx/logs/access.log
error_log错误、警告、调试信息/usr/local/nginx/logs/error.log

5.2 自定义日志格式

http {
    # 定义名为 main 的日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    # 使用 main 格式记录访问日志
    access_log logs/access.log main;

    # 错误日志(级别可选:debug, info, notice, warn, error, crit)
    error_log logs/error.log warn;
}

日志输出示例:

192.168.1.100 - - [15/Dec/2024:10:30:00 +0800] "GET /index.html HTTP/1.1" 200 1024 "-" "Mozilla/5.0" "-"
字段值含义
$remote_addr192.168.1.100客户端 IP
$remote_user-认证用户名
$time_local15/Dec/2024:10:30:00请求时间
$requestGET /index.html HTTP/1.1请求行
$status200HTTP 状态码
$body_bytes_sent1024发送的字节数
$http_referer-来源页面
$http_user_agentMozilla/5.0浏览器信息

5.3 日志切割(轮转)

日志文件会越来越大,需要定期切割归档。

创建轮转配置文件 /usr/local/nginx/nginx_log_rotate.conf:

/usr/local/nginx/logs/access.log
/usr/local/nginx/logs/error.log {
    daily
    rotate 30
    missingok
    notifempty
    compress
    dateext
    sharedscripts
    postrotate
        /bin/kill -USR1 `cat /usr/local/nginx/logs/nginx.pid 2>/dev/null` 2>/dev/null || true
    endscript
}

设置定时任务:

# 每天凌晨 2 点执行日志切割
crontab -e
0 2 * * * /usr/sbin/logrotate -f /usr/local/nginx/nginx_log_rotate.conf

说明:kill -USR1 信号告诉 Nginx 重新打开日志文件,这样切割后 Nginx 会写入新的日志文件。

5.4 日志分析实战

# 统计访问量最高的前 10 个 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10

# 统计某一天的总 PV(页面浏览量)
grep '15/Dec/2024' access.log | wc -l

# 统计不同 IP 的访问量(按访问量排序)
grep '15/Dec/2024' access.log | \
  awk '{ips[$1]++} END {for(i in ips) print i, ips[i]}' | \
  sort -k2 -rn | head -20

# 找出访问量超过 100 次的 IP(可能是爬虫或攻击)
grep '15/Dec/2024' access.log | \
  awk '{ips[$1]++} END {for(i in ips) if(ips[i]>100) print i, ips[i]}' | \
  sort -k2 -rn

# 统计访问量最高的前 10 个页面
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -10

# 统计 HTTP 状态码分布
awk '{print $9}' access.log | sort | uniq -c | sort -rn

互动问题:如果你发现某个 IP 一天内访问了你的网站 50000 次,你应该怎么做?

答案:首先通过日志确认该 IP 的请求特征(User-Agent、访问路径),判断是正常爬虫还是恶意攻击。如果是恶意攻击,可以在 Nginx 中添加 deny 规则封禁该 IP,或者使用 limit_conn 和 limit_rate 限制其访问频率。


6. 反向代理 -- 你的私人"代购"

问题:后端服务器在内网,用户无法直接访问怎么办?

生活类比:反向代理就像你的私人"代购"。你想买国外的东西,但不能直接联系卖家,于是把需求告诉代购,代购帮你下单、收货、再转寄给你。对你来说,代购就是卖家;对卖家来说,代购就是买家。

正向代理 vs 反向代理:

正向代理(代理客户端):
  你 → [代理] → 目标服务器
  (代理隐藏了你的身份,服务器不知道你是谁)
  例:VPN、翻墙工具

反向代理(代理服务器端):
  客户端 → [反向代理] → 后端服务器
  (代理隐藏了服务器的身份,客户端不知道真正的服务器是谁)
  例:CDN、Nginx 反向代理

6.1 基本反向代理配置

http {
    upstream backend {
        server 192.168.10.201:80;
    }

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

        location / {
            proxy_pass http://backend;

            # 传递真实信息给后端
            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_pass指定后端服务器地址
proxy_set_header Host $host将原始主机名传递给后端(否则后端收到的是 proxy_pass 中的地址)
proxy_set_header X-Real-IP传递客户端真实 IP
proxy_set_header X-Forwarded-For传递完整的代理链路 IP

6.2 反向代理 + 缓存

缓存是提高网站速度的最有效手段之一。

http {
    # 定义缓存存储路径和参数
    proxy_cache_path /usr/local/nginx/proxy_cache \
        levels=1:2 \
        keys_zone=my_cache:500m \
        inactive=1d \
        max_size=1g;

    proxy_temp_path /usr/local/nginx/proxy_temp;

    upstream backend {
        server 192.168.10.201:80;
    }

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

        # 添加响应头,显示缓存状态
        add_header X-Cache $upstream_cache_status;
        add_header X-via $server_addr;

        location / {
            proxy_cache my_cache;
            proxy_cache_valid 200 302 10m;   # 200/302 响应缓存 10 分钟
            proxy_cache_valid 404 1m;         # 404 响应缓存 1 分钟
            proxy_cache_valid any 1m;
            proxy_cache_key $host$uri$is_args$args;

            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            expires 1d;
        }

        # 手动清除缓存(需安装 ngx_cache_purge 模块)
        location ~ /purge(/.*) {
            allow 127.0.0.1;
            allow 192.168.10.100;
            deny all;
            proxy_cache_purge my_cache $host$1$is_args$args;
        }
    }
}

缓存状态值说明:

状态含义
MISS未命中缓存,从后端获取
HIT命中缓存,直接返回
EXPIRED缓存已过期,重新从后端获取
UPDATING缓存正在更新
STALE返回过期的缓存内容(后端不可用时)

测试缓存是否生效:

# 第一次请求:应该看到 X-Cache: MISS
curl -I http://www.example.com/index.html

# 第二次请求:应该看到 X-Cache: HIT
curl -I http://www.example.com/index.html

6.3 动态请求不缓存

# JSP、PHP 等动态页面直接代理,不走缓存
location ~* \.(jsp|php|asp)$ {
    proxy_pass http://backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

6.4 WebSocket 支持

location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;   # WebSocket 长连接超时
}

7. 负载均衡 -- 网站的"交通警察"

问题:一个服务器扛不住这么多请求怎么办?

生活类比:负载均衡就像十字路口的"交通警察"。当一条车道堵了,警察会把车辆引导到其他车道,确保每条路都均匀分担车流量。在学校门口,保安也会引导家长分不同入口进入,避免拥堵。

                    ┌─── 服务器 A (192.168.8.101)
                    │
用户 → [Nginx LB] ──┼─── 服务器 B (192.168.8.102)
                    │
                    └─── 服务器 C (192.168.8.103)

7.1 五种负载均衡策略

策略一:轮询(默认)

每个请求按时间顺序依次分配给后端服务器,最简单也最公平。

upstream backend {
    server 192.168.0.14:80;
    server 192.168.0.15:80;
}

策略二:权重(Weight)

性能好的服务器分配更多请求。

upstream backend {
    server 192.168.0.14:80 weight=10;  # 高配服务器,承担 2/3
    server 192.168.0.15:80 weight=5;   # 低配服务器,承担 1/3
}

数学理解:假设 weight 分别为 10 和 5,总权重为 15。服务器 A 分到 10/15 = 66.7% 的请求,服务器 B 分到 5/15 = 33.3%。这就是一个简单的比例分配问题。

策略三:IP Hash

同一个 IP 的用户始终访问同一台后端服务器(解决 Session 问题)。

upstream backend {
    ip_hash;
    server 192.168.0.14:80;
    server 192.168.0.15:80;
}

原理:对客户端 IP 做 Hash 运算,结果对服务器数量取模,确保同一个 IP 总是落在同一台服务器上。就像学校按学生学号的最后一位数字分班——0-4 去 A 班,5-9 去 B 班。

策略四:最少连接(Least Connections)

把请求分配给当前连接数最少的服务器。

upstream backend {
    least_conn;
    server 192.168.0.14:80;
    server 192.168.0.15:80;
}

策略五:Fair(第三方模块)

按后端服务器响应时间分配,响应最快的优先。

upstream backend {
    server 192.168.0.14:80;
    server 192.168.0.15:80;
    fair;
}

7.2 服务器状态参数

upstream backend {
    server 192.168.0.14:80 weight=5 max_fails=3 fail_timeout=30s;
    server 192.168.0.15:80 weight=3 max_fails=3 fail_timeout=30s;
    server 192.168.0.16:80 backup;     # 备用服务器
    server 192.168.0.17:80 down;       # 暂时下线
}
参数默认值含义
weight1权重,越大分配越多请求
max_fails1允许的最大失败次数
fail_timeout10s达到 max_fails 后暂停调度的时间
backup-备用机,其他服务器都不可用时才启用
down-标记为下线,不参与负载均衡

7.3 完整负载均衡配置示例

# 按请求类型分流到不同的后端集群
upstream html_servers {
    ip_hash;
    server 192.168.22.69:80;
    server 192.168.22.30:80;
}

upstream php_servers {
    ip_hash;
    server 192.168.22.11:80;
    server 192.168.22.12:80;
}

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

    location / {
        root html;
        index index.html index.htm;

        # HTML 请求分发到 HTML 服务器集群
        if ($request_uri ~* \.html$) {
            proxy_pass http://html_servers;
        }

        # PHP 请求分发到 PHP 服务器集群
        if ($request_uri ~* \.php$) {
            proxy_pass http://php_servers;
        }
    }

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

互动问题:为什么在 Session 没有共享的情况下必须使用 ip_hash?如果不使用会怎样?

答案:用户登录网站后,Session(会话信息,如登录状态)通常保存在某一台后端服务器上。如果下次请求被轮询到了另一台服务器,那台服务器上没有你的 Session,就会认为你没登录,导致反复被踢到登录页。ip_hash 确保同一用户总是访问同一台服务器,Session 就不会丢失。


8. LNMP 架构 -- 搭建完整的网站"生态链"

问题:怎么从零搭建一个能跑 PHP + MySQL 的完整网站?

生活类比:LNMP 就像开一家餐厅的四个关键角色:

  • Linux = 餐厅所在的大楼(操作系统基础设施)
  • Nginx = 前台接待员(接收客人点单,转给后厨)
  • MySQL = 仓库管理员(管理所有食材/数据)
  • PHP = 厨师(根据菜单烹饪出菜品/动态页面)
用户浏览器 → Nginx (80端口)
               ├── 静态文件(.html/.css/.js) → 直接返回
               └── 动态文件(.php) → PHP-FPM (9000端口) → MySQL (3306端口)

8.1 安装步骤总览

步骤组件端口角色
1Linux (CentOS/Ubuntu)-操作系统
2Nginx80Web 服务器
3MySQL/MariaDB3306数据库
4PHP + PHP-FPM9000动态内容处理

8.2 安装 PHP-FPM

PHP-FPM(FastCGI Process Manager)是 PHP 的进程管理器,就像一个"厨房调度员",管理多个 PHP 进程来处理并发请求。

# 安装 PHP 编译依赖
yum -y install libjpeg libjpeg-devel libpng libpng-devel \
  freetype freetype-devel libxml2 libxml2-devel \
  libcurl libcurl-devel libxslt-devel openssl-devel

# 编译安装 PHP
tar xf php-7.4.33.tar.gz
cd php-7.4.33

./configure \
  --prefix=/usr/local/php \
  --with-curl \
  --with-freetype-dir \
  --with-gd \
  --with-gettext \
  --with-iconv-dir \
  --with-jpeg-dir \
  --with-libxml-dir \
  --with-mysqli \
  --with-openssl \
  --with-pcre-regex \
  --with-pdo-mysql \
  --with-png-dir \
  --with-zlib \
  --enable-fpm \
  --enable-bcmath \
  --enable-mbstring \
  --enable-opcache \
  --enable-pcntl \
  --enable-sockets \
  --enable-xml

make && make install

配置文件:

# PHP-FPM 进程配置(运维人员关注:进程数、最大连接数等)
cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf

# PHP 运行配置(开发者关注:上传大小、时区、扩展功能等)
cp php.ini-production /usr/local/php/lib/php.ini

启动 PHP-FPM:

# 方式一:直接启动
/usr/local/php/sbin/php-fpm

# 方式二:配置 Systemd 服务
cp sapi/fpm/php-fpm.service /usr/lib/systemd/system/
systemctl daemon-reload
systemctl start php-fpm
systemctl enable php-fpm

# 验证
netstat -tnlp | grep :9000
# 应看到:tcp  0  0  127.0.0.1:9000  0.0.0.0:*  LISTEN  php-fpm

8.3 配置 Nginx 对接 PHP-FPM

编辑 /usr/local/nginx/conf/nginx.conf:

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

    # 设置网站根目录和默认首页
    location / {
        root html;
        index index.php index.html index.htm;
    }

    # 所有 .php 请求交给 PHP-FPM 处理
    location ~ \.php$ {
        root html;

        # 方式一:TCP 连接(推荐,跨网络可用)
        fastcgi_pass 127.0.0.1:9000;

        # 方式二:Unix Socket 连接(本机更快)
        # fastcgi_pass unix:/dev/shm/php-fpm.sock;

        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

如果使用 Socket 方式,需同时修改 PHP-FPM 配置:

# /usr/local/php/etc/php-fpm.conf
; listen = 127.0.0.1:9000
listen = /dev/shm/php-fpm.sock

8.4 重启所有服务

# 重载 Nginx
/usr/local/nginx/sbin/nginx -s reload

# 重启 PHP-FPM
systemctl restart php-fpm

# 验证端口
netstat -tnlp | grep -E ':(80|9000|3306)'

8.5 测试验证

测试 1:PHP 能否正常运行

# 创建测试文件
cat > /usr/local/nginx/html/info.php << 'EOF'
<?php
phpinfo();
?>
EOF

浏览器访问 http://服务器IP/info.php,如果看到 PHP 信息页面,说明 Nginx + PHP-FPM 整合成功。

测试 2:PHP 能否连接 MySQL

cat > /usr/local/nginx/html/db_test.php << 'EOF'
<?php
$link = mysqli_connect('127.0.0.1', 'root', 'your_password');
if ($link) {
    echo "MySQL 连接成功!";
} else {
    echo "MySQL 连接失败:" . mysqli_connect_error();
}
mysqli_close($link);
?>
EOF

测试 3:部署实际应用(以 Discuz 论坛为例)

# 创建数据库和用户
mysql -uroot -p123456 -e "CREATE DATABASE bbs; \
  GRANT ALL ON bbs.* TO 'bbsuser'@'localhost' IDENTIFIED BY 'bbspass'; \
  FLUSH PRIVILEGES;"

# 上传 Discuz 程序到网站目录
# 通过浏览器访问安装向导完成安装

互动问题:在 LNMP 架构中,如果 Nginx 正常工作但 PHP 页面返回 "502 Bad Gateway",你会按什么顺序排查问题?

排查步骤:

  1. 检查 PHP-FPM 是否在运行:systemctl status php-fpm
  2. 检查 PHP-FPM 监听的端口/socket 是否与 Nginx 配置一致
  3. 检查 PHP-FPM 日志:/usr/local/php/var/log/php-fpm.log
  4. 检查 Nginx 的 fastcgi_param SCRIPT_FILENAME 路径是否正确
  5. 检查文件权限:Nginx 用户是否有权限读取 PHP 文件

第二部分:Tomcat 篇

9. Tomcat 介绍 -- Java 世界的"咖啡师"

问题:我写了一个 Java Web 应用,用什么来运行它?

生活类比:如果说 Nginx 是前台接待员(处理静态请求),那 Tomcat 就是一位专业的"咖啡师"——它专门处理 Java 程序的请求。你把咖啡豆(Java 代码)交给它,它帮你研磨、冲泡,最后端出一杯香浓的咖啡(动态网页)。

9.1 Tomcat 是什么?

Tomcat 是 Apache 软件基金会的开源 Java Web 应用服务器,它是开发和调试 JSP/Servlet 程序的首选平台。

核心概念:

概念解释类比
ServletJava 编写的服务器端程序,处理 HTTP 请求咖啡师的标准操作流程
JSP在 HTML 中嵌入 Java 代码的动态网页技术带定制选项的菜单
容器运行 Servlet/JSP 的环境咖啡机

9.2 Tomcat 内部架构

Server(服务器)
└── Service(服务)
    ├── Connector(连接器)
    │   ├── HTTP Connector (8080) ← 接收浏览器请求
    │   └── AJP Connector (8009) ← 接收 Apache/Nginx 转发请求
    └── Engine(引擎)
        └── Host(虚拟主机)
            └── Context(Web 应用)
                └── Servlet/JSP
组件作用
Server整个 Tomcat 实例
Service一组 Connector + 一个 Engine 的集合
Connector监听端口,接收请求并返回响应
Engine请求处理引擎
Host虚拟主机(一个域名对应一个 Host)
Context一个 Web 应用程序

9.3 一次请求的处理流程

当用户访问 http://localhost:8080/myapp/index.jsp 时:

1. 请求到达本机 8080 端口
2. HTTP Connector 接收请求,转交给 Engine
3. Engine 根据 "localhost" 匹配 Host
4. Host 根据 "/myapp" 匹配 Context
5. Context 根据 "/index.jsp" 匹配对应的 Servlet(JspServlet)
6. JspServlet 编译执行 JSP,生成 HTML
7. 结果沿原路返回给客户端

9.4 版本选择建议

Tomcat 版本Servlet 规范JSP 规范JDK 要求推荐场景
8.53.12.3JDK 7+传统项目
9.04.02.3JDK 8+主流选择
10.0+5.0+3.0+JDK 11+新项目(Jakarta EE)

注意:Tomcat 10+ 将 javax.* 包名改为 jakarta.*,老项目可能不兼容。对于学习和大多数生产场景,推荐使用 Tomcat 9.0。


10. Tomcat 部署 -- 亲手搭建 Java Web 服务器

问题:怎样在 Linux 服务器上运行 Java Web 应用?

10.1 安装 JDK

# 方式一:使用包管理器安装(简单)
yum -y install java-1.8.0-openjdk java-1.8.0-openjdk-devel

# 方式二:手动安装 Oracle JDK(生产环境推荐)
# 下载 jdk-8u202-linux-x64.tar.gz
tar xf jdk-8u202-linux-x64.tar.gz -C /usr/java/

配置环境变量:

vim /etc/profile

添加以下内容:

export JAVA_HOME=/usr/java/jdk1.8.0_202
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
# 使配置生效
source /etc/profile

# 验证
java -version

10.2 安装 Tomcat

# 下载并解压
tar xf apache-tomcat-9.0.85.tar.gz -C /usr/local/

# 创建软链接(方便后续升级)
ln -s /usr/local/apache-tomcat-9.0.85 /usr/local/tomcat

# 设置 Tomcat 环境变量
echo 'CATALINA_HOME=/usr/local/tomcat' >> /etc/profile
echo 'export CATALINA_HOME' >> /etc/profile
source /etc/profile

10.3 目录结构

/usr/local/tomcat/
├── bin/            # 启动/停止脚本
│   ├── startup.sh
│   ├── shutdown.sh
│   └── catalina.sh
├── conf/           # 配置文件
│   ├── server.xml       # 核心配置(端口、虚拟主机等)
│   ├── web.xml          # 全局 Servlet 映射
│   ├── tomcat-users.xml # 管理用户
│   └── context.xml      # 全局 Context 配置
├── lib/            # 共享 JAR 包
├── logs/           # 日志文件
├── temp/           # 临时文件
├── webapps/        # Web 应用部署目录
│   ├── ROOT/       # 默认应用(访问 / 时显示)
│   ├── manager/    # 管理界面
│   └── docs/       # 文档
├── work/           # JSP 编译缓存
└── LICENSE

10.4 三个关键端口

端口用途说明
8080HTTP 服务端口用户访问网站用这个
8005管理端口(Shutdown)用于远程关闭 Tomcat
8009AJP 端口与 Apache/Nginx 对接时使用

10.5 启动与停止

# 启动
/usr/local/tomcat/bin/startup.sh

# 停止
/usr/local/tomcat/bin/shutdown.sh

# 验证
netstat -tnlp | grep java
# 应看到三个端口:8005, 8009, 8080

10.6 配置 Systemd 服务

vim /etc/init.d/tomcat
#!/bin/bash
# chkconfig: 2345 96 14
# description: Tomcat Server

JAVA_OPTS='-Xms64m -Xmx128m'
JAVA_HOME=/usr/java/jdk1.8.0_202
CATALINA_HOME=/usr/local/tomcat
export JAVA_OPTS JAVA_HOME CATALINA_HOME

exec $CATALINA_HOME/bin/catalina.sh $*
chmod a+x /etc/init.d/tomcat
chkconfig --add tomcat
chkconfig tomcat on

# 测试
service tomcat start
service tomcat stop

10.7 配置管理用户

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

添加以下内容(在 </tomcat-users> 之前):

<role rolename="manager-gui"/>
<role rolename="manager-scripts"/>
<role rolename="manager-status"/>
<user username="tomcat" password="123456"
      roles="manager-gui,manager-scripts,manager-status"/>

重启 Tomcat 后,访问 http://服务器IP:8080/manager/html 即可看到管理界面。

10.8 连接 MySQL

Tomcat 默认不包含 MySQL 驱动,需要手动添加:

# 下载 MySQL Connector/J
tar xf mysql-connector-java-8.0.33.tar.gz
cd mysql-connector-java-8.0.33

# 复制 JAR 文件到 Tomcat 的 lib 目录
cp mysql-connector-java-8.0.33.jar /usr/local/tomcat/lib/

# 重启 Tomcat 生效
/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh

11. 虚拟主机 -- 一台服务器托管多个网站

问题:我有多个 Java Web 项目,要安装多个 Tomcat 吗?

不需要!就像 Nginx 可以在一台服务器上跑多个虚拟主机一样,Tomcat 也支持在同一实例中配置多个 Host,每个 Host 运行不同的 Web 应用。

11.1 配置 server.xml

编辑 /usr/local/tomcat/conf/server.xml,在 <Engine> 元素内添加 Host:

<!-- 网站一:博客 (www.blog.com) -->
<Host name="www.blog.com" appBase="webapps"
      unpackWARs="true" autoDeploy="true">
    <!-- Context path="" 表示这是默认应用 -->
    <Context path="" docBase="/abc" reloadable="true" />
    <Context path="/test" docBase="/usr/local/tomcat/webapps/test"
             reloadable="true" />

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

<!-- 网站二:论坛 (www.bbs.com) -->
<Host name="www.bbs.com" appBase="webapps"
      unpackWARs="true" autoDeploy="true">
    <!-- docBase="bbs" 指向 webapps/bbs 目录 -->
    <Context docBase="bbs" path="" />
</Host>

<!-- 网站三:社交 (www.sns.com) -->
<Host name="www.sns.com" appBase="/webroot"
      unpackWARs="true" autoDeploy="true">
    <Valve className="org.apache.catalina.valves.AccessLogValve"
           directory="logs"
           prefix="sns_access_log." suffix=".txt"
           pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>

11.2 关键属性说明

属性含义示例
name虚拟主机的域名www.blog.com
appBase应用基础目录webapps 或绝对路径
unpackWARs是否自动解压 WAR 包true
autoDeploy是否自动部署新应用true

Context 元素属性:

属性含义
pathURL 路径("" 表示根路径)
docBase应用的实际目录
reloadable是否监控文件变化自动重载(开发环境设为 true,生产设为 false)

11.3 URL 到目录的映射关系

以上配置完成后:

访问 URL实际目录
www.blog.com:8080//abc
www.blog.com:8080/test//usr/local/tomcat/webapps/test
www.bbs.com:8080/webapps/bbs
www.sns.com:8080//webroot/ROOT

11.4 部署实战:论坛网站

# 1. 创建数据库
mysql -uroot -p123 -e "CREATE DATABASE bbs;"

# 2. 导入表结构
mysql -uroot -p123 bbs < easyjforum_mysql.sql

# 3. 部署应用文件
mkdir /usr/local/tomcat/webapps/bbs
cp -rf ejforum/* /usr/local/tomcat/webapps/bbs/

# 4. 修改应用数据库连接配置
# 编辑 /usr/local/tomcat/webapps/bbs/WEB-INF/conf/config.xml
# 修改 username, password, url 等字段

# 5. 重启 Tomcat
/usr/local/tomcat/bin/shutdown.sh
/usr/local/tomcat/bin/startup.sh

互动问题:reloadable="true" 在开发环境很方便,为什么生产环境要设为 false?

答案:reloadable="true" 会让 Tomcat 持续监控应用目录的文件变化,一旦检测到变化就自动重新加载应用。这个过程会消耗 CPU 资源并导致短暂的服务中断。在生产环境中,应用更新频率低,且每次重载都会影响用户体验,所以应设为 false,改用手动重启或热部署方案。


12. 多实例 -- 让 Tomcat "影分身"

问题:我需要在一台服务器上运行多个独立的 Tomcat 实例

生活类比:多实例就像一个公司在一栋大楼里开了多家分店。每家店(实例)有自己的收银台(端口)、仓库(webapps)和账本(logs),但共用同一个品牌和管理制度(Tomcat 安装目录/bin/lib)。

使用场景:

  • 多实例运行不同的应用(类似虚拟主机,但更隔离)
  • 多实例运行相同的应用(实现负载均衡,提高并发能力)

12.1 核心概念:CATALINA_HOME vs CATALINA_BASE

变量含义指向
CATALINA_HOMETomcat 的安装目录共享的 bin/、lib/
CATALINA_BASE实例的工作目录独立的 conf/、logs/、webapps/、work/、temp/

关键理解:多个实例共享 CATALINA_HOME(安装目录),各自拥有独立的 CATALINA_BASE(工作目录)。

12.2 目录规划

/usr/local/tomcat/              # CATALINA_HOME (共享)
├── bin/                         # 启动脚本(共享)
├── lib/                         # JAR 包(共享)
└── multi-ins/
    ├── instance1/               # 实例 1 的 CATALINA_BASE
    │   ├── conf/                # 独立配置
    │   │   └── server.xml
    │   ├── logs/                # 独立日志
    │   ├── temp/
    │   ├── work/
    │   ├── webapps/             # 独立应用
    │   │   └── ROOT/
    │   └── ins1.sh              # 启动脚本
    ├── instance2/               # 实例 2 的 CATALINA_BASE
    │   ├── conf/server.xml
    │   ├── logs/
    │   ├── temp/
    │   ├── work/
    │   ├── webapps/ROOT/
    │   └── ins2.sh
    └── instance3/               # 实例 3 的 CATALINA_BASE
        └── ...

12.3 配置各实例的 server.xml

关键点:每个实例的端口必须不同!

mkdir -p /usr/local/tomcat/multi-ins/instance{1,2}/{conf,logs,temp,work,webapps/ROOT}

实例 1 的 server.xml (multi-ins/instance1/conf/server.xml):

<?xml version='1.0' encoding='utf-8'?>
<Server port="8091" shutdown="SHUTDOWN">

  <Service name="Catalina">
    <!-- HTTP 端口:8081 -->
    <Connector port="8081" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    <!-- 注释掉 AJP 连接器(不需要的话) -->
    <!-- <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> -->

    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost" appBase="/webapps"
            unpackWARs="true" autoDeploy="true">
      </Host>
    </Engine>
  </Service>
</Server>

实例 2 的 server.xml (multi-ins/instance2/conf/server.xml):

<?xml version='1.0' encoding='utf-8'?>
<Server port="8092" shutdown="SHUTDOWN">

  <Service name="Catalina">
    <!-- HTTP 端口:8082 -->
    <Connector port="8082" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost" appBase="/webapps"
            unpackWARs="true" autoDeploy="true">
      </Host>
    </Engine>
  </Service>
</Server>

端口规划对照表:

实例HTTP 端口Shutdown 端口
instance180818091
instance280828092
instance380838093

12.4 编写启动脚本

实例 1 的启动脚本 (multi-ins/instance1/ins1.sh):

#!/bin/bash
# Instance 1 启动脚本

CATALINA_HOME=/usr/local/tomcat
CATALINA_BASE=/usr/local/tomcat/multi-ins/instance1

export CATALINA_HOME CATALINA_BASE

# 设置 JVM 参数
JAVA_OPTS='-Xms64m -Xmx128m'
export JAVA_OPTS

case "$1" in
    start)
        $CATALINA_HOME/bin/startup.sh
        ;;
    stop)
        $CATALINA_HOME/bin/shutdown.sh
        ;;
    restart)
        $CATALINA_HOME/bin/shutdown.sh
        sleep 2
        $CATALINA_HOME/bin/startup.sh
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        exit 1
        ;;
esac
# 赋予执行权限
chmod +x /usr/local/tomcat/multi-ins/instance1/ins1.sh
chmod +x /usr/local/tomcat/multi-ins/instance2/ins2.sh

# 启动两个实例
/usr/local/tomcat/multi-ins/instance1/ins1.sh start
/usr/local/tomcat/multi-ins/instance2/ins2.sh start

# 验证
netstat -tnlp | grep java
# 应看到:
# :::8081  LISTEN  java  (实例1)
# :::8082  LISTEN  java  (实例2)
# :::8091  LISTEN  java  (实例1 shutdown端口)
# :::8092  LISTEN  java  (实例2 shutdown端口)

互动问题:如果你要在这台服务器上运行 5 个 Tomcat 实例,至少需要开放多少个端口?

答案:每个实例至少需要 2 个端口(HTTP 端口 + Shutdown 端口),如果使用 AJP 则还需额外 1 个端口。所以 5 个实例至少需要 10 个端口(不使用 AJP 的情况下)。


13. 负载均衡 -- Nginx + Tomcat 集群作战

问题:单个 Tomcat 扛不住高并发怎么办?

生活类比:就像一家火爆的餐厅只有一位厨师忙不过来,需要开多家分店,然后请一个"调度中心"(Nginx)来统一分配客人去哪家分店。

                    ┌─── Tomcat 实例 1 (8081)
                    │
用户 → [Nginx LB] ──┼─── Tomcat 实例 2 (8082)
                    │
                    └─── Tomcat 实例 3 (8083)

13.1 Nginx 配置

http {
    upstream tomcat_pool {
        ip_hash;    # 使用 ip_hash 保持会话
        server 192.168.122.43:8081 weight=1 max_fails=2 fail_timeout=2;
        server 192.168.122.43:8082 weight=1 max_fails=2 fail_timeout=2;
        server 192.168.122.43:8083 weight=1 max_fails=2 fail_timeout=2;
    }

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

        location / {
            proxy_pass http://tomcat_pool;
            proxy_redirect off;

            # 传递真实客户端信息
            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_connect_timeout 5s;
            proxy_read_timeout 60s;
            proxy_send_timeout 60s;
        }

        # 静态资源由 Nginx 直接处理,减轻 Tomcat 负担
        location ~* \.(jpg|jpeg|png|gif|css|js|ico|swf)$ {
            root /data/static;
            expires 7d;
        }
    }
}

13.2 Session 管理方案

在负载均衡环境下,Session(会话)管理是一个关键问题。有三种主要方案:

方案一:会话保持(最简单)

upstream tomcat_pool {
    ip_hash;   # 同一个 IP 始终访问同一台 Tomcat
    server 192.168.122.43:8081;
    server 192.168.122.43:8082;
}
  • 优点:配置简单,无需修改应用代码
  • 缺点:如果某台 Tomcat 宕机,其上的 Session 就丢失了

方案二:Session 复制(Tomcat 集群原生支持)

在 Tomcat 的 server.xml 中配置集群:

<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"/>
    </Channel>
</Cluster>
  • 优点:任何一个实例宕机,Session 都不会丢失
  • 缺点:所有实例间同步 Session 数据,网络开销大

方案三:Session 共享(推荐,生产环境首选)

使用 Redis 或 Memcached 作为集中式 Session 存储:

用户 → Nginx → Tomcat → Redis (存储 Session)
  • 优点:完全无状态,任意 Tomcat 实例都可处理请求
  • 缺点:需要引入 Redis 等额外组件

互动问题:如果用户的网络环境是经过 NAT(网络地址转换)的,比如公司里几百个人共用一个公网 IP,使用 ip_hash 会有什么问题?

答案:NAT 环境下,所有内网用户对外表现为同一个公网 IP。使用 ip_hash 时,所有用户会被分配到同一台 Tomcat 实例,导致负载完全失衡。解决方案是使用基于 Cookie 的会话保持,或者使用 Session 共享方案。


附录:常用运维命令速查

Nginx 命令速查

# 启动/停止/重载
systemctl start nginx
systemctl stop nginx
systemctl reload nginx
systemctl restart nginx
systemctl status nginx

# 配置文件检测
nginx -t

# 查看进程
ps aux | grep nginx

# 查看端口
netstat -tnlp | grep :80

# 查看实时日志
tail -f /usr/local/nginx/logs/access.log
tail -f /usr/local/nginx/logs/error.log

# 平滑升级
# 1. 备份旧程序
# 2. 编译新版本
# 3. kill -USR2 旧主进程PID
# 4. kill -WINCH 旧主进程PID
# 5. kill -QUIT 旧主进程PID

Tomcat 命令速查

# 启动/停止
/usr/local/tomcat/bin/startup.sh
/usr/local/tomcat/bin/shutdown.sh

# 查看日志
tail -f /usr/local/tomcat/logs/catalina.out

# 查看端口
netstat -tnlp | grep java

# 查看 JVM 内存使用
ps aux | grep java

# 热部署 WAR 包
cp myapp.war /usr/local/tomcat/webapps/
# Tomcat 会自动解压并部署

常见故障排查清单

问题排查步骤
Nginx 启动失败1. nginx -t 检查配置 2. 检查端口是否被占用 3. 查看 error.log
502 Bad Gateway1. 检查后端服务是否运行 2. 检查 proxy_pass 地址是否正确 3. 检查防火墙
504 Gateway Timeout1. 后端处理太慢,增加 proxy_read_timeout 2. 检查后端服务器性能
403 Forbidden1. 检查文件权限 2. 检查 allow/deny 配置 3. 检查 SELinux
Tomcat 启动慢1. 检查 /dev/urandom 熵值 2. 增加 -Djava.security.egd=file:/dev/./urandom
Java 内存溢出1. 调大 JAVA_OPTS 中的 -Xmx 2. 检查应用是否有内存泄漏

总结:Nginx 与 Tomcat 的配合关系

互联网用户
    │
    ▼
[Nginx] ── 静态资源直接返回(HTML/CSS/JS/图片)
    │
    ├── 反向代理 ──→ [PHP-FPM] ──→ [MySQL]    (LNMP 架构)
    │
    └── 负载均衡 ──→ [Tomcat 集群]              (Java Web 架构)
                        ├── 实例 1
                        ├── 实例 2
                        └── 实例 3

核心要点回顾:

  1. Nginx 的定位:高性能的静态 Web 服务器 + 反向代理 + 负载均衡器
  2. Tomcat 的定位:Java Web 应用容器,专门处理 Servlet/JSP 动态请求
  3. 最佳实践:Nginx 做前端入口(处理静态资源 + 负载均衡),Tomcat 做后端处理(运行业务逻辑)
  4. 配置修改后:一定要先 nginx -t 测试语法,再 reload 重载
  5. 日志是最好的朋友:遇到问题先看日志,80% 的故障都能从日志中找到线索

默认分类
中间件
License:  CC BY 4.0
Share

Further Reading

Jul 4, 2026

Nginx与Tomcat

Nginx 与 Tomcat 完全指南 目录 第一部分:Nginx 篇 1. Nginx 介绍 -- 你的网站为什么需要一位"前台接待员"?

Jul 4, 2026

网络协议与安全

网络协议与安全 -- 从入门到实战 目录 IP地址 -- 网络世界的门牌号 TCP/IP协议 -- 数据快递的工作流程 iptables防火墙

Jul 4, 2026

数据库与自动化运维

第四篇:数据库与自动化运维 本篇从"为什么需要数据库"这个根本问题出发,逐步带你掌握 MySQL 关系型数据库、Redis 内存数据库,以及自动化运维的核心技能。每一个知识点都遵循"遇到问题 -> 分析原因 -> 动手解决"的思路。 目录 第一部分:MySQL 数据库 数据库介绍 | 2. MySQ

OLDER

Docker与Kubernetes

NEWER

网络协议与安全

Recently Updated

  • 监控虚拟化与集群
  • Docker与Kubernetes
  • Nginx与Tomcat
  • 网络协议与安全
  • 数据库与自动化运维

Trending Tags

安全 Linux系统 nginx 日志管理 Linux服务 网络 Linux高级 pxe 中间件 python

Contents

©2026 青青子衿的拾枝杂谈 . Some rights reserved.

Using the Halo theme Chirpy