OpenResty 1.29.2.5 部署 + Nginx平滑升级运维文档

一、前言

bash 复制代码
因 Nginx 存在 CVE-2026-42945 安全漏洞,需将现有 Nginx 1.19.6 升级至 1.30.1 版本。当前业务依赖 FastDFS 5.0.5 及 Lua 模块,常规编译升级后 Nginx 无法正常启动,报错提示使用了非 OpenResty 原生 LuaJIT、缺失lua-resty-core模块。手动加装该模块并配置 Lua 库路径的方式部署繁琐,且无法发挥 Lua 环境最优性能。综合考虑业务稳定性、运行性能及漏洞修复需求,决定在不中断业务的前提下,平滑迁移升级至 OpenResty 1.29.2.5 + FastDFS 5.0.5组合。OpenResty 自带原生 Lua 运行环境,既能彻底修复安全漏洞,也可保障 Lua 模块正常运行并发挥最佳性能。

二、前置说明

bash 复制代码
操作窗口:业务低峰期执行;
权限:root 权限;
网络:服务器可外网下载源码包;
预留磁盘空间:≥2GB(具体根据nginx占用空间大小决定)

三、OpenResty 编译部署

1、备份原有Nginx环境

bash 复制代码
# 导出原有版本信息
/data/nginx/sbin/nginx -V > /data/nginx/nginx.old.version.txt 2>&1
# 全目录备份(带日期)
cp -a /data/nginx /data/nginx_bak_$(date +%F)

2、安装编译依赖

bash 复制代码
yum install -y gcc gcc-c++ make pcre pcre-devel zlib zlib-devel openssl-devel

3、下载并解压依赖源码包

进入工具目录统一管理源码

bash 复制代码
cd /opt

# 1. 下载 lua-cjson 2.1.0.9
wget https://github.com/openresty/lua-cjson/archive/refs/tags/2.1.0.9.tar.gz
tar -zxvf 2.1.0.9.tar.gz

# 2. 下载 OpenResty 1.29.2.5
wget https://openresty.org/download/openresty-1.29.2.5.tar.gz
tar -zxvf openresty-1.29.2.5.tar.gz

# 3. 解压 FastDFS-Nginx 模块(提前上传压缩包)
unzip 5e5f3566bbfa57418b5506aaefbe107a42c9fcb1.zip
mv fastdfs-nginx-module-5e5f3566bbfa57418b5506aaefbe107a42c9fcb1 fastdfs-nginx-module

# 4. 下载并解压 OpenSSL 1.1.1w
wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz
tar -zxvf openssl-1.1.1w.tar.gz

4、预编译配置 OpenResty

bash 复制代码
cd /opt/openresty-1.29.2.5

./configure \
--prefix=/data/openresty \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_sub_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-pcre \
--with-http_flv_module \
--with-http_dav_module \
--with-http_addition_module \
--with-openssl=/opt/openssl-1.1.1w \
--add-module=/opt/fastdfs-nginx-module/src

5、替换并编译 lua-cjson

bash 复制代码
# 移除原有旧版本 cjson
rm -rf build/lua-cjson-2.1.0.17
# 移入新版 cjson 并重命名适配目录
mv /opt/lua-cjson-2.1.0.9 build/
mv build/lua-cjson-2.1.0.9 build/lua-cjson-2.1.0.17

# 进入 cjson 目录编译
cd build/lua-cjson-2.1.0.17
cp Makefile Makefile.bak
sed -i 's/ -pedantic//g' Makefile
make clean
make CC=gcc CJSON_CFLAGS="-g -fpic -std=gnu99"
cd ../..

6、编译 & 安装 OpenResty

bash 复制代码
# 多线程编译
make -j$(nproc)
# 安装到指定目录
make install

7、配置 systemd 系统服务

bash 复制代码
#编辑服务文件
#vim /etc/systemd/system/openresty.service
[Unit]
Description=OpenResty (Nginx) High Performance Web Server
After=network.target

[Service]
Type=forking
PIDFile=/data/openresty/nginx/logs/nginx.pid
ExecStartPre=/data/openresty/nginx/sbin/nginx -t
ExecStart=/data/openresty/nginx/sbin/nginx
ExecReload=/data/openresty/nginx/sbin/nginx -s reload
ExecStop=/data/openresty/nginx/sbin/nginx -s quit
PrivateTmp=true
Restart=on-failure

[Install]
WantedBy=multi-user.target

# 重载系统服务配置,只设开机自启,不执行 start
systemctl daemon-reload
# 设置开机自启,机器重启后自动接管
systemctl enable openresty
# 严禁执行:systemctl start openresty  (会和线上旧Nginx端口冲突)

四、平滑流量切换(业务低峰期执行)

1、 同步线上 Nginx 配置

