Shell脚本如何实现滚动更新策略

wen 实用脚本 22

Shell脚本实现滚动更新策略:从零搭建自动化无损部署方案

目录导读

  • 什么是滚动更新策略及其核心价值
  • 滚动更新的技术原理与流程拆解
  • Shell脚本实现滚动更新的完整代码示例
  • 关键功能模块的设计与解析
  • 常见问题与QA环节
  • 性能优化与生产环境注意事项

什么是滚动更新策略及其核心价值

在现代微服务架构中,服务实例通常以多副本方式运行(如Kubernetes的Deployment或自建的Nginx+Tomcat集群)。滚动更新是一种逐步替换旧版本实例为新版本的部署策略,其核心目标是在不停机(Zero Downtime)的前提下,保证服务整体可用性

Shell脚本如何实现滚动更新策略

滚动更新像“换轮胎时车不需要停下”:每次只停止一个旧实例,启动一个新实例,确认健康后再处理下一个,这种方式与“蓝绿部署”或“一次性全量替换”相比,资源占用更低,更适合预算有限或需要持续服务的中小团队。

该策略的关键收益:

  • 避免全量更新导致的流量瞬间中断
  • 允许回滚至上一版本(通过保留旧实例状态)
  • 降低资源峰值需求(不需要双倍资源)

滚动更新的技术原理与流程拆解

一个标准的滚动更新脚本通常包含以下五个阶段:

检查当前运行实例数(假设为N个)
2. 从负载均衡(如Nginx/Haproxy)中移除第i个实例
3. 发送SIGTERM信号给该实例,等待优雅关闭(Graceful Shutdown)
4. 启动新版本实例,并等待其健康检查通过
5. 将实例重新加入负载均衡,重复步骤2,直至所有实例更新完成

关键抽象: 脚本本质是一个“控制循环”,根据实例健康状态决定下一次替换的时机,若任何步骤失败(如新实例无法启动),则脚本应触发回滚或停止更新。


Shell脚本实现滚动更新的完整代码示例

以下是一个可直接运行的Bash脚本,适用于后端服务(如基于Nginx+Tomcat)或任意经过HTTP健康检查的实例。

#!/bin/bash
# 滚动更新部署脚本 v2.1
# 用法: ./rolling-update.sh <旧版本路径> <新版本路径> <实例前缀> <副本数>
set -euo pipefail
# ---------- 配置参数 ----------
OLD_VER_PATH="${1:-/opt/app/old}"
NEW_VER_PATH="${2:-/opt/app/new}"
INSTANCE_PREFIX="${3:-app}"
REPLICA_COUNT="${4:-3}"
LB_CHECK_SCRIPT="/usr/local/bin/check_health.sh"
GRACEFUL_SHUTDOWN_SEC=15
HEALTH_CHECK_RETRIES=10
HEALTH_CHECK_INTERVAL=3
# -----------------------------
log_info() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}
die() {
    echo "[ERROR] $*" >&2
    exit 1
}
# 健康检查函数
check_instance_health() {
    local instance_id="$1"
    for ((i=1; i<=HEALTH_CHECK_RETRIES; i++)); do
        if bash "$LB_CHECK_SCRIPT" "$instance_id" >/dev/null 2>&1; then
            return 0
        fi
        sleep "$HEALTH_CHECK_INTERVAL"
    done
    return 1
}
# 优雅停止实例
graceful_stop() {
    local instance_id="$1"
    log_info "正在停止实例 $instance_id ..."
    app_pid=$(systemctl show --property MainPID "${INSTANCE_PREFIX}-${instance-id}" 2>/dev/null | cut -d= -f2)
    if [[ -n "$app_pid" && "$app_pid" -ne 0 ]]; then
        kill -TERM "$app_pid"
        sleep "$GRACEFUL_SHUTDOWN_SEC"
        if kill -0 "$app_pid" 2>/dev/null; then
            log_info "强制终止实例 $instance_id ..."
            kill -9 "$app_pid"
        fi
    fi
}
# 启动新版本实例
start_new_instance() {
    local instance_id="$1"
    log_info "正在启动新实例 $instance_id (版本: ${NEW_VER_PATH}) ..."
    # 假设每个实例通过systemd管理,配置文件预先不发布即可
    systemctl start "${INSTANCE_PREFIX}-${instance-id}" || return 1
    # 等待健康检查
    if check_instance_health "$instance_id"; then
        log_info "实例 $instance_id 健康检查通过"
        return 0
    else
        log_info "实例 $instance_id 健康检查失败,开始回滚"
        return 1
    fi
}
# 从负载均衡移除实例(假设使用Nginx上游列表)
remove_from_lb() {
    local instance_id="$1"
    log_info "从负载均衡移除实例 $instance_id ..."
    # 示例:修改nginx上游配置文件
    sed -i "/server 127.0.0.1:${instance_id}-port/s/^/#/" /etc/nginx/conf.d/upstream.conf
    nginx -s reload 2>/dev/null
}
# 将实例加回负载均衡
add_to_lb() {
    local instance_id="$1"
    log_info "将实例 $instance_id 加回负载均衡 ..."
    sed -i "/# server 127.0.0.1:${instance_id}-port/s/^#//" /etc/nginx/conf.d/upstream.conf
    nginx -s reload 2>/dev/null
}
# ---------- 主更新逻辑 ----------
main() {
    # 1. 检查前置条件
    [[ ! -d "$NEW_VER_PATH" ]] && die "新版本路径不存在: $NEW_VER_PATH"
    [[ ! -d "$OLD_VER_PATH" ]] && die "旧版本路径不存在: $OLD_VER_PATH"
    log_info "开始滚动更新,总实例数: $REPLICA_COUNT"
    # 2. 遍历每个实例
    for (( idx=1; idx<=REPLICA_COUNT; idx++ )); do
        instance_id="${INSTANCE_PREFIX}-${idx}"
        log_info "=== 开始更新实例 $instance_id ($idx/$REPLICA_COUNT) ==="
        # 3. 从负载均衡移除
        remove_from_lb "$instance_id"
        # 4. 优雅关闭旧实例
        graceful_stop "$instance_id"
        # 5. 替换程序文件(实际可软链接切换)
        rm -rf "/data/${INSTANCE_PREFIX}/${instance_id}/current"
        ln -sf "$NEW_VER_PATH" "/data/${INSTANCE_PREFIX}/${instance_id}/current"
        # 6. 启动新实例
        if ! start_new_instance "$instance_id"; then
            # 启动失败,执行回滚
            log_info "实例 $instance_id 启动失败,立即回滚"
            # 恢复旧版本文件
            rm -rf "/data/${INSTANCE_PREFIX}/${instance_id}/current"
            ln -sf "$OLD_VER_PATH" "/data/${INSTANCE_PREFIX}/${instance_id}/current"
            graceful_stop "$instance_id"
            start_new_instance "$instance_id" || die "回滚也失败,请联系运维"
            add_to_lb "$instance_id"
            die "滚动更新已中止于实例 $instance_id"
        fi
        # 7. 将实例重新加入负载均衡
        add_to_lb "$instance_id"
        log_info "实例 $instance_id 更新完成"
        # 可选:实例间加入短暂延迟,让负载均衡器均衡流量
        sleep 2
    done
    log_info "所有实例滚动更新完成,新版本已全量上线"
}
# 执行主流程
main "$@"

