Ceph 服务器日志系统通过宏定义和线程机制实现高效日志记录。首先,通过枚举和数组定义日志子系统(如osd、mon),并利用 dout 、 dendl 等宏展开为条件判断和日志组装代码。服务启动时,global_init 初始化上下文并创建独立的 log 线程作为消费者。日志输出时,生产者通过宏替换生成实际代码,判断日志级别后,将日志条目提交到队列,若队列满则阻塞。 log 线程从队列取出日志,根据配置将日志输出到文件、 stderr 、 syslog 、 graylog 或 journald 等多个目标,支持灵活的日志级别和输出控制。整个流程实现了异步、可配置的多目标日志记录。

注意: 以下的分析基于 Ceph V20.2.0

一、服务器侧日志

1.1、日志环境准备

1.1.1、日志子系统

日志子系统的枚举定义:

// 枚举定义
enum ceph_subsys_id_t
{
ceph_subsys_, // default
#define SUBSYS(name, log, gather) ceph_subsys_##name,
#define DEFAULT_SUBSYS(log, gather)
#include "common/subsys.h"
#undef SUBSYS
#undef DEFAULT_SUBSYS
ceph_subsys_max
};


// 展开宏之后的枚举定义示例
enum ceph_subsys_id_t
{
ceph_subsys_, // default (0)
ceph_subsys_osd, // 1
ceph_subsys_mon, // 2
ceph_subsys_client, // 3
ceph_subsys_max // 4
};

日志子系统的数组定义:

// 函数定义
constexpr static std::array<ceph_subsys_item_t, ceph_subsys_get_num()> ceph_subsys_get_as_array()
{
#define SUBSYS(name, log, gather) ceph_subsys_item_t{#name, log, gather},
#define DEFAULT_SUBSYS(log, gather) ceph_subsys_item_t{"none", log, gather},

return {
#include "common/subsys.h"
};
#undef SUBSYS
#undef DEFAULT_SUBSYS
}

// 展开宏之后的函数定义示例
constexpr static std::array<ceph_subsys_item_t, 4> ceph_subsys_get_as_array()
{
return {
ceph_subsys_item_t{"none", 0, 0}, // DEFAULT_SUBSYS(0, 0)
ceph_subsys_item_t{"osd", 1, 5}, // SUBSYS(osd, 1, 5)
ceph_subsys_item_t{"mon", 1, 5}, // SUBSYS(mon, 1, 5)
ceph_subsys_item_t{"client", 0, 5}, // SUBSYS(client, 0, 5)
};
}

日志子系统的最长名称定义: (目前该函数没有被实际使用。)

// 函数定义
constexpr static std::size_t ceph_subsys_max_name_length()
{
return std::max({
#define SUBSYS(name, log, gather) strlen_ct(#name),
#define DEFAULT_SUBSYS(log, gather) strlen_ct("none"),
#include "common/subsys.h"
#undef SUBSYS
#undef DEFAULT_SUBSYS
});
}

// 展开宏之后的函数定义示例
constexpr static std::size_t ceph_subsys_max_name_length()
{
// 展开为:
return std::max({
strlen_ct("none"), // DEFAULT_SUBSYS(0, 0) → "none" (4个字符)
strlen_ct("osd"), // SUBSYS(osd, 1, 5) → "osd" (3个字符)
strlen_ct("mon"), // SUBSYS(mon, 1, 5) → "auth" (4个字符)
strlen_ct("client"), // SUBSYS(client, 0, 5) → "client" (6个字符)
});
}

1.1.2、日志输出宏定义

日志输出示例:

dout(1) << " ignoring boot message without a port" << dendl;

