Files

89 KiB
Raw Permalink Blame History

YMS Daemon 更新体系设计与实施计划

状态:研讨中,核心方向已收敛
当前阶段:冻结 native/container 混合过渡、本地 Nginx gateway、Docker standalone、客户 PC 交付工作台、离线包协议与 Jenkins 高频更新语义 本文档只描述设计和实施计划。除本文档外,现网代码、脚本、systemd、Nginx 和容器均未修改。

1. 目标

使用独立 Go 程序 yms-daemon 替换客户服务器上现有更新脚本,统一处理:

  • 离线更新包接收、验签、校验和暂存。
  • backendfrontendnodeSsr 三个组件的独立更新与整体更新。
  • 过渡阶段的 native、container 和混合部署。
  • Docker standalone 环境下的零停机蓝绿更新。
  • 更新失败后的自动回切和人工回滚。
  • 开发环境的高频自动更新。
  • 客户 PC 按客户查询 repack 版本、轮询新包、下载、断点续传、服务器交付、更新触发和状态展示。
  • 后续分布式主机与 Kubernetes 部署执行器。

这套更新体系必须同时服务客户现场和开发环境,不能再出现:

开发环境一套快捷脚本
客户现场另一套交付脚本

container 目标链路是:

CI 构建一次镜像
  -> 记录 multi-platform image index digest 和每个平台 manifest digest
  -> 开发环境验证其实际平台 manifest digest
  -> 发布中台归档同一次构建的全部平台 digest
  -> 客户离线包交付客户平台对应的原始 digest

native 过渡链路中,backend 保持同一次 CI 生成的 JAR 及其 SHA-256。frontend 保持同一次 CI 生成的源归档及其 SHA-256;当前 repack 会把该归档展开到客户 ZIP 的 dist/,因此客户侧最终以 manifest 逐文件声明的 dist/... 路径、长度和 SHA-256 为安装身份。

开发环境 backend 支持显式直传 JAR,不要求 Jenkins 生成 repack ZIP

yms-daemon update --service backend --native-jar /home/yms/tmp/<完整JAR文件名>.jar

--native-jar 与 repack 的 -f 互斥。CLI 通过 Unix Socket 的 inputType 精确传递 native-jarrepack-zip,daemon 不通过扩展名推断输入类型。直传 JAR 的文件名按不透明字符串处理,不解析版本;daemon 完整读取 JAR、计算 SHA-256,并以 direct/<SHA-256前12位>/<原始JAR文件名> 作为 /home/yms/lib/releases 下的不可变相对路径。目录名精确截取完整小写十六进制 SHA-256 的前 12 位,事务幂等和内容校验仍使用完整 SHA-256,目录冲突时拒绝覆盖。

本地 Unix Socket 的 update 响应采用连续 JSON 消息:过程消息的 kind 精确为 progress,最终消息的 kind 精确为 result。CLI 普通模式实时输出校验、事务、槽位准备、systemd 启动、Actuator 健康检查、gateway 切流、drain、旧 unit 停止和提交阶段。精确参数 --quite 关闭正常过程与成功结果输出,错误仍写入 stderr,退出码语义不变;CLI 断开不取消服务端持久化事务。

backend native 的零停机重启命令精确为 yms-daemon restart --service backend,并支持 --quite。restart 读取当前兼容 JAR 指向的精确 release,创建独立事务,将同一 release 绑定到非活动槽,启动、健康检查、切流、drain 并停止旧槽;每次新调用都会执行一次真实槽位轮转,不使用历史 update 的内容幂等键。存在未完成 restart 事务时,同一命令恢复该事务;存在其他未完成事务时拒绝新建 restart。

2. 已确认的精确事实

2.1 仓库与组件

  • 主业务系统:ymswell
  • 前端:YMSwellClient
  • 发布中台:deploy
  • 更新助手:yms-daemon

业务组件标识固定为:

  • backend
  • frontend
  • nodeSsr

发布归档中的产物类型为:

  • BACKEND
  • FRONTEND
  • NODE_SSR

不对上述标识进行大小写转换、别名匹配或模糊匹配。

2.2 当前发布包

当前 repack ZIP 根目录包含 artifact-selection.json。现有字段包括:

  • customerCode
  • customerDisplayName
  • versionId
  • items
  • backendArtifacts
  • frontendArtifacts
  • nodeSsrArtifacts
  • remark

现有 manifest 尚未完整描述 ZIP 中 native 文件的最终路径、长度和 SHA-256,也未完整描述 container 归档的最终路径、长度、SHA-256 和 OCI digest,因此不能直接作为 daemon 的最终部署协议。

2.3 当前容器制品基础

发布归档已经管理以下五类制品:

  • BACKEND/native
  • BACKEND/container
  • FRONTEND/native
  • FRONTEND/container
  • NODE_SSR/container

当前发布中台要求的容器平台包括:

  • linux/amd64
  • linux/arm64

repack 当前通过以下步骤生成离线镜像归档:

docker pull --platform <platform> <imageRef>
docker image save --platform <platform> ...

当前镜像 tag 的实际示例为 20260814-093609-d7ed70f0-v1.1.8.1,末尾 v1.1.8.1 可以不存在。daemon 将完整 tag 作为不透明字符串透传,不拆分日期、提交号或业务版本,也不使用 tag 判定镜像身份;镜像身份仍以本次更新明确给出的 digest 为准。

当前 archive_artifact 已经分别保存 digest ref 形式的 imageRef 和原始 imageTag,但 repack 的 artifact-selection.json 只输出 imageRef,未输出 imageTag。最终 repack 必须利用这两个已保存值生成完整 tagged image ref,以该完整引用执行 docker image save,并在 daemon manifest 中直接给出该引用和独立的 imageDigest;daemon 不负责拼接仓库名和 tag,也不得从其中一个值推导另一个值。

2.4 当前前端镜像

YMSwellClient/scripts/deploy/Dockerfile 已经执行:

COPY apps/web-antd/dist /usr/share/nginx/html

因此容器模式下:

  • dist 在 CI 构建阶段进入 frontend 镜像。
  • 客户服务器不再接收和覆盖独立 dist/ 目录。
  • daemon 不逐文件安装前端资源。
  • frontend 镜像 digest 覆盖静态资源和静态服务配置。

当前 frontend 容器内 Nginx 监听 8080

实际 dist 顶层目前包含:

index.html
_app.config.js
assets/
css/
js/
jse/
static/
svg/
templates/

当前生产配置使用相对地址:

VITE_GLOB_API_URL=/
VITE_GLOB_YMS_SERVICE=/yms_services

同一 frontend 镜像不需要写入客户服务器 IP。

2.5 当前运行端口和健康检查

从现有仓库确认的值:

组件 容器内部端口 已确认健康路径
backend 8080 /yms/actuator/health
frontend 8080 尚未冻结
nodeSsr 8910 /health

前端健康检查的成功状态码、响应内容以及静态资源检查规则仍需冻结。

2.6 当前数据库迁移

现有 deploy/src/main/resources/deploy/env/yms.env 包含:

YMS_FLYWAY_ENABLED=true

蓝绿期间新旧 backend 会重叠运行,Flyway 不能由两个 backend 实例无约束地并发执行。数据库迁移必须从普通 backend 启动过程里形成明确的单次执行边界。

上述约束同时适用于两个 native JVM 实例。Flyway 的单次执行边界不能依赖 backend 的运行类型。

2.7 当前客户 repack 查询与下载接口

客户 PC 查询的“版本”不是通用 release 版本列表,而是发布中台已经按客户生成的 repack ZIP。

deploy 当前已有以下接口:

  • GET /repack/packages?customerCode=...:按 customerCode 查询该客户目录下的 ZIP。
  • POST /repack/packages/page:按客户编码和文件名关键字分页查询。
  • POST /repack/packages/{customerCode}/files/{fileName}/ticket:为指定客户包生成一次性下载令牌。
  • GET /repack/download-tickets/{ticket}:使用下载令牌获取文件。

当前 RepackPackageFileVO 精确包含:

  • customerCode
  • fileName
  • size
  • sizeHuman
  • lastModifiedAt
  • manifest

当前包列表从客户包目录扫描 ZIP,不读取 customer_package_record 老表。查询和创建下载令牌需要登录认证;下载令牌接口允许使用令牌直接下载。现有下载服务已经通过测试确认支持 HTTP Range。

当前返回值没有整包 SHA-256。PC 下载完成后的强身份校验需要发布中台扩展整包 SHA-256,并与包列表或下载令牌响应一同返回;精确字段名在修改 deploy 前同时冻结。

2.8 当前客户 /home/yms 目录

2026-08-15 提供的客户服务器目录结果确认以下现状:

  • /home/yms/bin 保存现有运维和更新脚本。
  • /home/yms/bin/env 保存现有环境配置。
  • /home/yms/lib/releases 保存 backend 历史 JAR。
  • /home/yms/lib/glory-soft-yms.jar 是指向 /home/yms/lib/releases 中当前 JAR 的软链接。
  • /home/yms/client-releases 保存 frontend 历史版本目录。
  • /home/yms/client 是指向 /home/yms/client-releases 中当前 frontend 版本的软链接。
  • /home/yms/tmp 已存在。
  • /home/yms/deploy/deploy-sync.sh 仍然存在于现场。

这表明当前 native backend 和 frontend 已经具有“版本存储目录 + 当前入口软链接”的目录基础。该事实不直接冻结 native 双槽的 systemd unit、槽位名称和切流方式;零停机执行器仍须保证两个并行实例分别引用明确版本,不能在运行期间只依赖一个全局当前软链接表达两个实例状态。

native backend 的 systemd 模板名称已冻结为:

/etc/systemd/system/yms-backend@.service

模板实例标识固定使用端口号,完整 unit 名称为:

yms-backend@8080.service
yms-backend@8081.service

两个并行实例对应的槽位 JAR 软链接路径固定为:

/home/yms/lib/glory-soft-yms-8080.jar
/home/yms/lib/glory-soft-yms-8081.jar

当前人工运维兼容入口固定保留 /home/yms/lib/glory-soft-yms.jar,并指向当前接流版本。daemon 不把该单一兼容入口同时作为两个槽位的版本身份。

当前客户服务器执行 command -v systemctl 的精确输出为 /bin/systemctl,该现场的 daemon 配置使用此绝对路径。其他服务器如果精确输出 /usr/bin/systemctl,由该服务器的本机配置明确记录 /usr/bin/systemctl;daemon 不在两个路径之间自行选择。

native backend 模板使用 Type=simple,不再调用 yms-service.sh 或其他启动脚本。冻结后的 ExecStart 为:

/usr/bin/java -Xms1024m -Xmx10240m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/yms/dump -XX:ErrorFile=/home/yms/log/hs_err_pid%p.log -XX:+ExitOnOutOfMemoryError -jar /home/yms/lib/glory-soft-yms-%i.jar --server.port=%i

模板继续读取现有 /home/yms/bin/env/yms.env,工作目录为 /home/yms/libstdout 和 stderr 进入 journal。模板中 systemd 对字面量 %p 需要写为 %%pJVM 最终接收到的参数仍为 -XX:ErrorFile=/home/yms/log/hs_err_pid%p.logTimeoutStopSec 固定为 150min,与应用 spring.lifecycle.timeout-per-shutdown-phase 的默认 150m 对齐,不能沿用旧 unit 的 30 秒强制停止上限。

RPM 安装阶段创建 /home/yms/dump,属主和用户组固定为 root:root,权限固定为 0755。RPM 同时安装 tmpfiles 配置,并在安装事务中执行对应的目录创建;unit 不通过 ExecStartPre 临时创建目录。

