glog/spdlog 兼容层设计:零成本迁移到高性能日志引擎
深入分析 Seastar Log Engine 的 compat_glog 兼容层设计,包括 RAII 流式 API、LogEngine 绑定机制、路由支持以及与原生 glog 的性能对比。
背景与动机
为什么需要兼容层?
在将现有服务迁移到 Seastar Log Engine 时,最大的阻力往往是日志 API 的变更:
// 原有代码(使用 glog)
LOG(INFO) << "Processing request: " << request_id;
// 迁移后需要改成
co_await log_engine.append(LogMessage{
.level = LogLevel::info,
.payload = "Processing request: " + request_id,
});
这种侵入式修改不仅工作量巨大,还容易引入 bug。compat_glog 兼容层通过提供与 glog 完全一致的 API,让迁移成本降到零。
设计目标
- API 兼容:宏定义与 glog 完全一致,
LOG_INFO << "msg"无需改动 - 零成本抽象:运行时没有额外开销
- 路由增强:在兼容的基础上支持消息路由
- 优雅降级:未初始化时输出到 stderr,不丢失日志
核心设计
RAII 流式日志模式
compat_glog 采用 RAII 模式,每条日志语句创建一个 LogLine 对象:
// 使用 glog 风格的宏
LOG_INFO << "Processing request: " << request_id;
展开后等价于:
// 1. 构造 LogLine 对象(返回 stream)
::log_engine::compat::LogLine(
::log_engine::LogLevel::info,
__FILE__,
__LINE__
).stream() << "Processing request: " << request_id;
// ↑
// stream() 返回 ostream
// ↓
// 当这行语句结束时,LogLine 析构函数自动调用 send()
流程图:
┌─────────────────────────────────────────────────────────────┐
│ LOG_INFO << "message" │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ LogLine 构造 │
│ - 保存 level、file、line │
│ - 创建 ostringstream │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ .stream() 返回 ostream& │
│ 用户通过 << 写入内容到内部 stream │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 语句结束 → LogLine 析构 → send() │
│ - 提取 stream 中的内容 │
│ - 添加源码位置前缀 │
│ - 加入 pending_messages 队列 │
└─────────────────────────────────────────────────────────────┘
核心数据结构
// include/log_engine/compat_glog.hh
class LogLine {
public:
LogLine(LogLevel level, const char* file, int line, std::string route_key = {});
~LogLine(); // RAII: 析构时自动发送
std::ostream& stream() noexcept { return _stream; }
void send();
private:
LogLevel _level;
std::string _file;
int _line;
std::string _route_key;
std::ostringstream _stream; // 流式写入
bool _sent = false; // 防止重复发送
};
发送机制
// src/compat_glog.cc
void LogLine::send() {
if (_sent) return; // 防止重复发送
_sent = true;
// 1. 提取流中的内容
auto payload = _stream.str();
if (payload.empty()) return;
// 2. 添加源码位置前缀
auto full_message = _file.empty()
? payload
: ("[" + _file + ":" + std::to_string(_line) + "] " + payload);
// 3. 降级到 stderr(未初始化时)
if (!bound_engine) {
std::fprintf(stderr, "[log_engine::compat] logger not initialized: %s\n", full_message.c_str());
return;
}
// 4. 加入待发送队列
pending_messages.push_back(LogMessage{
.level = _level,
.payload = std::move(full_message),
.route_key = std::move(_route_key),
});
}
LogEngine 绑定机制
绑定与解绑
namespace log_engine::compat {
namespace {
LogEngine* bound_engine = nullptr; // 单例指针
std::vector<LogMessage> pending_messages; // 待发送队列
} // namespace
void bind(LogEngine& engine) noexcept {
bound_engine = &engine;
pending_messages.clear();
}
void unbind() noexcept {
bound_engine = nullptr;
pending_messages.clear();
}
bool is_initialized() noexcept {
return bound_engine != nullptr;
}
批量刷新
seastar::future<> flush() {
if (!bound_engine || pending_messages.empty()) {
co_return;
}
// 移动消息队列,避免锁竞争
auto messages = std::move(pending_messages);
pending_messages.clear();
// 批量提交到 LogEngine
for (auto& message : messages) {
co_await bound_engine->append(std::move(message));
}
}
设计要点:
- 批量提交:减少跨 shard 通信次数
- 移动语义:避免消息拷贝
- 无锁设计:每个 shard 独立的 pending_messages
初始化流程
seastar::future<> init_compat(const EngineConfig& config) {
// 1. 创建 LogEngine
auto engine = std::make_unique<LogEngine>();
co_await engine->start(config);
// 2. 绑定到 compat 层
log_engine::compat::bind(*engine);
// 3. 保存 engine 指针供后续使用
_engine = std::move(engine);
}
seastar::future<> shutdown_compat() {
// 1. 刷新所有待发送消息
co_await log_engine::compat::flush();
// 2. 解绑
log_engine::compat::unbind();
// 3. 停止 engine
co_await _engine->stop();
}
宏定义与 API
标准 glog 兼容宏
// 基础宏
#define LOG_INFO LOG_ENGINE_LOG(info)
#define LOG_WARNING LOG_ENGINE_LOG(warn)
#define LOG_ERROR LOG_ENGINE_LOG(error)
// 展开后
#define LOG_ENGINE_LOG(level) \
::log_engine::compat::LogLine(\
::log_engine::LogLevel::level, \
__FILE__, \
__LINE__\
).stream()
增强的路由宏
// 带路由键的宏
#define LOG_INFO_R(route_key) LOG_ENGINE_LOG_R(info, (route_key))
#define LOG_WARNING_R(route_key) LOG_ENGINE_LOG_R(warn, (route_key))
#define LOG_ERROR_R(route_key) LOG_ENGINE_LOG_R(error, (route_key))
// 展开后
#define LOG_ENGINE_LOG_R(level, route_key) \
::log_engine::compat::LogLine(\
::log_engine::LogLevel::level, \
__FILE__, \
__LINE__, \
(route_key) // 额外的路由键参数
).stream()
API 映射表
| glog API | compat_glog API | 说明 |
|---|---|---|
LOG_INFO << msg | LOG_INFO << msg | 完全兼容 |
LOG_WARNING << msg | LOG_WARNING << msg | 完全兼容 |
LOG_ERROR << msg | LOG_ERROR << msg | 完全兼容 |
| - | LOG_INFO_R("route") << msg | 增强:路由支持 |
google::FlushLogFiles() | compat::flush() | 功能等价 |
| - | compat::bind(engine) | 增强:绑定 LogEngine |
完整使用示例
迁移前(使用 glog)
#include <glog/logging.h>
int main(int argc, char** argv) {
google::InitGoogleLogging(argv[0]);
LOG(INFO) << "Starting application";
LOG(INFO) << "Processing request: " << request_id;
LOG(WARNING) << "Rate limit approaching: " << current_rate;
LOG(ERROR) << "Failed to connect: " << error_code;
google::ShutdownGoogleLogging();
}
迁移后(使用 compat_glog)
#include "log_engine/compat_glog.hh"
int main(int argc, char** argv) {
seastar::app_template app;
return app.run(argc, argv, [&app] () -> seastar::future<> {
// 初始化 LogEngine 并绑定
co_await log_engine::compat::init(config);
// 代码完全不变!
LOG_INFO << "Starting application";
LOG_INFO << "Processing request: " << request_id;
LOG_WARNING << "Rate limit approaching: " << current_rate;
LOG_ERROR << "Failed to connect: " << error_code;
// 使用增强的路由功能
LOG_INFO_R("user-service") << "User logged in: " << user_id;
LOG_INFO_R("order-service") << "Order created: " << order_id;
// 刷新并关闭
co_await log_engine::compat::flush();
co_await log_engine::compat::shutdown();
});
}
完整演示程序
// src/compat_demo.cc
#include "log_engine/compat_glog.hh"
int main(int argc, char** argv) {
seastar::app_template app;
namespace bpo = boost::program_options;
app.add_options()
("log-dir", bpo::value<std::string>()->default_value("logs"), "Log directory")
("messages", bpo::value<std::uint64_t>()->default_value(100), "Message count")
("batch-size", bpo::value<std::size_t>()->default_value(8192), "Batch size")
("routing-strategy", bpo::value<std::string>()->default_value("hash_modulo"),
"Routing strategy");
return app.run(argc, argv, [&app] () -> seastar::future<> {
auto& conf = app.configuration();
// 1. 配置引擎
log_engine::EngineConfig config;
config.log_dir = conf["log-dir"].as<std::string>();
config.batch_size = conf["batch-size"].as<std::size_t>();
config.routing_strategy = parse_routing_strategy(conf["routing-strategy"].as<std::string>());
// 2. 初始化并绑定
co_await log_engine::compat::init(config);
std::cout << "=== compat_glog Demo ===" << std::endl;
// 3. 使用类 glog API
const auto total = conf["messages"].as<std::uint64_t>();
for (std::uint64_t i = 0; i < total; ++i) {
LOG_INFO << "compat-demo-" << i; // 普通日志
LOG_WARNING_R("compat-route") << "warning-" << i; // 带路由
}
// 4. 刷新
std::cout << "Flushing " << total << " messages..." << std::endl;
co_await log_engine::compat::flush();
std::cout << "Done!" << std::endl;
co_await log_engine::compat::shutdown();
});
}
与原生 glog 的性能对比
Benchmark 程序
// src/glog_bench.cc(原生 glog)
#include <glog/logging.h>
int main(int argc, char** argv) {
FLAGS_log_dir = "logs-glog";
FLAGS_logbufsecs = 0; // 立即刷新
google::InitGoogleLogging(argv[0]);
for (std::uint64_t i = 0; i < messages; ++i) {
LOG(INFO) << "glog-bench-" << i << ' ' << payload;
}
google::FlushLogFiles(google::INFO);
google::ShutdownGoogleLogging();
}
性能对比结果
| 日志库 | 吞吐量 (msg/s) | 特性 |
|---|---|---|
| Seastar Log Engine (write_ack) | 1,500,000 | 异步批量写入 |
| Seastar Log Engine (sync_ack) | 150,000 | 同步确认 |
| glog (buffered) | 648,441 | 同步,有缓冲 |
| glog (unbuffered) | ~100,000 | 同步,无缓冲 |
| spdlog (async) | 1,024,460 | 异步 |
关键发现:
- 异步优势:Seastar Log Engine (write_ack) 比同步 glog 快 2.3 倍
- 批量收益:批量提交策略使吞吐提升一个数量级
- 功能增强:除性能外,还支持路由、rotate、checkpoint
测试命令
# 1. 构建
./script/build.sh
# 2. 运行对比 benchmark
./script/compare_bench.sh --messages 50000 --payload-size 256
路由增强
为什么需要路由?
传统 glog 所有日志写入同一个文件,查询时需要全文搜索。compat_glog 在保持 API 兼容的同时,支持按业务维度路由:
// 普通日志(写入本地 shard)
LOG_INFO << "Application started";
// 带路由的日志(按服务分发)
LOG_INFO_R("user-service") << "User logged in: " << user_id;
LOG_INFO_R("order-service") << "Order created: " << order_id;
LOG_INFO_R("payment-service") << "Payment processed: " << payment_id;
路由效果
未使用路由:
logs/shard-0.log → 所有日志混在一起
使用路由:
logs/shard-0.log → user-service 的日志(shard 0)
logs/shard-1.log → order-service 的日志(shard 1)
logs/shard-2.log → payment-service 的日志(shard 2)
查询优化
# 查询特定服务的日志
curl "http://localhost:18080/v1/records?route=user-service&limit=100"
注意事项
1. shard 内单例
compat_glog 使用 shard 内单例,每个 shard 独立的 bound_engine:
// 每个 shard 独立的状态
namespace {
LogEngine* bound_engine = nullptr;
std::vector<LogMessage> pending_messages;
} // namespace
含义:
- 适合 Seastar 的 per-shard 架构
- 不同 shard 之间完全隔离
- 跨 shard 日志需要通过路由机制
2. 异步刷新时机
// 错误:析构函数可能不等待
{
LOG_INFO << "message";
} // 可能还没发送出去,scope 就结束了
// 正确:显式刷新
{
LOG_INFO << "message";
}
co_await compat::flush(); // 确保发送完成
3. 线程安全
compat_glog 不是线程安全的。如果需要在多线程场景使用,应该:
- 使用 Seastar 的 per-shard 架构
- 或者使用
submit()直接提交,不依赖宏
// 多线程场景的安全用法
co_await log_engine::compat::submit(
LogLevel::info,
"Thread-safe message",
"route-key"
);
4. 未初始化时的降级
// 未调用 bind() 时,输出到 stderr
LOG_INFO << "Test message";
// 输出: [log_engine::compat] logger not initialized: Test message
源码结构
文件清单
seastar-log-engine/
├── include/log_engine/
│ └── compat_glog.hh # 头文件 + 宏定义
├── src/
│ ├── compat_glog.cc # 核心实现
│ ├── compat_demo.cc # 演示程序
│ └── glog_bench.cc # glog benchmark
└── script/
└── compare_bench.sh # 对比测试脚本
头文件完整源码
// include/log_engine/compat_glog.hh
#pragma once
#include <memory>
#include <optional>
#include <ostream>
#include <sstream>
#include <string>
#include <seastar/core/future.hh>
#include "log_engine/config.hh"
#include "log_engine/log_engine.hh"
namespace log_engine::compat {
void bind(LogEngine& engine) noexcept;
void unbind() noexcept;
seastar::future<> flush();
bool is_initialized() noexcept;
seastar::future<> submit(LogLevel level, std::string message, std::string route_key = {});
class LogLine {
public:
LogLine(LogLevel level, const char* file, int line, std::string route_key = {});
~LogLine();
std::ostream& stream() noexcept { return _stream; }
void send();
private:
LogLevel _level;
std::string _file;
int _line;
std::string _route_key;
std::ostringstream _stream;
bool _sent = false;
};
} // namespace log_engine::compat
// 标准 glog 兼容宏
#define LOG_ENGINE_LOG(level) \
::log_engine::compat::LogLine(::log_engine::LogLevel::level, __FILE__, __LINE__).stream()
#define LOG_ENGINE_LOG_R(level, route_key) \
::log_engine::compat::LogLine(::log_engine::LogLevel::level, __FILE__, __LINE__, (route_key)).stream()
#define LOG_INFO LOG_ENGINE_LOG(info)
#define LOG_WARNING LOG_ENGINE_LOG(warn)
#define LOG_ERROR LOG_ENGINE_LOG(error)
#define LOG_INFO_R(route_key) LOG_ENGINE_LOG_R(info, (route_key))
#define LOG_WARNING_R(route_key) LOG_ENGINE_LOG_R(warn, (route_key))
#define LOG_ERROR_R(route_key) LOG_ENGINE_LOG_R(error, (route_key))
实现文件完整源码
// src/compat_glog.cc
#include "log_engine/compat_glog.hh"
#include <cstdio>
#include <exception>
#include <utility>
#include <vector>
namespace log_engine::compat {
namespace {
LogEngine* bound_engine = nullptr;
std::vector<LogMessage> pending_messages;
} // namespace
void bind(LogEngine& engine) noexcept {
bound_engine = &engine;
pending_messages.clear();
}
void unbind() noexcept {
bound_engine = nullptr;
pending_messages.clear();
}
seastar::future<> flush() {
if (!bound_engine || pending_messages.empty()) {
co_return;
}
auto messages = std::move(pending_messages);
pending_messages.clear();
for (auto& message : messages) {
co_await bound_engine->append(std::move(message));
}
}
bool is_initialized() noexcept {
return bound_engine != nullptr;
}
seastar::future<> submit(LogLevel level, std::string message, std::string route_key) {
if (!bound_engine) {
std::fprintf(stderr, "[log_engine::compat] logger not initialized: %s\n", message.c_str());
co_return;
}
co_await bound_engine->append(LogMessage{
.level = level,
.payload = std::move(message),
.route_key = std::move(route_key),
});
}
LogLine::LogLine(LogLevel level, const char* file, int line, std::string route_key)
: _level(level)
, _file(file ? file : "")
, _line(line)
, _route_key(std::move(route_key)) {
}
LogLine::~LogLine() {
send();
}
void LogLine::send() {
if (_sent) return;
_sent = true;
auto payload = _stream.str();
if (payload.empty()) return;
auto full_message = _file.empty()
? payload
: ("[" + _file + ":" + std::to_string(_line) + "] " + payload);
if (!bound_engine) {
std::fprintf(stderr, "[log_engine::compat] logger not initialized: %s\n", full_message.c_str());
return;
}
pending_messages.push_back(LogMessage{
.level = _level,
.payload = std::move(full_message),
.route_key = std::move(_route_key),
});
}
std::ostream& LogLine::stream() noexcept {
return _stream;
}
} // namespace log_engine::compat
总结
compat_glog 兼容层通过以下设计实现零成本迁移:
- RAII 流式 API:析构函数自动发送,无需手动管理
- 宏兼容:LOG_INFO/LOG_WARNING/LOG_ERROR 完全一致
- 路由增强:在兼容的基础上支持业务路由
- 优雅降级:未初始化时输出到 stderr
适用场景:
- 现有 glog/spdlog 代码的快速迁移
- 需要日志路由能力的场景
- 希望利用 Seastar 异步优势的场景
性能收益:
- 比同步 glog 快 2.3 倍
- 支持异步批量写入
- 支持多种确认语义
相关阅读:
源码位置:
include/log_engine/compat_glog.hhsrc/compat_glog.ccsrc/compat_demo.ccsrc/glog_bench.cc