前言:空气隔离内网的“战役”

在金融、政企、国企或军工等高安全等级的项目中,“空气隔离(Air-gapped)” 是一道无法愈越的物理红线。当进入客户的内网机房时,你面对的是几台彻底无法访问外网的物理服务器或虚拟机。这里没有 apt install,没有 Docker Hub,没有网络镜像源,甚至连找个搜索引擎查看报错都做不到。

手头仅有的一件武器,就是你在外部联网环境下下载好、并通过跳板机或安全 U 盘带入内网的“离线安装包”。

本文将真实还原某政企项目离线部署的完整全过程。我们将从 3 台纯内网 Ubuntu 22.04 LTS 服务器的规划开始,手把手完成 5 大核心中间件的离线底座构建,发布前后端服务,并深度剖析可以直接投产的 Nginx 完整配置。最后,我们将现场复盘四个经典的线上“救火”排错场景,并附赠两个企业级黄金运维脚本,助你掌握真正的生产线第一线部署与运维能力。


一、 架构拓扑与内网安全准入规则

1.1 生产主机矩阵与角色分配

为了实现高并发、高可用和数据隔离,我们规划了 3 台物理隔离的 Ubuntu 22.04 LTS 服务器,具体的职责划分和内网 IP 分配如下表所示:

主机名称 内网 IP 地址 部署的服务与角色 开放的端口白名单
Server A (网关节点) 192.168.10.11 Nginx (网关/负载均衡/静态资源托管), Keepalived 外部访问: 80, 443
运维访问: 50022 (SSH)
Server B (应用节点一) 192.168.10.12 Java 业务应用 Node-01, Redis (Master), MinIO 仅对 Server A: 8080 (Java)
仅对内网互信: 6379, 9000 (MinIO)
运维: 50022
Server C (应用节点二) 192.168.10.13 Java 业务应用 Node-02, MySQL 8.0 (主库) 仅对 Server A: 8080 (Java)
仅对内网互信: 3306 (MySQL)
运维: 50022

1.2 内网严苛安全组(UFW 防火墙)配置

在企业安全规范中,绝不允许数据库或缓存端口暴露在公网,甚至不允许非关联的内网主机随意探测。我们将通过 Ubuntu 自带的 ufw 工具进行精细化准入限制。

在网关节点 Server A 执行:

1
2
3
4
5
6
7
8
9
10
11
# 默认拒绝所有入站流量,允许所有出站流量
ufw default deny incoming
ufw default allow outgoing

# 开放公网 Web 访问端口与自定义 SSH 端口
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 50022/tcp

# 开启防火墙
ufw enable

在应用节点 Server B 执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ufw default deny incoming
ufw default allow outgoing

# 仅允许网关节点(Server A)访问后端的 Java 服务端口 8080
ufw allow proto tcp from 192.168.10.11 to any port 8080

# 仅允许内网互信节点(Server A, Server C)访问 Redis 和 MinIO
ufw allow proto tcp from 192.168.10.11 to any port 6379
ufw allow proto tcp from 192.168.10.13 to any port 6379
ufw allow proto tcp from 192.168.10.11 to any port 9000
ufw allow proto tcp from 192.168.10.11 to any port 9001

# 开放运维端口
ufw allow 50022/tcp

ufw enable

在应用节点 Server C 执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ufw default deny incoming
ufw default allow outgoing

# 仅允许网关节点(Server A)访问后端的 Java 服务端口 8080
ufw allow proto tcp from 192.168.10.11 to any port 8080

# 仅允许内网互信节点(Server B 上的后端服务)访问数据库 3306
ufw allow proto tcp from 192.168.10.12 to any port 3306
ufw allow proto tcp from 192.168.10.13 to any port 3306

# 开放运维端口
ufw allow 50022/tcp

ufw enable

二、 系统级安全加固与高并发性能调优

在正式安装任何软件之前,必须对 Ubuntu 操作系统进行全面的系统初始化和加固调优。这关系到线上系统的安全性和高并发承载能力。

2.1 SSH 生产连接与堡垒机密码代填规范

在金融和政企项目中,客户通常规定严禁随意在虚拟机上分发和注入自定义的 SSH 密钥对。所有的虚机账户必须通过统一的**堡垒机(如 JumpServer、行云管家等)**进行托管、审计与账号密码代填登录。各虚机分配的都是高强度的 root 账户与复杂随机密码。

因此,为了确保堡垒机能够正常代填密码并登录目标主机,我们绝不能粗暴地禁用 root 登录与密码验证。我们需要保留它们,而将安全红线收缩在堡垒机的准入限制和内网 UFW 防火墙上。

3 台服务器上修改默认的 /etc/ssh/sshd_config 配置文件,加入以下切合堡垒机环境的安全加固参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 修改 SSH 默认端口,防范局域网大范围暴力扫描
Port 50022

# 必须开启 root 直接登录,否则堡垒机将无法单点代填登录进行高权限部署
PermitRootLogin yes
# 必须允许密码验证,以便堡垒机代填口令进行身份确认
PasswordAuthentication yes

# 高阶加固:配置 SSH 会话空闲超时自动断开,防范运维人员挂机隐患(5分钟无操作自动离线)
ClientAliveInterval 300
ClientAliveCountMax 0

# 限制密码尝试次数,防范爆破
MaxAuthTries 3

修改完成后,重启 SSH 服务以使配置生效:

1
sudo systemctl restart sshd

[!IMPORTANT]
真实落地避坑指南:在政企交付现场,千万不要为了盲目追求所谓的“禁用密码/禁用 root”而修改配置。一旦修改并重启服务,堡垒机与目标虚机的底层通信和代填凭证将瞬间失效,你将被直接锁在系统之外!必须根据客户的安全规范(堡垒机白名单代填),保留 PermitRootLogin yes

