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,让迁移成本降到零。

设计目标

  1. API 兼容:宏定义与 glog 完全一致,LOG_INFO << "msg" 无需改动
  2. 零成本抽象:运行时没有额外开销
  3. 路由增强:在兼容的基础上支持消息路由
  4. 优雅降级:未初始化时输出到 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 APIcompat_glog API说明
LOG_INFO << msgLOG_INFO << msg完全兼容
LOG_WARNING << msgLOG_WARNING << msg完全兼容
LOG_ERROR << msgLOG_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异步

关键发现

  1. 异步优势:Seastar Log Engine (write_ack) 比同步 glog 快 2.3 倍
  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 不是线程安全的。如果需要在多线程场景使用,应该:

  1. 使用 Seastar 的 per-shard 架构
  2. 或者使用 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 兼容层通过以下设计实现零成本迁移:

  1. RAII 流式 API:析构函数自动发送,无需手动管理
  2. 宏兼容:LOG_INFO/LOG_WARNING/LOG_ERROR 完全一致
  3. 路由增强:在兼容的基础上支持业务路由
  4. 优雅降级:未初始化时输出到 stderr

适用场景

  • 现有 glog/spdlog 代码的快速迁移
  • 需要日志路由能力的场景
  • 希望利用 Seastar 异步优势的场景

性能收益

  • 比同步 glog 快 2.3 倍
  • 支持异步批量写入
  • 支持多种确认语义

相关阅读

源码位置

  • include/log_engine/compat_glog.hh
  • src/compat_glog.cc
  • src/compat_demo.cc
  • src/glog_bench.cc