2.9 第一阶段本机配置

daemon 本机配置文件固定为 /etc/yms-daemon/yms-daemon.toml。第一阶段已经冻结并实现 backend native 配置:

[daemon]
environment = "prod"

[backend]
type = "native"
release_dir = "/home/yms/lib/releases"
active_jar = "/home/yms/lib/glory-soft-yms.jar"
systemctl_path = "/bin/systemctl"

[backend.slot.8080]
unit = "yms-backend@8080.service"
jar = "/home/yms/lib/glory-soft-yms-8080.jar"
health_endpoint = "http://127.0.0.1:8080/yms/actuator/health"

[backend.slot.8081]
unit = "yms-backend@8081.service"
jar = "/home/yms/lib/glory-soft-yms-8081.jar"
health_endpoint = "http://127.0.0.1:8081/yms/actuator/health"

字段规则固定为:

  • daemon.environment 是机器级行为标记,只接受精确值 devproddev 使 container 从 Registry pullprod 使 container 从 repack ZIP load。该字段不改变 native 更新、健康检查、切流、事务或回滚行为。
  • table 和 key 必须与上例逐字一致;大小写不同、未知字段、未知槽位和重复字段全部拒绝。
  • backend.type 接受精确值 nativecontainer;daemon 不根据现场状态自动选择。
  • release_diractive_jar、两个槽位的 unitjarhealth_endpoint 必须与上例完全相同。
  • systemctl_path 必须由现场明确填写干净的绝对路径;当前现场填写 /bin/systemctl,精确检查结果为 /usr/bin/systemctl 的服务器填写 /usr/bin/systemctl
  • loader 不补默认值,不改写路径,不改写大小写,不根据端口拼接 unit、JAR 路径或健康地址。
  • RPM 安装示例文件位于 packaging/etc/yms-daemon/yms-daemon.tomlRPM spec 已将其以 noreplace 语义安装到固定路径,跨架构打包入口为 packaging/rpm/build-rpm.sh

backend container 开发环境配置已经冻结为:

[daemon]
environment = "dev"

[backend]
type = "container"
systemctl_path = "/bin/systemctl"

[backend.slot.8080]
container_name = "backend-8080"
health_endpoint = "http://127.0.0.1:8080/yms/actuator/health"

[backend.slot.8081]
container_name = "backend-8081"
health_endpoint = "http://127.0.0.1:8081/yms/actuator/health"

container slot 固定挂载 /home/yms/conf/yms.yaml:/app/config/yms.yaml:ro/home/yms/tmp:/home/yms/tmp,固定注入 SERVER_PORTSPRING_CONFIG_LOCATION=file:/app/config/yms.yaml,固定使用 host network、root 用户、restart policy no 和 9000 秒 graceful stop 上限。外挂 yms.yaml 是唯一 Spring Boot 配置入口,必须同时包含 Spring Boot 配置和原 config 文件中的业务配置,不再追加加载 JAR 内默认配置。开发更新入口固定为 yms-daemon update --service backend --container-image <完整tag引用>daemon pull 后读取实际 RepoDigest,并将事务后续步骤固定到 digest 引用。

3. 总体架构

daemon 内部先选择服务器已经明确登记的组件执行器,再进入相同的事务、健康检查、切流和回滚流程:

更新请求
  -> manifest 严格校验
  -> 读取本机显式部署配置
  -> 组件执行器
       ├── backend native systemd executor
       ├── backend Docker executor
       ├── frontend native directory executor
       ├── frontend Docker executor
       └── nodeSsr Docker executor
  -> host Nginx gateway controller

本机部署配置必须为每个组件明确记录精确值 nativecontainer。daemon 不根据文件名、目录、进程、systemd unit 或 Docker 容器自行推断运行类型。

当前制品事实决定第一阶段允许:

组件 native container
backend 支持 支持
frontend 支持 支持
nodeSsr 不支持,当前没有 NODE_SSR/native 制品 支持

因此合法的过渡部署可以是:

backend  = native
frontend = native
nodeSsr  = container

也可以在迁移完成后变为三个组件全部 container

3.1 Docker standalone 目标态

发布中台 deploy
  -> 按 customerCode 提供 repack ZIP 列表
  -> 创建下载令牌并提供 Range 下载
  -> 客户 PC yms-daemon client service
       -> SQLite 记录查询、下载、交付和更新任务
       -> Wails GUI 展示并接受操作
       -> 下载 repack ZIP 到 PC
       -> HTTPS 可续传上传到客户服务器 daemon
       -> WSS 提交更新并接收状态

客户服务器
├── yms-daemon.service              RPM 安装,宿主机常驻
├── host Nginx                     稳定入口,不随业务更新替换
└── Docker Engine
    ├── frontend blue/green         镜像内包含 dist 和静态 Nginx
    ├── backend blue/green
    └── nodeSsr blue/green

3.2 native/container 混合过渡态

客户服务器
├── yms-daemon.service
├── host Nginx gateway
├── backend native blue/green JVM + systemd
├── frontend native blue/green 版本目录
└── Docker Engine
    └── nodeSsr blue/green containers

native 兼容不是继续调用 deploy-sync.shyms-update.sh。native executor 仍由 Go daemon 直接完成预检、版本目录管理、systemd 操作、健康检查、gateway 切流和回滚。

native 到 container 必须是显式迁移事务,普通 update 不暗中改变组件运行类型。

3.3 开发环境

开发环境使用相同运行结构:

Jenkins / deploy
  │
  │ 构建完成后调用 yms-daemon CLI
  ▼
开发服务器 yms-daemon
  -> 同一 native/Docker executor
  -> 同一 gateway controller
  -> 同一健康检查
  -> 同一事务恢复

客户现场与开发环境只允许在以下方面采用不同策略:

  • 制品传输方式。
  • 签名和审批策略。
  • 旧实例或旧容器保留时长。
  • 自动更新触发策略。
  • 日志详细程度。

组件准备、健康检查、切流、验证和回滚实现必须完全复用。开发环境必须持续覆盖 native 和 container 两条执行路径,不能只验证目标态 Docker executor。

3.4 YMS 在线上传兼容入口

现有 YMS 在线更新入口继续接收更新 ZIP,但不再解压更新包、查找 deploy-sync.sh 或由 JVM 执行更新。YMS 与宿主机 daemon 的更新包共享收件目录固定为:

/home/yms/tmp

native 部署下,YMS 和 daemon 直接使用该宿主机目录。Docker standalone 部署下,backend blue/green 容器把宿主机 /home/yms/tmp bind mount 到容器内同名绝对路径,避免容器路径与宿主机路径转换。backend blue/green 容器明确以 root 用户运行,不依赖基础镜像的默认用户。

该目录只承担更新包交接,不保存 daemon 的 SQLite、事务内部文件或日志。YMS 完成文件写入并持久化后再发布完整文件;daemon 只接管已经完整发布的普通文件,并在接管成功后创建或关联更新事务。daemon 不通过目录监听自动执行更新,更新仍须经过显式认证和事务提交。

/home/yms/tmp 的属主和用户组固定为 root:root,权限固定为 0755。内部子目录、临时文件命名、容量限制、保留时间和异常文件清理规则仍需冻结。现有客户环境已经使用 /home/yms/tmp,实施时必须先核对现存文件用途,不能清空或覆盖既有文件。

4. 进程和命令入口

RPM 全局安装单一二进制:

/usr/bin/yms-daemon

同一个二进制提供:

yms-daemon
├── serve       客户服务器或开发服务器后台常驻服务
├── client      客户 PC 交付工作台
├── update      发起更新事务
├── rollback    发起人工回滚事务
├── status      查询状态
├── history     查询历史事务
├── logs        查询事务日志
└── version     查询 daemon 版本

4.1 ymsd

/usr/bin/ymsd

职责:

  • 监听本地 Unix Socket。
  • 为客户 PC 提供经过认证和授权的 HTTPS 可续传上传入口。
  • 为客户 PC 提供经过认证和授权的 WSS 控制与状态通道。
  • 串行执行更新事务。
  • 访问 Docker Engine API。
  • 通过 gateway controller 管理本地 Nginx 配置和 graceful reload。
  • 执行健康检查和业务验证。
  • daemon 或服务器重启后恢复未完成事务。

serve 是唯一有权修改运行状态的进程。

4.2 client

client 精确指安装在客户 PC 上的交付工作台,兼具离线中转能力:

yms-daemon client

职责:

  • 使用明确登记的 customerCode 周期轮询发布中台已有 repack ZIP。
  • 接受 GUI 的手动刷新,立即重新查询该客户的 repack ZIP。
  • 把远端包信息、下载状态、服务器交付状态和更新结果持久化到本地 SQLite。
  • 接受“获取”操作,通过一次性下载令牌把指定 ZIP 下载到客户 PC。
  • PC 下载支持暂停、恢复、失败重试和整包校验。
  • 接受“更新”操作,把已经完整校验的 ZIP 传输到指定客户服务器。
  • 传输完成后在服务器侧触发对应的 yms-daemon update --service ... -f ... 请求。
  • 持续查询服务器事务状态并同步到 GUI。
  • 不修改 manifest、不重新签名,也不在客户 PC 本地执行服务器业务更新。

自动轮询与手动刷新进入同一个查询流程。重复查询不得重复下载、重复传输或重复发起更新;是否执行更新只能由用户点击或后续明确配置的自动策略决定。

PC 到服务器固定使用 daemon 提供的管理协议:HTTPS 负责 ZIP 可续传上传,WSS 负责控制请求、事务状态、进度和日志摘要。两者可以共用一个 TLS 监听入口,精确监听地址和端口尚未冻结。

WSS 不执行远程 Shell。PC 提交结构化更新请求,serve 将其转换为与本地 CLI 完全相同的内部更新请求。服务器侧业务状态只能由 serve 修改。

4.3 update

已讨论的离线命令:

yms-daemon update --service all -f xxx.zip
yms-daemon update --service backend -f xxx.zip
yms-daemon update --service frontend -f xxx.zip
yms-daemon update --service nodeSsr -f xxx.zip

规则:

  1. --service 必填。
  2. 只接受 allbackendfrontendnodeSsr
  3. -f 在离线包模式下必填。
  4. CLI 只向 serve 提交请求,不直接操作 Docker 或业务文件。
  5. CLI 或 SSH 连接中断不得中止服务器端事务。
  6. daemon 未运行时明确失败,不退化为 CLI 特权执行。

客户 PC 点击“更新”后,通过 WSS 在服务器形成与上述 CLI 完全相同的内部更新请求。ZIP 必须先通过 HTTPS 完整落到服务器暂存位置并通过校验,WSS 再引用该已验证上传提交事务;PC 断开不能中止已经提交到 serve 的事务。

开发环境由 Jenkins 构建步骤调用同一 yms-daemon CLI 快速触发更新。在线更新的具体参数尚未冻结;它必须进入同一个内部更新请求模型,但不强制先生成包含完整镜像层的 ZIP。

第一阶段 backend native 命令已经实现。CLI 把相对 -f 路径解析为当前工作目录下的绝对路径,只通过 /run/yms-daemon/yms-daemon.sock 提交给 serve;CLI 进程不直接读取 SQLite、不调用 systemd、不改写 Nginx。当前只接受精确值 --service backend,其他 service 在对应执行器完成前明确拒绝。