相关宏:

  • dout(v): 输出日志调用的日志开头的宏;
  • dout_context:
  • dendl: 输出日志调用的日志结尾的宏;
  • dendl_impl: 由 dendl 宏定义中使用的宏,用于拼接输出日志的代码逻辑块;
  • ldout(cct, v): 由 dout(v) 宏定义中使用的宏;
  • dout_impl(cct, sub, v): 由 ldout(cct, v) 宏定义中使用的宏;
  • dout_subsys: 由 ldout(cct, v) 宏定义中使用的宏;
  • dout_prefix: 由 ldout(cct, v) 宏定义中使用的宏;

相关宏定义代码:

// src/common/debug.h 文件中定义 dout 宏
#define dout(v) ldout((dout_context), (v))

// 众多文件中定义 dout_context 宏
// 以下是一个示例,其中 g_ceph_context 是 ceph 全局上下文对象
#define dout_context g_ceph_context

// src/common/dout.h 文件中定义 ldout 宏
#define ldout(cct, v) dout_impl(cct, dout_subsys, v) dout_prefix

// src/common/dout.h 文件中定义 dendl 宏
#define dendl dendl_impl

// 众多文件中定义 dout_subsys 宏
// 以下是一个示例
#define dout_subsys ceph_subsys_auth

// 众多文件中定义 dout_prefix 宏
// 以下是一个示例
#undef dout_prefix
#define dout_prefix *_dout << "auth: "

// src/common/dout.h 文件中定义的 dout_impl 和 dendl_impl 宏
#ifdef WITH_CRIMSON
#define dout_impl(cct, sub, v) \
do { \
if (crimson::common::local_conf()->subsys.should_gather(sub, v)) { \
seastar::logger &_logger = crimson::get_logger(sub); \
const auto _lv = v; \
std::ostringstream _out; \
std::ostream *_dout = &_out;
#define dendl_impl \
""; \
_logger.log(crimson::to_log_level(_lv), "{}", _out.str().c_str()); \
} \
} \
while (0)
#else
#define dout_impl(cct, sub, v) \
do { \
const bool should_gather = [&](const auto cctX, auto sub_, auto v_) { \
/* The check is performed on `sub_` and `v_` to leverage the C++'s \
* guarantee on _discarding_ one of blocks of `if constexpr`, which \
* includes also the checks for ill-formed code (`should_gather<>` \
* must not be feed with non-const expresions), BUT ONLY within \
* a template (thus the generic lambda) and under the restriction \
* it's dependant on a parameter of this template). \
* GCC prior to v14 was not enforcing these restrictions. */ \
if constexpr (ceph::dout::is_dynamic<decltype(sub_)>::value || \
ceph::dout::is_dynamic<decltype(v_)>::value) { \
return cctX->_conf->subsys.should_gather(sub, v); \
} else { \
constexpr auto sub_helper = static_cast<decltype(sub_)>(sub); \
constexpr auto v_helper = static_cast<decltype(v_)>(v); \
/* The parentheses are **essential** because commas in angle \
* brackets are NOT ignored on macro expansion! A language's \
* limitation, sorry. */ \
return (cctX->_conf->subsys \
.template should_gather<sub_helper, v_helper>()); \
} \
}(cct, sub, v); \
\
if (should_gather) { \
ceph::logging::MutableEntry _dout_e(v, sub); \
static_assert( \
std::is_convertible<decltype(&*cct), CephContext *>::value, \
"provided cct must be compatible with CephContext*"); \
auto _dout_cct = cct; \
std::ostream *_dout = &_dout_e.get_ostream();

// 展开 dout_prefix 宏内容,比如:
// *_dout << "auth: "

// 继续输出对应的日志内容,比如:
// << " ignoring boot message without a port" << dendl;

#define dendl_impl \
std::flush; \
_dout_cct->_log->submit_entry(std::move(_dout_e)); \
} \
} \
while (0)
#endif // WITH_CRIMSON

1.1.3、日志线程初始化

每个服务组件启动的时候基本都会调用 global_init 函数进行一些初始化的流程,在这个流程中会初始化每个服务输出日志所需要的 context ,并创建处理日志的 log 线程。

