Day 03 / 共 20 天 · 第 1 周 环境与骨架

进程怎么启动

昨天看到了容器跑起来。今天钻进 backend/cmd/main.go,逐行看一个 Go 进程从零到能接请求,中间到底按什么顺序把 MySQL/Redis/ClickHouse 连上、把六大模块组装好。

📍 你在整门课的位置 · 第 1 周 环境与骨架(共 4 周 · 20 天)
D1 项目全景 D2 Docker 跑起来 D3 进程启动 D4 HTTP 网关 D5 Handler 桥接
L01

main.go 鸟瞰

🤔 痛点:main 函数动不动几十行,第一次看容易发懵 backend/cmd/main.gomain() 函数其实只做四件事,一件事一行就能说清楚——发懵往往是因为没先看"骨架"就直接抠细节。
💡 本质:main 只是"编排",四步走
func main() {
	ctx := context.Background()
	c, err := newComponent(ctx)              // ① 组装所有基础设施客户端
	handler, err := api.Init(ctx, c.idgen, c.db, ...) // ② 组装六大模块 handler
	initTracer(handler)                       // ③ 初始化链路追踪
	r := registry.NewConsumerRegistryWithShutdown(...)
	r.StartAll(ctx)                          // ④ 启动消息队列消费者
	go api.Start(handler)                    // ⑤ 启动 HTTP 服务(协程里跑,不阻塞退出监听)
	<-signalCtx.Done()                       // 阻塞等 SIGTERM/SIGINT
	r.StopAll(stopCtx)                       // 优雅停止消费者
}
backend/cmd/main.go:50-80 就是这五步的真实代码——注意它只做编排,没有一行业务逻辑,完全符合 ARCHITECTURE.mdcmd/ 的架构不变量。
🍼 类比:开餐厅前的四道准备工序 ① 先把水电煤气接通(基础设施);② 把各个厨房岗位的师傅招齐(六大模块 handler);③ 装好监控摄像头(tracer);④ 打开外卖接单软件的后台任务(consumer);最后才⑤挂牌营业(HTTP Start)。顺序不能乱——没水电就开炉子是要出事的。
L02

newComponent:组装基础设施客户端

backend/cmd/main.go:167-280newComponent 是"水电工",把所有基础设施的客户端连好、包成一个 component 结构体:

type component struct {
	idgen               idgen.IIDGenerator      // 分布式 ID 生成器(基于 Redis)
	db                  db.Provider             // MySQL 连接池
	redis               redis.Cmdable           // Redis 客户端
	cfgFactory          conf.IConfigLoaderFactory // 配置加载器工厂
	mqFactory           mq.IFactory              // RocketMQ 工厂
	objectStorage       fileserver.ObjectStorage // MinIO/S3 客户端
	ckDb                ck.Provider              // ClickHouse 连接
	benefitSvc          benefit.IBenefitService  // 权益服务(开源版是 Noop 空实现)
	auditClient         audit.IAuditService      // 审核服务(开源版是 Noop 空实现)
	// ...
}
细节:Redis 连接建立后立刻用来生成 ID main.go:250redis_gen.NewIDGenerator(redisCli, ...)——Coze Loop 用 Redis 实现分布式自增 ID(类似雪花算法的一种变体),而不是用数据库自增主键,这样多个应用实例并发建 Prompt 也不会 ID 冲突。
⚠️ 坑:benefitSvc / auditClient 是空实现(Noop) main.go:272-273benefitSvc: benefit.NewNoopBenefitService()auditClient: audit.NewNoopAuditService()。这是开源版和商业版的一个典型分野——"权益校验"(比如额度限制)和"内容审核"在商业版接了真实服务,开源版给的是"什么都不做,直接放行"的空实现。读代码时看到调用了 auditRPCProvider.AuditPrompt(...) 却发现什么都没发生,不要慌,去看它注入的是不是 Noop。
L03

infrastructure.yaml 怎么被读进来

数据库、Redis、ClickHouse 的连接参数不是硬编码在代码里,而是从 backend/conf/infrastructure.yaml 读的(注意:这份和 Day 02 的 docker-compose/conf/model_config.yaml 是两个不同的配置文件,各管各的):

# backend/conf/infrastructure.yaml(容器内路径 /coze-loop/conf/infrastructure.yaml)
infra:
  redis:
    host: "cozeloop-redis"
    port: 6379
  rds:
    host: "cozeloop-mysql"
    port: 3306
  ck_config:
    host: "cozeloop-clickhouse:9008"
  idgen:
    server_ids: [1]      # 分布式 ID 生成器的机器号
  log_level: "info"

main.go:117-129getComponentConfig 就是加载这份配置:

func getComponentConfig(configFactory conf.IConfigLoaderFactory) (*ComponentConfig, error) {
	componentConfigLoader, err := configFactory.NewConfigLoader("infrastructure.yaml")
	componentConfig := &ComponentConfig{}
	err = componentConfigLoader.UnmarshalKey(ctx, "infra", componentConfig)
	return componentConfig, nil
}
💡 本质:真正的连接地址其实来自环境变量,YAML 只兜底非敏感项 仔细看 newComponent,Redis/MySQL/ClickHouse/MinIO 的 host、密码全部通过 os.Getenv("COZE_LOOP_REDIS_DOMAIN") 这类函数读取(main.go:282-367 一整串 getXxx() 辅助函数)。而这些环境变量正是 Day 02 docker-compose.ymlapp 服务的 environment: 段注入的。infrastructure.yaml 只负责 idgen.server_idslog_level 这类"不敏感、不随部署环境变化"的参数。这是"敏感配置走环境变量、结构化配置走 YAML"的常见工程实践。
L04