当前 repack 兼容层读取 ZIP 根目录的 artifact-selection.json,并按 deploy 当前实际字段 backendArtifacts[].artifactKind/type/selectedType/fileName/sha256 选择唯一 native backend JAR。ZIP 条目、JSON table/key、重复字段、大小写变化、路径逃逸和符号链接全部严格拒绝。当前 deploy manifest 尚无文件长度字段,daemon 使用 ZIP 条目的实际解压长度并用 manifest SHA-256 校验内容;manifest 增加明确长度字段后再升级协议。

第一次从旧 native unit 迁移时,daemon 根据 Nginx managed upstream 的实际接流端口和 systemd 实际状态,在对应的旧 unit 与新模板 unit 中要求恰好一个正在运行。当前映射固定为 8080 对应 yms.service、8081 对应 ymsback.service;现场第一次更新从旧 ymsback.service@8081 切换到 yms-backend@8080.service,成功后停止旧 unit。后续只在两个 yms-backend@.service 实例之间轮换。

4.4 rollback

yms-daemon rollback --transaction <transaction-id>

人工回滚创建新的事务,不删除、不覆盖原更新记录。

5. 过渡运行模式与 Docker standalone

5.1 显式运行类型

daemon 本机配置和更新包 manifest 都必须明确给出每个组件的 type

native
container

执行前必须满足:

本机 services.<service>.type
    == 更新包 services.<service>.type

不一致时在创建暂存文件、启动进程或装载镜像之前失败。update --service backend 只选择组件,不改变该组件已经登记的运行类型。

5.2 native 过渡边界

native 模式支持:

  • backend:版本化 JAR 目录、两个互不冲突的 systemd 实例和 gateway 蓝绿切流。
  • frontend:两个版本目录并存,由 gateway 切换当前静态资源目标。
  • nodeSsr:当前无 native 制品,不提供 native executor。

当前 yms.serviceymsback.service 存在 Conflicts,不能直接承担 native 零停机双实例。首次启用 native 零停机前必须执行独立的基础设施迁移,安装互不冲突的双实例 unit 或 systemd template,并让每个实例固定引用自己的版本目录。

基础设施迁移失败必须恢复现有 native unit 和 gateway 配置;不能把该迁移隐藏在普通业务更新里。

5.3 Docker standalone 宿主机边界

宿主机保留:

  • yms-daemon.service
  • Docker Engine 及其 systemd 服务。
  • daemon 服务端 SQLite、详细日志和 gateway 配置目录。

全部组件迁移到 container 后,三个业务组件不再由独立 systemd unit 管理,业务生命周期统一交给 daemon 和 Docker Engine。native 过渡期仍保留由 daemon 管理的 backend 双实例 unit。

5.4 gateway

gateway 固定使用客户服务器上的本地 Nginx,直接占用外部业务端口。它不随 backendfrontendnodeSsr 的普通更新替换,也不进入 Docker Compose 或 Docker standalone 生命周期。

请求链路是分支结构:

浏览器
  -> host Nginx gateway
       ├── / 和静态资源       -> frontend 静态目录或 frontend 容器
       ├── /yms_services/      -> backend native unit 或容器
       └── 8910                -> nodeSsr 容器

frontend 静态 Nginx 只负责:

  • 从镜像内 /usr/share/nginx/html 提供静态文件。
  • SPA fallback。
  • 静态缓存响应头。
  • 对不存在的资源返回可识别的真实错误。

backend 和 nodeSsr 由本地 Nginx 直接代理。

5.5 本地 Nginx 配置与切流

daemon 只管理 /etc/nginx/nginx.conf 中冻结的 managed upstream 标记块,不覆盖整份 Nginx 配置。每次切流遵循:

  1. 读取当前完整配置并确认唯一活动 backend 端口;
  2. 生成下一份完整配置,仅改变 daemon 管理的 upstream 标记块;
  3. 写入事务目录并保存更新前快照;
  4. 使用 /usr/sbin/nginx -t -c /etc/nginx/nginx.conf 验证;
  5. 原子替换配置文件;
  6. 使用 /usr/sbin/nginx -s reload 平滑加载;
  7. 通过健康检查和实际配置再次确认切流结果;
  8. 任一步失败时恢复更新前配置并再次执行配置检查。

不使用 systemctl reload nginx.service 作为切流动作。systemd 只负责 Nginx 开机启动和进程守护,配置切换由 Nginx 原生命令完成。

Nginx 的两个 upstream 后端始终保留,daemon 只切换活动行的注释状态。max_fails=1fail_timeout=2s 由现场配置固定管理,daemon 不在普通更新中改写。

5.6 gateway 自身边界

本地 Nginx 不属于三个业务组件的普通更新事务。daemon 不升级 Nginx 二进制、不安装 Lua、不接管客户未标记的配置段。Nginx 本身的升级、证书更新和全局配置变更由操作系统运维流程负责。

第一版只保证业务组件普通更新期间 Nginx 进程不停止,并保证 daemon 的配置切换使用语法检查、原子替换和 graceful reload。

6. 蓝绿双槽与零停机切流

6.1 双槽模型

每个业务组件最多保留两个逻辑更新槽:

blue
green

任意时刻:

  • 一个槽是当前接流槽。
  • 另一个槽是待更新、待验证或上一可回退槽。
  • 不允许无限累积运行中的版本。

逻辑槽按运行类型落地为:

组件类型 blue/green 实体
backend native 两个 systemd 管理的 JVM 实例,各自固定版本化 JAR
frontend native 两个只读版本目录
backend/frontend/nodeSsr container 两个 Docker 容器

systemd unit、版本目录、容器、Docker network 和 label 的最终命名进入实现前单独冻结,不能从现有脚本名称推断。

6.2 流量唯一依据

第一版不建立独立路由数据库,不使用动态路由 shared dict,也不设置第二个持久化活动指针。

当前 gateway 配置   = 当前流量指向的唯一依据
systemd/Docker/目录 = 组件实际运行状态
daemon 服务端 SQLite = 操作意图、过程、历史和恢复线索

本地 Nginx 直接读取宿主机配置目录。daemon 只原子替换自身管理的配置文件,不覆盖未标记配置段。

6.3 单组件更新

容器模式下,以当前 blue 接流、更新 backend 为例:

1. 验证包签名、平台、文件摘要和 image digest
2. docker load 或按 digest pull 新镜像
3. 使用新镜像创建 backend green
4. green 不发布固定宿主机端口
5. Docker inspect 取得实际容器信息
6. daemon 直接检查 green 健康状态
7. 生成完整的下一份 gateway 配置
8. 使用 `/usr/sbin/nginx -t` 执行配置检查
9. 同目录原子替换当前配置
10. 使用 `/usr/sbin/nginx -s reload` 平滑加载
11. 新 worker 把新请求发往 green
12. 旧 worker 继续把已有连接交给 blue
13. 通过 gateway 执行业务验证
14. 成功后提交事务
15. blue 保留到连接排空和保留策略满足

核心切换:

旧 Nginx worker -> blue  -> 处理已有连接
新 Nginx worker -> green -> 接收新请求

配置检查失败时不得替换当前配置。reload 失败时旧 worker 继续使用旧配置,daemon 恢复更新前配置并再次检查。

全新机器首次部署必须同时满足:SQLite 不存在已提交的 container backend 部署记录、不存在历史 COMMITTED container backend 事务,并且 backend-8080backend-8081 均不存在。daemon 在 Nginx 当前未接流的槽位创建并启动首个容器,健康检查通过后切流并提交部署记录;因为没有旧容器,所以不执行 drain 和停止步骤。已提交记录、Nginx 和容器现场任一不一致时拒绝更新,不得降级为首次部署;FAILEDROLLED_BACK 首装事务不表示机器曾成功部署(见第 17 节决策 51)。

native backend 使用相同切流步骤,但准备阶段替换为:

1. 校验 native JAR 的 manifest 路径、长度和 SHA-256
2. 写入 green 专属版本目录
3. 确认 green systemd 实例固定引用该目录中的 JAR
4. 启动 green JVM
5. 直连 `/yms/actuator/health`
6. 健康后生成指向 green 的完整 gateway 配置
7. 配置检查、原子替换和 graceful reload
8. blue JVM 保留到连接排空和保留策略满足

native frontend 准备阶段为:

1. 按 manifest 精确写入并校验 green 版本目录
2. 目录完成前不进入 gateway 配置
3. 生成指向 green 目录或对应静态入口的完整 gateway 配置
4. 配置检查、原子替换和 graceful reload
5. blue 目录保留到旧资源策略允许清理

native executor 不覆盖正在运行实例引用的 JAR 或 frontend 目录。

6.4 all 更新

--service all 不逐个切流:

1. 为三个组件准备非活动槽
2. 三个新实例、目录或容器全部通过对应预检和健康检查
3. 生成同时指向三个新槽的一份完整 gateway 配置
4. 配置检查通过后只执行一次 HUP
5. 通过 gateway 验证完整业务组合
6. 全部成功后提交

切换期间会暂时存在旧 worker 与新 worker

旧 worker
├── frontend blue
├── backend blue
└── nodeSsr blue

新 worker
├── frontend green
├── backend green
└── nodeSsr green

因此新旧完整组合必须在兼容窗口内同时可用。

6.5 自动回切

切流后业务验证失败:

1. 恢复更新前 gateway 配置
2. 执行配置检查
3. 原子替换当前配置
4. HUP
5. 新请求重新进入旧槽
6. 验证旧槽健康
7. 停止失败的新槽
8. 事务进入 ROLLED_BACK 或 FAILED

6.6 daemon 异常恢复

daemon 启动后:

1. 读取未完成事务
2. 读取当前 gateway 配置,确定实际接流目标
3. 按显式运行类型检查 systemd 实例、版本目录或 Docker 容器
4. 执行健康检查
5. 当前目标健康:继续验证、提交或完成 drain
6. 当前目标故障且旧目标健康:恢复旧配置并 HUP
7. 两个目标都不健康:停止自动写操作,输出精确人工恢复信息

关键故障语义:

故障阶段 预期结果
native 文件写入或校验失败 当前槽不受影响
native systemd 实例启动失败 当前槽不受影响
新镜像装载失败 当前槽不受影响
新容器启动失败 当前槽不受影响
新实例或容器健康检查失败 当前槽不受影响
gateway 配置检查失败 不替换当前配置
HUP 失败 旧 worker 继续服务,恢复旧配置
切流后业务检查失败 回切旧配置,旧槽仍在
daemon 被强制终止 按当前配置与 systemd、目录或 Docker 实际状态恢复

6.7 长连接和旧槽

已有 SSE、上传和其他长连接可能长期停留在旧 worker 和旧实例。native JVM 或旧容器不能在 reload 后立即停止。

实现前必须冻结:

  • 最大 drain 时间。
  • 是否允许主动终止超时长连接。
  • 前端和客户端是否具备断线自动重连。
  • 旧实例、目录或容器达到保留上限后的行为。

6.8 frontend 旧资源

已经打开的旧页面可能在切流后继续请求旧 JS、CSS、图片或模板。仅保留旧 frontend 目录或容器不足以自动保证这些请求仍会路由到旧目标。

需要冻结一项前端资源兼容方案:

  1. 资源 URL 使用发布版本命名空间,由 gateway 路由到对应 frontend 目录或容器。
  2. 新 frontend 对文件请求返回真实 404,gateway 对静态资源回源上一槽。
  3. 组合使用版本命名空间和上一槽 fallback。

普通 proxy_cache 只能保存访问过的文件,不能单独作为完整旧版本存储。

7. 开发环境高频更新

7.1 目标

