HLog 是一个面向高并发场景的 C++20 异步日志库,核心热路径使用基于 CAS 的无锁环形队列替代 mutex 队列,重点优化多线程日志写入下的锁竞争、payload 构造和单消费者输出路径。
- docs/README.md:文档导航和阅读顺序。
- docs/perf.md:吞吐、调用延迟、payload 热路径和文件 sink benchmark 报告。
- docs/简历材料.md:项目简历写法、亮点提炼和面试讲法。
- docs/project-study-guide-complete.md:完整学习路线、代码阅读顺序、架构拆解和生产化边界。
- docs/面试问题完整回答.md:高频面试问题与完整回答。
- docs/source-code-walkthrough-complete.md:源码逐文件导读、核心函数索引和面试定位表。
- docs/integration-guide.md:日志库接入指南。
- docs/performance-tuning-playbook.md:性能实验与调优手册。
- docs/production-hardening-roadmap.md:生产化加固路线。
- 采用“日志器 + 输出端 + 后台线程”分层架构,支持日志分级、异步落盘和自定义输出端。
- 内置
FileSink / ConsoleSink / RotatingFileSink / MultiSink,支持文件、控制台、轮转文件以及多目标 fan-out 输出。 - 提供
PatternFormatter与LoggerConfig配置层,支持格式模板、sink 组合和运行时组装 logger。 - 使用基于
CAS的有界无锁环形队列,结合轻量级唤醒路径、原子票据和thread_local线程 ID 缓存降低多线程写日志时的热路径开销。 - 将 producer 热路径的 payload 构造改为
256Binline buffer +std::to_chars直接追加,减少常见短日志场景下的额外堆分配和流格式化开销。 - 在
FileSink内部增加 staging buffer 批量写,支持max_batch_size / flush_interval,把后台线程 drain 出来的多条日志拼成连续 buffer 后统一写出,并在空闲期按 deadline 自动刷盘。 - 后台 sink 抛出异常时不会直接
terminate整个进程,而是进入可观测失败状态,可通过failed()/failure_message()查询。 Stop()采用“两阶段关停”语义:先拒绝新的Log()/Flush()调用,再等待已入场操作完成发布并由后台线程排空队列。- 提供阻塞与丢弃两种满队列策略,并通过 install smoke test、sanitizer、内部基线 benchmark 和可选
spdlogbenchmark 量化与验证效果。
同步日志实现起来很直接,但在高并发场景下容易暴露几个典型问题:
- 多个业务线程会竞争同一把锁,日志路径容易成为热点。
- 队列节点的频繁申请与释放会放大额外开销。
- 唤醒与等待逻辑如果依赖互斥锁,线程协调成本会持续累积。
- 日志洪峰可能同时带来延迟抖动和内存膨胀。
目标是在高并发写日志场景下提供低开销异步写入能力,并补齐 sink 组合、安装导出、自动化验证和 benchmark 报告等工程闭环。
当前版本已经可以作为可复用的异步日志库集成到其他项目中;同时仍然保持清晰边界,重点覆盖并发队列、producer payload 和文件写路径优化,而不是扩展成大而全的日志生态集合。
- 异步写入:业务线程只负责构造日志并入队,后台线程统一顺序写出。
- 多线程安全:支持多生产者并发写日志,单消费者独占输出端。
- 日志分级:支持
trace / debug / info / warn / error / critical / off。 - 背压策略:支持
Block和DropNewest两种满队列处理方式。 - 刷盘同步:支持显式
Flush(),通过原子票据等待后台线程完成刷盘。 - 关停语义:
Stop()后拒绝新调用,但会继续 drain 已接受日志;Block策略下已进入热路径的 producer 不会被中途打断。 - 空闲期刷盘:
flush_interval到期后,即使没有新日志到来,也会自动 flush 已缓存批次。 - 故障可观测:后台线程捕获 sink 异常后会拒绝后续写入,并保留失败信息。
- 内置输出端:提供
ConsoleSink、FileSink、RotatingFileSink和MultiSink。 - 格式与配置:提供
PatternFormatter和LoggerConfigfactory。
业务线程
|
v
异步日志器
- 日志级别过滤
- 消息格式化
- CAS 无锁环形队列
- 原子唤醒与刷盘协调
|
v
后台消费线程
|
v
输出端(控制台 / 文件输出 / 轮转文件 / 组合输出)
核心组件:
hlog::LockFreeRingBuffer<T>:基于槽位序号的有界无锁队列。hlog::AsyncLogger:提供日志接口、等级过滤、异步投递、刷盘与统计能力。hlog::Sink:输出端抽象。hlog::ConsoleSink / FileSink / RotatingFileSink / MultiSink:内置输出端实现。hlog::PatternFormatter / LoggerConfig:格式模板与运行时配置装配层。
- 环形缓冲区可以预分配内存,避免日志热路径上的频繁动态分配。
- 容量有上界,日志洪峰不会无限制挤占内存。
- 队列容量取 2 的幂后,可以通过位运算完成回绕,减少取模开销。
- 多个生产者只竞争原子游标,而不是进入同一个临界区。
- 在高并发场景下,可显著降低
mutex + condition_variable带来的锁竞争与队头阻塞。 - 每个槽位维护独立序号,能够在无锁条件下判断“可写 / 可读 / 已满”等状态。
- 文件写出天然需要顺序性,单消费者更容易保证日志顺序。
- 输出端只由后台线程持有,不需要在写盘路径上重复加锁。
- 可以把并发问题收敛到“入队和出队”这条链路,而不是让队列竞争和输出竞争混在一起。
- 生产者通过
compare_exchange_weak抢占槽位并发布日志消息。 - 消费线程独占出队和输出端写入,避免多线程同时写文件。
Flush()会投递控制消息,并等待原子完成票据更新。Stop()会先关闭新操作入口,再等待已进入Log()/Flush()的调用完成发布或失败路径,最后由后台线程排空剩余队列。- 后台线程会同时根据工作信号和 sink 的下一次自动刷盘 deadline 协调等待与唤醒。
仓库中的 hlog_compare_benchmark 在同样的异步模型和同样的内存输出端下,对比两种方案:
- 互斥锁方案:
std::mutex + std::condition_variable + std::deque - 无锁方案:
CAS无锁环形队列 + 轻量级后台唤醒路径
示例命令:
./build/hlog_compare_benchmark 8 20000 5 1当前机器上一组样例结果如下(2026-05-07,1 次 warm-up + 5 次 measured rounds,取中位吞吐;对照组是仓库内 mutex + condition_variable + deque 基线,而不是外部日志库):
| 实现方案 | 吞吐 |
|---|---|
| 互斥锁异步队列 | 1.38e6 msg/s |
CAS 无锁异步队列 |
4.35e6 msg/s |
| 吞吐提升 | +215% |
说明:
- 这个压测刻意使用内存输出端,目的是隔离磁盘 I/O 干扰,专门观察队列与线程同步开销。
- benchmark 会输出 warm-up、每轮测量结果以及 summary,建议优先引用中位吞吐而不是单次最好成绩。
- 绝对数值会受机器配置、编译器和优化选项影响,但趋势可以反映锁竞争差异和扩展性差异。
- 更完整的多线程扩展数据、调用延迟表格和图表见 docs/perf.md。
仓库中的 hlog_spdlog_compare_benchmark 会在检测到 spdlog 后自动构建,使用同样的 producer payload、同样的单消费者异步模型和同样的内存计数 sink,对比:
spdlog异步 loggerhlog的CAS异步 logger
示例命令:
./build/hlog_spdlog_compare_benchmark 8 20000 5 1当前机器上一组样例结果如下(2026-05-07,1 次 warm-up + 5 次 measured rounds,取中位吞吐):
| 实现方案 | 吞吐 |
|---|---|
spdlog 异步 logger |
3.36e5 msg/s |
hlog CAS 异步 logger |
4.27e6 msg/s |
| 吞吐提升 | +1170% |
说明:
- 这组对比仍然刻意使用内存 sink,回答的是“现成异步日志库在相似写路径上的 producer + queue 开销对比”,不是完整文件落盘场景结论。
spdlogbenchmark 是可选 target;本仓库不会强制拉第三方依赖,但.github/workflows/ci.yml中的external-benchmark-smoke会在 Ubuntu 上安装libspdlog-dev并跑一轮 smoke。- 如果本机没有
spdlog,CMake 会跳过hlog_spdlog_compare_benchmark,其余构建、测试和 install/export 不受影响。
仓库中的 hlog_latency_benchmark 使用同样的异步模型和同样的内存输出端,测量 producer 侧单次 Log() 调用延迟:
./build/hlog_latency_benchmark 8 20000 5 1当前机器上一组样例结果如下(2026-05-07,1 次 warm-up + 5 次 measured rounds,取中位;表格单位为 us):
| 实现方案 | 平均调用延迟 | p95 | p99 |
|---|---|---|---|
| 互斥锁异步队列 | 9.40 |
47.58 |
85.27 |
CAS 无锁异步队列 |
1.52 |
3.17 |
6.78 |
说明:
- 这个 benchmark 只包围 producer 侧单次日志调用,适合观察业务线程写日志时的直接开销。
- 结果包含
steady_clock取时开销,因此更适合做相对比较,而不是解读成绝对“函数体净耗时”。 - 为了隔离磁盘 I/O 干扰,latency benchmark 同样使用内存输出端。
- 正式报告可通过
scripts/generate_benchmark_report.py重新生成。
仓库中的 hlog_payload_benchmark 保持同一个 hlog::AsyncLogger 和同一个内存输出端不变,只比较 producer 如何构造 payload:
prebuilt_*:先用std::ostringstream拼成std::string,再调用Info(),近似优化前热路径。variadic_short:直接调用Info("thread=", tid, " seq=", index),命中 inline payload。variadic_long:直接调用Info(..., " payload=", <512B blob>),触发 spillover 路径。
示例命令:
./build/hlog_payload_benchmark 8 20000 5 1当前机器上一组样例结果如下(2026-05-07,1 次 warm-up + 5 次 measured rounds,取中位;表格单位为 us):
| 场景 | 旧热路径均值 | 新热路径均值 | 均值改善 | 旧热路径 p99 | 新热路径 p99 |
|---|---|---|---|---|---|
| 短日志(inline) | 3.57 |
1.24 |
65.2% |
7.71 |
2.03 |
| 长日志(spill) | 5.05 |
1.61 |
68.1% |
14.48 |
5.11 |
说明:
- 这个 benchmark 不是
mutex对照,而是同一个 logger 内部对比“旧 payload 构造方式”和“新 payload 构造方式”。 - 它更适合回答“lock-free queue 之外,producer 侧格式化和分配还剩多少成本”这个问题。
- 更完整的多线程表格和图表见 docs/perf.md 中的
Payload Hot Path章节。
仓库中的 hlog_file_sink_benchmark 使用真实 FileSink 路径,对比:
unbatched_file_sink:max_batch_size=1,近似逐条写文件。batched_file_sink:max_batch_size=64 KiB、flush_interval=250 ms,把多条格式化日志拼成连续 buffer 后统一写出。
示例命令:
./build/hlog_file_sink_benchmark 8 1000 2 0当前机器上一组样例结果如下(2026-05-07,2 次 measured rounds,取中位):
| 场景 | 吞吐 |
|---|---|
| 非批量 file sink | 1.19e3 msg/s |
| 批量 file sink | 1.01e3 msg/s |
| 吞吐变化 | -15.5% |
说明:
- 这组 benchmark 使用
512B级别 payload,比内存 sink benchmark 更接近真实日志体积。 - 真实文件系统路径会引入缓存、元数据和存储设备噪声,所以它回答的是“端到端写路径局部优化是否有价值”,不是跨机器可复现的绝对指标。
- 这部分结果的符号和幅度都可能随本机负载、缓存状态和存储介质变化而波动,因此更适合展示方法和测试口径,而不是在 README 里承诺一个稳定的正向百分比。
LICENSE
Dockerfile
docker-compose.yml
CMakePresets.json
include/hlog/
async_logger.h
hlog.h
logger_config.h
log_level.h
pattern_formatter.h
sink.h
detail/
lock_free_ring_buffer.h
log_message.h
log_payload.h
sinks/
console_sink.h
file_sink.h
multi_sink.h
rotating_file_sink.h
cmake/
hlogConfig.cmake.in
src/
config/
logger_config.cpp
core/
async_logger.cpp
pattern_formatter.cpp
sinks/
console_sink.cpp
file_sink.cpp
rotating_file_sink.cpp
examples/
basic_example.cpp
benchmark_support.h
benchmark_main.cpp
compare_benchmark.cpp
find_package_consumer/
file_sink_benchmark.cpp
latency_benchmark.cpp
payload_benchmark.cpp
service_example.cpp
spdlog_compare_benchmark.cpp
tests/
async_logger_test.cpp
docs/
perf.md
notes/
resume-notes.md
scripts/
generate_benchmark_report.py
install_smoke_test.sh
本地构建、运行日志和安装产物统一收口到 out/:
out/build/<preset>/:CMake 构建目录out/runtime/:示例与 benchmark 默认日志输出out/install/package/:本地 install 前缀
cmake --preset dev
cmake --build --preset dev
ctest --preset dev示例程序:
./out/build/dev/hlog_example
./out/build/dev/hlog_benchmark
./out/build/dev/hlog_compare_benchmark
./out/build/dev/hlog_file_sink_benchmark
./out/build/dev/hlog_latency_benchmark
./out/build/dev/hlog_payload_benchmark
./out/build/dev/hlog_service_example如果本机已安装 spdlog,还会额外生成:
./out/build/dev/hlog_spdlog_compare_benchmark生成的日志默认写入 out/runtime/ 下的 example.log、benchmark.log 和 service.log。
如果希望改到别的位置,可以设置 HLOG_ARTIFACT_DIR=/your/path 或直接对服务示例指定 HLOG_LOG_PATH。
服务示例支持通过环境变量调整端口、级别、pattern 和 rotation 参数,例如:
HLOG_PORT=8080 \
HLOG_LEVEL=info \
HLOG_PATTERN="%Y-%m-%d %H:%M:%S.%e [%l] [%n] [tid=%t] %v" \
./out/build/dev/hlog_service_example也可以直接在容器里构建并运行服务示例:
docker compose build hlog-service
docker compose run --service-ports -e HLOG_MAX_REQUESTS=1 hlog-service
curl -sS http://127.0.0.1:18080/healthz如果只想把它当作库来构建,可以关闭 examples 和 tests:
cmake --preset package
cmake --build --preset package
cmake --install out/build/package安装后可以在其他 CMake 项目中通过 find_package 使用:
find_package(hlog CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE HLog::hlog)仓库内提供了可复现的安装烟测脚本和最小 consumer:
bash scripts/install_smoke_test.sh- 外部 consumer 工程位于
examples/find_package_consumer/ - 脚本会执行
cmake --install,再在临时目录里用find_package(hlog CONFIG REQUIRED)配置、链接并运行 consumer - GitHub Actions 中的
package-smokejob 会在ubuntu-latest和macos-latest上自动执行这套流程
当前测试覆盖:
- 日志等级过滤正确性
- 并发异步写入正确性
DropNewest策略下的溢出行为Flush()对未消费日志的等待语义Stop()后拒绝新写入,以及pending统计归零Stop()不显式Flush()时仍能排空已接受的日志Stop()与Block背压并发发生时,已进入Log()但暂时卡在满队列上的 producer 仍会被 drain 完成FileSink/RotatingFileSink在空闲期按flush_interval自动刷盘- 后台 sink 异常被捕获并转化为 logger 失败状态
PatternFormattertoken 展开与LogLevel解析MultiSinkfan-out 行为ConsoleSink格式化输出RotatingFileSink按大小轮转LoggerConfigfactory 构建 file sink logger
执行命令:
ctest --preset dev仓库包含 GitHub Actions workflow:.github/workflows/ci.yml。
- 常规 CI:在
ubuntu-latest和macos-latest上执行Release构建与测试 - 安装复用验证:在
ubuntu-latest和macos-latest上执行 install smoke test - 外部基线验证:在
ubuntu-latest上安装libspdlog-dev并运行hlog_spdlog_compare_benchmarksmoke - 并发/内存检查:在
ubuntu-latest + clang上执行ASAN + UBSAN与TSAN
本地也可以直接复用同一套开关:
cmake --preset asan
cmake --build --preset asan
ctest --preset asan
cmake --preset tsan
cmake --build --preset tsan
ctest --preset tsan仓库提供了性能报告生成脚本,会批量跑吞吐和延迟 benchmark,并生成 CSV、SVG 图表和 Markdown 报告:
python3 scripts/generate_benchmark_report.py \
--compare-binary ./out/build/dev/hlog_compare_benchmark \
--latency-binary ./out/build/dev/hlog_latency_benchmark \
--payload-binary ./out/build/dev/hlog_payload_benchmark \
--file-sink-binary ./out/build/dev/hlog_file_sink_benchmark \
--output-dir docs/perf \
--messages-per-thread 20000 \
--file-messages-per-thread 1000 \
--measured-rounds 5 \
--warmup-rounds 1 \
--file-measured-rounds 2 \
--file-warmup-rounds 0 \
--threads 1 2 4 8 16 \
--file-threads 1 2 4 8输出产物默认写入:
docs/perf.mddocs/perf/throughput.csvdocs/perf/latency.csvdocs/perf/payload-latency.csvdocs/perf/file-sink-throughput.csvdocs/perf/throughput.svgdocs/perf/mean-latency.svgdocs/perf/p99-latency.svgdocs/perf/payload-short-mean-latency.svgdocs/perf/payload-short-p99-latency.svgdocs/perf/payload-long-mean-latency.svgdocs/perf/payload-long-p99-latency.svgdocs/perf/file-sink-throughput.svg
HLog 采用“日志器 + 输出端 + 后台线程”的分层架构,使用基于 CAS 的有界无锁环形队列承接多线程日志写入,通过后台线程顺序写出,并提供日志分级、阻塞/丢弃两种背压策略以及显式 Flush() 同步机制。仓库当前内置 ConsoleSink / FileSink / RotatingFileSink / MultiSink,并通过 PatternFormatter 和 LoggerConfig 提供格式模板、sink 组合和运行时装配能力;同时支持 CMake install/export、最小 consumer、install smoke test 和一个多线程 HTTP 服务示例。为了验证核心优化路径,仓库还补充了与 mutex + condition_variable 异步队列的同模型对照压测、可选 spdlog 外部基线、producer 侧调用延迟、payload hot-path 和真实 file-sink benchmark。当前机器上的样例结果显示,在 8 线程、160000 条日志、1 次 warm-up + 5 次测量的中位口径下,队列热路径吞吐从约 1.38e6 msg/s 提升到约 4.35e6 msg/s,单次 Log() 平均延迟从约 9.40 us 降到约 1.52 us,短日志 payload 构造均值从约 3.57 us 降到约 1.24 us。真实 file-sink 路径的结果单独保留在 docs/perf.md 中,因为它比内存 sink benchmark 更容易受文件系统状态和本机负载影响。
本项目采用 MIT License。