2.2 彻底解决“Too many open files”高并发隐患

Linux 默认的最大文件打开数(nofile)通常是 1024,在流量稍大的生产环境,Nginx 或 SpringBoot 会在瞬间抛出 java.io.IOException: Too many open files 导致服务宕机。

3 台主机上编辑 /etc/security/limits.conf 文件,在文件末尾追加:

1
2
3
4
* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535

同时在 /etc/pam.d/common-session 中确保加入:

1
session required pam_limits.so

2.3 生产级 Linux 内核参数调优 (sysctl.conf)

针对高并发 Web 场景下大量 TCP 短连接产生的 TIME_WAIT 堆积,以及半连接队列溢出问题,在 3 台主机上编辑 /etc/sysctl.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 开启 TCP 连接重用,允许将 TIME_WAIT sockets 重新用于新的 TCP 连接
net.ipv4.tcp_tw_reuse = 1
# 减少 TIME_WAIT 的存活时间,默认为 60 秒,缩短为 15-30 秒
net.ipv4.tcp_fin_timeout = 30
# 决定了系统能够同时持有的 TIME_WAIT 描述符最大数量
net.ipv4.tcp_max_tw_buckets = 20000
# 开启 TCP 窗口缩放,在高延迟大带宽网络下极为重要
net.ipv4.tcp_window_scaling = 1
# 调整系统 TCP 缓冲区最大值
net.core.wmem_max = 16777216
net.core.rmem_max = 16777216
# 调整半连接队列长度,防止 SYN Flood 攻击或过载导致的丢包
net.ipv4.tcp_max_syn_backlog = 8192
# 调整最大挂起连接数(全连接队列)
net.core.somaxconn = 4096

执行以下命令,无须重启直接刷新内核参数:

1
sudo sysctl -p

2.4 企业级标准目录规范

为了防范混乱的运维操作,我们必须在多台主机上统一规范目录架构:

1
2
3
4
5
6
7
8
9
sudo mkdir -p /var/tmp/offline_packages  # 离线安装包暂存区
sudo mkdir -p /usr/local/services # 软件服务安装根路径
sudo mkdir -p /data/mysql # MySQL 数据存储盘
sudo mkdir -p /data/redis # Redis 数据存储盘
sudo mkdir -p /data/minio # MinIO 文件存储盘
sudo mkdir -p /var/log/apps # 业务系统统一日志输出路径

# 变更所有者为运维专属账号 ops
sudo chown -R ops:ops /usr/local/services /data /var/log/apps

三、 5大核心中间件完全离线安装

由于处于纯离线环境,所有中间件安装包必须由跳板机通过安全通道上传至各机器的 /var/tmp/offline_packages/ 目录下。

3.1 JDK 17 完全离线安装(部署于 Server B、Server C)

后端服务基于 Java 17 构建。

  1. openjdk-17.0.2_linux-x64_bin.tar.gz 上传至目标服务器。
  2. 解压并移动到规范目录:
    1
    2
    3
    cd /var/tmp/offline_packages
    tar -zxvf openjdk-17.0.2_linux-x64_bin.tar.gz
    sudo mv jdk-17.0.2 /usr/local/services/
  3. 创建版本软链接(这样做的好处是方便日后无缝平滑升级 JDK,业务脚本无须改动):
    1
    2
    cd /usr/local/services
    sudo ln -s jdk-17.0.2 jdk
  4. 配置全局环境变量,在 /etc/profile 底部追加:
    1
    2
    export JAVA_HOME=/usr/local/services/jdk
    export PATH=$JAVA_HOME/bin:$PATH
  5. 使配置立即生效并进行真实校验:
    1
    2
    3
    source /etc/profile
    java -version
    # 验证输出:openjdk version "17.0.2" 2022-01-18

3.2 MySQL 8.0 完全离线安装(GLIBC 二进制免编译版,部署于 Server C)

