第 28 章 / 共 51 章
卷四 · 热重载深潜

状态交接:让数据活过一次重载

静态变量按代隔离(第 5 章)意味着换代即清零——除非你显式交接。ServiceSwap / xsSwapTake 就是那座桥,本章把第 3 章埋的伏笔全部收回。

本章导读

站位:热重载工程价值的分水岭——没有交接,重载只适合无状态服务;有了交接,计数器、缓存、会话都能无缝续跑。读完的标准:能对任意静态状态做出"交接 / 重置"的正确决策。

28.1 引入

问题从第 5 章的坑里长出来:总请求数、在线连接数、热点缓存——这些"跨代有价值"的状态在换代时何去何从?答案是一对契约函数:旧代在退役前经 ServiceSwap 导出一份快照,新代在 ServiceInit 里经 xsSwapTake 取回。载体是 xvalue 树——这与 Custom 旋钮(ch09)、JSON(ch18)同构,所有权规则完全复用。

28.2 无破坏快照原则

reload 控制线程 │ 全量候选编译成功(无副作用预检通过) ▼ ServiceSwap(旧代,reload 控制线程) ── 导出 xvalue* 快照 │ /* 无破坏:导出后旧代仍可继续服务! */ ▼ 越过不可逆提交点(原子发布新代) ▼ ServiceInit(新代) ── xsSwapTake 取走快照 ── 续跑 ▼ 旧代引用归零 → GC(快照所有权已在 Take 时转移给新代)

"无破坏"三个字是重点:Swap 的调用不意味着旧代死亡。极端情况下(提交被取消、终态回调竞态),旧代在 Swap 之后还要继续服务——所以 Swap 里绝不能关闭资源、改变行为。它是"拍照",不是"搬家"。

28.3 什么该交接

状态类型决策理由与做法
统计计数(请求数/命中数)交接纯值,进快照即零成本续跑
内容缓存(热点数据)可交接可重建小缓存交接省预热;大缓存重建更干净(避免快照膨胀)
会话/连接注册表不交接连接对象归旧代的 lease 保护——换代本身意味着旧连接由旧代码服务到结束;新连接进新代表
文件句柄 / db 句柄不交接指针指针跨代是悬空之源。db 的两种正确姿势:新代重开(demo 做法,文件层天然共享数据),或把句柄包成全局资源显式管理(ch32 详述)
配置派生物(路由表等)重置Init 重建即可——它们本就来自配置,重建保证与新代码一致
判据一句话:值可交接,资源不可交接;资源背后的持久层(文件/数据库)天然跨代。会话类状态交接要小心"新代码读不懂旧结构"——给快照加版本字段。

28.4 完整实现:计数器与缓存

Swap/Take 完整闭环(契约原文模式)
static uint64 G_iTotal;        /* 总请求数:交接 */
static uint64 G_iHits;          /* 缓存命中:交接 */
/* G_Cache 大缓存:换代重建(决策见 28.3)*/

static bool ServiceSwap(XS_HostInfo* pHost, xvalue** ppShared)
{
    xvalue* pState = xrtValueObject();
    xrtValueObjectSetNew(pState, XRT_STR_LITERAL("ver"),
        xrtValueInt64(1));                       /* 快照版本 */
    xrtValueObjectSetNew(pState, XRT_STR_LITERAL("total"),
        xrtValueInt64((int64)G_iTotal));
    xrtValueObjectSetNew(pState, XRT_STR_LITERAL("hits"),
        xrtValueInt64((int64)G_iHits));
    *ppShared = pState;
    return true;      /* 快照有效;不关任何资源 */
}

static void ServiceInit(XS_HostInfo* pHost)
{
    xvalue* pState = xsSwapTake(pHost);          /* 仅此处有效 */
    if ( pState != NULL ) {
        if ( xrtValueInt64(xrtValueObjectGet(pState, "ver")) == 1 ) {
            G_iTotal = (uint64)xrtValueInt64(xrtValueObjectGet(pState, "total"));
            G_iHits  = (uint64)xrtValueInt64(xrtValueObjectGet(pState, "hits"));
        }
        xrtValueRelease(pState);                 /* 取走后归应用 */
    }
    Cache_Rebuild();                             /* 缓存换代重建 */
}

刚才发生了什么:注意三处细节——快照带 ver 字段(新代读不懂的版本直接丢弃,宁缺勿错);Take 的窗口只在 ServiceInit 内(错过就没有);Release 在读取之后(所有权已在 Take 时转移)。

28.5 契约

  • Swap 线:无破坏快照——不关资源、不改行为;返回 false 表示本轮不交接。
  • Take 线:仅新代 ServiceInit 内有效;取走即获得所有权。
  • 内容线:只交接值;句柄与指针不入快照;带版本字段。

常见错误

Swap 里关了数据库
症状
Swap 后旧代还在服务的请求突然全部报错(极端场景:提交取消后服务直接残废)。
原因
违反无破坏原则——Swap 是拍照。关闭类动作属于 ServiceUnit(真正的退役析构)。
把 void* 塞进快照
症状
新代取回的指针指向已被旧代 GC 回收的内存。
原因
xvalue 存不了指针语义——旧代析构时那块内存就没了。把资源放进全局单例(自管生命周期)或干脆换代重建。

编程练习

基础计数器续跑

实现总请求数交接:压测产生计数 → 重载 → 验证不归零并继续增长。

验收:重载前后 /stats 输出连续;重载期间压测零错误。
进阶带版本演进

给快照加 ver 字段;故意写一个 ver=2 但字段名不同的新代,观察旧快照被安全丢弃。

验收:不匹配版本时计数重置为 0 且无崩溃;日志可见丢弃。
挑战db 句柄决策实验

两个版本各实现一次:A=每代重开 db;B=全局单例句柄 + 引用计数跨代。压测下对比重载期间的请求错误率与延迟。

验收:能说出两种方案各自的适用条件(重开成本 vs 单例复杂度)。

小结

要点结论
机制ServiceSwap 导出无破坏快照 → 新代 Init 内 xsSwapTake 取回
判据值可交接;资源不可(重开或全局单例);配置派生物重置
版本快照带 ver 字段,读不懂就丢弃
窗口Take 仅在新代 ServiceInit;所有权转移给应用
xrt 延伸阅读
  • xrt 参考 · value — 快照载体的构造与访问规则(ch18 所有权口诀在此全部适用)