lifecycle — 服务生命周期
一个部署单元就是一个实现了 verticle.Service 的类型:Name()、
Setup(ctx, env)、BuildRouter(srv),剩下的启动、后台任务、
热更、优雅退出都由 verticle.Run 驱动。这个示例把四个可选钩子
(Block、AddCloser、ConfigWatcher、ReadyChecker)
全部接上,改一次配置文件就能看到热更生效。
运行
go run ./examples/lifecycle
curl -i localhost:8080/readyz # 前 3s 是 503(ready_window 预热)
sleep 4; curl -i localhost:8080/readyz # 之后 200
curl -i localhost:8080/api/v1/greeting/world
# 热更:改 examples/lifecycle/config.yaml 里的 greeting,保存
# 日志立刻出现 "config reloaded",接口返回值跟着变,不用重启
# Ctrl-C:观察优雅退出的顺序
没有外部依赖,不需要 make dev。监听 :8080;
-conf 默认指向 examples/lifecycle/config.yaml。
配置文件里 ready_window 是 3s,所以刚启动的三秒内
/readyz 会返回 503。
它演示了什么
- Service 的三个方法:
Name()同时是配置段 key、日志前缀与 metrics label。 - 后台任务:
env.Block("heartbeat", fn)随应用启动,ctx 取消时必须返回。 - 清理栈:
env.AddCloser(fn)注册的清理函数逆序执行。 - 配置热更:实现
OnServiceConfigChange(raw map[string]any),只有服务专属段会热更。 - 就绪门:实现
Ready(ctx) error,叠加在默认的就绪检查之上(默认已会 ping 已配置的基础设施)。 - 退出顺序:SIGINT/SIGTERM 之后是 http drain → blocks → closers → infra close。
后台任务与清理栈
这段来自 examples/lifecycle/main.go 的 Setup。
注意 Block 里的循环在 ctx.Done() 时必须返回;
注释也点明了失败语义:返回非 nil 的 error 会中止整个应用。
// A background task: it starts with the app and must return when ctx
// is done (returning a non-nil error aborts the whole app).
env.Block("heartbeat", func(ctx context.Context) error {
t := time.NewTicker(s.heartbeat())
defer t.Stop()
for {
select {
case <-ctx.Done():
obs.Info(ctx, "heartbeat stopped")
return nil
case <-t.C:
obs.Info(ctx, "heartbeat", "greeting", s.greeting())
}
}
})
// Cleanup. Closers run in reverse order, and infrastructure closers
// registered by Run are further out, so: http drain → blocks →
// your closers → infra close.
env.AddCloser(func() {
obs.Info(context.Background(), "service closer: flushing in-flight work")
})
热更与就绪门
同一份文件里的另外两个钩子。热更时的关键细节是:解析失败就保持旧值, 并且因为 watcher 与 handler 并发,配置的读写要加锁。
// OnServiceConfigChange is the hot-reload hook: the watcher calls it with
// the new service section after config.yaml changed (debounced).
func (s *lifecycleService) OnServiceConfigChange(raw map[string]any) {
var cfg lifecycleConfig
if err := config.ParseMap(raw, &cfg); err != nil {
obs.Warn(context.Background(), "reload rejected", "err", err)
return
}
s.mu.Lock()
before := s.cfg
s.cfg = cfg
s.mu.Unlock()
obs.Info(context.Background(), "config reloaded",
"greeting_before", before.Greeting, "greeting_after", cfg.Greeting,
"heartbeat", cfg.Heartbeat)
}
退出时的先后顺序
flowchart TD
S["启动
加载 config → obs.Init → metrics"] --> I["初始化配了的基础设施
db / redis / memcache / kafka"]
I --> SU["svc.Setup(ctx, env)
Block 注册 · AddCloser 注册"]
SU --> H["httpx server
/healthz /readyz /metrics"]
H --> BR["svc.BuildRouter(srv)"]
BR --> RUN["开始服务 + 监听配置变化"]
RUN --> SIG["SIGINT / SIGTERM"]
SIG --> D["1. http drain(10s)"]
D --> B["2. 等 blocks 退出(10s)"]
B --> C["3. closers(逆序)"]
C --> INF["4. infra close"]
classDef rev fill:#fdf6f0,stroke:#c8641f,color:#2b2722
class C rev
closers 内部本身也是逆序的。
验证
curl -i localhost:8080/readyz
# 启动后 3s 内:503(Ready 返回 errs.Unavailable(50301, "warming up"))
# 3s 之后:200
curl -i localhost:8080/api/v1/greeting/world
# 200 {"code":0,"msg":"ok","data":{"greeting":"Hello, world!"}}
curl -i localhost:8080/api/v1/uptime
# 200 data 里有 "uptime":"12s" 这类值(四舍五入到秒)
热更验证:把 config.yaml 里的 greeting 改成 "Yo, " 保存,标准输出会立刻出现一行
{"level":"INFO","msg":"config reloaded","greeting_before":"Hello, ","greeting_after":"Yo, ",…}
再请求 /api/v1/greeting/world 就得到 "Yo, world!"。
Ctrl-C 之后,日志会依次显示 heartbeat stopped 与
service closer: flushing in-flight work——顺序正是上面那张图。
测试
main_test.go 针对热更与就绪门断言:
TestConfigReloadSwapsTheGreeting:喂进{"greeting":"Yo, ","heartbeat":"1s"}后,greeting()是"Yo, ",heartbeat()是1s。TestConfigReloadRejectsBadValues:heartbeat给一个非法时长 ("not-a-duration")会让ParseMap失败, 此时配置必须原封不动——greeting仍是"keep me",heartbeat仍是1m。TestReadyWindow:ReadyWindow为time.Minute且刚启动时Ready必须返回错误;改成 0 之后返回 nil。TestGreetHandler:直接调用greet,返回的greeting是"Hi world!"。
go test ./examples/lifecycle
相关
相关示例
- examples/config:
ParseMap与 tag 语义。 - examples/cron:把 scheduler 挂到
env.Block上。 - examples/observability:日志、相关 id 与指标。