读者:准备在生产环境启用 wangshu 分层执行(
wangshu_p3wasm 层或wangshu_p4原生 JIT 层)的嵌入方。只用默认 build(P1 解释器)的用户不需要读本文——P1 没有额外的部署要求。设计文档侧的对应材料:design/architecture.md(分层结构)、design/p4-method-jit/(P4 详细设计)。本文只讲嵌入方需要知道的部分。
| 档位 | build tag | 执行方式 | 部署要求 |
|---|---|---|---|
| P1 crescent | (默认) | 纯 Go 解释器 | 无 |
| P3 gibbous-wasm | wangshu_p3 wangshu_profile |
wazero 解释 wasm | 无(纯 Go) |
| P4 gibbous-jit | wangshu_p4 wangshu_profile |
mmap 可执行页 + 原生机器码 | 见 §2 |
wangshu_profile是升层前置:不带它热度采样整段编译期消去,P3/P4 tag 形同虚设。wangshu_p3与wangshu_p4互斥。- P4 只支持 linux/darwin 的 amd64 + arm64;其他平台请用 P3 或 P1。
- 无论哪档,解释器永不退役:升不了层的形状(vararg、协程、不支持的 opcode)自动留在 P1 执行,输出与解释器逐字节一致(这是每 4 小时一轮的差分 fuzz 持续验证的口径)。
P4 在运行期通过 mmap(PROT_READ|PROT_WRITE) 分配代码页、写入机器码后 mprotect 翻到 PROT_READ|PROT_EXEC(W^X 纪律,写与执行权限不同时持有)。这在多数环境开箱即用,但以下环境需要确认:
- seccomp 严格模式 / 自定义 profile:需要放行
mmap+mprotect(含PROT_EXEC参数)。Docker 默认 profile 放行;seccomp=strict或自定义收紧的 profile 可能拦截。 - SELinux:
deny_execmem布尔值开启时,匿名可执行映射被拒。需要allow_execmem或对进程域放行execmem。 - PaX / grsecurity MPROTECT:阻止「先可写后可执行」的翻转,P4 无法工作。
- iOS / 部分沙盒平台:无 JIT entitlement 时不可用(本项目不以这些平台为目标)。
被拦截时的失败形式是 mmap/mprotect 报错 → 该 Proto 编译失败 → 落到 TierStats.StuckCompileFailed(见 §4),脚本仍由解释器正确执行——不会崩溃,但也没有 JIT 收益。部署到收紧环境前,建议用 §4 的观测接口在预发验证 Promoted > 0。
内存开销的量级:每个升层 Proto 一段代码页(典型几 KiB)+ 一段 64 KiB 自管 spill 栈。数百个热函数的场景总开销在 MB 量级。
SetTierEnabled 是生产 admin API(与 testing-only 的 SetForceAllPromote 不同),用于灰度期间不重启进程就把某个 State 降回纯解释执行:
st := wangshu.NewState(wangshu.Options{})
// 线上怀疑升层路径有问题:立刻关掉
st.SetTierEnabled(false)
// 之后的执行全部走 P1 解释器——包括已经升层的函数;
// 正在原生段内执行的那一次调用会正常跑完,下一次分派起生效。
// 排查完毕:恢复分层执行(编译产物还在缓存里,不需要重新编译)
st.SetTierEnabled(true)语义要点:
- 关闭后新升层停止(热度采样直接短路,也不再累积热度);
- 关闭后已升层的函数也回解释器——这是与「只挡新升层」的关键差别,保证降级是完整的;
- 开关是 State 级的。池化场景想全量降级,对池里每个 State 调一次(State 不跨 goroutine 共享,所以没有全局开关);
- 默认 build 下没有层可关,开关是 no-op,
TierEnabled()恒返true。
TierStatsSnapshot() 返回 State 级分层执行分布,用于回答「升层是否符合预期」「有没有值得排查的失败」:
stats := st.TierStatsSnapshot()
// stats.Promoted 已升层 proto 数
// stats.StuckNotCompilable 形状被排除(vararg/协程/不支持 opcode)——预期内
// stats.StuckDeclined 后端判不划算放弃——预期内
// stats.StuckCompileFailed 真编译失败(报错/panic)——非零值得排查
// stats.Profiled 有 profile 数据的 proto 数
// stats.TierEnabled 运行期开关状态生产接入建议:
- 把快照接进指标系统(周期性采集即可,开销是几次计数读取;不要逐帧轮询);
- 告警条件:
StuckCompileFailed > 0——两类 Stuck(NotCompilable / Declined)是设计内行为,编译失败不是; - 预发验证:预期的热函数跑热之后
Promoted应该 > 0,恒 0 说明升层没发生(build tag 缺失、环境拦截 exec-mmap、或负载形状全被排除),此时 P4 build 的数字就是解释器数字; - 配合 §3 的开关做对照实验:怀疑某个性能异常来自升层路径时,
SetTierEnabled(false)之后对比前后指标。
SetStepBudget 的语义在所有档位一致:超出预算的脚本以 instruction budget exceeded 错误终止。它是工作量预算,不是纯指令计数——每个 preempt 点(循环回跳、调用/帧入口、TFORLOOP)计一个单位,此外 bulk 字符串操作按搬运的字节数额外计费:CONCAT 与三个批量构造函数 string.rep / string.format / table.concat 都按结果字节数计 len>>6(1 单位 / 64 字节),使 byte-heavy 的字符串构造无法在小预算内无界运行(见 internal/crescent 的 chargeBulkWork / ChargeBulkWork,issue #166/#167 与 #222)。单次约 1 MiB 的构造计约 16K 单位;小于 64 字节的额外计 0。table.concat 按字节而不是元素个数计——它的遍历被表的长度界住,而字节数不受界。分层执行下实现方式不同,精度有差别:
- P1:在每个 preempt 点(函数/帧入口、负向
JMP/FORLOOP循环回跳、TFORLOOP)精确计一个单位,并额外计 CONCAT 与三个批量构造函数的字节工作量;普通直线 opcode(MOVE/ADD等)不逐条计费。它是各档里 preempt 点计量最精确的,但不是逐指令计量——不要据此按指令数计费;若确需逐指令口径,应作为独立能力单独实现和测试; - P3/P4:升层代码内不逐指令计数,改为在函数入口、调用点、循环回边按燃料(fuel)计费——每段进入时预支一段配额,回边/调用耗尽时结账并检查预算;CONCAT 字节计费经共享
doConcat、三个批量构造函数的字节计费经共享 stdlib 实现,对所有档位一致生效; - 因此 P4 下预算的触发点有一段配额的粒度误差(不会无限超支;纯算术死循环、自尾递归死循环都会被回边燃料按期截停);
context.Context取消同理:在回边/调用检查点生效,不是逐指令生效。
预算值因此在不同档位与不同负载之间是近似值,不是精确指令数。如果业务只是把预算当作「防脚本挂起 / byte-heavy 风暴」的兜底(绝大多数场景),各档粒度完全够用。没有哪一档提供逐指令精确计量:P1 的 preempt 点计量最细,但直线 opcode 不计费、CONCAT 与批量构造函数按字节计费,所以任何按指令数计费的口径都需要另行实现。
- build 带对 tag(
wangshu_p4 wangshu_profile),CI 里对该 build 跑测试; - 目标环境验证 exec-mmap 可用(预发跑真实负载,确认
TierStats.Promoted > 0且StuckCompileFailed == 0); - 接好
TierStatsSnapshot指标采集与StuckCompileFailed告警; - 降级预案演练:验证
SetTierEnabled(false)后业务结果不变、延迟符合解释器档预期; - 灰度顺序建议:先小流量开 P4,观察一至两周(nightly fuzz 的发现节奏参考:上一个正确性问题距今多久),再放量。