开发环境必须持续使用真实 daemon 更新路径,承担以下验证:

  • native JAR 和 repack 后 frontend dist/... 文件的暂存、校验与双槽切换。
  • 镜像装载或拉取。
  • native 实例与 Docker 容器的 blue/green 并行运行。
  • 本地 Nginx 的配置检查和 graceful reload。
  • backend 新旧版本兼容。
  • Flyway 单次迁移边界。
  • SSE、上传和会话兼容。
  • frontend 旧资源处理。
  • 自动回切和 daemon 重启恢复。

不能等到客户离线交付时才首次执行 daemon 更新链路。

7.2 同一次构建的制品身份晋级

CI 构建 multi-platform 镜像
  -> 记录 image index digest
  -> 记录每个平台 manifest digest
  -> 开发环境按其实际平台 digest 更新
  -> 自动化健康检查和业务测试
  -> 发布中台归档同一次构建的全部平台 digest
  -> repack 保存客户平台对应镜像
  -> 客户 daemon 验证并安装该平台的原始 digest

不同平台的 manifest digest 本身不同。这里要求的是同一次 multi-platform 构建中,每个平台的 digest 在 CI、归档、repack 和目标服务器之间保持不变。开发验证后不得为客户重新构建同版本业务镜像,否则开发环境验证的制品与客户收到的制品不再属于同一次构建。

native backend 的制品身份链路为:

CI 生成 backend JAR
  -> 记录 JAR 长度和 SHA-256
  -> native 开发环境安装并验证同一 JAR SHA-256
  -> 发布中台归档同一 JAR
  -> 客户 repack 交付同一 JAR SHA-256

native frontend 当前存在一次明确的形态转换:

CI 生成 frontend 源归档
  -> 记录源归档长度和 SHA-256
  -> native 开发环境从同一源归档展开并验证
  -> 发布中台归档同一源归档
  -> repack 把源归档展开到客户 ZIP 的 dist/
  -> 客户 manifest 枚举每个 dist/... 普通文件的实际路径、长度和 SHA-256
  -> 客户 daemon 按逐文件身份写入非活动槽

frontend 源归档本身不进入当前客户 ZIP,不能在客户 manifest 中把它写成安装文件。开发验证后不得重新构建 backend JAR 或 frontend 源归档;repack 只执行已确认的 frontend 展开操作,并为展开结果生成逐文件身份。

7.3 在线与离线传输

开发 container:从受控 Registry 按 digest pull,复用镜像层缓存
开发 nativebackend 获取 manifest 明确声明的 JARfrontend 获取已声明身份的源归档并展开、校验 dist 文件
客户现场:client 按 customerCode 查询并下载签名 repack ZIP,再传输到服务器并触发 updatecontainer 执行 docker loadnative 校验并写入非活动槽

两种方式在进入执行器前必须归一为同一份经过校验的组件安装描述,后续步骤完全相同。

container 镜像获取方式只由本机 TOML 的 daemon.environment 选择:精确值 dev 执行 Registry pull,精确值 prod 执行 repack ZIP load。该标记不附带其他 CLI、签名、更新或重启限制。

7.4 环境策略差异

策略 开发环境 客户现场
更新触发 CI/deploy 自动触发 交付人员或受控中转触发
制品获取 container 使用 Registry digestnative 使用 manifest 声明文件 签名离线包
旧槽保留 较短,支持高频复用 较长,优先保证回滚
自动回切 必须启用 必须启用
健康检查 与客户一致并增加测试 冻结的生产检查
日志 详细诊断 可审计且不泄漏凭据

开发环境不得通过关闭签名、跳过 digest/SHA-256、跳过健康检查、直接覆盖 native 当前槽或直接修改容器来形成另一套行为。开发环境可以使用独立信任密钥,但协议和校验路径必须相同。

7.5 高频更新与双槽复用

高频更新时:

blue 接流 -> 更新 green -> green 接流 -> 排空 blue
green 接流 -> 更新 blue -> blue 接流 -> 排空 green

只有非活动槽已经排空并完成清理后才能复用。存在未结束长连接时,不能直接删除该槽,需要按已冻结的 drain 策略处理。

7.6 Jenkins CLI 快速触发

开发环境不经过客户 PC GUI。Jenkins 在业务制品构建和身份信息生成完成后调用 yms-daemon CLI

Jenkins 构建成功
  -> 取得本次构建的 manifest、native 文件 SHA-256 或 container digest
  -> 调用开发环境 yms-daemon CLI
  -> CLI 向 serve 提交更新事务
  -> Jenkins 等待或查询事务结果
  -> 成功后晋级该制品,失败则保留 daemon 诊断和回滚结果

Jenkins 调用 CLI 不能绕过签名、SHA-256/digest、健康检查、切流和回滚。CLI 进程退出、Jenkins agent 断开或流水线超时不能取消已经提交的服务器事务。

Jenkins agent 与开发服务器是否位于同一主机、具体制品输入位置以及 CLI 参数,需要读取实际 Jenkinsfile 或 Job 配置后冻结;当前文档不推断这些值。

8. 数据库、任务和会话兼容

零停机不仅是网关切流问题。backend 新旧版本会短时间并行,必须检查:

  • Flyway 是否只执行一次。
  • 数据库变更是否同时兼容新旧代码。
  • 是否存在不可逆 SQL。
  • 定时任务是否重复执行。
  • PowerJob 执行器或其他调度进程如何选主。
  • 消息消费者是否重复消费或产生版本语义冲突。
  • Session 是否保存在进程内。
  • 本地缓存是否影响新旧版本并行。
  • 文件写入和共享目录是否具备并发安全性。

推荐的数据库发布边界:

Prepare
  -> 执行一次向前兼容迁移
  -> 启动新 backend
  -> 新旧 backend 兼容运行
  -> 切流
  -> 后续版本再清理旧字段

破坏性数据库迁移不能依赖镜像回滚恢复。

在读取并确认 ymswell 中 Flyway、定时任务、消费者、Session 和缓存的实际配置前,不实现 backend 蓝绿执行器。

9. 离线包协议

9.1 协议要求

daemon 必须从 manifest 精确获得:

  • packageFormatVersion
  • versionId
  • customerCode
  • 包含的业务组件
  • 每个组件的制品类型
  • ZIP 中实际路径
  • 文件长度和 SHA-256
  • native 安装文件列表
  • container 平台
  • container OCI image digest
  • container 镜像引用
  • 健康检查定义
  • 签发方和签名

daemon 不通过文件名、扩展名、目录名称或正则表达式推断组件归属。

9.2 manifest 扩展方向

在现有 artifact-selection.json 中增加:

{
  "packageFormatVersion": 1,
  "versionId": "V1.1.8",
  "services": {
    "backend": {
      "type": "native",
      "files": [
        {
          "path": "<ZIP中的实际JAR路径>",
          "size": 123456,
          "sha256": "<JAR文件SHA-256>"
        }
      ]
    },
    "frontend": {
      "type": "native",
      "files": [
        {
          "path": "dist/index.html",
          "size": 123456,
          "sha256": "<dist/index.html文件SHA-256>"
        },
        {
          "path": "dist/_app.config.js",
          "size": 123456,
          "sha256": "<dist/_app.config.js文件SHA-256>"
        }
      ]
    },
    "nodeSsr": {
      "type": "container",
      "artifacts": [
        {
          "platform": "linux/amd64",
          "path": "<ZIP中的实际镜像归档路径>",
          "size": 123456,
          "sha256": "<离线归档文件SHA-256>",
          "imageRef": "<CI发布的镜像引用>",
          "imageDigest": "sha256:<OCI digest>"
        }
      ]
    }
  }
}

上述新增字段是协议设计,不代表现有 artifact-selection.json 已经具备这些字段。

字段规则:

  • packageFormatVersion 必须为 daemon 明确支持的值。
  • services 的键只允许 backendfrontendnodeSsr
  • backend.typefrontend.type 只接受精确值 nativecontainer
  • nodeSsr.type 当前只接受精确值 container
  • manifest 中的组件 type 必须与本机显式部署配置完全相同。
  • native 使用 files,每个文件都必须声明 pathsizesha256
  • native frontend 的 files 必须枚举 repack 后客户 ZIP 的 dist/ 下每个普通文件;示例中的两个文件不代表完整文件集合。
  • container 使用 artifacts,并声明 platformpathsizesha256imageRefimageDigest
  • imageRef 必须是 repack 明确生成的完整 tagged image ref,其中 tag 使用发布归档保存的原始值,允许不包含末尾业务版本段;daemon 不拼接该引用。
  • container platform 必须与服务器精确平台相同。
  • path 必须是 ZIP 内明确的相对路径。
  • path 禁止绝对路径、..、空路径、重复路径和符号链接。
  • size 必须与 ZIP 条目及解压后文件一致。
  • native sha256 校验实际进入非活动槽的文件。
  • container sha256 校验离线镜像归档文件。
  • container imageDigest 校验装载后的 OCI 镜像身份。
  • container imageRef 不作为版本唯一依据。
  • manifest 未声明的业务载荷不得进入安装阶段。

完整三个组件 schema、健康检查 schema 和多平台数组规则需要与 deploy repack 同时冻结。

9.3 签名

建议更新包增加 artifact-selection.sig

  1. 发布中台使用 Ed25519 私钥签名 artifact-selection.json 原始字节。
  2. daemon 使用预置公钥验签。
  3. manifest 中的 SHA-256 覆盖全部离线载荷。
  4. native 文件由各自的 sha256 绑定,container 镜像由 imageDigest 绑定。
  5. 客户 PC 中转助手不得重写 manifest 或签名。

签名强制启用时间、旧包兼容和密钥轮换仍需确认。

10. 更新事务

10.1 三层事务控制

服务端不在本地文件、SQLite 和 .lock 中三选一,三者职责固定为:

唯一职责 明确不承担
OS advisory file lock 保证只有一个 serve 进程拥有服务器更新权限 事务状态和历史
服务端 SQLite 事务状态机、步骤、幂等、恢复和 WSS 事件位置的唯一持久化记录 ZIP、镜像归档、配置正文和完整日志
本地文件 ZIP、暂存文件、解压结果、gateway 配置快照、native 文件和详细日志 第二套事务状态真相

客户 PC SQLite 与服务端 SQLite 是两个独立数据库。PC SQLite 恢复查询、下载和交付工作;服务端 SQLite 恢复上传和业务更新事务,二者不能共享数据库文件。

更新事务是跨 SQLite、本地文件、systemd、Docker 和 gateway 的持久化 Saga。SQLite 事务只能原子提交 daemon 自己的状态记录,不能回滚已经发生的外部操作;外部操作失败时依赖明确的补偿步骤。

10.2 进程锁与更新互斥

serve 启动时打开固定锁文件,并通过 Linux advisory flock 获取非阻塞排他锁。锁由打开的文件描述符持有到进程退出:

  • 不能根据 .lock 文件是否存在判断 daemon 是否持锁。
  • 锁文件可以永久保留,不作为事务结束时需要删除的标志。
  • daemon 正常退出、崩溃或被强制终止后,由内核释放锁。
  • 第二个 serve 无法取得锁时必须拒绝启动。
  • 本地 CLI 不获取该锁,只通过 Unix Socket 调用已经持锁的 serve
  • 不为每个更新事务创建额外 .lock 文件。

单台服务器的并发保护分为三层:

OS flock
  -> 保证只有一个 serve

serve 进程内更新互斥
  -> 同一时间只调度一个更新事务

SQLite 持久化约束
  -> 最多存在一个未结束更新
  -> serve 重启后先恢复旧事务,再接受新事务

开发环境高频更新请求进入队列或明确返回更新锁占用,不能并发修改 blue/green 槽和 gateway 配置。

10.3 服务端 SQLite 边界

