Kratos 中异步任务需手动管理生命周期,必须通过 app.WithServer 或 app.WithRegistrar 注册,配合 context.Context 实现优雅启停与资源清理,禁止随意 go func()。

异步任务不能靠 goroutine 一扔了事
Kratos 不提供类似 Spring @Async 的注解式异步能力,所有后台协程都得你手动启、手动管。直接 go func() { ... }() 最常见后果是:程序启动后秒退(app.Run 不等待未命名 goroutine),或 panic: send on closed channel(channel 被提前关闭但协程还在写)。
- 必须用
app.WithServer()或app.WithRegistrar()注册后台任务,让 Kratos 知道这个 goroutine 属于应用生命周期 - 别在 handler 里临时起 goroutine 处理业务逻辑——它脱离了上下文取消机制,出错无法回收资源
- 若需处理耗时任务(如发邮件、写日志归档),应封装为独立的
task.Runner类型,配合context.Context做取消和超时
怎么安全地启停一个长周期消费协程
比如监听 RabbitMQ 队列、轮询数据库变更表、定时同步缓存——这类协程必须响应 app.Stop 信号,否则进程无法优雅退出。
- 启动时传入
app.Context(),并在循环中检查ctx.Err() != nil - 避免用
time.Sleep死等,改用time.AfterFunc或timer.Reset配合select监听 ctx.Done() - 关闭前要显式调
ch.Close()、conn.Close()等清理动作;RabbitMQ 场景下还必须确保msg.Ack(false)已完成,否则消息会重回队列 - 示例结构:
go func() { for { select { case
后台任务共享资源容易踩哪些坑
多个后台协程共用 DB 连接池、Redis 客户端、HTTP client 时,看似省资源,实则埋雷。
-
*sql.DB本身是并发安全的,但自定义的cache.MemcachedClient或amqp.Channel不是——每个消费者应独占一个Channel,不要复用 - HTTP client 若设置了
Timeout,复用没问题;但若加了自定义RoundTripper(如带 auth token 刷新逻辑),需确认其线程安全 - 全局变量 + mutex 不是银弹:锁粒度太粗会导致协程排队,太细则易漏锁;更推荐用
sync.Pool或 channel 做资源分发 - 特别注意日志:用
log.With()携带 traceID 的 logger 实例不能跨协程复用,否则字段污染
为什么你的 background task 总是漏执行
最隐蔽的问题不是崩溃,而是“看起来在跑,其实没干活”——比如定时器没重置、channel 缓冲区满后静默丢消息、consumer 启动早于 exchange 声明。
- RabbitMQ 场景下,
channel.ExchangeDeclare和channel.QueueBind必须在channel.Consume之前完成,且失败要 panic 或重试,不能忽略 error - 定时任务用
time.Ticker时,别把耗时操作放在case 分支里——如果 handle 耗时 > tick 间隔,后续 tick 会被跳过 - DB 查询轮询任务,记得加
FOR UPDATE SKIP LOCKED(PostgreSQL)或SELECT ... LIMIT 1 FOR UPDATE(MySQL),否则多实例会重复处理同一条记录 - 所有后台协程启动后,建议打一条
log.Infow("background task started", "name", "order-processor"),方便确认是否真起来了
Kratos 的后台协程管理没有魔法,核心就两条:显式注册进生命周期、每个环节检查 ctx.Done()。最容易被忽略的是 channel 关闭时机和 RabbitMQ 的 bind 顺序——它们不会报错,但会让任务“假装运行”。











