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。

它演示了什么

后台任务与清理栈

这段来自 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 针对热更与就绪门断言:

go test ./examples/lifecycle

相关

服务与生命周期概念页 Service 接口、Env 访问器、可选钩子与退出顺序。 配置 只有服务专属段热更,框架段要重启。 示例索引 全部 20 个可运行示例。

相关示例