Day 17 / 共 20 天 · 第 4 周 高级特性与实战
Leader 选举与共享数据
昨天(Day 15)用 Redis 做跨实例共享;今天讲另一种不依赖外部服务的跨 VM 共享——proxy-wasm 自带的 SharedData。有些活只该"一个实例干"(定时上报、清理过期数据),不能每个 VM 都干一遍。今天看 wasm-go 怎么靠 SharedData + CAS 选出一个 Leader。这是分布式系统的经典问题。
📍 你在整门课的位置(wasm-go 20 天 · 第 4 周 高级特性与实战)
日志 D16→
Leader 选举 D17→
Token 解析 D18→
性能测试 D19→
构建部署 D20
L01
为什么要选 Leader
💡 本质:选 Leader = 一群人抢一块"值班牌"
想象一屋子平等的员工,桌上有唯一一块"今日值班"牌子。谁先抢到就负责当天的独占活(打扫、上报),别人待命。难点是"同时伸手抢时怎么保证只有一个人拿到"——靠 CAS(拿牌时报一下牌子的编号,编号没变才算你拿到)。而且这牌子要定时打卡续期,值班的人跑路了(VM 崩了),牌子过期,别人就能重新抢。全程没有"班长"来分配,纯靠这块牌子自组织。
"这活只能一个人干"
网关有很多实例、每个实例很多 VM。有些任务重复干会出问题:比如"每分钟向监控系统上报一次汇总"——如果每个 VM 都报,数据翻了 N 倍;"每小时清理一次过期缓存"——多个一起清会冲突。解法:选出一个 Leader,只有它干这些独占任务,其余待命。这就是"Leader 选举"。难点在于:没有 ZooKeeper/etcd 这种协调服务,怎么在一堆平等的 VM 里选出唯一一个?
L02
SharedData 与 CAS
答案是 proxy-wasm 的 SharedData——一块跨 VM 共享的键值存储(同一 Envoy 进程内所有 VM 可见,弥补 Day 03 "全局变量各 VM 独立"的限制)。关键是它支持 CAS(Compare-And-Swap):
data, cas, err := proxywasm.GetSharedData(key) // 读值 + 版本号 cas
proxywasm.SetSharedData(key, newValue, cas) // 只有 cas 匹配才写成功
CAS = 原子的"没人改过我才写"
读的时候拿到一个版本号
cas;写的时候带上它——如果期间别人改过(版本变了),你的写入失败。这保证了"多个 VM 同时抢,只有一个能成功写入"。回想 Day 07 的乐观锁、Day 15 的 Redis SetNX——都是同一个思想:用原子操作在竞争中选出唯一赢家。SharedData + CAS 是 wasm-go 不依赖外部服务就能做 Leader 选举的基础。(Day 09 的规则备份也存在 SharedData 里。)图注:两个 VM 都读到 cas=7 后抢写。A 先写成功、版本变 8;B 带着旧的 cas=7 再写,版本对不上被拒——唯一赢家 = A。
L03
Lease 租约
plugin_wrapper.go:563-566 定义租约:
type Lease struct {
VMID string `json:"vmID"` // 谁持有租约
Timestamp int64 `json:"timestamp"` // 何时续的
}
为什么用"租约"而不是"永久任命"?
如果直接选一个 Leader 永久任命,万一它挂了(VM 崩溃/实例下线),这活就永远没人干了。租约机制:Leader 要定期"续租"(更新 Timestamp)证明自己还活着;租约过期(比如 60 秒没续)就视为 Leader 挂了,其他 VM 可以抢。好比"值班牌"要定时打卡,不打卡就换人。这样 Leader 挂了能自动故障转移。
VMID(Day 03 的 uuid)标识是哪个 VM 持有。L04
抢/续租实现
tryAcquireOrRenewLease(plugin_wrapper.go:576-608):
data, cas, err := proxywasm.GetSharedData(ctx.leaderLeaseKey())
if 没有租约 { return ctx.setLease(now, cas) } // 没人占,我占
var lease Lease; json.Unmarshal(data, &lease)
// 如果租约是我自己的 → 直接续;如果过期超 60s → 抢过来
if lease.VMID == ctx.vm.vmID || now-lease.Timestamp > 60 {
lease.VMID = ctx.vm.vmID; lease.Timestamp = now
return ctx.setLease(now, cas) // CAS 写入,成功=当选/续租成功
}
return false // 别人的有效租约,我不是 Leader
读法:三种情况:①没租约→抢;②租约是我的→续;③租约是别人的但过期>60s→抢。都通过
setLease(CAS 写入)落实——多个 VM 同时抢时,CAS 保证只有一个成功。返回 true 表示"我是 Leader"。L05
setLease 写入
setLease(plugin_wrapper.go:610-627):把自己的 VMID + 当前时间戳序列化,用带 cas 的 SetSharedData 写入。
lease := Lease{VMID: ctx.vm.vmID, Timestamp: timestamp}
leaseByte, _ := json.Marshal(lease)
if err := proxywasm.SetSharedData(ctx.leaderLeaseKey(), leaseByte, cas); err != nil {
return false // CAS 失败(别人抢先了),我没当选
}
return true // 写入成功,我是 Leader
CAS 失败就认输
如果
SetSharedData 因 cas 不匹配失败,说明有别的 VM 在你读和写之间抢先改了租约——你老实认输返回 false。这就是 CAS 保证唯一性的地方:并发抢租约时,只有拿到正确 cas 并抢先写入的那个成功。leaderLeaseKey(:572-574)用插件指纹拼 key,不同插件互不干扰。L06
配合 tick 续租
DoLeaderElection(plugin_wrapper.go:568-570)调 tryAcquireOrRenewLease 并更新 isLeader。典型用法是在 tick 里定期调(Day 10 的定时任务):
func parseConfig(ctx wrapper.PluginContext, json gjson.Result, config *Config) error {
wrapper.RegisterTickFunc(3000, func() { // 每 3 秒
ctx.DoLeaderElection() // 续租/抢租
if ctx.IsLeader() {
reportMetrics() // 只有 Leader 上报
}
})
return nil
}
tick + 选举 = 自动故障转移
每 3 秒跑一次选举:Leader 借此续租(刷新时间戳);非 Leader 借此检查"现任 Leader 是否过期"。Leader 挂了 → 停止续租 → 60 秒后租约过期 → 下次某个 VM 的 tick 检测到过期 → 抢成新 Leader。整个过程无需人工干预、无需外部协调服务——纯靠 SharedData+CAS+tick 自组织。这是很优雅的分布式设计。
📝 单步走查:Leader 挂了之后发生了什么
t=0s VM-A 是 Leader,每 3 秒 tick 续租,Timestamp 一直刷新。
t=10s VM-A 所在实例崩溃 → 不再 tick → 租约的 Timestamp 停在 10s。
t=13/16/…s VM-B 的 tick 照常跑
t=71s B 的某次 tick 发现
t=10s VM-A 所在实例崩溃 → 不再 tick → 租约的 Timestamp 停在 10s。
t=13/16/…s VM-B 的 tick 照常跑
DoLeaderElection,读到租约还是 A 的、且 now-Timestamp 还没超 60s → B 判定"现任有效",不抢,继续待命。t=71s B 的某次 tick 发现
now-Timestamp = 61 > 60 → 租约过期 → B 用 CAS 抢写成功 → B 成为新 Leader,独占任务自动接管,全程无人干预。L07
IsLeader 用法
IsLeader()(plugin_wrapper.go:629-631)返回当前 VM 是否是 Leader。在需要"只干一次"的地方判断它。
读法:典型场景:定时上报统计、定时清理、定时拉取远端配置——用
if ctx.IsLeader() 包起来,保证全局只有一个实例执行。注意 isLeader 是每次 DoLeaderElection 后更新的快照,所以要在 tick 里持续选举保持新鲜。这个能力让单个 Wasm 插件也能安全地做"集群级单例任务"。⚠️ 常见误解:小白以为"调一次
DoLeaderElection 当选后就永远是 Leader"。其实 isLeader 只是上一次选举后的快照——必须放进 tick 里持续续租,否则时间戳不刷新、60 秒后被判过期,你会"莫名"丢失 Leader 身份。L08
今日小结 + 动手
🧠 今天你应该能回答
- 为什么需要 Leader 选举?哪些任务只该一个实例干?
- SharedData + CAS 怎么在竞争中选唯一赢家?
- 为什么用租约而不是永久任命?60 秒过期的意义?
- 怎么配合 tick 实现自动故障转移?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '563,631p' pkg/wrapper/plugin_wrapper.go
grep -n "GetSharedData\|SetSharedData" pkg/wrapper/plugin_wrapper.go pkg/matcher/*.go
明天预告 · Day 18:AI Token 用量解析——
tokenusage 包怎么从各家大模型(OpenAI/Gemini/Anthropic)不同格式的响应里,统一提取出 Token 用量。这是 AI 网关计费/限流的数据源头。