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"
相关推荐
lpfasd12313 小时前
WinSW在Win7上失败真相-实测与修复
windows·nginx
lpfasd12316 小时前
nginx服务化五套方案原理解剖与选型
运维·nginx
ue星空17 小时前
ALS V4 中文拆解手册
运维·nginx
吴声子夜歌2 天前
Nginx应用与运维——Nginx集群负载与配置管理(二)
运维·nginx
cpolar技术支持2 天前
外网测试机的异常传不回内网?自托管 Sentry,用 cpolar 打通错误上报链路
python·nginx·docker·cpolar·sentry
吴声子夜歌2 天前
Nginx应用与运维——Nginx在Kubernetes中的应用(一)
运维·nginx·kubernetes
螺蛳粉 螺蛳粉2 天前
第一篇:Keepalived 高可用实战:VIP 漂移与 Nginx 主备切换完整指南
运维·nginx·负载均衡·keepalived·高可用
吴声子夜歌2 天前
Nginx应用与运维——Nginx在Kubernetes中的应用(三)
运维·nginx·kubernetes
吴声子夜歌2 天前
Nginx应用与运维——Nginx在Kubernetes中的应用(二)
运维·nginx·kubernetes
Wang's Blog2 天前
Java 项目实战: 外卖平台优化-Nginx配置文件结构与块层级
java·开发语言·nginx