创建 log 线程流程示意图:

graph LR
    A[global_init] --> B[global_pre_init]

    B --> C[common_preinit <br/>初始化context]
    C --> C1[CephContext::CephContext <br/>初始化 log 对象]

    B --> D[global_init_set_globals<br/>设置 g_ceph_context]

    B --> E[Log::start<br/>创建名为 log 的线程]

    E --> F[Thread::create]
    F --> G[Thread::try_create]
    G --> H[Thread::_entry_func]
    H --> I[Thread::entry_wrapper]
    I --> J[Log::entry]

    style E fill:#e8f5e8,stroke:#333,stroke-width:1px
    style J fill:#e8f5e8,stroke:#333,stroke-width:1px

创建 log 线程流程:

+ global_init -> global_pre_init
+ global_pre_init -> common_preinit -初始化context-> CephContext::CephContext(初始化 log 对象)
+ global_pre_init -> global_init_set_globals(设置 g_ceph_context)
+ global_pre_init -> Log::start -创建名为 log 的线程-> Thread::create -> Thread::try_create -> Thread::_entry_func -> Thread::entry_wrapper -> Log::entry

1.2、日志输出流程

当执行日志输出的,我们需要经过一些宏替换,并实现最终的输出日志的代码。

  • 原始的输出代码示例: dout(1) << "Updating MDS map to version " << epoch << " from " << m->get_source() << dendl;
  • 实际的输出日志示例: 2025-11-21T03:07:59.122+0800 7f8365678700 1 mds.node01 Updating MDS map to version 158945 from mon.0

1.2.1、日志生产者

代码中的这种输出日志的代码 dout(1) << "Updating MDS map to version " << epoch << " from " << m->get_source() << dendl; ,实际在运行的时候会被替换为如下的示例代码:

// 展开 dout 及相关的宏
do {
const bool should_gather = [&](const auto cctX, auto sub_, auto v_) {
if constexpr (ceph::dout::is_dynamic<decltype(sub_)>::value ||
ceph::dout::is_dynamic<decltype(v_)>::value) {
return cctX->_conf->subsys.should_gather(dout_subsys, 1);
} else {
constexpr auto sub_helper = static_cast<decltype(sub_)>(dout_subsys);
constexpr auto v_helper = static_cast<decltype(v_)>(1);
return (cctX->_conf->subsys
.template should_gather<sub_helper, v_helper>());
}
}(dout_context, dout_subsys, 1);

if (should_gather) {
ceph::logging::MutableEntry _dout_e(1, dout_subsys);
static_assert(
std::is_convertible<decltype(&*dout_context), CephContext *>::value,
"provided cct must be compatible with CephContext*");
auto _dout_cct = dout_context;
std::ostream *_dout = &_dout_e.get_ostream();

// 展开 dout_prefix 宏
*_dout << "mds." << name << ' '

// 用户输出的日志内容以及 dendl 宏
// << "Updating MDS map to version " << epoch << " from " << m->get_source() << dendl;
<< " Updating MDS map to version 158945 from mon.0" << std::flush;
_dout_cct->_log->submit_entry(std::move(_dout_e));
}
} while (0);

之后就走到了 Log::submit_entry 函数的执行逻辑中,注意: 如果日志队列中的元素超过 m_max_new (对应 log_max_new 参数, 默认为 1000) ,则线程会阻塞等待,直到队列有空闲位置。当将需要记录的日志提交到 m_new 中之后,就会通过条件变量来通知 log 线程处理日志。

void Log::submit_entry(Entry &&e)
{
std::unique_lock lock(m_queue_mutex);
m_queue_mutex_holder = pthread_self();

if (unlikely(m_inject_segv))
*(volatile int *)(0) = 0xdead;

// wait for flush to catch up
while (is_started() &&
m_new.size() > m_max_new)
{
if (m_stop)
break; // force addition
m_cond_loggers.wait(lock);
}

m_new.emplace_back(std::move(e));
m_cond_flusher.notify_all();
m_queue_mutex_holder = 0;
}