bash 复制代码
cd /data/openresty/nginx
# 备份默认配置
cp -r conf conf.default
# 同步线上业务配置
/bin/cp -rf /data/nginx/conf/* conf/

# 修改nginx.conf的include路径
sed -i 's#include[[:space:]]\+/data/nginx/conf/vhost/\*\.conf;#include /data/openresty/nginx/conf/vhost/*.conf;#g' /data/openresty/nginx/conf/nginx.conf

# 强制校验配置语法(关键),这个有问题先处理好,再进行下一步操作,严禁这一步有问题继续执行下面操作
/data/openresty/nginx/sbin/nginx -t -c /data/openresty/nginx/conf/nginx.conf

2、替换系统软链接 + 旧 Nginx 二进制

bash 复制代码
# 备份原有系统 nginx 软链接(这一步可选,之前操作过一定要改,没有操作过可以不执行)
mv /usr/sbin/nginx /usr/sbin/nginx.old
ln -s /data/openresty/nginx/sbin/nginx /usr/sbin/nginx

# 备份旧 Nginx 二进制
mv /data/nginx/sbin/nginx /data/nginx/sbin/nginx.old
# 将 OpenResty 二进制拷贝到旧路径(热升级依赖原有启动路径)
/bin/cp /data/openresty/nginx/sbin/nginx /data/nginx/sbin/nginx

3、执行平滑热升级(零中断切换)

bash 复制代码
# 精准获取旧 Nginx Master 进程 PID
OLD_PID=$(ps -eo pid,args | grep 'nginx: master' | grep -v grep | awk '{print $1}')

# 1. USR2:拉起新版 OpenResty,新旧进程共存
kill -USR2  $OLD_PID
ps aux | grep nginx | grep -v grep

# 2. WINCH:旧进程停止接收新连接,新进程承载全部新流量
kill -WINCH $OLD_PID

4、业务观测(必做,观测 5~15 分钟)

bash 复制代码
检查项:页面访问、接口请求、HTTPS、HTTP2、日志报错、连接数、FastDFS 上传下载、全量业务功能。
确认全部正常再执行下一步。

5、优雅下线旧 Master 进程

注意:因为nginx版本跨度过大,这个操作很可能会存在两个master同时退出的情况,强烈要求在业务量非常小的情况下,执行,如果失败可以用systemctl start openresty恢复

bash 复制代码
kill -QUIT ${OLD_MASTER_PID}

# 最终校验,仅保留新版进程
CUR_MASTER_PID=$(ps -eo pid,args | grep 'nginx: master' | grep -v grep | awk '{print $1}')
RUN_BIN=$(readlink -f /proc/${CUR_MASTER_PID}/exe)
ps aux | grep nginx | grep -v grep
${RUN_BIN} -V

6、删除旧Nginx配置及Nginx的systemctl的services文件

bash 复制代码
#测试openresty完全没有问题的时候,需要进行下面操作
#删除旧的nginx配置
rm -rf /data/nginx
#删除nginx的service文件
mv /usr/lib/systemd/system/nginx.service

五、应急回滚步骤(升级异常立即执行)

注意:此回滚操作是基于两个master进程还同时存在时,如果已经执行了步骤四5、(优雅下线旧 Master 进程)则直接从下面第5个步骤开始操作

bash 复制代码
# 1. 获取新旧主进程 PID
OLD_PID=$(ps -eo pid,args | grep 'nginx: master' | grep -v grep | head -n1 | awk '{print $1}')
NEW_PID=$(ps -eo pid,args | grep 'nginx: master' | grep -v grep | tail -n1 | awk '{print $1}')

# 2. 新版停止接收新流量
kill -WINCH ${NEW_PID}
# 3. 恢复旧版本流量
kill -CONT ${OLD_PID}
# 4. 关闭新版进程
kill -QUIT ${NEW_PID}

# 5. 恢复旧二进制
cd /data/nginx/sbin
mv nginx nginx.new
mv nginx.old nginx
chmod +x nginx

# 6. 恢复系统软链接(这个根据实际情况执行)
rm -f /usr/sbin/nginx
mv /usr/sbin/nginx.old /usr/sbin/nginx

# 6. 重载配置
/data/nginx/sbin/nginx -t
/data/nginx/sbin/nginx -s reload

六、日常运维命令

bash 复制代码
# 停止
systemctl stop openresty
# 重载配置
systemctl reload openresty
# 查看状态
systemctl status openresty
# 查看日志
journalctl -u openresty -f

七、Nginx升级Openresty脚本

1、openresty_install.sh 编译部署脚本(只enable、不start)

bash 复制代码
#vim /root/script/openresty_install.sh
#!/bin/bash
# OpenResty 1.29.2.5 编译部署脚本【正式环境专用】
# 仅编译安装+配置systemd,不启动服务,避免和原有Nginx端口冲突
set -eo pipefail

# ===================== 全局变量 & 版本常量 =====================
# 路径
TOOLS_DIR="/opt"
OLD_NGINX_DIR="/data/nginx"
OPENRESTY_PREFIX="/data/openresty"
LOG_FILE="/var/log/openresty_install_$(date +%F).log"

# 版本&下载地址(统一管理,便于后续维护)
OR_VERSION="1.29.2.5"
LUA_CJSON_VERSION="2.1.0.9"
OPENSSL_VERSION="1.1.1w"
FASTDFS_MODULE_ZIP="5e5f3566bbfa57418b5506aaefbe107a42c9fcb1.zip"
FASTDFS_MODULE_DIR="fastdfs-nginx-module"

# 下载链接
OR_URL="https://openresty.org/download/openresty-${OR_VERSION}.tar.gz"
LUA_CJSON_URL="https://github.com/openresty/lua-cjson/archive/refs/tags/${LUA_CJSON_VERSION}.tar.gz"
OPENSSL_URL="https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz"
FASTDFS_MODULE_URL="https://github.com/happyfish100/fastdfs-nginx-module/archive/${FASTDFS_MODULE_ZIP}"

# 包文件名
OR_TAR="openresty-${OR_VERSION}.tar.gz"
LUA_CJSON_TAR="${LUA_CJSON_VERSION}.tar.gz"
OPENSSL_TAR="openssl-${OPENSSL_VERSION}.tar.gz"
# ==============================================================

# 日志函数
log(){
    echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $1" | tee -a "${LOG_FILE}"
}
# 错误退出
error_exit(){
    log "ERROR: $1"
    exit 1
}

# ===================== 通用函数:检测目录、包、自动下载解压 =====================
# 参数说明:
# $1: 目标目录名
# $2: 压缩包文件名
# $3: 下载地址
# $4: 解压命令 (tar/unzip)
check_and_extract() {
    local target_dir="$1"
    local pkg_file="$2"
    local dl_url="$3"
    local extract_cmd="$4"

    cd "${TOOLS_DIR}" || error_exit "进入工具目录 ${TOOLS_DIR} 失败"

    # 1. 目录已存在,直接返回
    if [ -d "${target_dir}" ]; then
        log "目录 ${target_dir} 已存在,跳过解压下载"
        return 0
    fi

    # 2. 目录不存在,检查压缩包是否存在
    if [ ! -f "${pkg_file}" ]; then
        log "未找到压缩包 ${pkg_file},开始在线下载..."
        # 使用curl/wget 下载,优先wget
        if command -v wget &> /dev/null; then
            wget -q "${dl_url}" -O "${pkg_file}" || error_exit "下载 ${pkg_file} 失败,请检查网络或链接"
        elif command -v curl &> /dev/null; then
            curl -fsSL "${dl_url}" -o "${pkg_file}" || error_exit "下载 ${pkg_file} 失败,请检查网络或链接"
        else
            error_exit "系统未找到 wget/curl,无法自动下载,请手动上传 ${pkg_file} 至 ${TOOLS_DIR}"
        fi
    fi

    # 3. 解压文件
    log "开始解压 ${pkg_file} 至 ${target_dir}"
    case "${extract_cmd}" in
        tar)
            tar -zxf "${pkg_file}" || error_exit "解压 ${pkg_file} 失败"
            ;;
        unzip)
            unzip -q "${pkg_file}" || error_exit "解压 ${pkg_file} 失败"
            ;;
        *)
            error_exit "未知解压命令: ${extract_cmd}"
            ;;
    esac

    # fastdfs 特殊处理:解压后目录重命名
    if [[ "${target_dir}" == "${FASTDFS_MODULE_DIR}" ]]; then
        local unzip_dir=$(unzip -qql "${pkg_file}" | head -n1 | awk '{print $4}' | sed 's/\/$//')
        [ -d "${unzip_dir}" ] && mv "${unzip_dir}" "${FASTDFS_MODULE_DIR}" || error_exit "fastdfs 目录重命名失败"
    fi

    log "${pkg_file} 解压完成"
}

# ===================== 前置环境校验 =====================
log "开始前置环境校验"
[ ! -d "${TOOLS_DIR}" ] && mkdir -p "${TOOLS_DIR}" && log "自动创建工具目录 ${TOOLS_DIR}"
[ ! -d "${OLD_NGINX_DIR}" ] && error_exit "原有Nginx目录 ${OLD_NGINX_DIR} 不存在,终止部署"

# ===================== 1. 交互确认 =====================
clear
echo "========================================="
echo "   OpenResty ${OR_VERSION} 编译部署脚本   "
echo "  正式环境:仅安装配置,不启动服务       "
echo "========================================="
read -p "确认开始部署?(y/n) " CHOICE
[[ ${CHOICE} != [Yy] ]] && log "用户取消,脚本退出" && exit 0

# ===================== 2. 备份原有 Nginx =====================
log "===== 1. 备份原有 Nginx 环境 ====="
${OLD_NGINX_DIR}/sbin/nginx -V > ${OLD_NGINX_DIR}/nginx.old.version.txt 2>&1
cp -a "${OLD_NGINX_DIR}" "${OLD_NGINX_DIR}_bak_$(date +%F)" || error_exit "Nginx 目录备份失败"
log "Nginx 备份完成"

# ===================== 3. 安装编译依赖 =====================
log "===== 2. 安装编译依赖 ====="
yum install -y gcc gcc-c++ make pcre pcre-devel zlib zlib-devel openssl-devel || error_exit "依赖安装失败"

# ===================== 4. 统一检测+下载+解压 所有源码包 =====================
log "===== 3. 检测/下载/解压 源码及模块 ====="
# 1) lua-cjson
check_and_extract "lua-cjson-${LUA_CJSON_VERSION}" "${LUA_CJSON_TAR}" "${LUA_CJSON_URL}" "tar"
# 2) openresty
check_and_extract "openresty-${OR_VERSION}" "${OR_TAR}" "${OR_URL}" "tar"
# 3) fastdfs-nginx-module
check_and_extract "${FASTDFS_MODULE_DIR}" "${FASTDFS_MODULE_ZIP}" "${FASTDFS_MODULE_URL}" "unzip"
# 4) openssl
check_and_extract "openssl-${OPENSSL_VERSION}" "${OPENSSL_TAR}" "${OPENSSL_URL}" "tar"

# ===================== 5. OpenResty 预编译 =====================
log "===== 4. 预编译配置 OpenResty ====="
cd "${TOOLS_DIR}/openresty-${OR_VERSION}" || error_exit "进入OpenResty源码目录失败"

./configure \
--prefix=${OPENRESTY_PREFIX} \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_sub_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-pcre \
--with-http_flv_module \
--with-http_dav_module \
--with-http_addition_module \
--with-openssl=${TOOLS_DIR}/openssl-${OPENSSL_VERSION} \
--add-module=${TOOLS_DIR}/${FASTDFS_MODULE_DIR}/src || error_exit "configure 失败"

# ===================== 6. 编译 lua-cjson =====================
log "===== 5. 编译 lua-cjson ====="
rm -rf build/lua-cjson-2.1.0.17
mv "${TOOLS_DIR}/lua-cjson-${LUA_CJSON_VERSION}" build/
mv "build/lua-cjson-${LUA_CJSON_VERSION}" build/lua-cjson-2.1.0.17

cd build/lua-cjson-2.1.0.17 || error_exit "进入lua-cjson目录失败"
cp Makefile Makefile.bak
sed -i 's/ -pedantic//g' Makefile
make clean
make CC=gcc CJSON_CFLAGS="-g -fpic -std=gnu99" || error_exit "cjson 编译失败"
cd ../../ || exit 1

# ===================== 7. 编译安装 OpenResty =====================
log "===== 6. 编译 & 安装 OpenResty ====="
make -j$(nproc) || error_exit "make 编译失败"
make install || error_exit "make install 失败"

# ===================== 8. 配置 systemd 服务(只配置、不启动) =====================
log "===== 7. 配置 systemd 服务,仅设开机自启,不启动服务 ====="
cat > /etc/systemd/system/openresty.service << EOF
[Unit]
Description=OpenResty (Nginx) High Performance Web Server
After=network.target

[Service]
Type=forking
PIDFile=${OPENRESTY_PREFIX}/nginx/logs/nginx.pid
ExecStartPre=${OPENRESTY_PREFIX}/nginx/sbin/nginx -t
ExecStart=${OPENRESTY_PREFIX}/nginx/sbin/nginx
ExecReload=${OPENRESTY_PREFIX}/nginx/sbin/nginx -s reload
ExecStop=${OPENRESTY_PREFIX}/nginx/sbin/nginx -s quit
PrivateTmp=true
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
# 仅开机自启,不启动服务,避免端口冲突
systemctl enable openresty

# ===================== 9. 部署收尾 =====================
log "========================================="
log " OpenResty 编译+配置完成,未启动服务 "
log " 下一步:业务低峰期执行切换脚本 "
log " 日志路径:${LOG_FILE}"
log "========================================="

2、平滑切换 & 热升级脚本 openresty_switch.sh

执行这个脚本,选择下线旧master后,因为nginx版本跨度过大,这个操作很可能会存在两个master同时退出的情况,强烈要求在业务量非常小的情况下,执行,如果失败脚本会尝试用systemctl start openresty恢复;另外,此脚本为了安全起见,没有添加删除旧Nginx配置及Nginx的systemctl的services文件的操作,可以参考步骤四、6手动进行处理。

bash 复制代码
#vim /root/script/openresty_switch.sh
#!/bin/bash
# OpenResty 平滑流量切换 + 热升级脚本【生产环境专用】
# 执行时机:业务低峰期,编译安装完成后使用
# 功能:同步配置、替换二进制/软链、USR2+WINCH 零中断热升级
# 【重要风险提示】本方式为跨二进制热升级,存在新版进程静默退出兼容风险
set -eo pipefail

# ===================== 全局路径变量(统一维护,勿随意修改) =====================
OLD_NGINX_HOME="/data/nginx"
NEW_OR_HOME="/data/openresty/nginx"
SYS_BIN_NGINX="/usr/sbin/nginx"
LOG_FILE="/var/log/openresty_switch_$(date +%F_%H%M%S).log"
TMP_ERR_LOG="/tmp/openresty_check.log"
SCRIPT_LOCK="/var/lock/openresty_switch.lock"
# ==============================================================================

# 日志输出函数
log() {
    echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $1" | tee -a "${LOG_FILE}"
}

# 错误退出函数
error_exit() {
    log "【ERROR】$1"
    echo -e "\033[31m 操作失败,脚本强制退出!\033[0m"
    exit 1
}

# 彩色提示
info_msg() {
    echo -e "\033[32m $1 \033[0m"
}
warn_msg() {
    echo -e "\033[33m $1 \033[0m"
}

# -------------------------- 安全辅助函数 --------------------------
# 安全获取nginx master pid数组,规避pipefail导致脚本直接退出
get_nginx_master_pids() {
    ps -eo pid,args 2>/dev/null \
    | grep -E '(nginx|openresty): master process' \
    | grep -v grep \
    | awk '{print $1}' || true
}

# 获取Worker数量
# 缺陷说明:无法区分worker归属哪个master,仅做基础存活判断
get_worker_count() {
    ps -ef \
    | grep "worker process" \
    | grep -v grep \
    | wc -l
}

# 检查端口监听
check_listen_port() {
    local port="$1"
    if ss -lntp 2>/dev/null | grep -E ":[[:space:]]*${port}([[:space:]]|$)" >/dev/null; then
        log "端口 ${port} 监听正常"
        return 0
    fi
    return 1
}

# 从nginx配置读取error_log路径
get_error_log_path() {
    local conf_path="$1"
    grep -E '^\s*error_log\s+' "${conf_path}" | head -1 | awk '{print $2}' || echo "${OLD_NGINX_HOME}/logs/error.log"
}

check_error_log() {
    local main_conf="${NEW_OR_HOME}/conf/nginx.conf"
    local log_file=$(get_error_log_path "${main_conf}")

    rm -f "${TMP_ERR_LOG}"
    [ ! -f "${log_file}" ] && return 0

    grep -Ei \
    "emerg|panic|segmentation fault|failed" \
    "${log_file}" \
    | tail -20 > "${TMP_ERR_LOG}" || true

    if [[ -s "${TMP_ERR_LOG}" ]]; then
        log "发现疑似异常日志(${log_file}):"
        cat "${TMP_ERR_LOG}"
        echo ""
        read -p "检测到异常日志,是否继续?(YES继续): " ERR_CONFIRM
        [[ "${ERR_CONFIRM}" != "YES" ]] \
            && error_exit "用户终止切换"
    fi
}

# 校验PID对应的进程是否真实存活
is_pid_alive() {
    local pid="$1"
    [[ -n "${pid}" && -d "/proc/${pid}" ]]
}

# 获取进程可执行文件真实路径(排除 deleted 标记)
get_proc_exe() {
    local pid="$1"
    local path
    path=$(readlink -f "/proc/${pid}/exe" 2>/dev/null || echo "")
    if [[ "${path}" == *"(deleted)"* ]]; then
        echo ""
        return
    fi
    echo "${path}"
}
# ------------------------------------------------------------------------

# ===================== 前置全局校验 =====================
# 1. 校验root权限
if [[ $EUID -ne 0 ]]; then
    echo "必须使用 root 用户执行此脚本" >&2
    exit 1
fi

# 2. 排他锁,防止并发执行
exec 9>"${SCRIPT_LOCK}"
if ! flock -n 9; then
    echo "脚本已在运行中,禁止重复执行" >&2
    exit 1
fi

# 清理临时文件
rm -f "${TMP_ERR_LOG}"

log "========== 开始执行 OpenResty 平滑切换脚本 =========="
# 校验目录存在
[ ! -d "${OLD_NGINX_HOME}" ] && error_exit "旧 Nginx 目录 ${OLD_NGINX_HOME} 不存在"
[ ! -d "${NEW_OR_HOME}" ] && error_exit "新 OpenResty 目录 ${NEW_OR_HOME} 不存在"

# 校验 Nginx 进程是否正常运行(使用安全函数)
MASTER_PID_INIT=($(get_nginx_master_pids))
VALID_INIT_PIDS=()
for pid in "${MASTER_PID_INIT[@]}"; do
    if is_pid_alive "${pid}"; then
        VALID_INIT_PIDS+=("${pid}")
    fi
done
OLD_PID="${VALID_INIT_PIDS[0]}"

if [[ -z "${OLD_PID}" ]]; then
    error_exit "未检测到 Nginx Master 进程,请先启动原有 Nginx 再执行"
fi
log "检测到原有 Nginx Master PID: ${OLD_PID}"

# 二次交互确认(生产强制确认)
clear
warn_msg "============================ 重要警告 ============================"
warn_msg "  此脚本为生产流量切换+热升级,必须在【业务低峰期】执行!"
warn_msg "  执行前请确认:原有 Nginx 运行正常、配置无误!"
warn_msg "=================================================================="
read -p "确认继续执行平滑切换?(输入 YES 继续,其他退出): " CONFIRM
[[ "${CONFIRM}" != "YES" ]] && log "用户取消操作,脚本退出" && exit 0

# ===================== 步骤1:同步原有Nginx配置 =====================
log "===== 步骤1:同步线上 Nginx 业务配置 ====="
cd "${NEW_OR_HOME}" || error_exit "进入 ${NEW_OR_HOME} 目录失败"

# 备份新程序默认配置
[ -d "conf.default" ] || cp -r conf conf.default || error_exit "新配置目录备份失败"
log "已备份 OpenResty 默认配置为 conf.default"

# 同步旧配置至新目录
/bin/cp -rf "${OLD_NGINX_HOME}/conf/"* conf/ || error_exit "同步业务配置失败"
log "业务配置同步完成"

# 修改 nginx.conf 中 vhost include 路径
log "===== 修改 nginx.conf 中 vhost include 路径 ====="
OLD_INCLUDE_PATH="${OLD_NGINX_HOME}/conf/vhost/*.conf"
NEW_INCLUDE_PATH="${NEW_OR_HOME}/conf/vhost/*.conf"
NGINX_CONF="${NEW_OR_HOME}/conf/nginx.conf"

# 路径中 * / . 属于正则元字符,统一转义
escape_regex() {
    echo "$1" | sed 's/[.*\/]/\\&/g'
}
OLD_INC_ESC=$(escape_regex "${OLD_INCLUDE_PATH}")
log "调试:正则匹配模板:include[[:space:]]+${OLD_INC_ESC};"

# 检查是否存在待替换行
if ! grep -qE "include[[:space:]]+${OLD_INC_ESC};" "${NGINX_CONF}"; then
    error_exit "未在 ${NGINX_CONF} 中找到待替换的 include 行:${OLD_INCLUDE_PATH}"
fi

# 执行替换
sed -i "s|include[[:space:]]\+${OLD_INC_ESC};|include ${NEW_INCLUDE_PATH};|g" "${NGINX_CONF}"

# 二次校验替换结果
if grep -qE "include[[:space:]]+${OLD_INC_ESC};" "${NGINX_CONF}"; then
    error_exit "替换失败:${OLD_INCLUDE_PATH} 仍然存在于 ${NGINX_CONF}"
fi

log "已执行替换:${OLD_INCLUDE_PATH} → ${NEW_INCLUDE_PATH}"

# 强制校验配置语法(核心卡点,出错直接终止)
log "开始校验 OpenResty 配置语法..."
"${NEW_OR_HOME}/sbin/nginx" -t -c "${NEW_OR_HOME}/conf/nginx.conf" || error_exit "OpenResty 配置语法错误,请修复后再执行"
log "OpenResty 配置语法校验通过"

# ===================== 步骤2:替换系统软链接 & 旧二进制文件 =====================
log "===== 步骤2:替换系统软链接与旧 Nginx 二进制 ====="

# 备份系统原有 nginx 软链
if [ -L "${SYS_BIN_NGINX}" ]; then
    mv -f "${SYS_BIN_NGINX}" "${SYS_BIN_NGINX}.old" || error_exit "备份系统 nginx 软链失败"
    log "已备份系统软链接: ${SYS_BIN_NGINX}.old"
fi
# 创建新软链指向 OpenResty
ln -sf "${NEW_OR_HOME}/sbin/nginx" "${SYS_BIN_NGINX}" || error_exit "创建系统软链失败"
log "系统 nginx 软链接已指向 OpenResty"

# 备份旧 Nginx 二进制
OLD_BIN="${OLD_NGINX_HOME}/sbin/nginx"
if [ -f "${OLD_BIN}" ]; then
    mv -f "${OLD_BIN}" "${OLD_BIN}.old" || error_exit "备份旧 Nginx 二进制失败"
    log "已备份旧二进制文件: ${OLD_BIN}.old"
fi
# 拷贝 OpenResty 二进制到旧路径(适配热升级原有路径)
/bin/cp -f "${NEW_OR_HOME}/sbin/nginx" "${OLD_BIN}" || error_exit "拷贝 OpenResty 二进制失败"
log "OpenResty 二进制已拷贝至旧 Nginx 路径"

# ===================== 步骤3:执行标准热升级(零中断) =====================
log "===== 步骤3:执行 USR2 拉起新版进程 ====="
# 校验旧PID仍然存活
if ! is_pid_alive "${OLD_PID}"; then
    error_exit "原旧 Master PID ${OLD_PID} 已不存在,无法继续热升级"
fi
kill -USR2 "${OLD_PID}" || error_exit "发送 USR2 信号失败"
log "等待新版 Master/Worker 完全启动..."
sleep 10

# 校验是否产生新 Master 进程(使用安全函数+存活校验)
MASTER_PID_AFTER_USR2=($(get_nginx_master_pids))
VALID_USR2_PIDS=()
for pid in "${MASTER_PID_AFTER_USR2[@]}"; do
    if is_pid_alive "${pid}"; then
        VALID_USR2_PIDS+=("${pid}")
    fi
done
PID_COUNT=${#VALID_USR2_PIDS[@]}

if [[ ${PID_COUNT} -lt 2 ]]; then
    error_exit "拉起新版进程失败,当前仅 ${PID_COUNT} 个 Master 进程"
fi

log "检测到 ${PID_COUNT} 个 Master 进程"

WORKER_NUM=$(get_worker_count)

if [[ ${WORKER_NUM} -eq 0 ]]; then
    error_exit "未检测到 Worker 进程"
fi

log "检测到 Worker 数量: ${WORKER_NUM}"

check_listen_port 80 \
    || error_exit "80端口未监听"

if grep -RqsE '^[[:space:]]*listen[[:space:]]+.*443' "${NEW_OR_HOME}/conf"; then
    check_listen_port 443 \
        || error_exit "443端口未监听"
fi

check_error_log

log "新版进程启动检查通过"

ps -ef | grep nginx | grep -v grep || true

# 发送 WINCH 信号:旧进程停止接收新连接
log "===== 步骤4:发送 WINCH 信号,旧进程停止接收新流量 ====="
if ! is_pid_alive "${OLD_PID}"; then
    error_exit "原旧 Master PID ${OLD_PID} 已不存在,无法发送 WINCH"
fi
kill -WINCH "${OLD_PID}" || error_exit "发送 WINCH 信号失败"
sleep 1
log "旧 Master 已停止接收新连接,新进程全权承载流量"

# ===================== 步骤4:业务观测提示(人工值守) =====================
echo ""
info_msg "=================================================================="
info_msg "  【重要】请人工观测业务 5~15 分钟:"
info_msg "  1. 页面/接口访问、HTTPS、HTTP2 正常"
info_msg "  2. 服务日志无报错、连接数正常"
info_msg "  3. FastDFS 上传/下载功能正常"
info_msg "  全部业务正常后,再执行【优雅下线旧Master】操作!"
info_msg "=================================================================="
read -p "业务观测正常,准备下线旧Master?(输入 YES 继续): " DOWN_CONFIRM
[[ "${DOWN_CONFIRM}" != "YES" ]] && log "暂停下线操作,脚本结束" && exit 0

# ===================== 步骤5:优雅下线旧 Master 进程 + 最终校验 =====================
log "===== 步骤5:优雅退出旧 Master 进程 ====="
if ! is_pid_alive "${OLD_PID}"; then
    error_exit "原旧 Master PID ${OLD_PID} 已不存在,无法发送 QUIT"
fi
kill -QUIT "${OLD_PID}" || error_exit "发送 QUIT 信号关闭旧进程失败"
sleep 2

# 获取并过滤有效存活PID + 过滤已删除二进制进程
MASTER_PID_LIST=($(get_nginx_master_pids))
VALID_PIDS=()
NEW_OR_MASTER_PIDS=()
for pid in "${MASTER_PID_LIST[@]}"; do
    if is_pid_alive "${pid}"; then
        exe_path=$(get_proc_exe "${pid}")
        if [[ -n "${exe_path}" ]]; then
            VALID_PIDS+=("${pid}")
            # 判断是否为 OpenResty 二进制
            if [[ "${exe_path}" == "${NEW_OR_HOME}/sbin/nginx" || "${exe_path}" == "${OLD_NGINX_HOME}/sbin/nginx" ]]; then
                NEW_OR_MASTER_PIDS+=("${pid}")
            fi
        fi
    fi
done
PID_NUM=${#VALID_PIDS[@]}
NEW_OR_PID_NUM=${#NEW_OR_MASTER_PIDS[@]}

# 校验进程存活状态
if [[ ${PID_NUM} -eq 0 ]]; then
    log "【严重异常】未检测到任何 Nginx/OpenResty Master 进程!"
    log "当前系统 nginx 相关进程快照:"
    ps aux | grep -E 'nginx|openresty' | grep -v grep || true

    log "配置文件前期已校验语法正常,尝试执行 systemctl start openresty 恢复服务"
    if systemctl start openresty; then
        log "systemctl start openresty 执行成功,正在重新检测 Master 进程"
        sleep 2
        MASTER_PID_LIST=($(get_nginx_master_pids))
        VALID_PIDS=()
        NEW_OR_MASTER_PIDS=()
        for pid in "${MASTER_PID_LIST[@]}"; do
            if is_pid_alive "${pid}"; then
                exe_path=$(get_proc_exe "${pid}")
                if [[ -n "${exe_path}" ]]; then
                    VALID_PIDS+=("${pid}")
                    if [[ "${exe_path}" == "${NEW_OR_HOME}/sbin/nginx" || "${exe_path}" == "${OLD_NGINX_HOME}/sbin/nginx" ]]; then
                        NEW_OR_MASTER_PIDS+=("${pid}")
                    fi
                fi
            fi
        done
        PID_NUM=${#VALID_PIDS[@]}
        NEW_OR_PID_NUM=${#NEW_OR_MASTER_PIDS[@]}
        if [[ ${PID_NUM} -eq 0 ]]; then
            log "尝试启动 openresty 后仍无存活 Master 进程,需人工介入排查"
            error_exit "所有 Nginx 进程异常退出,自动恢复失败,请人工执行应急恢复!"
        else
            log "自动恢复成功,检测到 ${PID_NUM} 个存活 Master 进程,新版进程数: ${NEW_OR_PID_NUM}"
        fi
    else
        log "systemctl start openresty 执行失败,请人工介入排查"
        error_exit "所有 Nginx 进程异常退出,自动恢复失败,请人工执行应急恢复!"
    fi
fi

# 多Master强制人工确认
if [[ ${PID_NUM} -gt 1 ]]; then
    warn_msg "警告:系统仍存在多个 Master 进程!"
    ps aux | grep -E 'nginx: master' | grep -v grep
    read -p "确认多个Master状态可接受并继续?(输入 YES 继续): " MULTI_CONFIRM
    [[ "${MULTI_CONFIRM}" != "YES" ]] && error_exit "用户终止,存在多个Master进程"
fi

# 优先取新版 OpenResty Master
if [[ ${NEW_OR_PID_NUM} -ge 1 ]]; then
    FINAL_PID="${NEW_OR_MASTER_PIDS[0]}"
else
    FINAL_PID="${VALID_PIDS[0]}"
fi

log "检测到 ${PID_NUM} 个存活 Master 进程,新版 OpenResty Master 数: ${NEW_OR_PID_NUM},选取运行PID: ${FINAL_PID}"

WORKER_NUM=$(get_worker_count)

if [[ ${WORKER_NUM} -eq 0 ]]; then
    error_exit "最终校验失败:Worker数量为0"
fi

log "最终 Worker 数量: ${WORKER_NUM}"

RUN_BIN=$(get_proc_exe "${FINAL_PID}")
if [[ -n "${RUN_BIN}" ]]; then
    log "当前运行二进制: ${RUN_BIN}"
    if [[ -x "${RUN_BIN}" ]]; then
        "${RUN_BIN}" -V
    else
        log "二进制文件无执行权限"
    fi
else
    log "无法读取进程二进制路径(进程二进制已被删除)"
fi

log "========== OpenResty 平滑切换 & 热升级 执行完成 =========="
if [[ ${NEW_OR_PID_NUM} -ge 1 ]]; then
    echo -e "\033[32m 切换成功!当前已全量使用 OpenResty 提供服务 \033[0m"
else
    warn_msg "未检测到新版 OpenResty Master,请人工核查!"
fi

3、应急回滚脚本 openresty_rollback.sh

适配两种场景:

刚热升级、新旧 Master 都存在(未执行 kill -QUIT)→ 完整回滚流量 + 进程

已下线旧 Master → 仅恢复二进制、软链、重载配置

bash 复制代码
#vim /root/script/openresty_rollback.sh
#!/bin/bash
# OpenResty 应急回滚脚本【生产故障专用】
# 场景1:新旧Master进程同时存在 → 流量切回旧版本 + 关闭新版
# 场景2:旧Master已下线 → 仅恢复二进制、软链、重载配置
set -eo pipefail

# ===================== 全局路径变量(与切换脚本保持一致) =====================
OLD_NGINX_HOME="/data/nginx"
NEW_OR_HOME="/data/openresty/nginx"
SYS_BIN_NGINX="/usr/sbin/nginx"
LOG_FILE="/var/log/openresty_rollback_$(date +%F_%H%M%S).log"
# ==============================================================================

# 日志输出函数
log() {
    echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $1" | tee -a "${LOG_FILE}"
}

# 错误退出函数
error_exit() {
    log "【ERROR】$1"
    echo -e "\033[31m 回滚操作异常,请人工介入排查!\033[0m"
    exit 1
}

# 彩色提示
warn_msg() {
    echo -e "\033[33m $1 \033[0m"
}

# ===================== 前置校验 & 场景判断 =====================
log "========== 开始执行 OpenResty 应急回滚 =========="
# 获取所有 Master PID
PID_LIST=($(ps -eo pid,args | grep 'nginx: master' | grep -v grep | awk '{print $1}'))
PID_NUM=${#PID_LIST[@]}

log "当前检测到 Nginx Master 进程数量: ${PID_NUM}"

# 二次强确认
clear
warn_msg "==================== 应急回滚警告 ===================="
warn_msg "  此操作会将服务切回原 Nginx,仅故障时使用!"
warn_msg "======================================================"
read -p "确认执行回滚?(输入 ROLLBACK 继续): " CONFIRM
[[ "${CONFIRM}" != "ROLLBACK" ]] && log "用户取消回滚,脚本退出" && exit 0

# ===================== 场景1:两个Master进程共存(优先处理流量切回) =====================
if [[ ${PID_NUM} -eq 2 ]]; then
    log "【场景判断】新旧Master进程共存,执行流量切回旧版本"
    OLD_PID=${PID_LIST[0]}
    NEW_PID=${PID_LIST[1]}
    log "旧版本PID: ${OLD_PID}  新版本PID: ${NEW_PID}"

    # 新版停止接收新流量
    kill -WINCH "${NEW_PID}" || error_exit "停止新版进程接收流量失败"
    sleep 1
    # 恢复旧版本接收流量
    kill -CONT "${OLD_PID}" || error_exit "恢复旧版本流量失败"
    sleep 1
    log "流量已切回原有 Nginx"

    # 优雅关闭新版进程
    kill -QUIT "${NEW_PID}" || error_exit "关闭新版 OpenResty 进程失败"
    sleep 2
    log "新版 OpenResty 进程已关闭"
fi

# ===================== 统一恢复:二进制文件 =====================
log "===== 恢复旧 Nginx 二进制文件 ====="
cd "${OLD_NGINX_HOME}/sbin" || error_exit "进入旧 Nginx sbin 目录失败"

# 恢复原始 nginx 二进制
if [ -f "nginx.old" ]; then
    mv -f nginx nginx.new 2>/dev/null || true
    mv -f nginx.old nginx || error_exit "恢复旧二进制失败"
    chmod +x nginx
    log "旧 Nginx 二进制恢复完成"
else
    log "未找到二进制备份 nginx.old,跳过此步骤"
fi

# ===================== 统一恢复:系统软链接 =====================
log "===== 恢复系统 /usr/sbin/nginx 软链接 ====="
if [ -L "${SYS_BIN_NGINX}.old" ]; then
    rm -f "${SYS_BIN_NGINX}"
    mv -f "${SYS_BIN_NGINX}.old" "${SYS_BIN_NGINX}" || error_exit "恢复系统软链接失败"
    log "系统 nginx 软链接恢复完成"
else
    log "未找到软链接备份 nginx.old,跳过此步骤"
fi

# ===================== 重载配置,最终生效 =====================
log "===== 校验配置并重载服务 ====="
"${OLD_NGINX_HOME}/sbin/nginx" -t || error_exit "旧 Nginx 配置语法错误,回滚失败"
log "旧 Nginx 配置校验通过"

"${OLD_NGINX_HOME}/sbin/nginx" -s reload || error_exit "重载 Nginx 配置失败"
log "Nginx 配置重载完成"

# 最终状态输出
ps aux | grep nginx | grep -v grep
log "========== 应急回滚全部执行完成 =========="
echo -e "\033[32m 回滚成功,当前服务已切回原有 Nginx \033[0m"
相关推荐
Cry丶3 小时前
一次 FTP 上传引发 504 Gateway Timeout 的排查复盘
java·nginx·ftp·openresty·被动模式·504 gateway·生产问题排查
一勺菠萝丶18 小时前
生产环境平滑升级实战-Nginx维护页数据库迁移与安全回滚
数据库·nginx·安全
Helen_cai1 天前
OpenHarmony 路由封装 RouterUtil 页面跳转统一管理(API Version23+)
运维·服务器·nginx·华为·harmonyos
云计算磊哥@1 天前
运维开发宝典055-大型网站nginx服务器管理全集1
服务器·nginx·运维开发
碎碎念_4922 天前
前后端分离项目开发规范
nginx·vue·springboot·restful
刘某的Cloud2 天前
手工配置nginx的systemd服务
linux·运维·网络·nginx·systemd
刘某的Cloud2 天前
Nginx 最小安全配置模板
linux·运维·nginx·安全·系统
2501_915716723 天前
Nginx网关讲解_小白版
运维·nginx
悦儿遥遥雨14 天前
PXE + Kickstart 无人值守批量部署系统
linux·javascript·nginx