服务端 SQLite 只允许 serve 打开,数据库必须位于服务器本地文件系统,禁止放在 NFS 或其他网络文件系统。

第一版数据库策略固定为:

  • 使用禁用 CGO 的 SQLite 驱动。
  • 只使用一个实际数据库连接。
  • 使用 rollback journal,不启用 WAL。
  • 使用 synchronous=EXTRA;启动时必须验证生效,未生效则禁止进入可更新状态。
  • SQLite 写事务必须保持短小。
  • 不在 SQLite 写事务中等待 systemd、Docker、gateway、HTTP 健康检查或 drain。
  • 事务状态变化与对应的 WSS 可恢复事件在同一个 SQLite 事务中提交。
  • schema 迁移失败时禁止更新,不能跳过迁移继续写入。

ZIP、镜像归档、完整配置、native 文件和详细日志不作为 BLOB 写入 SQLite。SQLite 只保存这些文件的身份、路径、长度、SHA-256 和生命周期状态。

不能同时使用 JSON 文件保存另一套可写事务状态。诊断导出的 JSON 只能是 SQLite 和实际运行状态的只读快照,不能参与恢复决策。

10.4 状态机

CREATED
  -> VALIDATING
  -> PREPARED
  -> STARTING
  -> SWITCHING
  -> VERIFYING
  -> DRAINING
  -> COMMITTED

任一步骤失败:
  -> ROLLING_BACK
  -> ROLLED_BACK

无法自动恢复:
  -> FAILED

每次状态变化必须先提交到服务端 SQLite,再执行下一项外部操作。

10.5 每个事务至少记录

  • 事务 ID。
  • 来源请求的幂等标识。
  • 更新来源类型。
  • 离线 ZIP 路径和 SHA-256,或在线 manifest 身份。
  • packageFormatVersion
  • customerCode
  • versionId
  • 请求的 service
  • 每个组件明确的 type
  • native 更新前后的文件路径、长度和 SHA-256。
  • native blue/green systemd 实例和版本目录。
  • container 更新前后的 image digest、镜像 ID 和容器完整 ID。
  • 更新前 gateway 配置副本。
  • 更新后 gateway 配置副本。
  • 当前状态机阶段。
  • 每一步开始和结束时间。
  • systemd 或 Docker Engine 操作结果。
  • 本地 Nginx 配置检查与 reload 结果。
  • 健康检查和业务验证结果。
  • drain 状态。
  • 提交、失败或回滚结果。

daemon 必须先持久化事务和幂等标识,再向本地 CLI 或远程 WSS 调用方确认提交成功。相同幂等标识重复提交时返回已经创建的事务,不创建第二个更新事务。

日志不得记录签名私钥、数据库密码、Registry 凭据或完整环境变量。

10.6 本地文件提交边界

业务载荷和配置快照按以下顺序进入可使用状态:

写入同文件系统临时位置
  -> flush 文件内容
  -> 校验路径、长度和 SHA-256
  -> 原子 rename 到最终位置
  -> flush 最终文件所在目录
  -> 提交 SQLite 文件身份和生命周期状态

如果进程在原子 rename 后、SQLite 提交前崩溃,恢复流程把没有 SQLite 引用的文件视为未提交文件,不得直接安装。未提交文件的隔离和清理期限需要在固定目录设计时冻结。

gateway 更新前配置、新配置和它们的 SHA-256 必须在切流前持久化。当前 gateway 配置仍是实际流量目标的唯一依据,SQLite 不建立第二个活动槽指针。

10.7 外部步骤提交顺序

每个 systemd、Docker、文件安装或 gateway 操作遵循:

1. SQLite 记录待执行步骤、预期外部身份和补偿信息
2. 提交 SQLite
3. 执行一项外部操作
4. 查询该操作对应的实际外部状态
5. SQLite 提交实际结果和 WSS 事件
6. 进入下一步骤

不允许在一个 SQLite 写事务中连续执行多个外部操作。daemon 可能在第 3 步完成后、第 5 步提交前崩溃,因此每项外部操作必须能够通过 inspect、文件摘要、systemd 状态、gateway 配置或健康检查确认实际结果。

恢复时不能只根据 SQLite 中的“待执行”状态盲目重复外部操作。无法判断实际结果的步骤进入恢复失败,不继续切流。

10.8 启动恢复

serve 启动顺序固定为:

1. 获取 OS 文件锁
2. 打开服务端 SQLite 并执行已冻结的 schema 迁移
3. 读取未结束事务和未完成上传
4. 校验事务引用的本地文件
5. 读取当前 gateway 配置,确定实际流量目标
6. inspect systemd、Docker 和版本目录实际状态
7. 判断中断步骤未执行、已执行或失败
8. 继续、提交或执行补偿回滚
9. 恢复完成后才接受新更新

如果服务端 SQLite 无法打开、迁移失败或确认损坏:

  • 不停止当前 gateway、native 进程或业务容器。
  • 不接受新的上传完成、更新或 rollback 请求。
  • 保留数据库、本地文件和运行现场用于恢复。
  • 进入明确的不可更新维护状态,不能自动创建空数据库覆盖原状态。

数据库备份、恢复、完整性检查时机和 schema 迁移回退规则在选定禁用 CGO 的 SQLite 驱动后冻结。

11. 组件执行器集成

11.1 native executor

native executor 通过明确参数调用 systemd,并直接使用 Go 文件 API 管理非活动版本目录:

  • 不执行交付包中的脚本。
  • 不拼接 Shell 命令。
  • 不覆盖当前接流实例正在使用的 JAR 或 frontend 目录。
  • systemd 操作只允许 manifest、本机配置和固定模板共同确定的 unit。
  • 所有文件先写入同文件系统临时位置,校验完成后进入非活动槽。
  • rollback 使用事务记录中的旧实例和旧版本目录。

11.2 Docker executor

daemon 通过 Docker Engine API 管理:

  • 镜像装载和检查。
  • 容器创建、启动、停止和删除。
  • Docker network。
  • 容器 inspect。
  • 日志获取。
  • 向 gateway master 发送 HUP。

不依赖拼接 Shell 命令。是否完全不调用 Docker CLI,需要在选定 Go Docker 客户端并完成 API 版本兼容测试后冻结。

镜像装载后必须同时记录和验证:

  • 签名 manifest 中的 imageDigest
  • Docker Engine 返回的镜像 ID。
  • Docker 容器实际使用的镜像 ID。

不能只依赖 tag 或 latest

12. RPM、systemd 与构建矩阵

12.1 RPM

RPM 负责:

  • 安装 /usr/bin/yms-daemon
  • 安装和启用 yms-daemon.service
  • 创建 daemon 状态、日志和配置目录。
  • 安装受信任公钥。
  • 安装默认配置模板。
  • daemon 自身升级与卸载。

daemon 不通过业务更新事务自更新。daemon 自身版本升级由 RPM 处理。

Docker Engine 是否由 RPM 安装、作为前置依赖检查还是使用客户已有安装,需要根据客户操作系统和现场 Docker 版本冻结。

12.2 systemd

全部组件进入 Docker standalone 后,YMS 自有 unit 原则上只保留:

yms-daemon.service

native 过渡期还需要 daemon 管理的 backend blue/green unit。现有 yms.serviceymsback.serviceConflicts 必须通过独立基础设施迁移消除,不能直接复用为双实例。

业务容器由 Docker Engine 承载。现有 PowerJob 和其他独立 JVM 的最终归属需要逐项确认,不能直接删除现网 unit。

12.3 CGO 与平台

全部正式 Go 交付物必须使用 CGO_ENABLED=0

交付物 GOOS GOARCH GUI CGO
服务器 daemon linux amd64 禁用
服务器 daemon linux arm64 禁用
客户 PC 助手 windows amd64 Wails 3 禁用
客户 PC 助手 windows arm64 Wails 3 禁用

约束:

  • Linux daemon 不链接 Wails、GTK、WebKit。
  • Wails 3 只进入 Windows client gui 构建。
  • Windows Service 与 GUI 使用同一 Windows 二进制的不同入口。
  • 事务、传输、验签和协议代码由 Linux 与 Windows 构建共享。
  • 不使用依赖 CGO 的 SQLite 驱动。
  • CI 显式设置 CGO_ENABLED=0
  • Linux 构建验证 ELF 不存在非预期动态依赖。
  • 其他 ARM 目标取得客户精确 GOARCHGOARMuname -m 后再增加。

13. 客户 PC 工作台与 GUI

客户 PC 使用同一 Windows 二进制的两个运行入口:

yms-daemon client service
yms-daemon client gui
  • client service:Windows 后台服务,负责中台认证、轮询、下载、持久化、传输、远程更新触发和状态同步。
  • client gui:Wails 3 GUI,负责版本列表、手动刷新、“获取”、“更新”、进度、历史和错误展示。
  • 两者通过本地 IPC 通信。
  • GUI 不直接访问发布中台、不直接传输更新包、不直接打开 SQLite,也不直接持有服务器执行权限。

Windows Service 不能依赖交互式桌面,因此 service 与 GUI 不能作为同一个常驻交互进程。

13.1 客户 PC 操作流程

后台轮询或用户点击“刷新”
  -> client service 使用 customerCode 查询 deploy repack ZIP
  -> 写入或更新本地版本记录
  -> GUI 展示该客户可用包

用户点击“获取”
  -> 创建一次性下载令牌
  -> Range 下载到 PC 临时文件
  -> 校验长度、整包 SHA-256、签名和 manifest
  -> 原子进入本地可交付状态

用户点击“更新”
  -> 选择已登记的客户服务器和 service
  -> 把完整 ZIP 传输到服务器暂存位置
  -> 校验服务器接收文件身份
  -> 服务器侧提交 yms-daemon update
  -> 持续同步事务状态、日志摘要和最终结果

“获取”和“更新”是两个独立动作。未完整下载并校验的包不能执行更新;已经下载的包允许在 PC 断网后交付给客户服务器。

13.2 SQLite 边界

客户 PC 引入 SQLite 是合理且必要的,原因不是 GUI 展示本身,而是 Windows Service 需要在进程重启、PC 重启和网络中断后恢复业务状态。

SQLite 至少承载以下业务记录类别:

  • 已登记的发布中台连接和客户身份关联。
  • 每次轮询与手动查询得到的客户 repack 包元数据。
  • 本地下载文件、临时文件、长度、摘要和校验状态。
  • 客户服务器登记信息。
  • PC 到服务器的传输进度和恢复信息。
  • 已提交的服务器更新事务及其状态同步结果。
  • 操作时间、失败原因和可审计历史。

ZIP 文件本身保存在受控文件目录中,不作为 BLOB 写入 SQLite。SQLite 只保存文件身份、位置和业务状态。

client service 是 SQLite 的唯一读写者。Wails GUI 只能通过本地 IPC 查询和提交操作,避免两个进程直接竞争数据库锁,也保证 GUI 退出不影响轮询、下载和更新状态跟踪。

SQLite 驱动必须在 CGO_ENABLED=0 下构建。具体驱动、数据库固定路径、schema、迁移工具、备份恢复和损坏处理在实现前通过 Windows amd64、Windows arm64 构建与故障测试后冻结。认证凭据和长期令牌不得以明文写入 SQLite;Windows 凭据保护方式需要单独冻结。

