不能在 http handler 中直接调用 sarama.asyncproducer.input(),因其非阻塞写入内存队列、不等待 kafka 响应,导致日志丢失、请求阻塞、oom;需通过 logrus.hook + ring buffer 解耦采集与发送,并严格配置 requiredacks、retry.max、flush.bytes 和 maxopenrequests。

不能在 HTTP handler 里直接调 sarama.AsyncProducer.Input() —— 这是丢日志、阻塞请求、OOM 的根源。 日志归集不是“发消息”,而是“缓冲 + 批量 + 可控失败”的系统工程。真要跑得稳,得绕开同步等待、channel 泄漏、序列化逃逸这三道坎。
为什么 sarama.AsyncProducer 在日志场景下默认就不可用
常见错误现象:panic: send on closed channel、HTTP 超时、内存持续上涨、UnknownTopicOrPartitionError 频发。根本原因不是代码写错了,而是 AsyncProducer 的设计与 Web 请求生命周期天然冲突:
- Input() 是非阻塞写入内部内存队列,不等 Kafka 返回就返回 —— handler 已经结束,消息还在 buffer 里躺着
- 默认
Config.Producer.Flush.Frequency = 500ms,但 HTTP 请求通常几毫秒就完了,根本等不到 flush - 默认
ChannelBufferSize = 256,突发日志一来就满,新消息被静默丢弃(除非开了Return.Errors) - 没配
RequiredAcks和Retry.Max,网络抖动或 leader 切换时直接失败,不重试也不报错
用 logrus.Hook + ring buffer 做无锁中转
核心思路:把日志采集和 Kafka 发送彻底解耦。Hook 的 Fire() 方法只做本地内存写入,绝不碰网络、不碰 producer 实例。
- 定义带缓冲的 channel:
logChan := make(chan *logrus.Entry, 10000)—— 太小易阻塞 handler,太大吃内存;10k 是多数服务的合理起点 - 启动独立 goroutine 消费:
for range time.Tick(200 * time.Millisecond)或按条数/大小触发 flush(比如每 200 条或累计 1MB) - 消费逻辑里统一做序列化:
json.Marshal,别用fmt.Sprintf—— 后者会注入空格和换行,破坏 JSON 格式且逃逸严重 - 避免结构体指针逃逸:日志字段全用值类型(
int64、string),时间戳存time.UnixNano(),不用time.Time
sarama.AsyncProducer 必须重写的 4 个配置项
这些不是“优化项”,是让日志能进 Kafka 的底线配置。缺一不可:
-
Config.Producer.RequiredAcks = sarama.WaitForAll:否则acks=0,broker 接收即返,丢包就丢日志 -
Config.Producer.Retry.Max = 3:默认为 0,分区 leader 切换时直接失败,不重试 -
Config.Producer.Flush.Bytes = 1024 * 1024(1MB):避免小包泛滥,但别设更大——Kafka 单消息默认上限就是 1MB -
Config.Net.MaxOpenRequests = 1:防止并发请求过多触发 broker 的NOT_COORDINATOR错误
顺手关掉 Return.Successes:日志不需要逐条确认,开着只会增加 channel 切换开销。
消费端卡在 Rebalance 的真实原因和解法
日志消费端常表现为“消费停滞”“offset 不提交”“group coordinator not available”,本质是处理耗时超过了 Kafka 心跳超时窗口。
- 别在
ConsumeClaim的 for-loop 里做任何耗时操作(如写 ES、发 HTTP、解析大 JSON) - 把消息分发到 worker goroutine 异步处理,主循环只负责读取和投递
- 调大
Config.Group.Heartbeat.Interval至15s,并同步调大Config.Group.Session.Timeout至45s(注意 Kafka 服务端group.min.session.timeout.ms需 ≤45s) - 关掉
Config.Consumer.Offsets.AutoCommit.Enable,改用手动CommitOffsets—— 自动提交在 Rebalance 期间不可靠,易重复消费
最易被忽略的一点:所有日志结构体序列化前,必须检查单条是否超 1MB。一旦触发 MessageSizeTooLarge,整个 batch 会失败,且错误只出现在 Errors() channel 里 —— 不监听它,等于 blindfolded 发送。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