对于离线环境,选用 GLIBC 版二进制包(mysql-8.0.28-linux-glibc2.12-x86_64.tar.xz)是最优解,比 deb 包更纯净且免除底层动态库缺失导致的无法安装问题。

  1. 创建 MySQL 运行专属低权限用户与组:
    1
    2
    sudo groupadd mysql
    sudo useradd -r -g mysql -s /bin/false mysql
  2. 解压并规整安装目录:
    1
    2
    3
    cd /var/tmp/offline_packages
    tar -xvf mysql-8.0.28-linux-glibc2.12-x86_64.tar.xz
    sudo mv mysql-8.0.28-linux-glibc2.12-x86_64 /usr/local/services/mysql
  3. 编写符合真实生产环境参数的配置文件 /etc/my.cnf
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    [mysqld]
    user=mysql
    port=3306
    basedir=/usr/local/services/mysql
    datadir=/data/mysql
    socket=/tmp/mysql.sock
    log-error=/data/mysql/mysql-error.log
    pid-file=/data/mysql/mysql.pid

    # 生产核心性能参数
    max_connections=1000
    max_user_connections=800
    # 根据 Server C 物理内存的 60% 动态配置(例如 16G 内存配置为 9G)
    innodb_buffer_pool_size=9G
    innodb_log_file_size=1G
    innodb_log_buffer_size=16M
    innodb_flush_log_at_trx_commit=1
    innodb_flush_method=O_DIRECT

    # 字符集与时区
    character-set-server=utf8mb4
    collation-server=utf8mb4_general_ci
    default-time-zone='+8:00'
    lower_case_table_names=1
  4. 数据目录授权并执行核心初始化命令:
    1
    2
    3
    4
    sudo chown -R mysql:mysql /data/mysql
    cd /usr/local/services/mysql
    # 执行数据库初始化,注意记下输出最后一行随机生成的 MySQL 初始 root 密码!
    sudo ./bin/mysqld --defaults-file=/etc/my.cnf --initialize --user=mysql
  5. 将 MySQL 注册为 Systemd 服务。编写 /etc/systemd/system/mysqld.service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [Unit]
    Description=MySQL Server
    After=network.target

    [Service]
    Type=forking
    ExecStart=/usr/local/services/mysql/support-files/mysql.server start
    ExecStop=/usr/local/services/mysql/support-files/mysql.server stop
    User=mysql
    Group=mysql
    Restart=on-failure
    PrivateTmp=true

    [Install]
    WantedBy=multi-user.target
  6. 启动并配置开机自启:
    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl enable mysqld
    sudo systemctl start mysqld
  7. 登录数据库修改初始密码,并开通局域网内物理隔离的内网访问权限:
    1
    2
    # 使用刚刚记下的临时随机密码登录
    /usr/local/services/mysql/bin/mysql -uroot -p -S /tmp/mysql.sock
    在交互命令行中执行:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    -- 修改本地 root 密码为强密码
    ALTER USER 'root'@'localhost' IDENTIFIED BY 'Company@Secure#MySQL2026';

    -- 禁止 root 外部远程访问,仅允许本地访问以保证物理安全
    -- 为 Server B 的 Java 后端节点创建专属的内网通信账号,实现细粒度控制
    CREATE USER 'app_user'@'192.168.10.12' IDENTIFIED BY 'AppCon#Secure@2026';
    GRANT SELECT, INSERT, UPDATE, DELETE, CREATE ON *.* TO 'app_user'@'192.168.10.12';

    -- 刷新权限并退出
    FLUSH PRIVILEGES;
    EXIT;

3.3 Redis 7.2 完全离线安装(部署于 Server B)

  1. 上传 redis-7.2.4.tar.gz 到 Server B。
  2. 源码解压并编译安装:
    1
    2
    3
    4
    cd /var/tmp/offline_packages
    tar -zxvf redis-7.2.4.tar.gz
    cd redis-7.2.4
    make PREFIX=/usr/local/services/redis install
  3. 复制默认配置文件,进行生产安全加固修改:
    1
    2
    mkdir -p /usr/local/services/redis/conf
    cp redis.conf /usr/local/services/redis/conf/
    编辑 /usr/local/services/redis/conf/redis.conf
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # 严禁绑定 0.0.0.0。只监听本机回环以及 Server B 的内网 IP,防止外界探测
    bind 127.0.0.1 192.168.10.12
    port 6379
    # 以后台进程守护模式运行
    daemonize no
    # 设置极其强悍的访问密码
    requirepass Redis@Secure#AuthPassword2026
    # 数据库数据目录
    dir /data/redis

    # 生产级持久化策略配置:RDB 配合 AOF 双防灾方案
    save 900 1
    save 300 10
    save 60 10000
    appendonly yes
    appendfilename "appendonly.aof"
    appendfsync everysec

    # 安全加固:禁用高危命令以防黑客提权或误操作清空数据
    rename-command FLUSHALL "SECURE_FLUSHALL"
    rename-command FLUSHDB "SECURE_FLUSHDB"
    rename-command KEYS "SECURE_KEYS"
  4. 将其注册为 Systemd 服务统一托管。编写 /etc/systemd/system/redis.service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [Unit]
    Description=Redis In-Memory Data Store
    After=network.target

    [Service]
    User=ops
    ExecStart=/usr/local/services/redis/bin/redis-server /usr/local/services/redis/conf/redis.conf
    ExecStop=/usr/local/services/redis/bin/redis-cli -a Redis@Secure#AuthPassword2026 shutdown
    Restart=always
    LimitNOFILE=65535

    [Install]
    WantedBy=multi-user.target
  5. 启动服务:
    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl enable redis
    sudo systemctl start redis

3.4 MinIO 文件存储完全离线安装(部署于 Server B)