关键功能模块的设计与解析

健康检查机制是核心防线

脚本中的check_instance_health使用重试+间隔模式,防止因网络抖动静产生误判,生产环境中建议将健康检查独立为单独的脚本(如LB_CHECK_SCRIPT),支持HTTP/TCP/自定义命令多种模式。

优雅关闭的重要性

graceful_stop函数发送SIGTERM而非直接SIGKILL,给予现程序完成当前请求的时间(超时后强制关闭),超时值(GRACEFUL_SHUTDOWN_SEC)应根据业务SQL执行最长时间合理设置,一般10-30秒。

回滚逻辑与事务性

当新实例启动失败时,脚本自动恢复旧版本文件并重新启动,保证了每步更新的原子性,这是实现“安全滚动升级”的基石,实际场景中可进一步在回滚失败时发送告警通知。


常见问题与QA环节

Q1: 脚本执行过程中,如果剩余实例全部宕机怎么办?
A: 当前设计下,滚动更新期间每次只移除一个实例,剩余副本继续提供服务,但如果所有服务共享数据库或外部依赖出现问题,建议在步骤2增加前置检查:当前健康实例数 >= (REPLICA_COUNT/2 + 1),否则中止更新。

Q2: 如何适配云原生环境(如Kubernetes)?
A: Kubernetes原生支持rolling update,无需脚本,但你在自建Docker Swarm或使用systemd管理的传统环境时,本脚本可以直接改造:将systemctl替换为docker stop/start,加载均衡操作改为iptables规则或envoy的动态路由。

Q3: 脚本中的“实例ID”与负载均衡的映射如何实现?
A: 实际实现中,你可以在负载均衡配置文件中通过“端口号”或“特定标识”区分每个实例,例如后端Tomcat分别监听8081-8083端口,脚本通过索引$idx计算出对应端口,修改Nginx的upstream块。


性能优化与生产环境注意事项

  • 并行度控制:上述脚本是串行更新(逐个实例),若有高并发需求,可调整为并行更新2个实例(注意同时移除的实例数应小于总副本数-1,避免雪崩)。
  • 日志审计:所有操作应记录到文件(如/var/log/rolling-update.log),便于问题追溯。
  • 配置文件热重载:如果程序支持动态配置,可直接使用ln -sf切换文件后发送SIGHUP信号,而无需重启进程。
  • 外部健康探测:建议在负载均衡层使用主动健康检查(如Nginx的health_check模块),替代脚本内置的被动检查。

将以上脚本添加到你的CI/CD流水线中后,你只需执行:

./rolling-update.sh /opt/old_version /opt/new_version myapp 5

即可完成5个副本的无中断更新,修改LB_CHECK_SCRIPT中的逻辑,可以灵活对接任意负载均衡器或服务注册中心,滚动更新不是复杂的魔法,而是一套精心设计的脚本化流程——掌握它,你将拥有生产级部署的底层能力。

抱歉,评论功能暂时关闭!