应使用 std::atomic 和无锁队列(如 moodycamel::ConcurrentQueue)实现日志采集端,避免 std::mutex 在高频写入路径上造成争抢瓶颈;日志结构需 trivially copyable,时间戳由采集线程用 clock_gettime 获取,禁用格式化、堆分配及 gRPC 默认压缩与流控,监控告警须与日志 pipeline 解耦。

用 std::atomic 和无锁队列做日志采集端,别碰 std::mutex 在 hot path 上
高频率日志写入(比如每秒数万条)时,std::mutex 会成为瓶颈,尤其在多核争抢同一把锁时,延迟毛刺明显。真正低延迟的做法是:采集线程只做原子写入,把格式化、落盘、网络发送等重操作全剥离到独立消费者线程。
推荐用 moodycamel::ConcurrentQueue(轻量、无锁、支持批量 pop)或自己基于 std::atomic + 环形缓冲区实现简易版本。关键点:
- 日志结构体必须是 trivially copyable,避免构造/析构开销
- 时间戳统一由采集线程用
clock_gettime(CLOCK_MONOTONIC, ...)获取,不依赖std::chrono的高开销封装 - 禁止在采集线程中调用
strftime、std::to_string或任何堆分配操作
用 gRPC + Protocol Buffers 做传输层,但必须禁用默认流控和压缩
默认 gRPC 配置对日志场景不友好:HTTP/2 流控会阻塞小包发送;gzip 压缩在高频短日志上反而增加 CPU 占用且延迟不可控。实测在 10G 网卡下,关掉压缩后 p99 延迟下降 40%+。
必须显式配置:
立即学习“C++免费学习笔记(深入)”;
-
SetMaxMessageSize(INT_MAX)避免日志被截断 -
ChannelArguments::SetInt(GRPC_ARG_HTTP2_MAX_FRAME_SIZE, 16 * 1024)控制帧大小,防止大日志触发重传 -
ChannelArguments::SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 30000)关闭 keepalive,日志是单向流,不需要心跳保活
Protobuf message 定义里,所有字段用 optional(proto3.12+),避免默认值序列化开销;时间戳用 int64 存纳秒,不用 google.protobuf.Timestamp。
监控告警逻辑必须与日志 pipeline 解耦,走独立的 rocksdb + prometheus client_cpp 双通道
把告警判断逻辑塞进日志链路里,等于把实时性要求拉到毫秒级,还极易因告警规则变更导致整个 pipeline 暂停。正确做法是:日志落地后,由专用 agent 异步读取本地 RocksDB(按 service + minute 分表),计算 QPS、错误率、P95 延迟等指标,并通过 prometheus::Counter / Gauge 暴露。
告警触发走另一条路径:
- 用
prometheus::PushGateway推送瞬时告警事件(非指标),避免拉模式轮询延迟 - 告警规则用 Lua 脚本预编译(如
lua-resty-prometheus的思路),运行时只做数值比对,不解析表达式 - 静默期、抑制规则等状态存在本地
std::unordered_map<:string std::chrono::steady_clock::time_point></:string>,不查外部存储
线程安全不是靠加锁,而是靠“所有权移交”和“只读共享”
所谓“绝对线程安全”,在 C++ 里从来不是指所有对象都可并发读写,而是设计上让每个对象只被一个线程拥有,跨线程传递靠 move 或原子指针交换。比如:
- 每个采集线程独占一个
std::vector<logentry></logentry>缓冲区,满后 move 给发送线程,自己立即分配新缓冲 - 全局配置(如采样率、白名单)用
std::shared_ptr<const config></const>,更新时原子替换指针,旧配置等所有 reader 退出后再 delete - 禁止出现
std::shared_ptr<somemutableclass></somemutableclass>这种设计——它诱导你在线程间共享可变状态
真正的难点不在代码怎么写,而在于定义清楚每个数据的生命期归属。一旦模糊,加再多 std::atomic 也救不回数据竞争。



