MinIO 是极具生产代表性的单二进制文件架构,非常适合在无公网的私有化环境单机快速拉起。

  1. 上传可执行二进制包 minio 和客户端工具 mc/usr/local/services/minio/bin/
  2. 授予可执行权限并进行目录规整:
    1
    2
    3
    4
    5
    sudo mkdir -p /usr/local/services/minio/bin
    sudo mv /var/tmp/offline_packages/minio /usr/local/services/minio/bin/
    sudo mv /var/tmp/offline_packages/mc /usr/local/services/minio/bin/
    sudo chmod +x /usr/local/services/minio/bin/*
    sudo chown -R ops:ops /usr/local/services/minio /data/minio
  3. 建立环境变量配置文件 /etc/default/minio 以实现凭证与启动逻辑的解耦:
    1
    2
    3
    4
    5
    # 绑定内网 IP 端口与控制台端口
    MINIO_OPTS="--address 192.168.10.12:9000 --console-address 192.168.10.12:9001 /data/minio"
    # 超强管理员账户与密码
    MINIO_ROOT_USER=minio_admin_user
    MINIO_ROOT_PASSWORD=MinIO@Secure#AccessKey2026
  4. 编写 Systemd 服务托管文件 /etc/systemd/system/minio.service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [Unit]
    Description=MinIO Object Storage
    Documentation=https://docs.min.dev
    After=network.target

    [Service]
    User=ops
    Group=ops
    EnvironmentFile=/etc/default/minio
    ExecStart=/usr/local/services/minio/bin/minio server $MINIO_OPTS
    Restart=always
    LimitNOFILE=65535

    [Install]
    WantedBy=multi-user.target
  5. 启动 MinIO 并开启自启:
    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl enable minio
    sudo systemctl start minio

3.5 Nginx 1.24 完全离线安装(源码级静态链接编译,部署于 Server A)

离线编译 Nginx 最大的痛点是由于缺失网络,系统通常没有预装 PCRE (用于正则表达式)、Zlib (用于 Gzip 压缩)、OpenSSL (用于 SSL 证书加密) 的库。如果直接执行 ./configure 会疯狂报错。

黄金避坑技巧:将三大底层库的源码包打包带进内网,不安装它们,而是在 Nginx 编译时,直接静态绑定编译到 Nginx 的二进制文件中。这能创造一个完全无视目标环境依赖的、纯净的单体 Nginx 二进制!

  1. 确保上传了以下四个源码包到 /var/tmp/offline_packages/
    • nginx-1.24.0.tar.gz
    • pcre-8.45.tar.gz
    • zlib-1.2.13.tar.gz
    • openssl-1.1.1w.tar.gz
  2. 一键解压所有源码包:
    1
    2
    3
    4
    5
    cd /var/tmp/offline_packages
    tar -zxvf nginx-1.24.0.tar.gz
    tar -zxvf pcre-8.45.tar.gz
    tar -zxvf zlib-1.2.13.tar.gz
    tar -zxvf openssl-1.1.1w.tar.gz
  3. 进入 Nginx 源码目录,利用 --with-xxx= 参数静态绑定解压出的源码路径,执行极度优雅的高阶静态编译:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    cd nginx-1.24.0
    ./configure --prefix=/usr/local/services/nginx \
    --with-http_ssl_module \
    --with-http_gzip_static_module \
    --with-http_stub_status_module \
    --with-pcre=/var/tmp/offline_packages/pcre-8.45 \
    --with-zlib=/var/tmp/offline_packages/zlib-1.2.13 \
    --with-openssl=/var/tmp/offline_packages/openssl-1.1.1w

    # 执行编译与安装
    make -j$(nproc)
    sudo make install
  4. 编译安装完成后,验证 Nginx 二进制并查阅其内置绑定的库:
    1
    2
    /usr/local/services/nginx/sbin/nginx -V
    # 验证输出,你会发现 OpenSSL、PCRE、Zlib 全部以静态链接的形式内嵌在 Nginx 中!
  5. 托管为 Systemd 服务 /etc/systemd/system/nginx.service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [Unit]
    Description=The Nginx HTTP and reverse proxy server
    After=syslog.target network.target remote-fs.target nss-lookup.target

    [Service]
    Type=forking
    PIDFile=/usr/local/services/nginx/logs/nginx.pid
    ExecStartPre=/usr/local/services/nginx/sbin/nginx -t
    ExecStart=/usr/local/services/nginx/sbin/nginx
    ExecReload=/usr/local/services/nginx/sbin/nginx -s reload
    ExecStop=/bin/kill -s QUIT $MAINPID
    PrivateTmp=true

    [Install]
    WantedBy=multi-user.target
  6. 启动并允许开机自启:
    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl enable nginx
    sudo systemctl start nginx

四、 前后端发布流程与低权限安全托管

在 5 大中间件底座稳固搭建好之后,开始进入核心的前后端服务打包和部署工作。

4.1 离线制品的完整性 SHA256 检验

在跳板机上将前端制品打包压缩为 frontend_v1.0.tar.gz,后端 SpringBoot 打包为 app.jar。为了防止网络传输过程中由于磁盘块损坏或局域网抖动引起的文件损坏问题,必须在源端计算并校验 SHA256:

1
2
3
4
5
6
# 在跳板机执行计算
sha256sum app.jar > app.jar.sha256

# 上传文件与 sha256 校验文件至目标服务器 Server B/C,在目标端执行校验:
sha256sum -c app.jar.sha256
# 如果输出:app.jar: OK,则代表传输完整无误,方可开始升级,防范部署未知损坏二进制!

4.2 拒绝使用 root!SpringBoot 应用低权限 Systemd 系统服务托管

在线上绝对不允许使用 root 用户运行 Java 后端服务,一旦应用被黑客挖掘出反序列化等命令执行漏洞,攻击者将直接拿到系统底层最高控制权。

  1. Server B 和 Server C 上分别创建低权限隔离组和账号:
    1
    2
    sudo groupadd appgroup
    sudo useradd -r -g appgroup -s /bin/false -d /usr/local/services/app apprun
  2. 将后端程序放置在 /usr/local/services/app/ 目录下,并进行专属低权限授权:
    1
    2
    3
    4
    sudo mkdir -p /usr/local/services/app
    sudo cp /var/tmp/offline_packages/app.jar /usr/local/services/app/
    sudo chown -R apprun:appgroup /usr/local/services/app
    sudo chmod 500 /usr/local/services/app/app.jar # 仅允许只读执行,禁止写入防范被篡改
  3. 编写专属的 Systemd 应用服务托管单元 /etc/systemd/system/app.service。我们在这里融合了内存硬限制以及启动 JVM 参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    [Unit]
    Description=Java SpringBoot Application
    After=network.target mysql.service redis.service

    [Service]
    # 指定以低权限账户和组运行
    User=apprun
    Group=appgroup
    WorkingDirectory=/usr/local/services/app

    # JVM 堆大小等调优参数显式配置,防止内存无限蔓延
    Environment="JAVA_OPTS=-Xms512m -Xmx1G -XX:+UseG1GC -Dspring.profiles.active=prod"
    ExecStart=/usr/local/services/jdk/bin/java $JAVA_OPTS -jar /usr/local/services/app/app.jar

    # 发生非正常退出时 10 秒后自动拉起
    Restart=on-failure
    RestartSec=10s

    # 限制输出到标准输出的系统日志格式
    StandardOutput=syslog
    StandardError=syslog
    SyslogIdentifier=app-backend

    # 安全限制:拒绝写入临时文件等
    PrivateTmp=true

    [Install]
    WantedBy=multi-user.target
  4. 刷新并加载服务:
    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl enable app.service
    sudo systemctl start app.service

4.3 生产发布与自动回滚黄金运维脚本 (deploy.sh)

这是一个真正可以直接放在生产上使用的一键升级发布与容灾脚本。每次升级只需将最新的 app.jar 放入 /var/tmp/offline_packages/ 即可自动执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/bin/bash
# 线上后端服务平滑发布与失败自动回滚脚本
set -euo pipefail

APP_DIR="/usr/local/services/app"
BACKUP_DIR="/usr/local/services/app/backups"
NEW_JAR="/var/tmp/offline_packages/app.jar"
DATE_STR=$(date +%Y%m%d_%H%M%S)

echo "[$(date)] >>> 开始执行线上服务升级任务..."

# 1. 确保备份目录存在
mkdir -p "$BACKUP_DIR"

# 2. 备份当前正在运行 the 旧版本(如果存在)
if [ -f "$APP_DIR/app.jar" ]; then
echo "[$(date)] 备份现有版本至 $BACKUP_DIR/app.jar_$DATE_STR"
cp "$APP_DIR/app.jar" "$BACKUP_DIR/app.jar_$DATE_STR"
fi

# 3. 校验并覆盖最新代码
if [ ! -f "$NEW_JAR" ]; then
echo "ERROR: 未在暂存区发现最新发布物 app.jar,中止升级!"
exit 1
fi
echo "[$(date)] 覆盖目标程序包..."
sudo cp "$NEW_JAR" "$APP_DIR/app.jar"
sudo chown apprun:appgroup "$APP_DIR/app.jar"
sudo chmod 500 "$APP_DIR/app.jar"

# 4. 重启 Systemd 服务
echo "[$(date)] 正在平滑重启服务进程..."
sudo systemctl restart app.service

# 5. 持续进行生产健康探测(核心环节,防范发版崩溃)
echo "[$(date)] 开始持续监控后端状态,观察 15 秒..."
HEALTH_URL="http://127.0.0.1:8080/api/health"
IS_HEALTHY=0

for i in {1..15}; do
# 模拟探测请求
if curl -s -f --connect-timeout 2 "$HEALTH_URL" > /dev/null; then
echo "探测成功,后端服务已处于健康就绪状态!"
IS_HEALTHY=1
break
else
echo "后端拉起中... 第 $i 秒"
sleep 2
fi
done

# 6. 如果健康状态判定失败,执行终极自动秒级回滚
if [ $IS_HEALTHY -ne 1 ]; then
echo "WARNING: 新版本拉起失败,正在执行线上秒级回滚!"
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/app.jar_* | head -n 1)
echo "回滚到备份文件: $LATEST_BACKUP"
sudo cp "$LATEST_BACKUP" "$APP_DIR/app.jar"
sudo chown apprun:appgroup "$APP_DIR/app.jar"
sudo chmod 500 "$APP_DIR/app.jar"
sudo systemctl restart app.service
echo "ERROR: 升级失败已回滚,请排查原因!"
exit 2
else
echo "[$(date)] >>> 线上服务升级圆满成功!"
# 删除新制品,防止二次发布时逻辑混乱
rm -f "$NEW_JAR"
fi

五、 生产级可直接投产的完整的 nginx.conf 剖析

作为整个集群对外唯一的顶级七层网关,网关节点 Server A 的 nginx.conf 承载了安全性、高并发吞吐量以及静态前端托管的核心使命。以下是精心提炼且可直接覆盖到生产环境的完整配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# 运行账号,以 Linux 低权限的 www-data 组和用户运行,防止提权
user www-data;
# 与 CPU 核心数自动对齐,充分压榨多核处理性能
worker_processes auto;
# 显式配置每个 worker 进程能够打开的最大文件描述符,解决高并发限制
worker_rlimit_nofile 65535;

pid /usr/local/services/nginx/logs/nginx.pid;

events {
# 采用高吞吐下的 epoll I/O 多路复用模型(Linux 专属)
use epoll;
worker_connections 4096;
# 允许一个 worker 同时接受多个新连接
multi_accept on;
}

http {
include mime.types;
default_type application/octet-stream;

# ================= 1. 自定义高解析生产日志格式 =================
# 精准捕获客户端真实 IP、请求响应时间($request_time)和上游应用处理时间($upstream_response_time)
# 这是日后在生产线排查慢接口、分析性能瓶颈最核心的客观地基!
log_format production_main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'req_time=$request_time up_resp_time=$upstream_response_time';

access_log /usr/local/services/nginx/logs/access.log production_main;
error_log /usr/local/services/nginx/logs/error.log warn;

# ================= 2. 性能与安全参数加固 =================
# 开启高效的零拷贝发送,降低 CPU 与磁盘 I/O 握手开销
sendfile on;
tcp_nopush on;
tcp_nodelay on;

# 隐藏 Nginx 具体版本号,屏蔽黑客针对特定版本的安全溢出攻击
server_tokens off;

# 客户端请求体大小硬性限制,解决大文件上传被网关恶意打满和超时拒绝问题
client_max_body_size 50M;

# 网关与客户端的长连接超时时间
keepalive_timeout 65;

# ================= 3. 生产级 Gzip 网页压缩传输优化 =================
gzip on;
gzip_min_length 1k; # 小于 1KB 的文件不压缩,防止压缩后反而变大
gzip_comp_level 5; # 压缩级别(1-9),5 是性能与压缩比的完美黄金平衡点
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_vary on; # 往响应头写入 Vary: Accept-Encoding

# ================= 4. 七层负载均衡及长连接调优 =================
upstream backend_servers {
# 轮询分发,并配备健康检查机制:在 10s 内如果连续失败 3 次,将自动将该节点踢出,10s 内不分配流量
server 192.168.10.12:8080 weight=5 max_fails=3 fail_timeout=10s;
server 192.168.10.13:8080 weight=5 max_fails=3 fail_timeout=10s;

# 极度重要性能优化:开启 Nginx 与后端 Java 的 Keepalive 长连接连接池,
# 大幅降低高并发下 Nginx 与 SpringBoot 多次 TCP 三次握手和四次挥手的网络消耗!
keepalive 32;
}

# ================= 5. Web 网关 Server 块定义 =================
server {
listen 80;
server_name 192.168.10.11; # 或者绑定对应的域名

# 配置安全防扫描响应头
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";

# 托管 Vue 前端 SPA 静态目录
location / {
root /var/www/html/dist;
index index.html index.htm;

# 解决 Vue-Router / React-Router 采用 History 模式时,刷新子路径报 404 的致命经典难题
# 核心原理:如果找不到物理静态资源,强制全部静默重定向到 index.html 前端入口路由
try_files $uri $uri/ /index.html;
}

# 针对图片/CSS/JS等静态资源设置高强度客户端强缓存,大幅度分摊带宽压力
location ~* \.(jpg|jpeg|gif|png|css|js|ico|webp)$ {
root /var/www/html/dist;
expires 7d;
add_header Cache-Control "public, no-transform";
}

# ================= 6. 动态 API 反向代理配置 =================
location /api/ {
# 代理转发至后端负载均衡上游
proxy_pass http://backend_servers/;

# 使用 HTTP/1.1 协议(因为 HTTP/1.0 不支持上游 Keepalive 长连接)
proxy_http_version 1.1;
proxy_set_header Connection "";

# 传递真实客户端的客观网络拓扑参数,杜绝被代理后全部显示为 Nginx 网关本地 IP 的痛点
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 60s;
proxy_read_timeout 120s;
proxy_send_timeout 60s;
}

# 状态检查端点配置,防范白屏和宕机时无迹可查
location /nginx-status {
stub_status on;
access_log off;
# 仅限制内部特定的主机可探测
allow 127.0.0.1;
allow 192.168.10.12;
deny all;
}
}
}

六、 线上运维“救火”:日志分析与四大经典故障排错实战

真实环境的升级和发版,绝不会是一帆风顺的。当发版出现异常、请求全部报错时,作为资深运维或技术大拿,必须迅速祭出日志和连通性排查工具,完成精准“救火”。

6.1 线上日志诊断武器库

  • 实时追踪业务服务(Systemd 托管下的 Java)日志
    1
    2
    3
    4
    5
    # 实时监视后端最近 100 行日志输出,不产生分页
    journalctl -u app.service -f -n 100 --no-pager

    # 搜索特定时段或者包含 ERROR 字眼的异常崩溃栈
    journalctl -u app.service --since "2026-05-30 00:00:00" | grep -i -E "error|exception"
  • 实时追踪网关负载日志
    1
    2
    # 观察错误日志,定位网关和后端的通信错误
    tail -f /usr/local/services/nginx/logs/error.log

6.2 经典事故 1:调用 API 时前端一直转圈,最终报 502 Bad Gateway

  • 故障现象:在浏览器访问系统一切正常,但点击登录或加载表格时系统转圈,控制台狂报 502 Bad Gateway

第一现场排查链条:

  1. 查看 Nginx 的 error.log。在 Server A 网关发现类似如下报错:

    1
    2026/05/30 23:56:01 [error] 1405#0: *8 connect() failed (111: Connection refused) while connecting to upstream, client: 220.181.10.45, server: 192.168.10.11, request: "POST /api/login HTTP/1.1", upstream: "http://192.168.10.12:8080/login"

    专业诊断:这代表 Nginx 向后端 192.168.10.128080 发起 TCP 握手时,直接被应用节点Connection refused(连接拒绝)。代表后端服务可能根本没有监听该端口。

  2. 去应用主机 Server B 排查端口存活

    1
    2
    ss -ntlp | grep 8080
    # 发现输出一片空白!代表 Server B 的 8080 没有被监听。
  3. 使用 journalctl 排查 Java 业务进程的日志

    1
    journalctl -u app.service -n 150 --no-pager

    发现数据库相关的报错栈:

    1
    2
    3
    4
    Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
    The last packet successfully received from the server was 0 milliseconds ago. The driver has not received any packets from the server.
    ...
    Caused by: java.net.ConnectException: Connection refused (Connection refused)

    破案结论:SpringBoot 在启动时因为连不上 Server C 上的 MySQL 数据库,抛出底层通信异常,直接导致初始化失败、服务自动终止退出,从而引起 Nginx 返回 502。接下来需要按照事故 3的步骤去排查 Server C 上的数据库。


6.3 经典事故 2:静态网页加载失败,打开一片空白,报 403 Forbidden / Permission denied

  • 故障现象:刚把前端 dist.tar.gz 解压覆盖到 /var/www/html/dist 下,清理缓存刷新浏览器,结果直接显示 403 Forbidden

第一现场排查链条:

  1. 查看网关 Server A 上 Nginx 错误日志

    1
    2026/05/30 23:56:10 [error] 1405#0: *12 open() "/var/www/html/dist/index.html" failed (13: Permission denied), client: 220.181.10.45, server: 192.168.10.11, request: "GET / HTTP/1.1"

    专业诊断Permission denied(权限被拒绝) 代表操作系统的文件权限控制链失效了。Nginx 的 Worker 进程由于无权读取物理磁盘上的 index.html 导致直接抛出 403。

  2. 排查文件的 Linux 所有者与权限级别

    1
    2
    ls -ld /var/www/html/dist
    # 输出显示:drwx------ 3 root root 4096 May 30 23:00 /var/www/html/dist

    核心原理拆解

    • 看到没有?dist 目录的用户所有者和组所有者都是 root,且其权限为 700 (即除 root 之外的任何账户彻底无权读取甚至进入该目录)!
    • 而我们在 nginx.conf 顶部指定的 Worker 进程账户是 www-data,非 root 账号在 700 面前被操作系统无情挡下!
  3. 完美解决命令
    规范静态网页目录的归属,并将权限恢复为行业标准的 755 (目录) 与 644 (文件):

    1
    2
    3
    4
    5
    6
    # 将所有权正确递归变更为 www-data
    sudo chown -R www-data:www-data /var/www/html/dist
    # 将目录权限恢复为 755 (必须允许执行 x 权限,用户才能进入目录!)
    sudo find /var/www/html/dist -type d -exec chmod 755 {} \;
    # 将文件权限恢复为 644
    sudo find /var/www/html/dist -type f -exec chmod 644 {} \;

    刷新网页,瞬间成功渲染!


6.4 经典事故 3:后端启动崩溃,抛出数据库连接拒绝(最小化赋权纠偏)

  • 故障现象:如事故 1 中,SpringBoot 后端在拉起时抛出 Host '192.168.10.12' is not allowed to connect to this MySQL server 并自崩。

第一现场排查链条:

  1. 测试内网主机间底层的网络与端口可达性
    在 Server B 上执行 nc,探针 Server C 的数据库端口是否开启:

    1
    2
    nc -zv 192.168.10.13 3306
    # 输出:Connection to 192.168.10.13 3306 port [tcp/mysql] succeeded!

    专业诊断:网络连通性完美无损,且没有被 UFW 防火墙拦截。那么问题只可能出现在 MySQL 自身的授权机制上。

  2. 去 Server C 登录数据库,排查 MySQL 系统库的账号白名单

    1
    /usr/local/services/mysql/bin/mysql -uroot -p -S /tmp/mysql.sock
    1
    SELECT user, host FROM mysql.user;

    输出可能显示:

    1
    2
    3
    4
    5
    6
    +----------+-----------+
    | user | host |
    +----------+-----------+
    | root | localhost |
    | app_user | 127.0.0.1 |
    +----------+-----------+

    破案结论app_user 账号的白名单限制了只有来自 127.0.0.1 才能建立会话!现在后端是从 Server B(192.168.10.12)跨机器访问的,自然直接被拦截。

    防灾避坑警告:很多缺乏经验的运维图方便会直接把 host 设为 %(允许全球连接),这在严格审查的企业级内网是绝对不合格的,极易被内网渗透攻击一窝端。

  3. 安全纠偏授权

    1
    2
    3
    4
    -- 创建精准只服务于 192.168.10.12 的账号
    CREATE USER 'app_user'@'192.168.10.12' IDENTIFIED BY 'AppCon#Secure@2026';
    GRANT SELECT, INSERT, UPDATE, DELETE ON `company_prod_db`.* TO 'app_user'@'192.168.10.12';
    FLUSH PRIVILEGES;

    完成纠偏授权后,Server B 重启后端服务,直接平滑启动上线。


6.5 经典事故 4:单页面应用(Vue/React)在点击子页面刷新时,浏览器直接报 404

  • 故障现象:系统发布上线后,点击页面导航菜单跳转流畅,但在某些子页面(例如 /user/settings)按下浏览器刷新键 F5 时,网页没有加载,直接弹出了裸露的 Nginx 默认 404 Not Found 错误。

第一现场排查链条:

  1. 分析原理

    • 这是由于现代 Vue/React 的单页面应用(SPA)采用的是 前端路由模型
    • 当点击前端导航栏时,实际上只是前端 JS(如 Vue-Router)截获了路由请求并进行了虚拟页面渲染,完全没有请求过后端网关服务器,因原是平滑的。
    • 但是当你按下 F5 刷新时,浏览器会向 Nginx 服务器真实发起一个 GET /user/settings 的物理请求。而 Nginx 静态托管目录下根本没有 /var/www/html/dist/user/settings 这个物理目录或物理文件!最终抛出 404。
  2. 解决手段
    在网关配置 server 块托管静态资源的 location / 下,通过配置 try_files 进行静默转发拦截:

    1
    2
    3
    4
    5
    6
    7
    8
    location / {
    root /var/www/html/dist;
    index index.html index.htm;

    # 试探查找物理文件是否存在。如果都不存在,则静默重写请求至 index.html
    # 此时 index.html 会载入入口 JS,前端路由会自动识别 "/user/settings" 路径并完成业务渲染
    try_files $uri $uri/ /index.html;
    }

    修改 nginx.conf 后,热加载 Nginx 使配置平滑生效,故障彻底根除:

    1
    /usr/local/services/nginx/sbin/nginx -s reload

七、 企业生产运维黄金脚本(彩蛋福利)

真正有含金量的系统管理员,不仅会部署和修复故障,更有一套高效的日常巡检和备份体系。

7.1 三台服务器日常巡检黄金脚本 (cluster_check.sh)

由于物理隔离服务器禁用了自定义密钥分发,只允许 root + 密码通过堡垒机连接,那我们在局域网的运维管理机上该如何实现一键批量巡检?

实战终极方案

  1. 在管理机上离线安装 sshpass 命令行工具(在互联网环境提前下载好 sshpass_1.09-1_amd64.deb,放入 U 盘,在内网执行 sudo dpkg -i sshpass_*.deb 一键离线安装)。
  2. 为了不将高度机密的 root 密码硬编码在脚本文件中(防止越权泄露),脚本在启动时将交互式提示运维人员手动输入各主机的 root 密码,并临时缓存在脚本的内存变量中。既安全又实现了全自动一键批量巡检!

将此脚本部署在局域网内的运维管理机上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/bin/bash
# 线上集群一键巡检脚本(安全交互密码版,适用于堡垒机/无密钥环境)
set -u

HOSTS=("192.168.10.11" "192.168.10.12" "192.168.10.13")

echo "=========================================================="
echo " 开始执行集群多主机核心状态一键巡检 "
echo "=========================================================="

# 1. 交互式安全读入各主机的 root 密码,临时保存在内存中,避免硬编码
echo ">>> 请输入 Server A (192.168.10.11) 的 root 密码:"
read -s PASS_A
echo ">>> 请输入 Server B (192.168.10.12) 的 root 密码:"
read -s PASS_B
echo ">>> 请输入 Server C (192.168.10.13) 的 root 密码:"
read -s PASS_C

# 2. 建立密码矩阵关联
get_password() {
local ip=$1
case "$ip" in
"192.168.10.11") echo "$PASS_A" ;;
"192.168.10.12") echo "$PASS_B" ;;
"192.168.10.13") echo "$PASS_C" ;;
esac
}

# 3. 循环遍历利用 sshpass 进行非交互式安全密码代填登录巡检
for ip in "${HOSTS[@]}"; do
echo ""
echo ">>>>>>> 正在巡检服务器: $ip ..."

CURRENT_PASS=$(get_password "$ip")

# 利用 sshpass 带入密码进行 SSH 登录
sshpass -p "$CURRENT_PASS" ssh -p 50022 -o ConnectTimeout=3 -o StrictHostKeyChecking=no root@"$ip" "
echo '--- 1. 磁盘空间占用统计 ---'
df -h | grep -E '^/dev/' | awk '{print \$5, \$6}'

echo '--- 2. 系统平均负载 (Load Average) ---'
uptime | awk -F'load average:' '{print \$2}'

echo '--- 3. 内存空闲统计 ---'
free -h | grep Mem | awk '{print \"已用: \" \$3, \"/ 总量: \" \$2}'

echo '--- 4. 核心守护进程存活检查 ---'
for svc in nginx mysqld redis minio app; do
if systemctl is-active --quiet \$svc; then
echo \"服务 [\$svc]: 正常运行中 (Active)\"
else
echo \"服务 [\$svc]: !!异常挂载/关闭!! (Inactive)\"
fi
done
"
done

echo ""
echo "=========================================================="
echo " 集群健康大巡检完毕,警惕 inactive 服务 "
echo "=========================================================="

7.2 MySQL 物理/逻辑备份并自动离线上传 MinIO 的脚本 (mysql_backup.sh)

此脚本部署在 MySQL 主机 Server C 上。每天深夜自动执行,逻辑导出数据库并利用二进制客户端安全备份到 Server B 的 MinIO 桶中,且自动轮转保留 7 天:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash
# MySQL 逻辑备份并自动安全分发至内网 MinIO 存储脚本
set -euo pipefail

BACKUP_DIR="/data/mysql/backups"
DB_NAME="company_prod_db"
DATE_STR=$(date +%Y%m%d_%H%M%S)
FILE_NAME="$DB_NAME_$DATE_STR.sql.gz"
LOCAL_PATH="$BACKUP_DIR/$FILE_NAME"

# MinIO 配置信息
MINIO_URL="http://192.168.10.12:9000"
MINIO_BUCKET="db-backups"

echo "[$(date)] 开始执行逻辑备份,生成压缩文件..."
mkdir -p "$BACKUP_DIR"

# 导出并使用 gzip 直接极限压缩
/usr/local/services/mysql/bin/mysqldump --defaults-file=/etc/my.cnf \
-uapp_user -p'AppCon#Secure@2026' \
"$DB_NAME" | gzip > "$LOCAL_PATH"

echo "[$(date)] 备份成功。大小: $(du -sh "$LOCAL_PATH" | awk '{print $1}')"

# 配置 MinIO 客户端认证,以命令流形式上传
echo "[$(date)] 正在上传备份制品到集群内网存储..."
/usr/local/services/minio/bin/mc alias set local-minio "$MINIO_URL" minio_admin_user MinIO@Secure#AccessKey2026 --api S3v4
/usr/local/services/minio/bin/mc cp "$LOCAL_PATH" local-minio/"$MINIO_BUCKET"/

# 定期清理本地老旧备份,保留最近 7 天
echo "[$(date)] 执行本地自动文件轮转(清理 7 天前旧包)..."
find "$BACKUP_DIR" -type f -name "${DB_NAME}_*.sql.gz" -mtime +7 -exec rm -f {} \;

echo "[$(date)] MySQL 定期容灾备份完成!"

结语:稳固在实操里的力量

真正的部署与运维不是一成不变的教科书。在空气隔离的“深水内网”中,没有辅助工具,一切依靠你的基础功底。

通过掌握系统级内核调优,从源码彻底静态编译无任何依赖的 Nginx,利用标准的低权限 Systemd 安全托管 Java 服务,并通过严谨的故障日志链进行逻辑自洽的火速排错,你将彻底在脑海中构建出一个庞大系统的生产图景。

这,才是从一名开发走向顶级 DevOps/系统架构师的蜕变之路。