如何实现 Web 触发后的“离线”升级?Systemd 异步机制与 A/B 状态机切换详解

目录

[利用 Systemd Path 单元实现可靠系统升级 ------ 支持 Web 接口触发且父进程退出后仍可继续执行](#利用 Systemd Path 单元实现可靠系统升级 —— 支持 Web 接口触发且父进程退出后仍可继续执行)

[1. 背景与目标](#1. 背景与目标)

[2. 核心设计原则](#2. 核心设计原则)

[3. 系统架构概览](#3. 系统架构概览)

[4. 详细实现](#4. 详细实现)

[4.1 升级脚本(独立执行单元)](#4.1 升级脚本(独立执行单元))

[4.2 Systemd 单元配置](#4.2 Systemd 单元配置)

[4.3 Web 接口实现(FastAPI 示例)](#4.3 Web 接口实现(FastAPI 示例))

[5. 为何父进程退出不影响升级?](#5. 为何父进程退出不影响升级?)

进程关系图

[6. 增强可靠性建议](#6. 增强可靠性建议)

[6.1 使用持久化触发目录](#6.1 使用持久化触发目录)

[6.2 添加升级状态查询接口](#6.2 添加升级状态查询接口)

[6.3 升级脚本增加锁机制](#6.3 升级脚本增加锁机制)

[7. 总结](#7. 总结)

[AB 升级详解:基于软链接与配置切换的双环境热升级方案](#AB 升级详解:基于软链接与配置切换的双环境热升级方案)

[1. 什么是 AB 升级?](#1. 什么是 AB 升级?)

[2. 适用场景](#2. 适用场景)

[3. 核心设计](#3. 核心设计)

[3.1 目录结构](#3.1 目录结构)

[3.2 状态管理](#3.2 状态管理)

[4. 升级流程(详细步骤)](#4. 升级流程(详细步骤))

[5. 完整可执行示例](#5. 完整可执行示例)

[5.1 升级脚本:/opt/ab-upgrade.sh](#5.1 升级脚本:/opt/ab-upgrade.sh)

[5.2 回滚脚本(可选):/opt/ab-rollback.sh](#5.2 回滚脚本(可选):/opt/ab-rollback.sh)

[5.3 systemd 单元(与前文一致)](#5.3 systemd 单元(与前文一致))

[5.4 Web 接口(FastAPI)保持不变](#5.4 Web 接口(FastAPI)保持不变)

[6. 关键技术点解析](#6. 关键技术点解析)

[6.1 原子性软链接切换](#6.1 原子性软链接切换)

[6.2 环境轮换逻辑](#6.2 环境轮换逻辑)

[6.3 与主服务解耦](#6.3 与主服务解耦)

[7. 优势与局限](#7. 优势与局限)

优势

局限

[8. 总结](#8. 总结)


离线升级演示

利用 Systemd Path 单元实现可靠系统升级 ------ 支持 Web 接口触发且父进程退出后仍可继续执行

1. 背景与目标

在许多嵌入式或远程管理场景中,系统需支持通过 HTTP 接口(如 FastAPI、Flask)接收升级包并触发升级流程。但存在一个关键问题:

Web 服务进程(如 Python 进程)若在升级过程中被终止(例如因重启、崩溃或主动退出),如何确保升级任务不中断?

本文将介绍一种完全解耦的方案:

  • 用户通过 HTTP 接口上传升级包;
  • 后端仅负责校验、保存文件并"发出信号";
  • 真正的升级逻辑由 systemd 管理,与 Web 进程无依赖关系
  • 即使 Web 服务进程退出、被 kill 或系统重启前短暂运行,升级仍能继续完成。

核心机制:Systemd Path 单元 + 原子性触发文件 + 独立升级脚本


2. 核心设计原则

原则 说明
触发与执行分离 Web 接口只负责"写一个文件",不执行任何升级逻辑
升级进程独立于 Web 服务 升级由 systemd 启动的新进程执行,生命周期不受 Web 进程影响
原子性切换 使用软链接或配置切换,避免中间状态
幂等与可重试 触发文件可重复创建,升级脚本具备幂等性
日志可追溯 所有操作记录到文件和 journal,便于审计

3. 系统架构概览

复制代码
[HTTP Client]
     ↓ (POST /upgrade with .tar file)
[FastAPI App] 
     ↓
  1. 校验文件格式
  2. 保存到 /opt/upgrade/package.tar
  3. 清理旧触发标记
  4. systemctl enable --now upgrade-trigger.path
  5. 在后台任务中:touch /tmp/upgrade.trigger
     ↓(立即返回响应)
[systemd]
     ↓ 监听到 /tmp/upgrade.trigger 存在
[upgrade-runner.service]
     ↓ 启动独立进程 /opt/run-upgrade.sh
[run-upgrade.sh]
     → 解压包 → 验证 → AB 切换 → 重启服务/系统

关键点:从 touch /tmp/upgrade.trigger 开始,后续所有操作均由 systemd 管理,与 FastAPI 进程无关。


4. 详细实现

4.1 升级脚本(独立执行单元)

复制代码
# /opt/run-upgrade.sh
#!/bin/bash
set -e

LOG_FILE="/var/log/system-upgrade.log"
PACKAGE_DIR="/opt/upgrade"
PACKAGE_PATH="$PACKAGE_DIR/package.tar"
ACTIVE_LINK="/opt/app/active"
RELEASES_DIR="/opt/app/releases"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

log "=== 升级流程开始 ==="

# 1. 检查升级包是否存在
if [ ! -f "$PACKAGE_PATH" ]; then
    log "错误:升级包不存在 ($PACKAGE_PATH)"
    exit 1
fi

# 2. 创建新版本目录(假设包内有 version.txt)
TMP_DIR=$(mktemp -d)
tar -xf "$PACKAGE_PATH" -C "$TMP_DIR"

VERSION_FILE="$TMP_DIR/version.txt"
if [ ! -f "$VERSION_FILE" ]; then
    log "错误:升级包中缺少 version.txt"
    rm -rf "$TMP_DIR"
    exit 1
fi

VERSION=$(cat "$VERSION_FILE" | tr -d '[:space:]')
TARGET_DIR="$RELEASES_DIR/$VERSION"

# 3. 若版本已存在,跳过部署(幂等)
if [ -d "$TARGET_DIR" ]; then
    log "版本 $VERSION 已存在,跳过解压"
else
    log "部署新版本: $VERSION"
    mkdir -p "$RELEASES_DIR"
    mv "$TMP_DIR" "$TARGET_DIR"
fi

# 4. 切换软链接(原子操作)
log "切换 active 指向 $VERSION"
ln -sfn "$TARGET_DIR" "$ACTIVE_LINK"

# 5. 重载 systemd 并重启主服务
systemctl daemon-reload
systemctl restart myapp.service

# 6. 清理触发文件(避免重复触发)
rm -f /tmp/upgrade.trigger

# 7. 可选:清理旧版本(保留最近两个)
(ls -1t "$RELEASES_DIR" | tail -n +3) | while read oldver; do
    [ -n "$oldver" ] && rm -rf "$RELEASES_DIR/$oldver"
done

log "=== 升级流程完成 ==="

赋予执行权限:

复制代码
chmod +x /opt/run-upgrade.sh

4.2 Systemd 单元配置

服务单元 /etc/systemd/system/upgrade-runner.service

复制代码
[Unit]
Description=System Upgrade Runner
After=network.target

[Service]
Type=oneshot
ExecStart=/opt/run-upgrade.sh
User=root
Group=root
StandardOutput=journal
StandardError=journal
TimeoutSec=300

[Install]
WantedBy=multi-user.target

路径监控单元 /etc/systemd/system/upgrade-trigger.path

复制代码
[Unit]
Description=Monitor upgrade trigger file
After=multi-user.target

[Path]
PathExists=/tmp/upgrade.trigger

[Install]
WantedBy=multi-user.target

注意:PathExists 表示只要文件存在就触发一次。触发后 .path 单元会进入 inactive 状态,直到下次 reload 或重新 enable。


4.3 Web 接口实现(FastAPI 示例)

复制代码
from fastapi import FastAPI, File, UploadFile, BackgroundTasks, HTTPException
from pydantic import BaseModel
import subprocess
import os
from pathlib import Path

app = FastAPI()

UPGRADE_DIR = "/opt/upgrade"
UPGRADE_PACKAGE = os.path.join(UPGRADE_DIR, "package.tar")
TRIGGER_FILE = "/tmp/upgrade.trigger"
PATH_UNIT = "upgrade-trigger.path"


def trigger_upgrade():
    """后台任务:创建触发文件"""
    # 确保目录存在
    os.makedirs("/tmp", exist_ok=True)
    Path(TRIGGER_FILE).touch()
    print(f"[Trigger] Created {TRIGGER_FILE}")


@app.post("/upgrade")
async def upgrade_system(
    background_tasks: BackgroundTasks,
    file: UploadFile = File(...)
):
    # 1. 校验文件类型
    if not file.filename.endswith('.tar'):
        raise HTTPException(status_code=400, detail="仅支持 .tar 文件")

    # 2. 创建升级目录
    os.makedirs(UPGRADE_DIR, exist_ok=True)

    # 3. 保存文件
    with open(UPGRADE_PACKAGE, "wb") as f:
        content = await file.read()
        f.write(content)

    # 4. 清理旧触发文件(避免误触发)
    if os.path.exists(TRIGGER_FILE):
        os.remove(TRIGGER_FILE)

    # 5. 重载并启用 path 单元(确保监听器就绪)
    subprocess.run(["systemctl", "daemon-reload"], check=True)
    subprocess.run(["systemctl", "enable", "--now", PATH_UNIT], check=True)

    # 6. 提交后台任务:仅创建触发文件(不执行升级!)
    background_tasks.add_task(trigger_upgrade)

    return {"status": "success", "message": "升级任务已提交,系统将在后台执行"}

关键说明

  • trigger_upgrade() 仅调用 Path(TRIGGER_FILE).touch(),不涉及任何耗时操作。
  • 该函数在 FastAPI 的事件循环中异步执行,即使 Web 服务随后退出,/tmp/upgrade.trigger 文件已存在
  • systemd 的 upgrade-trigger.path 会独立检测到该文件,并启动 upgrade-runner.service
  • 此时升级进程是 systemd 的子进程,与 Python 进程无任何关联。

5. 为何父进程退出不影响升级?

进程关系图

复制代码
systemd (PID 1)
├─ fastapi-app (PID 1234)
│   └─ (background task: touch /tmp/upgrade.trigger)
└─ upgrade-runner.service (PID 5678) ← 由 systemd 直接 fork
  • FastAPI 进程(PID 1234)在 touch 完成后即可退出。
  • /tmp/upgrade.trigger 是一个普通文件,其存在与否与创建者进程无关。
  • systemd 内核级 inotify 监听器持续监控该路径。
  • 一旦文件存在,systemd 主动 fork 新进程执行 run-upgrade.sh(PID 5678),该进程直接由 PID 1 管理。

因此,无论 FastAPI 是否存活,升级都会继续

补充:即使系统在 touch 后立即断电,只要文件写入磁盘(或 tmpfs 持久化),重启后 systemd 仍会触发升级(取决于文件系统是否保留 /tmp 内容)。对于关键场景,可将触发文件放在持久化目录(如 /var/lib/upgrade/trigger)。


6. 增强可靠性建议

6.1 使用持久化触发目录

将触发文件改为:

复制代码
[Path]
PathExists=/var/lib/upgrade/trigger

并在脚本中:

复制代码
TRIGGER_FILE = "/var/lib/upgrade/trigger"
os.makedirs("/var/lib/upgrade", exist_ok=True)

避免 /tmp 在某些系统重启后清空的问题。

6.2 添加升级状态查询接口

复制代码
@app.get("/upgrade/status")
def get_upgrade_status():
    if os.path.exists("/var/log/system-upgrade.log"):
        with open("/var/log/system-upgrade.log") as f:
            lines = f.readlines()[-10:]  # 返回最后10行
        return {"log": lines}
    return {"log": []}

6.3 升级脚本增加锁机制

run-upgrade.sh 开头加入:

复制代码
LOCK_FILE="/var/run/upgrade.lock"
if [ -f "$LOCK_FILE" ]; then
    log "升级已在进行中,退出"
    exit 0
fi
touch "$LOCK_FILE"
trap "rm -f $LOCK_FILE" EXIT

防止并发触发。


7. 总结

本文提供了一套完整的、生产可用的系统升级方案,其核心优势在于:

  • 完全解耦:Web 接口仅负责"发信号",不参与执行;
  • 进程隔离:升级由 systemd 管理,不受 Web 服务生命周期影响;
  • 高可靠:基于 systemd 的原生监控机制,无需额外守护进程;
  • 可扩展:易于集成 AB 切换、版本回滚、远程 OTA 等高级功能。

AB 升级详解:基于软链接与配置切换的双环境热升级方案

1. 什么是 AB 升级?

AB 升级(也称 A/B 分区升级、双副本升级)是一种高可靠系统更新策略:

  • 系统维护 两个完全独立的运行环境:A 和 B。
  • 当前系统运行在其中一个(如 A),升级时将新版本写入另一个(B)。
  • 升级完成后,下次启动或服务重载时切换到 B
  • 若 B 启动失败,可自动或手动回退到 A。

目标:零停机部署、快速回滚、升级过程可中断且安全

虽然理想 AB 升级依赖双分区(如 Android 的 A/B OTA),但在无分区支持的通用 Linux 系统(如 Ubuntu、Debian、嵌入式设备)中,可通过目录隔离 + 软链接切换实现类似效果。


2. 适用场景

  • 嵌入式 Linux 设备(树莓派、工控机、网关)
  • 无法修改分区表的虚拟机或容器宿主机
  • 需要"不停服"升级的后台服务(如 API 网关、数据采集器)
  • 资源受限但要求高可用的边缘节点

3. 核心设计

3.1 目录结构

复制代码
/opt/myapp/
├── active ────────────────> /opt/myapp/releases/v20251201   # 当前生效版本(软链接)
├── releases/
│   ├── v20251201/         # A 环境(当前运行)
│   │   ├── app.bin
│   │   ├── config.yaml
│   │   └── version.txt
│   └── v20260104/         # B 环境(待切换)
│       ├── app.bin
│       ├── config.yaml
│       └── version.txt
├── config/
│   └── ab_state.conf      # 记录当前激活环境(active=A 或 active=B)
└── backup/
    └── ab_state.conf.bak  # 上一次状态备份(用于回滚)

3.2 状态管理

  • ab_state.conf 内容示例:

    复制代码
    active=A
    current_version=v20251201
  • 每次升级前,先备份该文件。

  • 切换时,更新 active 字段并指向新版本。

  • 若新版本启动失败,恢复备份文件并重启服务即可回滚。

注意:active=A/B 是逻辑标识,实际路径通过 current_version 映射到具体目录。


4. 升级流程(详细步骤)

  1. 接收新版本包

    上传 myapp-v20260104.tar,包含 app.binconfig.yamlversion.txt

  2. 解压到 releases 目录

    解压为 /opt/myapp/releases/v20260104

  3. 确定目标环境

    • 若当前 active=A,则新版本部署到 B(即 v20260104)。

    • 更新 ab_state.conf

      复制代码
      active=B
      current_version=v20260104
  4. 原子切换软链接

    执行:

    复制代码
    ln -sfn /opt/myapp/releases/v20260104 /opt/myapp/active
  5. 重载或重启主服务

    主程序始终从 /opt/myapp/active/app.bin 启动,因此自动使用新版本。

  6. 验证与提交

    • 若服务正常运行一段时间(如 5 分钟),视为升级成功。
    • 可选:清理旧版本(保留最近两个)。
  7. 失败回滚(可选自动)

    • 若服务崩溃或健康检查失败,恢复 ab_state.conf.bak
    • 切换软链接回旧版本。
    • 重启服务。

5. 完整可执行示例

5.1 升级脚本:/opt/ab-upgrade.sh

复制代码
#!/bin/bash
set -e

LOG="/var/log/ab-upgrade.log"
APP_ROOT="/opt/myapp"
RELEASES_DIR="$APP_ROOT/releases"
ACTIVE_LINK="$APP_ROOT/active"
STATE_FILE="$APP_ROOT/config/ab_state.conf"
BACKUP_STATE="$APP_ROOT/config/ab_state.conf.bak"
PACKAGE_PATH="$APP_ROOT/package.tar"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"
}

log "=== AB 升级开始 ==="

# 锁机制:防止并发
LOCK="/var/run/ab-upgrade.lock"
if [ -f "$LOCK" ]; then
    log "升级已在进行中,退出"
    exit 1
fi
touch "$LOCK"
trap "rm -f $LOCK" EXIT

# 备份当前状态
if [ -f "$STATE_FILE" ]; then
    cp "$STATE_FILE" "$BACKUP_STATE"
    CURRENT_ACTIVE=$(grep -oP 'active=\K\w+' "$STATE_FILE")
    CURRENT_VERSION=$(grep -oP 'current_version=\K\w+' "$STATE_FILE")
else
    log "状态文件不存在,初始化为 A"
    CURRENT_ACTIVE="A"
    CURRENT_VERSION="v0"
fi

log "当前环境: $CURRENT_ACTIVE ($CURRENT_VERSION)"

# 解压新版本
TMP_DIR=$(mktemp -d)
tar -xf "$PACKAGE_PATH" -C "$TMP_DIR"

VERSION_FILE="$TMP_DIR/version.txt"
if [ ! -f "$VERSION_FILE" ]; then
    log "错误:升级包中缺少 version.txt"
    rm -rf "$TMP_DIR"
    exit 1
fi

NEW_VERSION=$(cat "$VERSION_FILE" | tr -d '[:space:]')
NEW_DIR="$RELEASES_DIR/$NEW_VERSION"

# 若已存在,跳过部署
if [ ! -d "$NEW_DIR" ]; then
    log "部署新版本: $NEW_VERSION"
    mkdir -p "$RELEASES_DIR"
    mv "$TMP_DIR" "$NEW_DIR"
else
    log "版本 $NEW_VERSION 已存在,跳过解压"
    rm -rf "$TMP_DIR"
fi

# 确定目标环境(A/B 轮换)
if [ "$CURRENT_ACTIVE" = "A" ]; then
    NEXT_ACTIVE="B"
else
    NEXT_ACTIVE="A"
fi

# 更新状态文件
cat > "$STATE_FILE" <<EOF
active=$NEXT_ACTIVE
current_version=$NEW_VERSION
EOF

log "切换到环境 $NEXT_ACTIVE,版本 $NEW_VERSION"

# 原子切换软链接
ln -sfn "$NEW_DIR" "$ACTIVE_LINK"

# 重载服务
systemctl daemon-reload
systemctl restart myapp.service

# 清理触发文件(若使用 systemd path 触发)
rm -f /tmp/upgrade.trigger

# 清理旧版本(保留最近3个)
(cd "$RELEASES_DIR" && ls -1t | tail -n +4 | xargs -r rm -rf)

log "=== AB 升级完成 ==="

5.2 回滚脚本(可选):/opt/ab-rollback.sh

复制代码
#!/bin/bash
LOG="/var/log/ab-rollback.log"
APP_ROOT="/opt/myapp"
ACTIVE_LINK="$APP_ROOT/active"
STATE_FILE="$APP_ROOT/config/ab_state.conf"
BACKUP_STATE="$APP_ROOT/config/ab_state.conf.bak"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"
}

if [ ! -f "$BACKUP_STATE" ]; then
    log "无备份状态,无法回滚"
    exit 1
fi

# 恢复状态
cp "$BACKUP_STATE" "$STATE_FILE"
ACTIVE_ENV=$(grep -oP 'active=\K\w+' "$STATE_FILE")
VERSION=$(grep -oP 'current_version=\K\w+' "$STATE_FILE")

TARGET_DIR="/opt/myapp/releases/$VERSION"
if [ ! -d "$TARGET_DIR" ]; then
    log "回滚目标目录不存在: $TARGET_DIR"
    exit 1
fi

# 切换软链接
ln -sfn "$TARGET_DIR" "$ACTIVE_LINK"

# 重启服务
systemctl restart myapp.service

log "已回滚到 $ACTIVE_ENV ($VERSION)"

5.3 systemd 单元(与前文一致)

  • ab-upgrade-trigger.path 监听 /tmp/upgrade.trigger
  • ab-upgrade-runner.service 执行 /opt/ab-upgrade.sh

5.4 Web 接口(FastAPI)保持不变

仅需确保上传的 .tar 包包含 version.txt,例如:

复制代码
# version.txt
v20260104

6. 关键技术点解析

6.1 原子性软链接切换

复制代码
ln -sfn target link_name
  • -f:强制覆盖已有链接
  • -n:不将 link_name 解析为目录(避免创建 link_name/target
  • 整个操作是原子的,应用在任何时刻读取 active 都指向一个完整版本

6.2 环境轮换逻辑

通过 active=A/B 实现逻辑轮换,而非硬编码路径。即使未来扩展为多版本(A/B/C),也可通过状态文件灵活管理。

6.3 与主服务解耦

主服务的 systemd 配置应始终指向 active

复制代码
# /etc/systemd/system/myapp.service
[Service]
ExecStart=/opt/myapp/active/app.bin

这样无需修改 service 文件即可切换版本。


7. 优势与局限

优势

  • 无需分区支持,适用于任意 Linux 系统
  • 切换瞬间完成,服务中断时间 ≈ 服务重启时间
  • 回滚只需恢复配置 + 切换链接,秒级完成
  • 升级过程可中断(断电后重启仍可继续或回滚)

局限

  • 需要双倍存储空间(两个完整版本)
  • 不适用于内核或底层驱动升级(需 reboot 且可能涉及 bootloader)
  • 需要主程序支持从指定路径加载(不能硬编码路径)

8. 总结

本文提供的 AB 升级方案,通过状态文件 + 软链接 + 目录隔离 ,在通用 Linux 系统上实现了接近工业级的可靠升级能力。结合 systemd path 单元与 Web 接口,可构建出一套完全自动化、进程解耦、支持回滚的升级体系,特别适合边缘计算、IoT 设备和无人值守服务器。

相关推荐
小北方城市网1 天前
第 9 课:Python 全栈项目性能优化实战|从「能用」到「好用」(企业级优化方案|零基础落地)
开发语言·数据库·人工智能·python·性能优化·数据库架构
E_ICEBLUE1 天前
PPT 智能提取与分析实战:把演示文档变成结构化数据
数据库·python·powerpoint
JSU_曾是此间年少1 天前
pytorch自动微分机制探寻
人工智能·pytorch·python
敢敢のwings1 天前
VGGT-Long:极简主义驱动的公里级单目三维重建系统深度解析(Pytorch安装手册版)
人工智能·pytorch·python
aiguangyuan1 天前
CART算法简介
人工智能·python·机器学习
龘龍龙1 天前
Python基础学习(十)
服务器·python·学习
轻竹办公PPT1 天前
用 AI 制作 2026 年工作计划 PPT,需要准备什么
大数据·人工智能·python·powerpoint
Mqh1807621 天前
day58 经典时序预测模型
python
Amelia1111111 天前
day44
python