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

目录

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

1. 背景与目标

2. 核心设计原则

3. 系统架构概览

4. 详细实现

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

4.2 Systemd 单元配置

4.3 Web 接口实现(FastAPI 示例)

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

进程关系图

6. 增强可靠性建议

6.1 使用持久化触发目录

6.2 添加升级状态查询接口

6.3 升级脚本增加锁机制

7. 总结

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

1. 什么是 AB 升级?

2. 适用场景

3. 核心设计

3.1 目录结构

3.2 状态管理

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

5. 完整可执行示例

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

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

5.3 systemd 单元(与前文一致)

5.4 Web 接口(FastAPI)保持不变

6. 关键技术点解析

6.1 原子性软链接切换

6.2 环境轮换逻辑

6.3 与主服务解耦

7. 优势与局限

优势

局限

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 状态管理

  • 每次升级前,先备份该文件。
  • 切换时,更新 active 字段并指向新版本。
  • 若新版本启动失败,恢复备份文件并重启服务即可回滚。

ab_state.conf 内容示例:

active=A current_version=v20251201 
注意: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)。
  4. 重载或重启主服务
    主程序始终从 /opt/myapp/active/app.bin 启动,因此自动使用新版本。
  5. 验证与提交
    • 若服务正常运行一段时间(如 5 分钟),视为升级成功。
    • 可选:清理旧版本(保留最近两个)。
  6. 失败回滚(可选自动)
    • 若服务崩溃或健康检查失败,恢复 ab_state.conf.bak
    • 切换软链接回旧版本。
    • 重启服务。

原子切换软链接
执行:

ln -sfn /opt/myapp/releases/v20260104 /opt/myapp/active 

更新 ab_state.conf

active=B current_version=v20260104 

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 设备和无人值守服务器。

Read more

LLaMA Factory 从入门到精通,一篇讲完

LLaMA Factory 从入门到精通,一篇讲完

目录 一、LLaMA-Factory 简介 二、安装部署 三、数据微调 1、数据集的建立 2、数据集格式 3、模型参数 4、开始运行 5、导出模型 四、webui 评估预测与对话 导出 五、SFT 训练 命令行 六、LoRA 合并 合并 量化 七、推理 原始模型推理配置 微调模型推理配置 多模态模型 批量推理 八、评估 通用能力评估 NLG 评估 评估相关参数 一、LLaMA-Factory 简介 LLaMA Factory 是一个简单易用且高效的大型语言模型(Large

AI编程工具对比:Cursor、GitHub Copilot与Claude Code

AI编程工具对比:Cursor、GitHub Copilot与Claude Code

文章目录 * AI编程工具对比:Cursor、GitHub Copilot与Claude Code * 一、产品定位与核心架构 * 1.1 Cursor:AI原生IDE的代表 * 1.2 GitHub Copilot:代码补全的行业标杆 * 1.3 Claude Code:终端Agent的革新者 * 二、核心功能深度对比 * 2.1 代码生成与理解能力 * 2.2 自动化与工作流集成 * 2.3 隐私与数据安全 * 三、成本效益分析 * 3.1 定价模式对比 * 3.2 投资回报比 * 四、适用场景与用户画像 * 4.1 最佳应用场景 * 4.2 用户反馈摘要 * 五、

node-llama-cpp安装与配置:Windows、Linux和Mac全平台教程

node-llama-cpp安装与配置:Windows、Linux和Mac全平台教程 【免费下载链接】node-llama-cppRun AI models locally on your machine with node.js bindings for llama.cpp. Force a JSON schema on the model output on the generation level 项目地址: https://gitcode.com/gh_mirrors/no/node-llama-cpp node-llama-cpp是一个基于llama.cpp的Node.js绑定库,让你能够在本地机器上运行AI模型,并在生成级别强制模型输出符合JSON模式。本文将为你提供Windows、Linux和Mac全平台的安装与配置教程,帮助你快速上手这款强大的AI工具。 一、准备工作 在开始安装node-llama-cpp之前,请确保你的系统满足以下要求:

Meta-Llama-3-8B-Instruct性能对比:不同量化方式

Meta-Llama-3-8B-Instruct性能对比:不同量化方式 1. 引言 随着大语言模型在消费级硬件上的部署需求日益增长,如何在保持推理质量的同时降低显存占用和提升推理速度,成为工程落地的关键挑战。Meta-Llama-3-8B-Instruct 作为 Llama 3 系列中兼顾性能与效率的中等规模模型,凭借其 80 亿参数、支持 8k 上下文以及出色的指令遵循能力,成为单卡部署的理想选择之一。 然而,原始 FP16 模型约需 16 GB 显存,仍超出多数消费级 GPU 的承载能力。因此,量化技术成为释放其潜力的核心手段。本文将系统性地对比 GPTQ-INT4、AWQ、GGUF(Q4_K_M)等多种主流量化方案在 vLLM 与 llama.cpp 等推理框架下的表现,涵盖显存占用、推理速度、输出质量三大维度,并结合 Open WebUI