1.2.2、日志消费者

按照之前的描述,每个服务都有一个处理日志的名为 log 的线程,对应的线程入口函数为 Log::entry ,其内部关键的写日志的函数为 Log::_flush 。分析其具体实现,我们发现 ceph 支持将日志存储到五个地方,分别如下:

输出目标正常日志阈值崩溃日志阈值是否默认启用启用方法
file子系统级别子系统级别是(如果设置文件)set_log_file()
stderrm_stderr_logm_stderr_crashset_stderr_level()
syslogm_syslog_logm_syslog_crashset_syslog_level()
graylogm_graylog_logm_graylog_crashstart_graylog()
journaldm_journald_logm_journald_crashstart_journald_logger()

file 相关配置:

  • log_to_file: 是否输出日志到文件;
  • log_file: 指定输出日志文件的路径,默认路径为 /var/log/ceph/$cluster-$name.log

stderr 相关配置:

  • log_to_stderr:
    • 是否将日志日志输出到标准错误 stderr ,默认为 true ;
    • 如果为 true 表示将所有日志 (最高级别 99) 输出到 stderr ;
  • err_to_stderr:
    • 是否将错误日志输出到标准错误 stderr ,默认为 false ;
    • 如果 log_to_stderr 为 false 且 err_to_stderr 为 true ,表示只将错误日志输出到 stderr ;
    • 如果 log_to_stderr 为 false 且 err_to_stderr 为 false , 表示不向 stderr 输出任何日志;
  • log_stderr_prefix: 为输出到标准错误 stderr 的每条日志消息添加一个固定的前缀字符串

syslog 相关配置:

  • log_to_syslog:
    • 是否将日志写入系统日志中,默认为 false ;
    • 如果为 true 表示将所有日志 (最高级别 99) 输出到 syslog ;
  • err_to_syslog:
    • 是否将异常日志写入系统日志中,默认为 false ;
    • 如果 log_to_syslog 为 false 且 err_to_syslog 为 true ,表示只将错误日志输出到 syslog ;
    • 如果 log_to_syslog 为 false 且 err_to_syslog 为 false , 表示不向 syslog 输出任何日志;

graylog 相关配置:

  • log_to_graylog:
    • 是否将日志写入远程的 graylog 服务中,默认为 false ;
    • 如果为 true 表示将所有日志 (最高级别 99) 输出到远程的 graylog 服务中;
  • err_to_graylog:
    • 是否将异常日志写入远程的 graylog 服务中,默认为 false ;
    • 如果 log_to_graylog 为 false 且 err_to_graylog 为 true ,表示只将错误日志输出到远程的 graylog 服务中;
    • 如果 log_to_graylog 为 false 且 err_to_graylog 为 false , 表示不向远程的 graylog 服务输出任何日志;
  • log_graylog_host: 设置远程的 graylog 服务的地址;
  • log_graylog_port: 设置远程的 graylog 服务的端口;

journald 相关配置:

  • log_to_journald:
    • 是否将日志写入到 journald ,默认为 false ;
    • 如果为 true 表示将所有日志 (最高级别 99) 写入到 journald ;
  • err_to_journald:
    • 是否将异常日志写入到 journald ,默认为 false ;
    • 如果 log_to_journald 为 false 且 err_to_journald 为 true ,表示只将错误日志到 journald ;
    • 如果 log_to_journald 为 false 且 err_to_journald 为 false , 表示不向到 journald 写入任何日志;

1.3、日志相关操作

二、客户端侧日志

2.1、CephFS 客户端日志

2.1.1、Kernel 客户端日志

2.1.2、FUSE 客户端日志

2.2、CephRDB 客户段日志

2.2.1、Kernel 客户端日志

2.2.2、FUSE 客户端日志