服务端模块化、部分热更新与独立部署方案¶
本文基于 network_mapping_server 0.1.27 的现有服务端代码设计。目标是让业务模块逐步独立开发、测试和部署,更新部分业务时保持 API 入口、登录会话和扫描器通信可用。
建议采用:Cargo workspace 模块化 + 稳定 API 内核 + 可替换业务 worker + 版本化配置。 第一阶段仍可单进程部署;按更新频率与负载逐步把 worker 拆成进程。配置热更新、业务代码替换和内核滚动升级是三条不同的发布路径。
当前实施状态:现有库入口业务已迁移到领域模块;运行时与纯渲染成为独立 workspace 包;生产报表已接入模块接口、独立渲染进程及管理员版本切换。具体可运行能力、配置、接口和边界以实现说明为准。本文其余内容保留整体目标架构;未在实现说明中列为已接入的能力仍为后续设计。
1. 现有结构与必须保留的语义¶
1.1 改造前代码依据¶
| 现有位置 | 改造前职责与约束 | 对设计的影响 |
|---|---|---|
src/main.rs |
初始化数据库、ES、配置、中间件,直接启动大量 tokio::spawn,注册 HTTP 服务 |
抽出 composition root 与受管理的后台任务;不能复制一套启动流程来启动每个模块 |
改造前 src/lib.rs |
曾约 4,800 行,混合系统资源、密码、任务模型、调度、恢复、结果入库、DNS 子任务和加密 | 必须先按领域迁移,再形成 crate 边界 |
src/config.rs 的 init_routes |
统一注册 Actix 路由 | 路由逐领域注册,业务调用通过稳定适配器转发;已构建的 Actix 路由不是可动态替换的代码 |
src/runtime_config.rs |
RuntimeConfig<T> 已使用 Arc<RwLock<Versioned<T>>>、revision 和 watch;持久化具有乐观并发控制与历史修订 |
保留现有格式,补齐全域同步、应用状态与兼容性检查 |
src/lib.rs 的 apply_task、恢复函数 |
任务领取使用事务、行锁和 SKIP LOCKED;TASK_LEASE_MS = 90_000 |
延续既有任务租约,API 更新不能重置正在执行的扫描 |
src/lib.rs 的 save_scanner_results |
上传按 execution/upload/batch 身份与 checksum 去重;PG 回执和 ES 写入之间存在租约和重试 | 不引入第二套上传身份;不可把 PG 与 ES 写入误当成一个分布式事务 |
src/elasticsearch.rs |
原始结果与资产投影存储;后台投影工作 | 拆写入、查询、投影三种接口,复用稳定文档身份与 outbox |
src/analytics_snapshot.rs |
PG 分析快照、outbox、抢占与恢复;当前存在全局任务串行化保护 | 可以独立运行,但多开实例不会自动获得线性吞吐提升 |
src/reports/worker.rs |
报表计划、生成、投递混在同一个循环;已有 claim_token、五分钟租约与 SKIP LOCKED |
最先把纯渲染拆出去;投递副作用单独管理 |
src/scan_migration.rs |
普通实例持共享存储运行锁,离线 PG→ES 迁移持排他锁 | 拆分后相关读写进程都必须纳入迁移隔离,不能遗漏 worker |
src/scanner_diagnostics/mod.rs |
诊断目录持有 .storage.lock 独占锁,另有进程内分片 Mutex |
多个完整实例不能共享同一诊断目录启动;应引入独立文件存储所有者 |
src/main.rs 的备份恢复 |
启动时把所有 queued/running 备份标记失败 |
蓝绿启动新实例会干扰旧实例备份,必须先改为有所有者的超时恢复 |
配置同步的实际情况要以代码为准:general 有五秒刷新;monitor/scanner_health 通过 refresh_health 刷新;统计服务有自己的刷新与状态发布;报表邮件在生成时读取数据库。其余配置没有统一的全域刷新机制,不能假定多个 API/worker 实例会同时生效。
1.2 保持不变的外部契约¶
继续支持现有 Web 与扫描器 URL,如 /api/task/create、/api/task/apply、/api/scanner/heartbeat、/api/scanner/result。模块迁移初期不要求前端或扫描器改变调用方式。
保留 scan_plan.version = 1、既有 profile/能力匹配,以及结果协议的兼容处理。新增协议能力采用显式版本和能力协商,不能只根据 worker 自身版本推断扫描器支持程度。
PostgreSQL 继续作为用户、权限、任务、配置、回执、outbox 和派生分析快照的权威来源;Elasticsearch 继续作为原始扫描结果及端点投影的权威存储。正常运行不恢复已废弃的 PG 原始结果双写或回退。
现有 session 与 CSRF 数据在数据库中,保持 cookie、域名、密钥及验证语义兼容即可在业务 worker 更新时保留登录。现有扫描器是请求/心跳/任务上传模型;“保持在线”指避免中断这些通信与租约,不意味着迁移已有 TCP 连接到另一进程。
2. 热更新边界¶
| 更新内容 | 采用方式 | 生效边界 |
|---|---|---|
| 阈值、开关、保留期、DNS 策略、自动目标参数 | 现有运行配置机制,补齐校验与多实例同步 | 新请求或下一次任务领取;请求内固定快照 |
| 邮件模板、报表模板、解析规则等数据 | 版本化资源包,先编译/校验再替换不可变快照 | 新报表或新任务;已归档报告不改写 |
| 报表渲染、诊断解析、独立查询计算等 Rust 业务代码 | 启动新版本进程,预热,切路由/领取权,旧版本排空 | 新请求走新代际,旧请求完成于旧代际 |
| 认证内核、中间件、HTTP/TLS 栈、共享 DTO 的破坏性变化 | 兼容迁移后滚动升级 API 内核 | 负载均衡停止给旧实例新流量,等待请求完成 |
| 数据库破坏性变更、ES 不兼容映射、扫描协议大版本变化 | 独立迁移与协调发布;必要时维护窗口 | 不能承诺任意在线替换或即时回滚 |
Rust crate 拆分只改善代码边界,并不使已链接到主程序的代码可以热替换。不要用 libloading 直接跨 .so 传递 Rust trait object、String、Vec 或 Tokio future:Rust ABI、分配器、依赖版本、静态状态和后台任务的卸载生命周期难以稳定约束。
只有已有成熟 C ABI、明确内存所有权及无残留任务的计算库才考虑本机动态库。WASM 可作为后续纯规则计算扩展点,但数据库访问、SMTP、备份与扫描控制不需要为了热更新全部改写为 WASM。
3. 目标架构与模块职责¶
flowchart TB
UI[Web 控制台] --> API[稳定 API 内核 / nm-api]
SC[扫描器] --> API
API --> ID[身份与授权]
API --> CTRL[扫描控制应用服务]
API --> ING[结果接入应用服务]
API --> QUERY[资产与统计查询适配器]
API --> REG[模块路由快照]
REG --> R1[业务 worker 旧版本 / 排空]
REG --> R2[业务 worker 新版本 / 活跃]
SUP[发布协调器 / supervisor] --> REG
SUP --> R1
SUP --> R2
CTRL --> PG[(PostgreSQL)]
ING --> PG
ING --> ES[(Elasticsearch)]
QUERY --> ES
PG --> JOB[投影 / 分析 / 报表任务队列]
JOB --> BG[后台 worker]
BG --> PG
BG --> ES
API --> OPS[诊断存储 / 备份 agent]
OPS --> FS[本机目录 / pgBackRest]
第一阶段仅拆逻辑模块;第二阶段优先分离业务 worker 与运维 agent。低规模安装默认三个部署角色即可:api、worker、ops。报表或查询成为瓶颈后再把 worker 内某个模块独立部署,不要求一开始就有十几个微服务。
3.1 Cargo workspace¶
network-mapping/
├── Cargo.toml workspace + 统一依赖版本
├── crates/
│ ├── nm-contracts/ 内部 DTO、事件、错误码、协议 schema
│ ├── nm-platform/ 连接池、时钟、日志、通用配置快照
│ ├── nm-identity/ 登录、权限、Session、TOTP、凭据验证
│ ├── nm-scan-control/ 任务生命周期、调度、节点能力、恢复
│ ├── nm-ingest/ 结果校验、幂等回执、ES 原始写入
│ ├── nm-asset-query/ 搜索、资产详情、标签、查询编译
│ ├── nm-analytics/ 投影、分析快照、统计读模型
│ ├── nm-reports/ 报表计划、生成、模板、归档
│ ├── nm-notification/ 邮件投递与发送状态
│ ├── nm-operations/ 维护、诊断存储、备份
│ └── nm-module-runtime/ manifest、IPC、路由与生命周期
├── apps/
│ ├── nm-api/
│ ├── nm-worker/ 可用 --modules 选择运行模块
│ ├── nm-ops-agent/
│ ├── nm-module-supervisor/
│ └── nm-admin/ 迁移与维护 CLI
└── migrations/ 独立、带版本、可重复校验的迁移
依赖方向:apps → 领域 crate → contracts/platform。platform 不依赖任何业务领域;领域之间通过 contracts 定义的命令、只读查询和事件通信。尽量不让 nm-reports 直接依赖 nm-scan-control 的实现,更不能在两个 crate 中复制 save_scanner_results。
nm-contracts 不包含 Actix、数据库连接或 worker 生命周期。领域 crate 将 HTTP handler 与应用服务分开;只有 transport adapter 使用 HttpRequest、web::Data 和响应包装。禁止将现有 lib.rs 原样变成万能 common crate。
3.2 现有文件的迁移映射¶
| 模块 | 来源 | 状态所有权与公开能力 |
|---|---|---|
| identity | login.rs、admin.rs 的身份部分、用户 handler、认证/CSRF 中间件、lib.rs 密码与凭据函数 |
管理 users/session/TOTP;公开认证与授权能力,不导出数据库 Row |
| scan-control | lib.rs 的 CreateTask/STask/apply/recover/aggregate、handlers/task.rs 控制部分、scanner_config.rs、dns.rs、dns_follow_up.rs、target_generated |
管理 scan_task、scan_task_execution、scanner_info 和生成去重状态;唯一拥有任务状态机写入入口 |
| ingest | lib.rs 的 observation/上传模型和结果保存、handlers/task.rs 结果上传部分、ES 写入部分 |
管理 scan_result_batch 和原始 observation;负责上传验证、稳定文档 ID、回执与写入 token |
| asset-query | handlers/search*、handlers/assets.rs、标签 handler、查询与搜索投影读取 |
对扫描事实只读;标签属于独立资产注释,不让标签操作改写扫描事实 |
| analytics | analytics_snapshot.rs、statistics、elasticsearch/projection、ES 投影 worker |
拥有派生分析表与索引、analytics/projection outbox 的消费;读模型可以重建 |
| reports | reports、handlers/reports.rs、report_mail_assets、邮件模板的报表渲染部分 |
管理 report_schedule/run/input/job/mail_asset、归档内容与模板版本 |
| notification | reports/worker.rs 的 send_one、mail_template.rs 发送部分、监控与节点离线通知 |
管理发送任务与投递状态,隔离 SMTP 副作用 |
| operations | backup、scanner_diagnostics、system_maintenance、资源采集 |
备份与诊断文件由指定 agent 管理,API 只查询或提交命令 |
| observability/runtime | logs、audit、request_log、monitor、runtime_config、system_time |
日志、审计、策略快照及任务监督;高频请求日志与可靠业务事件分开 |
初期允许同一个 PostgreSQL 数据库、同一 schema,但建立表写入所有权。跨模块联表读取可作为迁移过渡,不能因此赋予各 worker 任意更新权限。后续独立部署使用模块专属 DB role;核心交易暂不拆数据库,避免过早引入分布式事务。
特殊交易边界: 当前结果最终确认还会更新执行状态、任务聚合、创建 DNS 后续任务及 outbox。这些动作必须保留同一个 PG 事务。第一阶段用一个终结应用服务协调领域函数并共享 transaction;ingest 只允许通过该服务完成任务状态写入。后续进程拆分时,将最终确认转为带唯一键的 completion outbox,由 scan-control 在自己的事务中消费,明确允许短暂的状态延迟;不能简单把事务中每条 SQL 换成 HTTP 请求。
4. 稳定内核与可替换模块的接口¶
4.1 两种适配器共享同一业务端口¶
应用服务不关心部署方式,例如报表计算端口:
// 接口设计片段:DTO 来自 nm-contracts,业务错误在此省略。
use std::{future::Future, pin::Pin};
pub trait ReportRenderer: Send + Sync {
fn render<'a>(
&'a self,
input: RenderReportInput,
) -> Pin<Box<dyn Future<Output = Result<RenderedReport, ModuleError>> + Send + 'a>>;
}
LocalReportRenderer 在进程内调用;RemoteReportRenderer 在 IPC 中调用独立 worker。切部署方式时 handler 仍调用相同的应用端口。这类 Rust trait 只用于同一程序内部,不会通过动态库或跨进程传递。
每个领域提供 configure_routes(cfg),让 config::init_routes 只组合领域路由。已有路径绑定到稳定 handler,handler 通过模块路由快照调用远端能力。更新 worker 可以替换能力实现;新增 HTTP 入口、鉴权模型和中间件仍需升级内核。
应用组装时按领域注入 IdentityServices、ScanServices、QueryServices 等小型状态对象,不再让每个 handler 都取得整套数据库、全局配置和密码参数。Bootstrap 负责连接与角色选择,ModuleRegistry 保存能力绑定与路由快照,Supervisor 管理运行单元,ConfigReconciler 负责配置同步;这些对象在应用工厂外创建、在所有 Actix worker 间共享,避免在 HttpServer::new 的闭包中重复启动后台服务。
模块生命周期统一为 prepare → ready → active → draining → stopped,另有 failed 状态。依赖先准备后启动,停机按依赖的反向顺序执行。关闭报表生成时先停止新领取,保留已归档报表的查询;关闭扫描调度时停止分配新任务,但必须继续接收已执行任务的心跳和上传。核心接入模块不能作为普通可选功能直接卸载。新增独立 worker 能力通过注册表绑定;旧能力失效时其 handler 返回明确的不可用状态,前端只显示业务功能是否可用,不暴露内部进程或发布协议。
4.2 IPC 与版本协商¶
同机首选父子进程管道或 Unix domain socket;独立机器使用 HTTP/gRPC + 服务身份验证。初版使用现有 serde/JSON 和 reqwest,避免为了模块化同时更换协议栈。JSON 外加长度前缀,限制帧大小;socket 模式复用连接与限制并发,stdout 管道模式必须把日志写入 stderr。
生产 RPC 请求建议为:
{
"rpcVersion": 1,
"requestId": "019c0000-0000-7000-8000-000000000001",
"module": "report-render",
"releaseId": "report-render-1.2.0-sha256prefix",
"generation": 42,
"method": "render",
"deadlineAtMs": 1791849605000,
"context": {"actorId": 12, "scope": "report:read", "configRevision": 18},
"payload": {"reportId": "019c0000-0000-7000-8000-000000000002", "inputChecksum": "sha256"}
}
context 由已认证的内核生成,不能直接相信浏览器提交的 actor/scope。响应返回相同 requestId、releaseId、generation,包含业务数据或稳定错误码。大报表、图片和结果批次通过受控流或对象引用传递,避免把大字节数组重复编码进 JSON。
建议错误码包括 MODULE_NOT_READY、MODULE_BUSY、DEADLINE_EXCEEDED、SCHEMA_INCOMPATIBLE、REVISION_CONFLICT、CLAIM_LOST。只读请求按剩余 deadline 有界重试;写入只有存在端到端幂等键才允许重试。控制面失效时继续使用最后已确认的兼容版本,不能在无可用模块时伪造成功响应。
4.3 发布 manifest¶
module_id = "report-render"
version = "1.2.0"
release_id = "report-render-1.2.0-sha256prefix"
runtime = "process"
artifact = "bin/nm-report-render"
artifact_sha256 = "完整 SHA-256"
target = "x86_64-unknown-linux-gnu"
rpc_versions = [1]
capabilities = ["report.render.v1"]
max_inflight = 8
request_timeout_ms = 5000
drain_timeout_ms = 30000
[compatibility]
host_api_min = 1
host_api_max = 1
database_schema_min = 27
database_schema_max = 28
input_schema = "report-input.v1"
output_schema = "rendered-report.v1"
[configuration]
namespaces = ["report_mail", "general"]
[requirements]
analytics_read_api = "v1"
manifest 不开放任意 URL、shell 命令或任意环境变量注入。发布协调器仅从可信制品仓库获取制品,校验签名、摘要、平台、协议及依赖;SHA-256 校验完整性,签名或可信仓库身份负责真实性。制品目录不可变,同版本不同摘要禁止覆盖。依赖清单按能力契约校验并检查循环;相关模块需要成组发布时使用同一个路由 bundle revision。
5. 热更新生命周期与切换算法¶
5.1 同步请求的切换¶
sequenceDiagram
participant C as 发布协调器
participant H as API 内核
participant O as 旧 worker
participant N as 新 worker
C->>N: 启动新制品 / generation 42
N-->>C: 版本握手 + 依赖就绪
C->>N: 无副作用自检 / 样本请求
H->>O: 请求 A 固定旧 generation
C->>H: CAS 发布路由 revision 42
H->>N: 请求 B 固定新 generation
N-->>H: B 完成
O-->>H: A 完成
C->>O: 确认 in-flight = 0 后关闭
- prepare:加载并校验制品,确认新旧协议、配置与 schema 都兼容;不修改活跃路由。
- start/warm:启动新进程;建立连接、准备规则和模板;探针检查真实依赖。新进程此时禁止抢任务、发邮件或修改生产状态。
- verify/canary:无副作用样本比对;只读能力可按稳定 request/report ID 进行灰度。写入 worker 的灰度要靠明确任务归属,不能把同一个任务同时投给新旧版本。
- activate:基于 expectedRevision CAS 更新持久化路由和发布事件;内核用一次短锁替换整个不可变路由快照。
- drain:旧版本不接受新请求、不领取新任务;已持有其句柄的请求继续完成。多个内核实例汇报各自已应用 revision。
- retire:确认所有入口切换完成且旧请求、旧任务、后台资源归零,再关闭旧进程;保留制品和发布历史供回滚。
一次请求开始时获取 Arc<RouteSnapshot> 并保存至整个请求链;不能在处理步骤之间重复取“当前版本”,否则会把旧输入送给新实现。RwLock/Mutex 只覆盖句柄读取或替换,网络调用、预热、渲染和数据库操作都在锁外。
Arc 只能证明本机同步请求的句柄生命周期,不能证明远端任务或 detached tokio::spawn 已完成。长任务另外使用持久化任务租约和 in-flight 统计。模块内部所有任务必须被 JoinSet/监督器追踪,禁止藏在构造函数里自行永久 spawn。
Rust 示例原型使用短 Mutex + Arc<Worker>;生产吞吐确认有需要后可采用 ArcSwap<RouteSnapshot>,但原子句柄交换不会替代任务租约、集群配置同步或完整回滚流程。
5.2 后台任务的新旧版本交接¶
后台 worker 不经过 API 请求路由,因此只切 API 的路由不够。领取事务必须检查模块活跃 generation,旧代际不再具有领取权。
为现有任务增加 assigned_generation、claim_owner_instance、必要的 payload_schema_version。已有 claim_token 与 lease 的队列继续复用;分析快照当前用 locked_at 等状态,可逐步补为显式 token。领取示意:
-- 拟实现:部署行的共享锁与激活事务的更新锁互斥,定义领取切换边界。
BEGIN;
SELECT generation, release_id
FROM module_route
WHERE module_id = 'reports'
FOR SHARE;
-- 应用校验:generation = 本实例代际;不是则回滚且不领取。
WITH picked AS (
SELECT report_id FROM report_job
WHERE status IN ('pending', 'retry_wait') AND available_at <= NOW()
ORDER BY available_at, report_id
FOR UPDATE SKIP LOCKED LIMIT 1
)
UPDATE report_job AS job
SET status = 'running', claim_token = $1,
lease_until = NOW() + INTERVAL '5 minutes',
assigned_generation = $2, claim_owner_instance = $3,
attempts = attempts + 1
FROM picked WHERE job.report_id = picked.report_id
RETURNING job.*;
COMMIT;
执行时保留 token 与 generation;需要较长处理时每 30 秒续租一次,续租必须带 token 且原租约尚未过期。完成事务同样以 token、任务状态和有效租约条件更新;受影响行数为零则拒绝陈旧提交。
旧 generation 的已领取任务仍可完成,不能仅因模块切换而禁止其提交。 活跃 generation 限制“领取”,claim token 限制“完成”。租约失效并被新 worker 接管后,旧 token 才失去提交权。重试前校验新版本能读旧任务输入,不能用新代码解释未知版本的 payload。
租约使用 PostgreSQL 时间,避免不同机器时钟决定所有权。失联 worker 在租约过期后由其他实例恢复;drain_timeout 不是租约过期时间。同步 RPC 可取消,长任务优先完成或可恢复检查点,不应在 30 秒超时后直接杀死五分钟的报表或长时间备份。
5.3 多实例发布与回滚¶
module_route 是持久化的期望路由,各内核通过通知加轮询加载快照,并保存已应用 revision。发布 API 返回 202 + deploymentId;状态分别展示 prepared、activating、converged、draining、completed。只有所有纳入发布的健康入口已确认新 revision,才能宣称集群切换完成。
通知不是全局原子广播,传播期间会有新旧版本并存,因此发布必须保证兼容窗口。如果业务要求一个更新瞬间全局生效,采用集中网关或临时停止该模块的新请求,等所有入口就绪再放行。
准备失败保持旧路由;激活后失败可以把旧制品发布为新的 generation/revision,不能把 revision 倒退。回滚只改变后续执行代码,不撤销已经成功写入的数据、已创建任务或已发送邮件。已有副作用按业务补偿或人工恢复处理。
6. 发布控制面与配置系统¶
6.1 新增控制表¶
下面为最小表结构建议;独立 migration 应补充模块注册、外键策略、状态检查、审计与索引,不直接加入当前初始化 SQL 后让每个 worker 启动执行。
CREATE TABLE module_release (
release_id TEXT PRIMARY KEY,
module_id TEXT NOT NULL,
version TEXT NOT NULL,
artifact_sha256 TEXT NOT NULL,
manifest JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (module_id, version)
);
CREATE TABLE module_route (
module_id TEXT PRIMARY KEY,
release_id TEXT NOT NULL REFERENCES module_release(release_id),
generation BIGINT NOT NULL CHECK (generation > 0),
revision BIGINT NOT NULL CHECK (revision > 0),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE module_instance (
instance_id UUID PRIMARY KEY,
module_id TEXT NOT NULL,
release_id TEXT NOT NULL REFERENCES module_release(release_id),
generation BIGINT NOT NULL,
state TEXT NOT NULL,
applied_route_revision BIGINT,
last_heartbeat_at TIMESTAMPTZ NOT NULL,
inflight_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT
);
CREATE TABLE module_deployment (
id UUID PRIMARY KEY,
module_id TEXT NOT NULL,
target_release_id TEXT NOT NULL REFERENCES module_release(release_id),
expected_revision BIGINT NOT NULL,
state TEXT NOT NULL,
requested_by INTEGER,
change_reason TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
generation 标识一次运行代际,releaseId 标识不可变制品,revision 标识发布次序;多模块 bundle 用独立 bundle revision 形成整体快照。一个 release 可被回滚后再次激活,但必须分配新 generation。
激活事务同时完成路由 CAS、部署状态、可靠发布 outbox 和操作审计;提交后发送唤醒通知。协调器重启时根据部署表与实例实际状态恢复,不依赖内存中的“已切换”标记。
拟新增管理接口:
| 接口 | 行为 |
|---|---|
GET /api/modules |
模块能力、制品版本、活跃代际、各实例状态 |
POST /api/modules/{id}/releases |
导入已签名制品并校验 manifest;不执行任意上传的程序 |
POST /api/modules/{id}/deployments |
expectedRevision + targetReleaseId + changeReason,创建预热与发布任务 |
GET /api/module-deployments/{deploymentId} |
查看激活传播、排空和错误 |
POST /api/modules/{id}/rollback |
将已知兼容旧制品按新的 revision 发布 |
GET /api/modules/{id}/configuration-status |
查看各实例已应用的 namespace revisions |
沿用管理员认证、CSRF、请求 ID 和审计。管理接口负责提出期望状态,独立 supervisor 负责执行;公共 API 进程无需具有写可执行文件目录或启动任意二进制的权限。
6.2 扩展现有运行配置¶
保留现有 /api/config/runtime/{namespace}、camelCase 发布请求、revision 历史和 RuntimeConfig::replace 的单调更新语义。
补齐流程:
- 校验 namespace 的完整 document、schemaVersion 以及当前所有活跃/排空版本的配置兼容范围。
- 在原配置事务中递增 revision、写历史、写可靠变更事件/审计;事务提交后 PostgreSQL
NOTIFY唤醒订阅者。 - 每个角色用专用连接执行
LISTEN并持续驱动连接 future;断线重连后先全量刷新。不能借用连接池连接暂时 LISTEN 后归还。 - 每五秒兜底轮询该角色订阅的所有 namespace revisions。通知只用于降低延迟,数据库文档是权威来源。
- 解析、校验、预编译成功后替换快照,再上报 appliedRevision;失败保留最后有效快照并报告落后,不覆盖为半成品。
- 超过设定同步容忍期的关键授权策略实例停止领取新任务,避免长期使用旧授权;非关键展示配置可继续使用最后有效值。
回滚配置应读取历史 document 并发布为新 revision。现有历史表可复用,但当前尚无运行配置历史/回滚 HTTP API,应明确另行实现。
跨 namespace 有耦合时定义配置 bundle 与单个 bundle revision;先构造完整有效快照再替换。watch 只通知同进程消费者,不负责多进程或跨机器同步。
DNS 后续任务和扫描授权采用双重规则:创建时保存策略版本与计划;执行/生成后续目标时仍检查当前授权限制。新策略收紧时不得仅因为旧任务保存了宽松范围就继续扩展扫描。模式、模板等可重现计算输入固定版本,授权撤销则即时限制后续副作用。
7. 数据一致性、副作用与异常恢复¶
7.1 扫描上传与 ES 投影¶
继续以 (execution_id, upload_id, batch_index)、payload checksum 和稳定 ES 文档身份为重试依据。重试必须保持原 observation 的 observed_at 与批次内容,不在更新后重新生成身份。
PG 提交、ES 写入和回执确认之间允许进程崩溃。已有 batch 写入 token 与租约机制需要保留并测试这些窗口;新旧写入版本必须生成兼容的文档键。只有 ES 所需写入已成功、回执状态和 final 连续性符合现有协议时才返回成功,不能为了异步拆分提前确认尚未持久化的结果。
投影和分析使用已有 scan_projection_outbox、analytics_outbox 等持久化队列;事件消费按 execution/事件唯一键幂等。可靠业务事件不使用 request_log 的可丢弃内存 channel。首期继续 PostgreSQL outbox,不引入 Kafka/RabbitMQ 作为拆分的前提。
7.2 邮件投递¶
报表生成与 SMTP 发送分离。渲染输出、模板版本、收件人快照和 content checksum 先持久化,再进入独立投递队列。
当前 report_delivery 已有 token 与五分钟租约,但邮件服务器接受邮件后、PG 标记 sent 前崩溃仍可能造成重发。普通 SMTP 无法仅靠数据库 token 提供严格 exactly-once。沿用稳定 Message-ID 便于追踪;若必须避免重复,需要支持幂等键的投递服务或把不确定状态标为 unknown 并复核,不能把未知结果当成必然未发送。
热更新期间停止旧发送器领取新任务,允许已进行的 SMTP 操作完成,避免直接杀进程制造不确定发送窗口。监控通知与节点离线通知也应逐步进入同一可靠投递端口。
7.3 备份与诊断¶
备份已有数据库全局“单个活跃任务”的部分唯一索引;BackupManager 另有本机 Semaphore。保留数据库约束并增加 ownerInstance、claimToken、leaseUntil、外部进程状态。新实例只恢复租约已过期且确认执行所有者失效的任务,移除“启动就失败所有未完成备份”的全局更新。
租约不能停止已运行的 pgBackRest 进程。agent 重启后要结合本机进程监督或 pgBackRest 状态判断,确认旧执行未运行后才能接管。无需为了模块更新迁移正在执行的外部进程;备份 agent 可延后升级。
诊断目录继续由单个 ops agent 持有独占锁,API 通过该 agent 上传、组装和下载。同机双 agent 更新时先完成文件作业并转移目录所有权;跨机器部署再评估对象存储、数据库文件元数据和分布式作业租约。不能直接删除 .storage.lock 来支持多实例。
7.4 故障决策¶
| 故障 | 系统行为 |
|---|---|
| 新制品启动/握手/自检失败 | 标记本次 deployment 失败,旧路由和旧领取权保持 |
| CAS revision 冲突 | 返回 409/冲突状态,读取当前发布状态后决定下一次操作 |
| 模块崩溃 | 同步请求返回可识别错误;监督器按退避重启或回滚;有 token 的长任务按租约恢复 |
| 控制面数据库暂时不可用 | 已加载的只读计算可继续;需要 PG 提交/领取的操作失败或等待,不伪造持久化成功 |
| 通知丢失 | 轮询和重连全量刷新补偿;展示 desired/applied 的差异 |
| 排空超过时限 | 标记 degraded 并明确阻塞任务;有可恢复检查点才强制结束 |
| 新版本写入不兼容数据 | 停止后续派发;按预定数据修复/迁移流程处理,不能声称二进制回滚已修复数据 |
8. 生命周期、资源与部署细节¶
8.1 后台监督器¶
把 main 中散布的永久 spawn 包装为受监督服务。每个角色显式注册服务名、启动依赖、重启退避、停止信号和 JoinHandle。可使用 tokio::sync::watch 发送停止意图、JoinSet 管理任务;现有 Tokio 足够实现第一版。
停止顺序:撤销 readiness → 停止领取/接收新任务 → 等待在途业务 → 刷新可保存缓冲与检查点 → 停止资源采集/通知 → 关闭连接与退出。请求日志仍可保留当前有界、允许丢弃的策略,但业务事件和审计不能套用这种语义。
MonitorHandle::new 当前内部启动通知与慢 SQL 任务,迁移时改为返回可启动/关闭的运行单元。worker 启动不能隐式执行创建管理员、全局备份恢复、全库 DDL 等副作用。
readiness 检查契约、配置、依赖和必要索引;liveness 只检查进程是否能响应。候选版本健康检查不能自动发邮件或真实扫描目标。API 内核避免把全部可选模块异常都升级为整个 API 不可用;按能力返回故障或降级读路径。
8.2 数据库与连接预算¶
迁移由独立 nm-admin migrate 一次执行,持迁移排他锁,记录 schema version 与 checksum。各应用启动仅检查 schema 兼容范围。普通运行角色纳入存储共享运行锁;锁连接失效必须使相关 worker 失去 readiness/领取能力,而不是仅记录警告继续运行。
兼容迁移采用 expand → 新代码 → 数据回填 → 停旧版本 → contract。删除列、改变字段语义或 ES 字段类型不能在旧版本还运行时执行。索引大规模变化采用新版本索引、回填、验证、再切 alias,并保留回滚读路径。
拆分后每个进程都有连接池。示例预算:2 个 API × 10 + 2 个普通 worker × 4 + 1 个 ops × 2 = 30;报表蓝绿窗口额外 2 个连接,再预留 LISTEN、运行锁、迁移与运维连接。根据实际 PostgreSQL 上限校验总量,不能每个模块默认再开十个连接。
现有 PG 客户端使用 NoTls,database.tls 不是可用加密实现;跨主机 worker 需要先补齐实际 PG TLS 或受保护链路。同机分进程不因此引入新的跨机器数据库链路。
8.3 制品和运行角色¶
/opt/network-mapping/releases/
api/<version>/...
report-render/<release-id>/manifest.toml + bin/...
/etc/network-mapping/ 配置与证书
/var/lib/network-mapping/ 发布状态缓存、诊断数据
/run/network-mapping/ 同机 IPC socket
服务用户只读制品,supervisor 验证并启动固定入口,ops agent 独占必要文件目录。密钥不打包到模块制品,也不把全部服务端配置复制给每个模块。报表渲染器不需要 scanner pepper、登录加密密钥或 SMTP 凭据。
拟实现角色命令:
nm-admin migrate --config /etc/network-mapping/config.toml
nm-api --config /etc/network-mapping/api.toml
nm-worker --modules projection,analytics,reports --config /etc/network-mapping/worker.toml
nm-ops-agent --config /etc/network-mapping/ops.toml
nm-module-supervisor --config /etc/network-mapping/modules.toml
单机使用 systemd 托管角色;跨机器或 Kubernetes 使用同样的角色与制品契约。API 滚动发布需要独立 readiness、连接排空、至少一个健康副本及负载均衡;无需把 Kubernetes 作为实现热更新的先决条件。
8.4 可观测性与初始阈值¶
日志贯穿 requestId、executionId/reportId、moduleId、releaseId、generation、routeRevision、configRevision、instanceId;不输出 token、密钥、原始凭据。指标记录请求数、失败率、p95/p99、队列积压、租约恢复、在途数、配置落后和排空耗时。
建议初始参数:预热 30 秒;普通计算 RPC 5 秒;同步请求排空 30 秒;配置兜底同步 5 秒;实例心跳 5 秒。后台任务租约按最坏执行时间制定,报表保留现有五分钟起点并补续租。以上是配置初值,不是已验证容量或停机承诺。
只读模块可先 5% 稳定分流,再 25%、100%;写入模块按任务类型或队列 cohort 分配。持续错误率超过基线容忍阈值或队列延迟超过 SLO 时停止扩量。若要保证 API 不间断,应验收业务 worker 更新期间登录、心跳、上传和查询成功率,而不是只观察进程仍存活。
9. 首个完整试点:报表渲染热更新¶
选择报表的纯渲染能力作为首个试点:输入可以固定,输出可校验,不需要改变扫描器协议,并且模板/图表计算更新较频繁。
从 reports/worker.rs 拆出三个阶段:
- 领取 report_job,获取 report_run 的任务快照、周期、统计 payload、模板源码/版本、report_mail/general 修订;固定 inputChecksum。
- 在数据库事务外调用
ReportRenderer。输入不含 DB client 或密钥;输出 HTML、纯文本、图表字节/受控引用、输入校验和与渲染器版本。API/应用服务校验输出大小、资源格式和模板约束。 - 开短事务重新验证 claimToken、有效租约和未完成状态,存档 payload、rendered_mail、contentChecksum、rendererReleaseId、rendererGeneration;同事务更新任务状态并创建投递记录。
当前生成流程在 PG 事务中做多项计算,不能仅把中间调用改成远端请求后继续长时间持有行锁。先捕获稳定输入,锁外计算,再使用 token 和状态进行提交保护。需严格一致的统计输入应在捕获阶段确定 cutoff/快照,避免渲染前后任意重查而混用数据。
渲染器 v2 预热后,新 report_job 使用新 generation;旧 report_job 在 v1 完成。更新期间已归档邮件和图片保持内容可复现,重新生成走现有“新 revision/supersedes”语义,不覆盖已发送报告。
首期保持计划调度、结果存档、邮件发送在宿主应用,只把 ReportRenderer 变为可替换进程。完成验证后再独立化整个报表 worker,最后迁移投递端口;这个顺序可避免第一次试点同时引入数据库所有权和 SMTP 去重变更。
10. 实施顺序与验收¶
| 阶段 | 实施内容 | 进入下一阶段的验收条件 |
|---|---|---|
| A:领域整理 | lib.rs 按模块移动;handler/应用服务/存储分层;各领域注册路由;兼容 re-export 逐步淘汰 | API 路径、响应、权限和扫描任务行为保持;现有测试通过 |
| B:workspace 与生命周期 | 建立 contracts/platform;受监督后台任务;统一角色配置;独立迁移 CLI | worker 无隐式启动副作用,启动/停止可观测,可完成排空 |
| C:多实例前置 | 全域配置同步;备份恢复改 owner/lease;诊断独立 agent;完整迁移隔离 | 两个 API 实例共存,配置最终一致,新实例不干扰旧任务或目录 |
| D:渲染热更新试点 | IPC、manifest、路由快照、发布状态和代际;抽取 ReportRenderer | 新旧渲染并存;更新不影响登录/心跳/上传;失败制品不激活;可回滚 |
| E:任务 worker 拆分 | 投影、分析、报表按角色部署;领取代际与 token fencing;可靠 outbox | 崩溃恢复、超时接管、陈旧提交拒绝;任务不丢失,幂等约束成立 |
| F:按负载服务化 | 搜索/统计/通知独立扩缩,跨机器 IPC 与权限 | 有可量化瓶颈和容量收益;不因拆分突破 PG/ES 资源预算 |
实施工作量需要结合团队、现有测试与部署环境评估;不能仅从文件数量给出可靠工期。优先交付 A–D,足以让一个高频变化的业务能力独立热更新,再按收益推进 E–F。
生产验证应覆盖:
- 配置双发布冲突、NOTIFY 丢失、监听重连、无效文档保留旧快照、关键授权策略收紧。
- 旧请求在切换前取得句柄,跨切换完成仍使用旧实现;新请求使用新实现;失效候选不影响活跃路由。
- 任意候选启动、激活持久化、路由应用、旧任务排空窗口的协调器崩溃与恢复。
- 长任务过租约后旧 token 无法提交;旧 generation 已领取任务在租约有效时仍可完成。
- ES 成功/PG 未确认、PG 回执已存在/客户端未收到响应、final 批次重复等现有上传幂等窗口。
- 新实例启动不误标旧备份;诊断文件目录只由授权 agent 持有;迁移锁不会漏掉独立 worker。
- SMTP 接受后状态未落库的未知结果处理,以及报表归档的版本与内容可复现。
- 满负载蓝绿更新的成功率、队列延迟、内存峰值、连接峰值、停止与回滚耗时。
11. 本次交付与验证边界¶
docs/examples/modular-hot-update 提供只依赖 Rust 标准库的原型,编译出业务行为不同的 v1/v2 二进制,使用父子进程管道完成握手、健康探针、自检、CAS 激活、请求版本固定、旧进程排空和回滚。
该独立原型验证进程替换的核心生命周期,不接入现有 Actix 服务、PG、ES、扫描器、生产鉴权或部署控制表;也不实现集群共识、完整 manifest 签名、跨模块依赖和长任务租约。运行时和渲染实现现已位于独立 workspace 包,并已接入生产报表工作流,具体能力和边界见实现说明。生产改造按上述阶段和实施步骤进行。