13.3 轮询与一致性

  • 自动轮询和手动刷新复用同一查询实现。
  • 远端包以发布中台返回的客户归属和文件身份登记,不根据文件名解析客户或版本。
  • 同一个远端包重复出现时更新本地元数据,不创建重复业务记录。
  • 轮询只发现并记录新包,不自动执行“获取”或“更新”。
  • 下载令牌过期时重新申请令牌,并从已确认位置继续下载。
  • client service 重启后从 SQLite 和本地文件实际状态恢复,不只相信数据库中的进度值。
  • 服务器更新提交结果不明确时,必须先查询服务器事务状态,不能直接重复提交。

13.4 PC 到 daemon 的管理协议

PC 到客户服务器采用两个职责分离的通道:

HTTPS
  -> 创建上传
  -> 查询 daemon 已确认的上传偏移
  -> 从确认偏移继续上传 ZIP
  -> 完成整包校验

WSS
  -> 提交结构化更新请求
  -> 查询事务快照
  -> 接收进度、状态和日志摘要事件
  -> 断联后恢复事件

HTTPS 上传遵循 tus 1.0 core 的偏移语义:创建上传资源,使用 HEAD 查询服务器当前确认偏移,使用 PATCH 从该偏移继续写入。请求偏移与服务器偏移不一致时拒绝本次追加,不覆盖已确认内容。精确 URL、元数据和是否声明完整 tus 兼容在协议实现前冻结。

daemon 只有在上传字节已经写入暂存文件并持久化确认偏移后,才向 PC 确认新偏移。上传达到声明长度后必须校验整包 SHA-256、签名和 manifest;全部通过后才能原子进入可更新状态。

WSS 是控制与状态通道,不传输 ZIP,也不提供远程 Shell。WebSocket Ping/Pong 和读写超时只用于发现失联,不能作为事务存活条件。

断联恢复规则固定为:

断联阶段 daemon 行为 PC 重连行为
ZIP 上传未完成 保留已确认偏移和暂存文件 查询服务器偏移并继续上传
ZIP 已校验、更新未提交 保留可更新上传记录 查询上传状态后提交更新
更新请求已发送、确认响应丢失 按幂等标识保留唯一事务 先按幂等标识查询,不直接重复创建
更新事务执行中 独立继续事务、切流或回滚 获取事务快照后恢复状态订阅
状态事件传输中断 持久化事务状态和可恢复事件位置 从最后确认的事件位置继续;无法续接时重新获取完整快照
PC 或 client service 重启 不影响服务器事务 从 SQLite 恢复上传、幂等标识、事务 ID 和最后确认事件位置

WSS 与 HTTPS 可以由 serve 的同一个 TLS 管理监听入口提供,但管理入口与普通业务 gateway 分离。TLS、PC 身份认证、服务器身份校验、客户与服务器授权绑定、监听地址和端口必须在实现前冻结。

本地 CLI 继续通过 Unix Socket 调用 serve。远程 WSS 请求和本地 CLI 请求在认证入口之后进入同一个更新请求模型、状态机和并发锁。

14. 分布式主机与 Kubernetes

核心事务不能把“更新目标”等同于本机 Docker 或 systemd。内部结构需要保留拓扑执行器边界:

签名发布描述
  -> 事务协调器
      -> Docker standalone executor
      -> distributed host executor
      -> Kubernetes executor

14.1 分布式主机

  • 每个节点安装 RPM daemon。
  • 一个协调节点创建全局事务。
  • 所有节点先完成签名、平台、空间和镜像预检。
  • 按批次 drain、更新、验证和恢复流量。
  • 每个节点保存子事务。
  • 失败按相反批次回滚。
  • 节点间使用明确身份认证和加密通信。

精确节点发现、负载均衡摘除和恢复方式必须读取客户现场实际配置后确定。

14.2 Kubernetes

Kubernetes executor 不在每个业务 Pod 中运行 daemon。它负责:

  • 将离线 OCI 镜像导入客户内部 Registry。
  • 使用不可变 digest 更新 workload。
  • 观察 rollout、readiness 和业务健康。
  • 失败时恢复旧 digest。
  • 将数据库迁移作为独立且只执行一次的步骤。

仓库中尚未发现实际 Kubernetes/Helm/kustomize 资源。实现前必须取得:

  • Namespace。
  • workload 的准确类型和名称。
  • 内部 Registry。
  • Ingress。
  • ConfigMap 和 Secret。
  • StorageClass、PVC 和挂载。
  • readiness/liveness/startup probes。
  • rollout 参数。
  • Service 和流量入口。

15. 故障测试矩阵

15.1 包与镜像

  • ZIP 不存在、截断或重复条目。
  • manifest 或签名缺失。
  • 不支持的 packageFormatVersion
  • 签名失败。
  • 路径穿越、绝对路径或符号链接。
  • 文件长度或 SHA-256 不一致。
  • 服务器平台与制品平台不一致。
  • docker load 失败。
  • 装载后 image digest 不一致。
  • tag 指向与 manifest digest 不一致。

15.2 容器准备

  • Docker Engine 不可用。
  • Docker Engine API 版本不兼容。
  • Docker network 不存在或创建失败。
  • 新容器创建、启动或 inspect 失败。
  • 新容器退出。
  • 直连健康检查失败或超时。
  • 容器资源不足。

15.3 native 准备

  • 本机配置要求 native,更新包只包含 container
  • 本机配置要求 container,更新包只包含 native
  • native 文件路径、长度或 SHA-256 不一致。
  • 非活动版本目录空间不足或权限错误。
  • backend 双实例 unit 未完成基础设施迁移。
  • systemd unit 存在 Conflicts,无法同时运行。
  • 非活动 JVM 启动失败或健康检查超时。
  • frontend 非活动目录未完整写入。
  • native rollback 时旧 JAR 或旧 frontend 目录缺失。

15.4 gateway

  • 本地 Nginx 不存在或未运行。
  • 配置目录不可写。
  • 生成配置失败。
  • 配置检查失败。
  • 原子替换失败。
  • HUP 失败。
  • reload 后新 worker 未启动。
  • reload 后业务检查失败。
  • 恢复旧配置失败。

15.5 长连接和前端资源

  • SSE 在切流前已建立。
  • 大文件上传跨越切流时间点。
  • 旧页面切流后加载旧 JS chunk。
  • 新旧版本存在同名无 hash 静态文件。
  • drain 超时。
  • 高频更新时上一槽仍未排空。

15.6 backend 兼容

  • Flyway 重复执行。
  • 新迁移与旧 backend 不兼容。
  • 两个版本重复执行定时任务。
  • 消费者重复运行。
  • Session 无法跨版本使用。
  • 本地缓存导致新旧版本行为不一致。

15.7 进程和服务器恢复

  • 第二个 serve 启动并尝试获取同一 OS 文件锁。
  • .lock 文件存在但当前没有进程持锁。
  • daemon 在每个状态机阶段被强制终止。
  • daemon 在外部操作完成、SQLite 结果提交前被强制终止。
  • gateway 在更新期间重启。
  • Docker Engine 在更新期间重启。
  • 服务器在切流前、切流后、提交前重启。
  • 当前槽和旧槽均不健康。
  • 服务端 SQLite 无法打开、schema 迁移失败或数据库损坏。
  • SQLite 所在路径被错误配置为网络文件系统。
  • SQLite 提交、文件 flush、原子 rename 或父目录 flush 失败。
  • 服务端 SQLite 可用,但事务引用的 ZIP、配置快照或版本文件缺失。
  • 存在已经原子 rename、但尚未被 SQLite 引用的未提交文件。

15.8 开发环境高频更新

  • 连续快速提交多次更新。
  • 更新锁占用时收到新请求。
  • Registry 短暂不可用。
  • 镜像层缓存命中和未命中。
  • CI 更新成功但业务验证失败。
  • 自动回切后继续下一次更新。
  • 开发环境验证 digest 与 repack digest 不一致时拒绝发布。
  • native 开发验证 SHA-256 与 repack SHA-256 不一致时拒绝发布。

15.9 客户 PC 工作台

  • 发布中台不可达、认证过期或权限不足。
  • customerCode 不存在或没有 repack ZIP。
  • 自动轮询与手动刷新同时发生。
  • 下载令牌在下载前或断点续传时过期。
  • Range 下载被中断或服务端返回内容与续传位置不一致。
  • PC 磁盘空间不足、临时文件丢失或整包 SHA-256 不一致。
  • client service 在下载、传输或更新提交时终止。
  • GUI 退出或升级时后台任务继续运行。
  • SQLite 事务中断、迁移失败或数据库损坏。
  • PC 到服务器传输中断后恢复。
  • WSS 心跳超时、网络切换或中间设备关闭连接。
  • ZIP 已传输,但远程更新提交结果未知。
  • 更新请求已经持久化,但 WSS 确认响应丢失。
  • 服务器事务已创建,但 PC 随后断网。
  • 状态事件丢失、重复或恢复位置已经过期。
  • 同一包、同一服务器和同一 service 被重复点击更新。

16. 分阶段实施计划

阶段一:冻结设计

  • 确认 client 指安装在客户 PC 上的交付工作台,兼具离线中转能力。
  • 确认 RPM 全局安装单一 Go 二进制。
  • 确认正式 Go 交付物禁用 CGO。
  • 确认过渡阶段支持显式 nativecontainer 和混合部署。
  • 确认 native 兼容层不执行现有更新脚本。
  • 确认 Docker standalone 是 native 客户的主要迁移方向。
  • 确认 frontend dist 在 CI 阶段进入 frontend 镜像。
  • 确认本地 Nginx 作为稳定 gateway。
  • 确认业务组件使用 blue/green 和 graceful reload 零停机切流。
  • 确认第一版不使用 Lua shared dict 作为核心切流状态。
  • 确认开发环境使用同一更新体系进行高频更新。
  • 确认 PC 到 daemon 使用 HTTPS 可续传上传和 WSS 控制状态通道。
  • 确认 WSS 断联不取消上传记录或服务器更新事务。
  • 确认服务端使用 OS file lock、SQLite 和本地文件三层事务控制。
  • 确认服务端 SQLite 是 daemon 事务状态的唯一持久化记录,本地文件不保存第二套事务状态。
  • 确认第一版服务端 SQLite 使用单连接、rollback journal 和 synchronous=EXTRA
  • 冻结 frontend 旧资源兼容方案。
  • 冻结本机组件运行类型配置 schema。
  • 冻结 native backend 模板 /etc/systemd/system/yms-backend@.service、实例 yms-backend@8080.serviceyms-backend@8081.service、release 目录和两个槽位 JAR 路径。
  • 冻结 native frontend 双版本目录。
  • 冻结 backend 蓝绿期间 Flyway、任务、消费者和 Session 约束。
  • 冻结 gateway 配置目录、容器命名、network 和 labels。
  • 冻结健康检查和业务验证协议。
  • 冻结 drain 和旧槽保留规则。
  • 冻结离线 manifest 完整 schema 和签名规则。
  • 冻结开发环境在线更新接口。
  • 冻结 Jenkins 调用 CLI 的拓扑、输入和退出码语义。
  • 冻结 PC 到服务器管理协议的 URL、TLS 身份、授权、监听地址、端口和事件恢复规则。
  • 冻结客户 PC SQLite 边界、schema、驱动和文件保留策略。
  • 冻结服务端锁文件、SQLite、事务文件和日志路径,以及状态机和退出码。
  • 冻结服务端 SQLite schema、迁移、备份、完整性检查和损坏恢复流程。

