fan-in模式是聚合多源日志的最简可靠路径,因其通过独立goroutine监听各日志源并转发至统一通道,避免阻塞、保障上下文与可靠性,是go并发下日志聚合的底线设计。

直接用 log 包或裸写 fmt.Printf 上报日志,在分布式场景下等于主动放弃日志可靠性——它不带上下文、不落盘、无重试、不缓冲,网络一抖就丢,节点一重启就断。
为什么 fan-in 模式是聚合多源日志的最简可靠路径
多个服务实例、多个模块、多个文件源的日志,必须汇入统一通道再做后续处理(如序列化、转发、过滤)。fan-in 不是“高级技巧”,而是 Go 并发模型下避免阻塞的底线设计。
- 每个日志源(比如一个
chan LogEntry)必须由独立 goroutine 拉取:go func() { for entry := range src { outChan - 主聚合 goroutine 只做
select转发,不主动range任何源 channel —— 否则某个源卡住,整个聚合就停摆 -
outChan必须带缓冲(如make(chan LogEntry, 1024)),否则下游消费慢会反压上游,导致 goroutine 堆积 - 别用
sync.WaitGroup等所有源结束再关通道:分布式日志是长生命周期流,节点随时上下线,关通道时机只应来自明确的ctx.Done()
zap + gRPC 流式上报时,客户端必须自己控制重试和缓冲
gRPC 的 ClientStreaming 看似自动,但默认不保活、不重连、不缓存。一旦 collector 重启或网络瞬断,日志就静默丢失。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 初始化 client conn 时,必须显式加
grpc.WithBlock()+ 超时:grpc.DialContext(ctx, addr, grpc.WithBlock(), grpc.WithTimeout(5*time.Second)) - 发送逻辑外层包一层指数退避重试(如
backoff.Retry),失败时把LogEntry写入内存队列(如ringbuffer或带 TTL 的 map) - 不要依赖
zapcore.AddSync直接套 gRPC writer:它只是同步写,没缓冲也没错误反馈;应自己实现WriteEntry方法,在内部做队列 + 异步 flush - 本地落盘仍是兜底:用
lumberjack.Logger配合zapcore.AddSync同时写文件,设MaxSize: 100,MaxBackups: 7,Compress: true
trace_id 透传失败,90% 是因为没在请求生命周期里真正“流动”
日志里有 trace_id 字段不等于链路能串起来。它必须随 HTTP/gRPC 请求上下文一起流动,否则服务 A 打的 log 和服务 B 打的 log 永远对不上。
- HTTP 中间件中从
c.Request.Header.Get("X-Request-ID")提取,注入到 zap logger:logger.With(zap.String("trace_id", tid)) - gRPC server 端用
metadata.FromIncomingContext(ctx)拿trace-idkey,client 端调用下游前必须用metadata.AppendToOutgoingContext(ctx, "trace-id", tid) - 绝对不要用全局变量或
init()里生成trace_id:并发下必然串,Go 唯一安全载体是context.Context - 如果用了 OpenTelemetry,确保
otel.GetTextMapPropagator().Inject()在每次 outbound 请求前被调用
真正难的不是写通一条链路,而是在节点滚动更新、网络分区、磁盘满、collector OOM 这些真实故障下,日志还能不丢、不错序、可追溯。缓冲、重试、落盘、上下文透传,这四件事漏掉任何一个,分布式日志系统就只是个幻觉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










