用 mongo-go-driver + 连接池 + 批量写入 + 结构体映射可支撑每秒数千日志写入;禁用已归档且不兼容 MongoDB 6+ 的 mgo;需全局复用 client,启动时初始化并用 shutdown ctx 调用 Disconnect;批量插入应分批(如200条/500ms)并设2–3秒超时;索引仅建必要项(timestamp、service+timestamp),避免 message 文本索引;启用 TTL 自动清理;通过 ring buffer 缓存失败日志,指数退避重试;务必使用客户端传入的时间戳而非服务端生成。

直接用 mongo-go-driver + 连接池 + 批量写入 + 结构体映射,就能撑住每秒数千条日志写入。别碰 mgo,它已归档且不支持 MongoDB 6+ 的新特性。
为什么不能每次请求都 new 一个 client
Go 的 mongo.Client 是线程安全、带内置连接池的全局对象。每次请求都调用 mongo.Connect() 会:创建大量 TCP 连接、耗尽文件描述符、触发 MongoDB 的 maxIncomingConnections 限制、让 Ping 检测失效。
- 必须在应用启动时初始化一次
client,作为全局变量或注入到 handler - 务必调用
defer client.Disconnect(ctx),但注意:这个ctx应该是 shutdown 专用上下文(比如带 10s 超时),不是 HTTP 请求上下文 - 验证连通性用
client.Ping(ctx, nil),别只靠err == nil就认为可用
批量插入日志时如何避免 OOM 和超时
单条 InsertOne 写日志在高并发下是性能黑洞;但盲目用 InsertMany 传 10 万条也会导致内存暴涨或 MongoDB 拒绝(默认 maxWriteBatchSize=1000)。
- 按时间窗口或数量分批:例如每 200 条或 500ms 触发一次
InsertMany - 使用
context.WithTimeout包裹每次批量操作,超时设为 2–3 秒,防止卡死整个 worker - 插入前用
bson.M{"timestamp": bson.M{"$gt": time.Now().Add(-1 * time.Hour)}}做简单校验,过滤明显异常的时间戳 - 不要把原始日志字符串直接塞进 struct;提前解析成字段(如
level,service,trace_id),方便后续聚合查询
日志文档结构与索引怎么设计才不拖慢写入
MongoDB 写入性能和索引数量强相关。日志场景下,写远多于读,索引要克制。
立即学习“go语言免费学习笔记(深入)”;
- 必建索引:
{"timestamp": 1}(按时间范围查)、{"service": 1, "timestamp": -1}(服务+时间倒序) - 避免对
message字段建文本索引——除非真有模糊搜索需求;否则用正则$regex查即可 - 结构体用
bson:"timestamp,omitempty"标签,空值不存;日志中缺失字段(如error_stack)不占空间 - 考虑启用 TTL 索引自动清理:
db.logs.createIndex({"timestamp": 1}, {expireAfterSeconds: 259200})(保留 3 天)
如何让日志微服务真正“高可用”而不是单点故障
日志微服务挂了不能丢数据,但也不能因重试机制把 MongoDB 拖垮。
- 在内存里用
sync.Map或 ring buffer 缓存待写日志(限大小,比如 10k 条),client 失联时暂存 - 写失败后不要立即重试,用指数退避(100ms → 300ms → 1s),并记录告警指标
- 连接字符串里加
?replicaSet=rs0&readPreference=nearest,避免主节点压力集中 - 监控关键指标:
client.ListDatabaseNames()调用延迟、collection.EstimatedDocumentCount()每分钟增长率、连接池inUse和available数量差值
最容易被忽略的是:日志时间戳必须来自客户端传入,而非服务端 time.Now() —— 分布式系统里各机器时钟不同步,会导致按时间范围查询结果错乱。哪怕多加一个 header 或字段,也要坚持用源头时间。



