阶段二:开发环境最小闭环

  • 实现 serve、本地 Unix Socket 和 backend native update 命令。
  • 实现 backend native 的 repack ZIP -f 与开发环境直传 JAR --native-jar 两种显式互斥输入,并让二者进入同一事务执行链。
  • 实现 update Unix Socket 流式过程消息和 CLI 实时阶段输出,并提供精确静默参数 --quite
  • 实现 yms-daemon restart --service backend 的同版本零停机槽位轮转、流式进度和未完成事务恢复。
  • 实现 rollback、查询命令以及 frontend、nodeSsr、all 更新入口。
  • 实现本地 Unix Socket、Linux advisory flock 和进程内更新互斥。
  • 实现服务端禁用 CGO 的 SQLite 驱动、单连接配置、rollback journal 和 synchronous=EXTRA 启动校验。
  • 实现服务端事务、步骤、幂等和 WSS 事件的 SQLite 持久化约束。
  • 实现临时写入、文件 flush、摘要校验、原子 rename 和父目录 flush。
  • 实现统一组件执行器和 gateway controller 接口。
  • 实现 backend native 本机 TOML 配置、逐字键树校验、type 严格校验和 8080/8081 槽位查询。
  • 在精确 schema 冻结后扩展 frontend、nodeSsr 和 container 本机配置。
  • 实现 Docker Engine API 客户端和兼容性检查。
  • 实现镜像 digest 校验。
  • 实现 blue/green 统一生命周期。
  • 实现本地 Nginx gateway controller 的配置生成、检查、原子替换和 /usr/sbin/nginx -s reload
  • 实现 backend、frontend、nodeSsr 健康检查。
  • 实现基于 SQLite 意图、gateway 配置和 systemd/Docker/文件实际状态的重启恢复。
  • 在开发环境按 native 文件 SHA-256 或 container digest 完成高频更新闭环。
  • 接入 Jenkins 构建后 CLI 触发、事务查询和构建结果回写。

阶段三:native/hybrid 与应用兼容改造

  • 实现 backend native systemd executor 内核:不可变 JAR、非活动槽位绑定、systemd 启停、Actuator 健康检查、事务恢复和切流前补偿;实际双实例 unit 与槽位路径仍由冻结后的本机配置接入。
  • 实现 frontend native directory executor。
  • 实现 host Nginx gateway controller。
  • 实现 native/container 混合 all 事务。
  • 实现现有冲突 unit 到双实例 unit 的独立基础设施迁移。
  • 在开发环境持续执行 native 与 container 两条更新链路。
  • Flyway 从并发 JVM 或容器启动中形成单次执行边界。
  • 定时任务、PowerJob 和消费者支持蓝绿并行。
  • 确认 Session 和缓存兼容。
  • 完成 frontend 旧资源兼容。
  • 完成 SSE、上传和 drain 验证。

阶段四:离线交付

  • 扩展 artifact-selection.json
  • 为 native 文件生成实际 ZIP 路径、长度和 SHA-256,为 container 归档生成实际 ZIP 路径、长度、SHA-256 和 image digest。
  • 扩展客户包查询或下载令牌响应,提供整包 SHA-256。
  • 实现 Ed25519 签名和验签。
  • 实现安全 ZIP 读取和镜像暂存。
  • 实现符合已冻结 tus 1.0 core 语义的 HTTPS 上传和断点续传。
  • 实现 WSS 结构化更新提交、事务快照、事件推送和断联恢复。
  • 使用真实 repack 包完成端到端更新与回滚。

阶段五:RPM 与客户迁移

  • 构建 linux/amd64linux/arm64 RPM。
  • 验证 CGO_ENABLED=0 和 ELF 依赖。
  • 冻结 Docker Engine 前置条件。
  • 设计现有 native 到 daemon 管理 native 双槽的显式迁移事务。
  • 设计 daemon 管理 native 到 Docker standalone 的显式迁移事务。
  • 任一迁移失败时恢复迁移前运行类型和服务。
  • 在独立现场等价环境完成演练。

阶段六:客户 PC GUI

  • 实现 Windows client service
  • 实现发布中台认证和 customerCode 绑定。
  • 实现 repack ZIP 自动轮询和手动刷新。
  • 实现一次性令牌、Range 下载、暂停恢复和整包校验。
  • 引入禁用 CGO 的 SQLite 驱动并实现本地状态恢复。
  • 实现本地包文件目录、空间检查和保留策略。
  • 实现 PC 到服务器 HTTPS 上传、偏移续传和接收校验。
  • 实现 WSS 心跳、重连、服务器更新触发、事务快照、事件恢复和重复提交防护。
  • 实现 Wails 3 client gui
  • 实现本地 IPC。
  • 实现查询、获取、更新、进度、历史和错误界面。
  • 复用签名、摘要、传输和状态协议并完成断网演练。

阶段七:分布式和 Kubernetes

  • 获取真实分布式客户拓扑和负载均衡操作。
  • 设计全局事务与节点子事务。
  • 获取真实 Kubernetes 资源。
  • 实现 OCI 导入、digest rollout、readiness 和 rollback。

17. 已确认决定

  1. client 精确指安装在客户 PC 上的交付工作台,兼具离线中转能力。
  2. RPM 全局安装一个 yms-daemon Go 二进制。
  3. 同一二进制提供 serveclientupdaterollback 和查询入口。
  4. 特权更新只由常驻 serve 执行。
  5. 全部正式 Go 交付物使用 CGO_ENABLED=0
  6. Docker standalone 是 native 客户向容器化迁移的主要方向。
  7. container 制品使用 OCI digest 作为身份,不依赖 tag 判断版本;native 制品使用 manifest 声明的文件路径、长度和 SHA-256。
  8. container frontend 的 dist 在 CI 阶段烘焙进 frontend 镜像;native frontend 由 repack 展开源归档,客户 manifest 逐文件声明 dist/... 的路径、长度和 SHA-256,daemon 把这些文件写入非活动版本目录。
  9. 目标态由客户服务器本地 Nginx 作为稳定 gatewayfrontend 静态 Nginx 不代理 backend。
  10. 三个业务组件通过 blue/green 双槽和 gateway graceful reload 实现普通更新零停机。
  11. 第一版核心切流不引入动态 Lua 路由 shared dict 和独立路由数据库。
  12. 开发环境必须使用同一 daemoncontainer 验证同一次 multi-platform 构建的实际平台 digestnative 验证 backend JAR 或 frontend 源归档及展开文件的 SHA-256,并复用同一切流和回滚执行器进行高频更新。
  13. 开发环境验证通过后,客户交付不得重新构建同版本业务镜像、backend JAR 或 frontend 源归档。
  14. gateway 自身升级与业务组件普通更新分离。
  15. 过渡阶段正式支持 nativecontainer 和按组件混合部署。
  16. backendfrontend 支持 native/container 双执行器;nodeSsr 当前只支持 container。
  17. 本机配置和 manifest 必须明确声明组件 typedaemon 不从现场状态推断。
  18. 普通 update 不改变运行类型;native 到 container 使用独立迁移事务。
  19. native executor 使用 Go 直接管理文件和 systemd,不执行现有更新脚本。
  20. native 与 container 复用事务、健康检查、gateway 切流和回滚框架。
  21. 客户 PC 查询的是按 customerCode 隔离的 repack ZIP,不是通用 release 版本列表。
  22. 客户 PC 同时支持后台轮询和 GUI 手动刷新;轮询只发现版本,不自动下载或更新。
  23. “获取”负责从发布中台下载并校验到 PC,“更新”负责传输到服务器并触发服务器 yms-daemon update
  24. 客户 PC 使用 SQLite 保存可恢复的业务状态;SQLite 由 client service 独占读写,Wails GUI 只通过本地 IPC 访问。
  25. 客户 PC SQLite 驱动必须支持 CGO_ENABLED=0ZIP 文件不存入数据库。
  26. 开发环境由 Jenkins 调用 yms-daemon CLI 快速触发更新,并进入与客户现场相同的服务器事务、校验、切流和回滚框架。
  27. PC 到 daemon 使用 HTTPS 传输 ZIP,上传遵循 tus 1.0 core 的偏移恢复语义。
  28. PC 到 daemon 使用 WSS 提交结构化更新请求并接收事务状态、进度和日志摘要;WSS 不传输 ZIP,也不执行远程 Shell。
  29. daemon 更新事务独立于 WSS 连接存活;PC 断联或重启后通过 SQLite、上传偏移、幂等标识、事务快照和事件位置恢复。
  30. daemon 必须先持久化更新事务再确认提交成功;重复幂等请求只返回原事务。
  31. 远程 WSS 与本地 Unix Socket CLI 进入同一个内部更新请求模型、状态机和并发锁。
  32. 服务端事务采用 OS advisory file lock、服务端 SQLite 和本地文件三层控制;三层职责不重叠。
  33. OS file lock 只保证单一 serve,不能根据 .lock 文件是否存在判断锁状态,也不为每个更新创建锁文件。
  34. 服务端 SQLite 是事务状态、步骤、幂等和 WSS 恢复事件的唯一持久化记录;本地文件保存载荷和快照,不保存第二套可写事务状态。
  35. 第一版服务端 SQLite 只使用一个实际连接、rollback journal 和 synchronous=EXTRA,数据库禁止放在网络文件系统。
  36. SQLite 状态提交和外部操作分步执行,SQLite 写事务中禁止等待 systemd、Docker、gateway、健康检查或 drain。
  37. gateway 配置、systemd、Docker 和本地文件是恢复时的实际状态依据;SQLite 记录执行意图和历史,不能取代现场核对。
  38. 服务端 SQLite 损坏时保留当前业务运行状态并拒绝新更新,不能用空数据库自动覆盖。
  39. YMS 与宿主机 daemon 的更新包共享收件目录固定为 /home/yms/tmp,属主和用户组固定为 root:root,权限固定为 0755Docker backend blue/green 容器使用同名绝对路径 bind mount,并明确以 root 用户运行;该目录不保存 daemon 的 SQLite、事务内部文件或日志。
  40. native backend 模板固定为 /etc/systemd/system/yms-backend@.service,实例标识固定为 80808081,完整 unit 名称固定为 yms-backend@8080.serviceyms-backend@8081.service,槽位 JAR 软链接固定为 /home/yms/lib/glory-soft-yms-8080.jar/home/yms/lib/glory-soft-yms-8081.jar;当前接流版本的人工运维兼容入口固定保留 /home/yms/lib/glory-soft-yms.jar
  41. 当前客户服务器的 systemctl 可执行文件绝对路径为 /bin/systemctl;其他服务器使用其本机明确配置的绝对路径,daemon 不自行尝试替代路径。
  42. native backend unit 使用 Type=simple 直接执行 /usr/bin/java,不调用启动脚本;堆转储目录固定为 /home/yms/dump,其属主和用户组固定为 root:root、权限固定为 0755 并由 RPM 创建;JVM 错误日志固定为 /home/yms/log/hs_err_pid%p.logstdout 和 stderr 进入 journalTimeoutStopSec 固定为 150min
  43. daemon 本机配置文件固定为 /etc/yms-daemon/yms-daemon.toml,格式固定为 TOML;使用纯 Go 的 github.com/pelletier/go-toml/v2 严格解码,并在解码前逐字校验完整键树,拒绝大小写变化、未知字段、未知槽位和重复字段,保持 CGO_ENABLED=0
  44. daemon.environment 固定为必填机器级字段,只接受精确值 devprod;它只选择 container 镜像获取行为,dev 为 Registry pullprod 为 repack ZIP load。
  45. 第一阶段 backend native 配置精确使用 [backend][backend.slot.8080][backend.slot.8081];key、必填规则和精确值以 2.9 节为准,loader 不提供配置默认值。
  46. daemon 服务端路径固定为:Unix Socket /run/yms-daemon/yms-daemon.sock、进程锁 /run/yms-daemon/yms-daemon.lock、SQLite /var/lib/yms-daemon/yms-daemon.db、事务工作目录 /var/lib/yms-daemon/work、本地日志 /var/log/yms-daemon/yms-daemon.log
  47. 当前 gateway 固定管理 /etc/nginx/nginx.conf 中唯一一组 # yms-update managed upstream begin/end 标记,Nginx 可执行文件固定为 /usr/sbin/nginxsystemd unit 固定为 nginx.service;配置完整原子替换后执行 /usr/sbin/nginx -t/usr/sbin/nginx -s reload,任一步失败恢复替换前完整配置。
  48. 第一次旧 unit 运行态迁移固定映射为 8080=yms.service、8081=ymsback.service;Nginx 当前接流端口对应的旧 unit 和 yms-backend@<port>.service 必须恰好一个处于运行态,否则拒绝更新。
  49. backend native 更新输入分为 repack-zipnative-jar;前者由 -f 提交,后者由 --native-jar 提交,二者互斥且不通过扩展名推断。直传 JAR 使用完整文件 SHA-256 作为幂等身份,在 /home/yms/lib/releases 下保存为 direct/<SHA-256前12位>/<原始JAR文件名>;目录名精确取完整小写十六进制 SHA-256 的前 12 位,完整 SHA-256 继续用于幂等和内容校验,目录冲突时拒绝覆盖。直传 JAR 复用与 repack ZIP 相同的双槽、健康检查、切流、提交和补偿流程。
  50. backend native 零停机重启命令固定为 yms-daemon restart --service backend,可附加 --quite。restart 使用当前接流 release 创建独立事务并轮转到非活动槽,不调用 systemctl restart,也不重新上传 JAR;当前 JAR 已在 release 目录时直接复用,兼容入口仍是普通文件时按内容身份导入 release 目录。未完成 restart 由同一命令恢复,其他未完成事务阻止新 restart。
  51. container backend 的 update --service backend --container-image 仅在 SQLite 不存在已提交部署记录、不存在历史 COMMITTED container backend 事务,且 backend-8080backend-8081 都不存在时按首次部署处理:目标固定为 Nginx 当前未接流的槽位,容器启动并通过健康检查后执行一次正常切流,随后原子提交事务状态和活动容器部署记录;因为不存在 previous 容器,所以不执行 drain 和停止步骤。存在已提交记录或历史 COMMITTED container backend 事务时,容器、记录和 Nginx 任一不一致均按状态漂移拒绝更新,不静默按首次部署处理;FAILEDROLLED_BACK 首装事务不构成成功部署证据。该语义只覆盖 container backendnative backend 首装保持现状。
  52. 外部步骤一次执行失败后,同一条命令在本次调用内不自动重试。交付人员修复 Nginx、systemd、容器运行时或文件系统后再次人工执行命令时,协调器必须先 inspect 已失败步骤:APPLIED 直接补记成功,NOT_APPLIED 才重新 ApplyUNKNOWN 继续拒绝推进。回滚完成为 ROLLED_BACK 后,再次提交相同载荷时原子归档旧事务幂等键并创建新事务;历史 COMMITTED 事务仍保持幂等返回。container 首装回滚遗留的已停止目标容器由新事务准备阶段检查并清理,不要求人工删除。

