kafka 不适合作为 web 应用日志后端,因其非日志系统,直接使用会导致丢日志、请求阻塞和 oom;正确做法是采用异步缓冲+批量投递架构,通过 logrus hook 将日志写入 ring buffer 或限速 channel,由独立 goroutine 批量消费并投递至 kafka。

Kafka 不适合直接当 Web 应用的日志后端——它不是日志系统,强行塞会导致丢日志、阻塞请求、OOM。真要高吞吐收日志,得用「异步缓冲 + 批量投递」架构,Golang 里关键在控制好 sync.Pool、chan 容量和 sarama.AsyncProducer 的错误处理路径。
为什么不能直接在 HTTP handler 里调 producer.Input()
常见错误现象:panic: send on closed channel、HTTP 超时、Kafka client 内存持续上涨。因为 sarama.AsyncProducer 的 Input() 是非阻塞写入内存队列,但队列满或网络卡顿时,消息会堆积在 producer 内部 buffer;若 handler 不等 Successes() 或 Errors() 就返回,消息实际没发出去就丢了。
- Web 请求生命周期短(毫秒级),Kafka 网络 RTT 和批量攒批时间(默认
Config.Producer.Flush.Frequency是 500ms)天然不匹配 -
AsyncProducer的Input()不保证送达,也不抛错,只往内部 chan 塞——塞不进就 panic 或静默丢弃(取决于Config.Producer.Return.Errors) - 每个请求 new 一个 producer 开销大,复用又容易因重连、分区变更导致
InvalidTopicError或UnknownTopicOrPartitionError
用 logrus + firehose 模式做日志中转
核心是把日志写入本地无锁 ring buffer 或带限速的 chan,再由单独 goroutine 批量消费、序列化、投 Kafka。避免 handler 与 Kafka 网络耦合。
- 用
logrus.Hook实现自定义 hook,Fire()方法只做select { case logChan ,不碰 Kafka client -
logChan建议设为带缓冲的 channel(如make(chan *logrus.Entry, 10000)),太小易阻塞 handler,太大吃内存 - 消费 goroutine 用
time.Ticker触发 flush,或按条数(如每 200 条)/大小(如累计 1MB)触发 batch send,别依赖 Kafka 自动 flush - 序列化用
json.Marshal而非fmt.Sprintf,后者易注入空格、换行破坏 JSON 格式
sarama.AsyncProducer 必须配的 4 个参数
默认配置在日志场景下几乎必出问题:消息乱序、重复、丢失、OOM。这些不是“可选优化”,是保底可用的前提。
立即学习“go语言免费学习笔记(深入)”;
-
Config.Producer.RequiredAcks = sarama.WaitForAll:否则acks=0时 broker 接收即返,网络丢包就丢日志 -
Config.Producer.Retry.Max = 3:默认 0,分区 leader 切换时直接失败,不重试 -
Config.Producer.Flush.Bytes = 1024 * 1024(1MB):避免小包频繁发,但别设太大,否则延迟飙升 -
Config.Net.DialTimeout = 5 * time.Second和Config.Net.ReadTimeout = 10 * time.Second:防止 broker 暂不可用时 goroutine 卡死
怎么知道日志到底发没发成功?
不能只看 Successes() channel 是否有数据——它只在 RequiredAcks != NoResponse 且 broker 确认后才写入,而 Errors() 才是真正兜底。漏看 Errors() 是线上丢日志最常见原因。
- 必须起独立 goroutine 消费
producer.Errors(),打印err.Msg和err.Err,常见值如NetworkError、InvalidMessage、MessageSizeTooLarge -
Successes()只建议用于调试,生产环境关掉(Config.Producer.Return.Successes = false),省内存和 GC 压力 - 对关键业务日志(如支付、登录),可加本地 fallback:
Errors()收到失败时,把原始*logrus.Entry写入本地/var/log/app/fallback.log,后续人工补发
真正难的不是连上 Kafka,而是让日志在流量毛刺、broker 重启、磁盘满这三种情况里都不丢——这要求你亲手压测 logChan 满载时的 handler 延迟、模拟 Errors() 频发时的 fallback 路径是否真能写磁盘、确认 Flush.Bytes 在 99% 流量下不超 100ms。这些没法靠文档,只能跑真实日志流看 metrics。











