ceph分布式存储-日志和调试
1. 介绍
Log 是每个项目必须的,他是跟踪问题的最直接的依据。Ceph 也设计自己的log机制。
一般来说,你应该在运行时增加调试选项来调试问题;也可以把调试选项添加到 Ceph 配置文件里来调试集群启动时的问题,然后查看 /var/log/ceph
(默认位置)下的日志文件。
Tip: 调试输出会拖慢系统,这种延时有可能掩盖竞争条件。
日志记录是资源密集型任务。如果你碰到的问题在集群的某个特定区域,只启用那个区域对应的日志功能即可。例如,你的 OSD 运行良好、元数据服务器却有问题,这时应该先打开那个可疑元数据服务器实例的调试日志;如果不行再打开各子系统的日志。
重要: 详尽的日志每小时可能超过 1GB ,如果你的系统盘满了,这个节点就会停止工作。
如果你要打开或增加 Ceph 日志级别,确保有足够的系统盘空间。滚动日志文件的方法见下面的 加快日志更迭 小节。集群稳定运行后,可以关闭不必要的调试选项以优化运行。集群在运行中记录调试输出信息会拖慢系统、且浪费资源。
2. 运行时
要在运行时激活 Ceph 的调试输出(即 dout()
),用 ceph tell
命令把参数注入运行时配置:
ceph tell {daemon-type}.{daemon id or *} injectargs --{name} {value} [--{name} {value}]
用 osd
、 mon
或 mds
替代 {daemon-type}
。还可以用星号( *
)把配置应用到同类型的所有守护进程,或者指定具体守护进程的 ID 。例如,要给名为 ods.0
的 ceph-osd
守护进程提高调试级别,用下列命令:
ceph tell /run/ceph/ceph-osd.0.asok injectargs --debug-osd 0/5
ceph tell
命令会通过 monitor 起作用。如果你不能绑定 monitor,仍可以登录你要改的那台主机然后用 ceph daemon 来更改。例如:
ceph daemon /run/ceph/ceph-osd.0.asok config set debug_osd 0/5
3. 启动时
要在启动时激活调试输出(即 dout()
),你得把选项加入配置文件。各进程共有配置可写在配置文件的 [global]
段下,某类进程的配置可写在对应的守护进程段下(如 [mon]
、 [osd]
、 [mds]
)。例如:
[global]
debug ms = 1/5
[mon]
debug mon = 20
debug paxos = 1/5
debug auth = 2
[osd]
debug osd = 1/5
debug filestore = 1/5
debug journal = 1
debug monc = 5/20
[mds]
debug mds = 1
debug mds balancer = 1
debug mds log = 1
debug mds migrator = 1
4. 加快日志更迭
如果你的系统盘比较满,可以修改 /etc/logrotate.d/ceph
内的日志滚动配置以加快滚动。在滚动频率后增加一个日志 size
选项(达到此大小就滚动)来加快滚动(通过 cronjob )。例如默认配置大致如此:
rotate 7
weekly
compress
sharedscripts
增加一个 size
选项。
rotate 7
weekly
size 500M
compress
sharedscripts
然后,打开 crontab 编辑器。
crontab -e
最后,增加一条用以检查 /etc/logrorate.d/ceph
文件的语句。
30 * * * * /usr/sbin/logrotate /etc/logrotate.d/ceph >/dev/null 2>&1
本例中每 30 分钟检查一次 /etc/logrorate.d/ceph
文件。
5. 子系统、日志和调试选项
大多数情况下,你可以通过子系统打开调试日志输出。
5.1 CEPH 子系统概览
各子系统都有日志级别用于分别控制其输出日志和暂存日志(内存中),你可以分别为这些子系统设置不同的记录级别。 Ceph 的日志级别从 1 到 20 , 1 是简洁、 20 是详尽。通常,内存驻留日志不会发送到输出日志,除非:
- 致命信号出现,或者
- 源码中的
assert
被触发,或者 - 明确要求发送。
调试选项允许用单个数字同时设置日志级别和内存级别,这会将二者设置为一样的值。比如,如果你指定 debug ms = 5
, Ceph 会把日志级别和内存级别都设置为 5
。也可以分别设置,第一个选项是日志级别、第二个是内存级别,二者必须用斜线( / )分隔。假如你想把 ms
子系统的调试日志级别设为 1
、内存级别设为 5
,可以写为 debug ms = 1/5
,如下:
debug {subsystem} = {log-level}/{memory-level}
#for example
debug mds log = 1/20
Ceph 子系统及其默认日志和内存级别具体见 SUBSYSTEM, LOG AND DEBUG SETTINGS 。一旦你完成调试,应该恢复默认值,或一个适合平常运营的级别。
5.2 日志记录选项
关于日志记录选项的详细信息,可以参考 Ceph 官方文档的对应内容 LOGGING SETTING 。
5.3 日志打印
ceph维护了一个m_recent队列,所有的消息都会存放到该队列中去,哪怕优先级比较低,不会打印到日志文件中··去。当然了,队列是有大小的限制,否则队列就会膨胀,导致内存耗尽。这个默认的限制是10000.
while (m_recent.m_len > m_max_recent) {
delete m_recent.dequeue();
}
ceph提供了方法来查看最近的log,这就是log dump方法。
root@hwh:/run/ceph# ceph daemon /run/ceph/ceph-osd.0.asok log dump
{}
表面看啥也没输出,实际上将log dump到了日志文件中,比如我们例子,在log文件中出现:
root@hwh:/var/log/ceph# more ceph-osd.0.log
2023-04-24T04:16:23.028+0000 7ffff72f53c0 0 set uid:gid to 1001:1001 (ceph:ceph)
2023-04-24T04:16:23.028+0000 7ffff72f53c0 0 ceph version 364b013f7 (8364b013f73c0eb09e0795aa96bf222e708831c9) quincy (stable), process ceph-osd, pid 25341
2023-04-24T04:16:23.028+0000 7ffff72f53c0 0 pidfile_write: ignore empty --pid-file
2023-04-24T04:16:23.028+0000 7ffff72f53c0 1 bdev(0x55555861f400 /var/lib/ceph/osd/ceph-0/block) open path /var/lib/ceph/osd/ceph-0/block
2023-04-24T04:16:23.036+0000 7ffff72f53c0 1 bdev(0x55555861f400 /var/lib/ceph/osd/ceph-0/block) open size 128844824576 (0x1dffc00000, 120 GiB) block_size 4096 (4 KiB) non-rotational discard not supported
2023-04-24T04:16:23.036+0000 7ffff72f53c0 1 bluestore(/var/lib/ceph/osd/ceph-0) _set_cache_sizes cache_size 3221225472 meta 0.45 kv 0.45 data 0.06
2023-04-24T04:16:23.036+0000 7ffff72f53c0 1 bdev(0x55555861f900 /var/lib/ceph/osd/ceph-0/block) open path /var/lib/ceph/osd/ceph-0/block
2023-04-24T04:16:23.044+0000 7ffff72f53c0 1 bdev(0x55555861f900 /var/lib/ceph/osd/ceph-0/block) open size 128844824576 (0x1dffc00000, 120 GiB) block_size 4096 (4 KiB) non-rotational discard not supported
2023-04-24T04:16:23.044+0000 7ffff72f53c0 1 bluefs add_block_device bdev 1 path /var/lib/ceph/osd/ceph-0/block size 120 GiB
2023-04-24T04:16:23.044+0000 7ffff72f53c0 1 bdev(0x55555861f900 /var/lib/ceph/osd/ceph-0/block) close
2023-04-24T04:16:23.328+0000 7ffff72f53c0 1 bdev(0x55555861f400 /var/lib/ceph/osd/ceph-0/block) close
6. 源码分析
- 启动日志
CephContext::CephContext(uint32_t module_type_,
enum code_environment_t code_env,
int init_flags_)
{
...
_log = new ceph::logging::Log(&_conf->subsys);
_log_obs = new LogObs(_log);
_conf.add_observer(_log_obs);
...
}
void global_pre_init(
const std::map<std::string,std::string> *defaults,
std::vector < const char* >& args,
uint32_t module_type, code_environment_t code_env,
int flags)
{
...
//读取配置文件内容,命令里不指明的话,默认使用/etc/ceph/ceph.conf。
int ret = conf.parse_config_files(c_str_or_null(conf_file_list),
&cerr, flags);
if (ret == -EDOM) {
cct->_log->flush();
cerr << "global_init: error parsing config file." << std::endl;
_exit(1);
}
else if (ret == -ENOENT) {
if (!(flags & CINIT_FLAG_NO_DEFAULT_CONFIG_FILE)) {
if (conf_file_list.length()) {
cct->_log->flush();
cerr << "global_init: unable to open config file from search list "
<< conf_file_list << std::endl;
_exit(1);
} else {
cerr << "did not load config file, using default settings."
<< std::endl;
}
}
}
else if (ret) {
cct->_log->flush();
cerr << "global_init: error reading config file. "
<< conf.get_parse_error() << std::endl;
_exit(1);
}
// environment variables override (CEPH_ARGS, CEPH_KEYRING)
conf.parse_env(cct->get_module_type());
// command line (as passed by caller)
conf.parse_argv(args);
// 启动日志
if (!cct->_log->is_started()) {
cct->_log->start();
}
...
}
- 日志flush
void Log::flush()
{
std::scoped_lock lock1(m_flush_mutex);
m_flush_mutex_holder = pthread_self();
{
/*
队列m_flush执行swap将m_new里面的log都接了过去。所谓接过去,不过是将指针指向
队列内容的事情交给临时队列,m_new 头指针和尾指针置成NULL,做了这件事之后,m_new
又变成了空的队列,好心地给其他线程发了广播之后,就可以解锁m_queue_mutex互斥量了。
*/
std::scoped_lock lock2(m_queue_mutex);
m_queue_mutex_holder = pthread_self();
assert(m_flush.empty());
m_flush.swap(m_new);
m_cond_loggers.notify_all();
m_queue_mutex_holder = 0;
}
//主要的写日志的工作就委托给了__flush函数。
_flush(m_flush, false);
m_flush_mutex_holder = 0;
}
void Log::_flush(EntryVector& t, bool crash)
{
long len = 0;
if (t.empty()) {
assert(m_log_buf.empty());
return;
}
if (crash) {
len = t.size();
}
for (auto& e : t) {
auto prio = e.m_prio;
auto stamp = e.m_stamp;
auto sub = e.m_subsys;
auto thread = e.m_thread;
auto str = e.strv();
bool should_log = crash || m_subs->get_log_level(sub) >= prio;
bool do_fd = m_fd >= 0 && should_log;
bool do_syslog = m_syslog_crash >= prio && should_log;
bool do_stderr = m_stderr_crash >= prio && should_log;
bool do_graylog2 = m_graylog_crash >= prio && should_log;
bool do_journald = m_journald_crash >= prio && should_log;
if (do_fd || do_syslog || do_stderr) {
const std::size_t cur = m_log_buf.size();
std::size_t used = 0;
const std::size_t allocated = e.size() + 80;
m_log_buf.resize(cur + allocated);
char* const start = m_log_buf.data();
char* pos = start + cur;
if (crash) {
used += (std::size_t)snprintf(pos + used, allocated - used, "%6ld> ", -(--len));
}
used += (std::size_t)append_time(stamp, pos + used, allocated - used);
used += (std::size_t)snprintf(pos + used, allocated - used, " %lx %2d ", (unsigned long)thread, prio);
memcpy(pos + used, str.data(), str.size());
used += str.size();
pos[used] = '\0';
ceph_assert((used + 1 /* '\n' */) < allocated);
if (do_syslog) {
syslog(LOG_USER|LOG_INFO, "%s", pos);
}
if (do_stderr) {
std::cerr << m_log_stderr_prefix << std::string_view(pos, used) << std::endl;
}
/* now add newline */
pos[used++] = '\n';
if (do_fd) {
m_log_buf.resize(cur + used);
} else {
m_log_buf.resize(0);
}
if (m_log_buf.size() > MAX_LOG_BUF) {
_flush_logbuf();
}
}
if (do_graylog2 && m_graylog) {
m_graylog->log_entry(e);
}
if (do_journald && m_journald) {
m_journald->log_entry(e);
}
/*
ceph维护了一个m_recent队列,所有的消息都会存放到该队列中去,哪怕优先级比
较低,不会打印到日志文件中去。 ceph提供了方法来查看最近的log,这就是log dump方法。
ceph daemon /var/run/ceph/ceph-mon.*asok log dump
*/
m_recent.push_back(std::move(e));
}
t.clear();
_flush_logbuf();
}
- 日志打印
#define dout_prefix *_dout
#define dout_impl(cct, sub, v) \
do { \
const bool should_gather = [&](const auto cctX) { \
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 { \
/* 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, v>()); \
} \
}(cct); \
\
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();
#define dendl_impl std::flush; \
_dout_cct->_log->submit_entry(std::move(_dout_e)); \
} \
} while (0)
// dout_impl 生成了log_entry 写入到日志文件, dout_prefix 标准输出
#define ldout(cct, v) dout_impl(cct, dout_subsys, v) dout_prefix
int AdminSocket::register_command(std::string_view cmddesc,
AdminSocketHook *hook,
std::string_view help)
{
int ret;
std::unique_lock l(lock);
string prefix = cmddesc_get_prefix(cmddesc);
auto i = hooks.find(prefix);
if (i != hooks.cend() &&
i->second.desc == cmddesc) {
//打印日志
ldout(m_cct, 5) << "register_command " << prefix
<< " cmddesc " << cmddesc << " hook " << hook
<< " EEXIST" << dendl;
ret = -EEXIST;
} else {
ldout(m_cct, 5) << "register_command " << prefix << " hook " << hook
<< dendl;
hooks.emplace_hint(i,
std::piecewise_construct,
std::forward_as_tuple(prefix),
std::forward_as_tuple(hook, cmddesc, help));
ret = 0;
}
return ret;
}
提交日志关键函数 void Log::submit_entry(Entry&& e)
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,然后通知线程刷日志
m_new.emplace_back(std::move(e));
m_cond_flusher.notify_all();
m_queue_mutex_holder = 0;
}