第 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;所有权转移给应用 |
本章涉及的 API 参考
xrt 延伸阅读
- xrt 参考 · value — 快照载体的构造与访问规则(ch18 所有权口诀在此全部适用)