18. 当前必须继续研讨的精确问题

  1. frontend 健康检查 URL、状态码、响应内容和静态文件检查规则。
  2. frontend 旧资源采用版本命名空间、上一槽 fallback 还是组合方案。
  3. backend 新旧版本并行时 Flyway 的单次执行方式。
  4. backend 定时任务、PowerJob、消费者、Session 和缓存的实际行为。
  5. /etc/yms-daemon/yms-daemon.toml 中 frontend、nodeSsr 和 container 的精确 table、key 和必填规则。
  6. native backend 从现有 /home/yms/bin/env/yms.env 迁移到 /home/yms/conf 外挂配置的时机和兼容规则。
  7. native frontend 双版本目录的精确值。
  8. gateway 容器镜像、配置目录、Docker network、blue/green 容器名和 label 的精确值。
  9. 本地 Nginx 配置检查和 /usr/sbin/nginx -s reload 成功的确认方式。
  10. SSE 和上传的最大 drain 时间与强制结束规则。
  11. 三个组件的旧槽保留时间、磁盘清理和镜像清理规则。
  12. /home/yms/tmp 的内部子目录、容量限制、保留时间和异常文件清理规则。
  13. Docker Engine 支持的最低版本和 API 版本范围。
  14. RPM 是安装 Docker Engine、声明依赖还是只做运行前检查。
  15. 现有 native 到 daemon 管理 native 双槽的迁移命令、路径、权限和失败恢复流程。
  16. native 到 Docker standalone 的迁移命令、数据目录、配置目录、权限和失败恢复流程。
  17. 开发环境在线更新使用 CLI、deploy RPC、Webhook 还是任务拉取;需结合现有 deploy 接口设计。
  18. 是否从第一版强制要求 Ed25519,未签名旧包如何处理。
  19. 签名密钥轮换流程。
  20. PowerJob 是否纳入业务容器和 daemon 更新范围。
  21. 客户实际 CPU、操作系统、Docker 版本和 ARM 精确架构。
  22. 分布式现场的节点清单、负载均衡入口和 drain 操作。
  23. Kubernetes 的真实 Namespace、workload、Registry、Ingress、ConfigMap、Secret、PVC 和 probes。
  24. 客户 PC 使用哪个发布中台认证流程,以及登录身份与 customerCode 的精确授权关系。
  25. 客户 PC 自动轮询间隔、失败退避、手动刷新限流和多客户切换规则。
  26. 发布中台整包 SHA-256 的字段名、生成时机和返回接口。
  27. PC 本地包目录、临时文件后缀、磁盘配额和清理保留规则。
  28. daemon 管理协议的 TLS 身份、PC 与服务器授权绑定、监听地址、端口和证书轮换方式。
  29. HTTPS 上传的精确 URL、元数据、服务器暂存路径、过期清理规则以及是否声明完整 tus 1.0 兼容。
  30. 客户 PC SQLite 驱动、固定路径、schema、迁移版本和损坏恢复流程。
  31. Windows 认证凭据和长期令牌的保护方式。
  32. Jenkins agent 与开发服务器的实际拓扑、构建产物位置、CLI 参数和结果回写方式。
  33. WSS 消息 schema、幂等标识生成规则、事务快照结构、事件位置、保留时间和无法续接时的完整同步规则。
  34. WSS 心跳、读写超时、重连退避和最大离线时间。
  35. 服务端禁用 CGO 的 SQLite 驱动及其精确 SQLite 版本。
  36. 服务端 SQLite schema、迁移版本、备份、完整性检查和损坏恢复流程。
  37. 未提交文件的隔离目录、识别规则、保留期限和清理流程。

上述精确项冻结前,不修改现网更新脚本、systemd、Nginx 配置和发布包格式。


附录:YMS 命令入口与操作查询方案

状态:方案确认。ymsd/ymsctl 拆分、list、status、doctor、reconcile 已实现;restart 的 container 后端已实现;rollback 待实现。

1. 命令职责

命令入口采用 daemon/client 分离,命名参考 chronyd / chronyc

命令 职责 是否常驻
ymsd 更新助手服务端,监听 Unix Socket,执行事务和恢复
ymsctl 运维和交付 CLI,提交更新、查询状态、诊断和回滚
yms-gui 客户 PC 图形化中转助手,名称后续确定

ymsd 不承担交互式更新命令;更新请求由 ymsctl 通过 Unix Socket 提交。POC 阶段不对外提供 serve 子命令,systemd 的 ExecStart 直接启动 /usr/bin/ymsd

2. systemd 与安装

systemd unit 名称继续固定为:

yms-daemon.service

unit 的启动目标为:

/usr/bin/ymsd

POC 阶段不保留 yms-daemon 命令兼容入口,直接采用新命名。更新命令统一使用:

ymsctl update ...
ymsctl restart ...

3. ymsctl 命令层级

3.1 更新与回滚

ymsctl update --service backend -f <repack.zip>
ymsctl update --service backend --native-jar <backend.jar>
ymsctl update --service backend --container-image <image:tag>
ymsctl restart --service backend
ymsctl rollback --service backend --transaction <transaction-id>

--native-jar--container-image 互斥。rollback 必须指定明确的事务 ID 或已提交版本身份,不允许根据目录排序、文件修改时间或镜像 tag 猜测回滚目标。

3.2 历史操作查询

ymsctl list
ymsctl list --limit 50
ymsctl list --state FAILED
ymsctl list --service backend
ymsctl list --json

默认显示最近 20 次操作,按创建时间倒序。每条记录至少展示:

  • 创建时间和结束时间;
  • transaction ID
  • service
  • operation
  • 输入类型和版本或镜像 digest
  • 来源(CLI、PC client、Jenkins、server API);
  • 最终状态;
  • 失败原因。

--json 输出稳定 JSON,供 Jenkins 和 PC client 使用。列表数据只读取 SQLite,不直接扫描 systemd、Docker 或 Nginx 生成历史记录。

3.3 当前状态与诊断

ymsctl status --service backend
ymsctl doctor --service backend
ymsctl reconcile --service backend
ymsctl reconcile --service backend --apply
  • status:读取 SQLite 后校验当前 systemd、Docker、Nginx 和活动槽位状态;
  • doctor:只读诊断,输出漂移项和建议动作;
  • reconcile:只读生成修复计划;
  • reconcile --apply:执行明确授权的修复动作,并创建可审计事务。

4. 状态漂移处理原则

SQLite 是事务事实来源,外部系统是待校验运行状态。发现不一致时默认拒绝更新,不自动覆盖现场。

只有以下类型允许自动修复:

  1. SQLite 记录的活动容器存在、身份匹配且健康,Nginx 仅指向错误槽位;
  2. 非活动槽位容器存在、已停止且没有提交记录;
  3. native 活动 JAR 链接与已提交事务明确记录的 release 不一致,且目标文件身份校验通过;
  4. 已完成事务的临时文件可以按照事务凭据清理。

以下情况必须进入 DRIFT_DETECTED,等待人工确认:

  • SQLite 记录的活动容器已经不存在;
  • 两个槽位同时运行且无法确定提交归属;
  • SQLite 没有部署记录,但现场已经存在容器或运行中的 native 服务;
  • Nginx 配置无法验证或存在非 daemon 管理的冲突;
  • systemd unit、容器镜像 digest、JAR SHA-256 与事务记录不一致。

任何修复动作都必须先写入事务,再执行外部变更;修复失败时沿用现有回滚和恢复机制。

5. systemd 服务与 ymsd 的边界

ymsd 负责:

  • 持有进程锁;
  • 打开 SQLite
  • 恢复未完成事务;
  • 调用 native/container 执行器;
  • 通过 Unix Socket 提供请求和进度响应;
  • 写入 stdout 和本地日志文件。

ymsctl 负责:

  • 参数校验;
  • 提交请求;
  • 展示进度;
  • 查询历史和当前状态;
  • 发起显式诊断和修复。

ymsctl 不直接写 SQLite,不直接修改 Nginx,不直接调用 Docker 或 systemd。

6. 实施顺序

  1. 增加 SQLite 最近操作查询接口;
  2. 实现 ymsctl list 和 JSON 输出;
  3. 拆分 ymsdymsctl 的编译入口;
  4. 安装 ymsdymsctl 和兼容入口 yms-daemon
  5. 实现 status 只读一致性检查;
  6. 实现 doctor 漂移报告;
  7. 实现带事务审计的 reconcile --apply
  8. 最后确定 GUI 名称和 PC client 的调用协议。