api.Init:组装六大模块 handler

基础设施连好之后,backend/api/api.goInit 函数把六大模块的 handler 逐个组装出来。这里能直接看到模块之间的依赖顺序

func Init(ctx context.Context, idgen ..., db ..., ...) (*apis.APIHandler, error) {
	foundationHandler, _ := apis.InitFoundationHandler(...)   // ① 最先:用户/空间/鉴权
	llmHandler, _ := apis.InitLLMHandler(..., loauth.NewLocalAuthService(foundationHandler.AuthService))
	promptHandler, _ := apis.InitPromptHandler(...,
		loruntime.NewLocalLLMRuntimeService(llmHandler.LLMRuntimeService), // 依赖 llm
		loauth.NewLocalAuthService(foundationHandler.AuthService),        // 依赖 foundation
		lofile.NewLocalFileService(foundationHandler.FileService),
	)
	dataHandler, _ := apis.InitDataHandler(...)
	evaluationHandler, _ := apis.InitEvaluationHandler(...,             // 依赖 data + prompt + llm + foundation
		lodataset.NewLocalDatasetService(dataHandler.IDatasetApplication),
		lomanage.NewLocalPromptManageService(promptHandler.PromptManageService),
	)
	observabilityHandler, _ := apis.InitObservabilityHandler(...)       // 依赖几乎所有前面模块
	return &apis.APIHandler{ PromptHandler: promptHandler, ... }, nil
}
依赖顺序揭示了模块的"地基"关系foundation 最先初始化(几乎所有模块都要靠它鉴权/查用户),llm 第二(prompt 要调模型),observability 最后(它要观测前面所有模块产生的调用)。这条顺序和 Day 01 讲的"六大模块"角色完全吻合——今天你终于在真实代码里看到了这种依赖关系是怎么落地的。
📌 注意到没有:这里传进去的不是 foundationHandler.AuthService 本身,而是包了一层 loauth.NewLocalAuthService(...)——这就是 Day 01 埋下的"模块间不直接互调"的钩子,Day 05/06 会专门揭秘这层包装是干什么的。
L05

consumer 与优雅退出

HTTP 之外,Coze Loop 还有一批基于 RocketMQ 的异步消费者(比如异步处理 Trace 摄入、评测任务)。backend/cmd/main.go:69-79

signalCtx, signalCancel := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
r := registry.NewConsumerRegistryWithShutdown(signalCtx, c.mqFactory).
	Register(MustInitConsumerWorkers(c.cfgFactory, c.mqFactory, handler, handler, handler, handler))
r.StartAll(ctx)

go api.Start(handler)   // HTTP 服务放进协程,不阻塞主流程
<-signalCtx.Done()      // 主 goroutine 在这里"睡着",直到收到退出信号

stopCtx, stopCancel := context.WithTimeout(context.Background(), 30*time.Second)
r.StopAll(stopCtx)      // 给消费者 30 秒把手头的消息处理完再退出
进程启动组装依赖 consumerStartAll HTTP协程里 Start 阻塞等待SIGTERM StopAll30s 内收尾
🍼 类比:下班关店流程 signal.NotifyContext 就像"店长盯着门口,一旦看到打烊信号(Ctrl+C 或容器要被杀)";StopAll 给 30 秒是"允许后厨把手上还在炒的最后几个菜炒完,而不是直接拔电闸"——这叫优雅停机(Graceful Shutdown),避免正在处理的消息/请求被硬生生打断导致数据不一致。
L06

今日小结 + 动手

🧠 今天你应该能回答

  • main 函数的五个步骤分别是什么?(组装基础设施 → 组装模块 handler → 初始化 tracer → 启动消费者 → 启动 HTTP + 阻塞等退出信号)
  • 数据库连接参数从哪来?(大部分敏感项走环境变量,infrastructure.yaml 只管非敏感结构化配置)
  • 六大模块初始化顺序说明了什么?(foundation 最先,observability 最后,体现依赖关系)
  • 什么是优雅停机?(收到退出信号后给消费者一段时间处理完手头任务再退出)

✋ 动手 5 分钟

# 1. 通读 main 函数骨架(先看骨架,别急着抠细节)
sed -n '50,80p' backend/cmd/main.go

# 2. 看 newComponent 组装了哪些基础设施
grep -n "type component struct" -A 15 backend/cmd/main.go

# 3. 看 api.Init 的六模块初始化顺序
grep -n "Init.*Handler" backend/api/api.go

# 4. 看本地容器里的 infrastructure.yaml 长什么样
cat release/deployment/docker-compose/conf/infrastructure.yaml 2>/dev/null || \
cat backend/conf/infrastructure.yaml
明天预告 · Day 04api.Start(handler) 只是启动了 Hertz 服务,具体一个 HTTP 请求怎么被路由到对应 handler,中间经过哪些中间件(Session/Locale)?我们会跟着一条真实路由 POST /api/prompt/v1/prompts 走一遍。
← 上一天 · Docker 跑起来 下一天 · HTTP 网关与路由 →