ceph log源码分析

ceph log源码分析

ceph中log的处理方法是,主线程生成并提交日志条目给log线程处理,典型用法如下:
ldout (cct, log_level)<< msg << dendl, 其中msg是要写入日志的内容.
以src/librbd/librbd.cc为例:

// 子系统名称,ceph的每个log模块是一个子系统
#define dout_subsys ceph_subsys_rbd
// 根据子系统重新定义dout_prefix,因为它最初定义在src/commom/dout.h中
#undef dout_prefix
#define dout_prefix *_dout << "librbd: "

// aio_read中log的用法, bl是读入数据的buffer
ldout(ictx->cct, 10) << "Image::aio_read() buf=" << (void *)bl.c_str() << "~"
			 << (void *)(bl.c_str() + len - 1) << dendl;

宏ldout和dend定义在文件src/common/dout.h中,如下所示:

#define dout_prefix *_dout

// sub: 子系统名称, v: 日志级别
#define dout_impl(cct, sub, v)						\
  do {									\
    if (cct->_conf->subsys.should_gather(sub, v)) {			\
    	if (0) {								\
        char __array[((v >= -1) && (v <= 200)) ? 0 : -1] __attribute__((unused)); \
  }									\
    static size_t _log_exp_length=80; \
    ceph::log::Entry *_dout_e = cct->_log->create_entry(v, sub, &_log_exp_length);	\
    ostream _dout_os(&_dout_e->m_streambuf);				\
    CephContext *_dout_cct = cct;					\
    std::ostream* _dout = &_dout_os;

#define ldout(cct, v)  dout_impl(cct, dout_subsys, v) dout_prefix

#define dendl std::flush;				\
  _ASSERT_H->_log->submit_entry(_dout_e);		\
    }						\
  } while (0)
// 宏替换之前
ldout(ictx->cct, 10) << "Image::aio_read() buf=" << (void *)bl.c_str() << "~"
			 << (void *)(bl.c_str() + len - 1) << dendl;
// 宏替换之后
do {									\
	if (cct->_conf->subsys.should_gather(ceph_subsys_rbd, v)) {			\
    	if (0) {								\
        	char __array[((v >= -1) && (v <= 200)) ? 0 : -1] __attribute__((unused)); \
        }									\
	    static size_t _log_exp_length=80; \
	    ceph::log::Entry *_dout_e = cct->_log->create_entry(v, ceph_subsys_rbd, &_log_exp_length);	\
	    ostream _dout_os(&_dout_e->m_streambuf);				\
	    CephContext *_dout_cct = cct;					\
	    std::ostream* _dout = &_dout_os;
	    *_dout << "librbd: "<< "Image::aio_read() buf=" << (void *)bl.c_str() << "~"
				 << (void *)(bl.c_str() + len - 1) << std::flush;				\
	  	_dout_cct->_log->submit_entry(_dout_e);		\
	}						\
} while (0)

从以上代码可以看出,由宏ldoutdendl组成一条完整的do { … } while(0)代码块,并在do中进行日志的生成和提交.其中利用**create_entry(来生成一个日志条目,利用submit_entry()**来提交一个日志条目.这两个工作都是由主线程调用log线程进行的处理的.

日志定义

log子系统主要的实现在目录src/log中,里面有一个很重要的类Log,它继承自线程类,自带线程处理日志功能. 因为打印日志,会影响系统的性能,特别是c++的流,对性能影响更明显.ceph这里采取了一些优化:

  1. ceph 对每个子系统的日志都预先定义了日志级别,并且可动态修改.
  2. 每条log都带有日志级别,日志级别越低,优先级越高.低于预先定义的级别才会被打印.
  3. log 信息只需要提交到Log类的线程即可,由log线程接管后台打印日志的任务.对提交log的线程影响较小

Log类的实现比较简单,维护两个队列,m_new用于提交新日志,flush的时候获取m_new的entry用来刷新,m_recent用来存放最近的日志,比如用户通过admin socket发送dump log的命令时,就会将m_recent的日志dump到文件:

class Log : private Thread
{
  Log **m_indirect_this;

  SubsystemMap *m_subs; // 每个子系统的日志级别的map
  
  pthread_mutex_t m_queue_mutex; // 这个锁专门用来提交日志
  pthread_mutex_t m_flush_mutex; // 这个锁用来打印提交的日志
  pthread_cond_t m_cond_loggers;
  pthread_cond_t m_cond_flusher;

  pthread_t m_queue_mutex_holder;
  pthread_t m_flush_mutex_holder;

  EntryQueue m_new;    // 提交日志的队列, EntryQueue定义见下文
  EntryQueue m_recent; // 存放最近的日志

  string m_log_file; // 日志文件名
  int m_fd; // 日志文件描述符
 
  // ceph的日志可以写入不同的地方
  int m_syslog_log, m_syslog_crash; // syslog: 系统标准日志
  int m_stderr_log, m_stderr_crash; // stderr: 系统标准输出
  int m_graylog_log, m_graylog_crash; // graylog日志管理系统

  shared_ptr<Graylog> m_graylog;

  bool m_stop; // 日志线程是否终止

  int m_max_new, m_max_recent;  // m_new, m_recent队列的最大长度

  void *entry(); // 线程入口,继承Thread的类必须定义自己的entry函数

  void _flush(EntryQueue *q, EntryQueue *requeue, bool crash); // 将队列中的日志刷新到日志文件中

  void _log_message(const char *s, bool crash);

public:
  Log(SubsystemMap *s);
  virtual ~Log();

  void set_flush_on_exit();
  void set_max_new(int n);
  void set_max_recent(int n);
  void set_log_file(std::string fn);
  void reopen_log_file();

  void flush(); // 刷新

  void dump_recent(); // 打印最近的日志,一般用来响应admin socket的请求

  Entry *create_entry(int level, int subsys); // 创建日志
  void submit_entry(Entry *e); // 提交日志, 日志实体Entry见下文

  void start(); // 启动日志的线程
  void stop(); // 终止日志线程
};

log日志实体

每一个日志条目除了包括最基本的日志内容外,还包括一系列的其它元数据,比如产生日志的时间和线程,日志的优先级等.ceph对日志条目进行了优化,假设每条日志的长度为80个字节,然后把日志保存在一个预先分配好的内存缓冲m_streambuf中,这样可以避免内存的分配所带来的性能损失.如果日志的长度大于内存缓冲的大小 ,那么日志内存直接使用string来保存,具体的可以参数**PrebufferedStreambuf **类.

struct Entry {
  utime_t m_stamp; // 生成日志的时间
  pthread_t m_thread; // 生成日志的线程
  short m_prio, m_subsys; // 日志的优先级以及日志所属的子系统
  Entry *m_next; // 在日志队列中指向下一条日志记录

  PrebufferedStreambuf m_streambuf; // 日志缓冲区,用于保存日志的内容
  size_t m_buf_len; // 日志缓冲区的长度
  size_t* m_exp_len; // 期待的日志长度
  char m_static_buf[1];

  // 将日志内容s添加到缓冲区中
  void set_str(const std::string &s) {
    ostream os(&m_streambuf);
    os << s;
  }

  // 从缓冲区返回日志内容
  std::string get_str() const {
    return m_streambuf.get_str();
  }

  // returns current size of content
  size_t size() const {
    return m_streambuf.size();
  }

  // 将日志缓冲的内容写到dst指定的buffer中
  int snprintf(char* dst, size_t avail) const {
    return m_streambuf.snprintf(dst, avail);
  }
};

log日志实体队列

ceph的日志队列使用单链表来实现

struct EntryQueue {
  int m_len; // 队列长度
  struct Entry *m_head, *m_tail; // 队头指针, 队尾打针
 
  bool empty() const {
    return m_len == 0;
  }
 
  // swap主用于刷新日志队列时,将日志队列中的所有日志放到另一队列上,然后对另一队列进行处理,从而避免影响原队列
  void swap(EntryQueue& other) {
    int len = m_len;
    struct Entry *h = m_head, *t = m_tail;
    m_len = other.m_len;
    m_head = other.m_head;
    m_tail = other.m_tail;
    other.m_len = len;
    other.m_head = h;
    other.m_tail = t;
  }

  void enqueue(Entry *e) {
    if (m_tail) {
      m_tail->m_next = e;
      m_tail = e;
    } else {
      m_head = m_tail = e;
    }
    m_len++;
  }

  Entry *dequeue() {
    if (!m_head)
      return NULL;
    Entry *e = m_head;
    m_head = m_head->m_next;
    if (!m_head)
      m_tail = NULL;
    m_len--;
    e->m_next = NULL;
    return e;
  }
};

log Thread

下来我们来看下log线程的具体操作

// 线程入口函数,如果日志队列不为空,即有日志,那么log线程把相应的日志flush到日志文件中.
void *Log::entry()
{
  pthread_mutex_lock(&m_queue_mutex);
  m_queue_mutex_holder = pthread_self();
  while (!m_stop) {
    if (!m_new.empty()) { // 有新日志
      m_queue_mutex_holder = 0;
      pthread_mutex_unlock(&m_queue_mutex);
      flush(); // 刷新日志
      pthread_mutex_lock(&m_queue_mutex);
      m_queue_mutex_holder = pthread_self();
      continue;
    }
    pthread_cond_wait(&m_cond_flusher, &m_queue_mutex);
  }
  m_queue_mutex_holder = 0;
  pthread_mutex_unlock(&m_queue_mutex);
  flush();
  return NULL;
}

void Log::flush()
{
  pthread_mutex_lock(&m_flush_mutex);
  m_flush_mutex_holder = pthread_self();
  pthread_mutex_lock(&m_queue_mutex);
  m_queue_mutex_holder = pthread_self();
  EntryQueue t; // 临时队列
  t.swap(m_new); // O(1)的交换,这样m_new又可以接收新日志的提交了
  pthread_cond_broadcast(&m_cond_loggers);
  m_queue_mutex_holder = 0;
  pthread_mutex_unlock(&m_queue_mutex); // 提前释放锁,以便其他线程继续提交
  _flush(&t, &m_recent, false); // 真正打印临时队列的信息, 并且会记录到m_recent
  // trim
  while (m_recent.m_len > m_max_recent) { // m_recent有大小限制,超出部分删除
    delete m_recent.dequeue();
  }
  m_flush_mutex_holder = 0;
  pthread_mutex_unlock(&m_flush_mutex);
}
// 将日志刷到到日志文件
void Log::_flush(EntryQueue *t, EntryQueue *requeue, bool crash)
{
  Entry *e;
  while ((e = t->dequeue()) != NULL) {
    unsigned sub = e->m_subsys;
    // 当crash为true或者日志的级别大于优先级时记录日志
    bool should_log = crash || m_subs->get_log_level(sub) >= e->m_prio;
    // 将日志写入哪里,日志文件还是syslog,stderr,还是graylog
    bool do_fd = m_fd >= 0 && should_log;
    bool do_syslog = m_syslog_crash >= e->m_prio && should_log;
    bool do_stderr = m_stderr_crash >= e->m_prio && should_log;
    bool do_graylog2 = m_graylog_crash >= e->m_prio && should_log;

    e->hint_size();
    if (do_fd || do_syslog || do_stderr) {
		// 日志长度的处理
      size_t buflen = 0;

      char *buf;
      size_t buf_size = 80 + e->size();
	  // 日志长度是否大于64K
      bool need_dynamic = buf_size >= 0x10000; //avoids >64K buffers
					       //allocation at stack
      char buf0[need_dynamic ? 1 : buf_size];
      if (need_dynamic) {
        buf = new char[buf_size];
      } else {
        buf = buf0;
      }
	  // 如果日志长度大于64K, buf[1];
	  // 如果日志长度小于64K, buf[buf_size]

      // 在日志中添加额外信息,包括时间,线程,优先级等
      if (crash)
	  	buflen += snprintf(buf, buf_size, "%6d> ", -t->m_len);
      // 日志的前缀,记录日志发生的时间
      buflen += e->m_stamp.sprintf(buf + buflen, buf_size-buflen);
	  // 日志的内容,包括线程ID,消息优先级
      buflen += snprintf(buf + buflen, buf_size-buflen, " %lx %2d ",
			(unsigned long)e->m_thread, e->m_prio);

      // 将日志缓冲中的内容写入buf中
      buflen += e->snprintf(buf + buflen, buf_size - buflen - 1);
	  // 将buf中的最后一字符置为'\0'
      if (buflen > buf_size - 1) { //paranoid check, buf was declared
				   //to hold everything
        buflen = buf_size - 1;
        buf[buflen] = 0;
      }

      // 将日志写入系统标准日志上
      if (do_syslog) {
        syslog(LOG_USER|LOG_INFO, "%s", buf);
      }

      // 将日志写入系统标准错误上
      if (do_stderr) {
        cerr << buf << std::endl;
      }

	  // 将日志写入日志文件中
      if (do_fd) {
        buf[buflen] = '\n';
        int r = safe_write(m_fd, buf, buflen+1);
		if (r != m_fd_last_error) {
		  if (r < 0)
		    cerr << "problem writing to " << m_log_file
			 << ": " << cpp_strerror(r)
			 << std::endl;
		  m_fd_last_error = r;
		}
      }
      if (need_dynamic)
        delete[] buf;
    }
    if (do_graylog2 && m_graylog) {
      m_graylog->log_entry(e);
    }

    // 将日志加入requeuen队列
    requeue->enqueue(e);
  }
}
Entry *Log::create_entry(int level, int subsys)
{
  if (true) {
    return new Entry(ceph_clock_now(NULL), // new一个entry对象
		   pthread_self(),
		   level, subsys);
  } else {
    // kludge for perf testing
    Entry *e = m_recent.dequeue();
    e->m_stamp = ceph_clock_now(NULL);
    e->m_thread = pthread_self();
    e->m_prio = level;
    e->m_subsys = subsys;
    return e;
  }
}

void Log::submit_entry(Entry *e)
{
  pthread_mutex_lock(&m_queue_mutex); // 前面提到的这把锁是用来提交entry的

  // wait for flush to catch up
  while (m_new.m_len > m_max_new)
    pthread_cond_wait(&m_cond_loggers, &m_queue_mutex);

  m_new.enqueue(e); // 进队列
  pthread_cond_signal(&m_cond_flusher);
  pthread_mutex_unlock(&m_queue_mutex);
}

log日志级别

ceph对不同的模块定义了不同的日志级别,只有当日志的级别小于等于预定义的值时,ceph才会把日志写入日志文件中,那么log线程是怎么样知道预定的日志级别呢,答案是Log类中的SubsystemMap,它记录了ceph中所有的日志模块以及相就的日志级别.它的定义如下:

// Subsystem代表ceph中的不同log子系统
struct Subsystem {
  int log_level, gather_level; // 子系统的日志级别,后者很少用
  std::string name; // 子系统名
  Subsystem() : log_level(0), gather_level(0) {}     
};

// SubsystemMap: 子系统图,用于保存ceph中所有的子系统
class SubsystemMap {
  std::vector<Subsystem> m_subsys;
  unsigned m_max_name_len;
  friend class Log;

public:
  SubsystemMap() : m_max_name_len(0) {}

  int get_num() const {
    return m_subsys.size();
  }

  int get_max_subsys_len() const {
    return m_max_name_len;
  }

  // 添加一个子系统信息
  void add(unsigned subsys, std::string name, int log, int gather);  
  void set_log_level(unsigned subsys, int log);

  int get_log_level(unsigned subsys) const {
    if (subsys >= m_subsys.size())
      subsys = 0;
    return m_subsys[subsys].log_level;
  }

  // 是否需要打印日志
  // 如果当前日志级别小于等于设置的日志级别,就打印日志,否则不打印日志
  bool should_gather(unsigned sub, int level) {
    assert(sub < m_subsys.size());
    return level <= m_subsys[sub].gather_level ||
      level <= m_subsys[sub].log_level;
  }
};

日志级别的载入是ceph配置项中很小的一部分,下面简单介绍下ceph配置项的初始化过程以及别的相关的知识.

ceph_context

ceph_context是ceph中最基础的类,它完成一些基本工作:比如说启动一些公共线程log,admin_socket,perfcount等,它还会负责ceph集群的配置初始化过程,这是通过新建m_config_t对象来实现的,当然ceph_context中还有其它一些重要的内容,限于篇幅,这里只介绍log相关的.

CephContext::CephContext(uint32_t module_type_)
  : nref(1),
    _conf(new md_config_t()), // 初始化配置对象
    _log(NULL),
    _module_type(module_type_),
    _crypto_inited(false),
    _service_thread(NULL),
    _log_obs(NULL),
    _admin_socket(NULL),
    _perf_counters_collection(NULL),
    _perf_counters_conf_obs(NULL),
    _heartbeat_map(NULL),
    _crypto_none(NULL),
    _crypto_aes(NULL),
    _lockdep_obs(NULL)
{
  ......
  _log = new ceph::log::Log(&_conf->subsys); // 创建日志管理类对象
  _log->start(); // 启动线程
  ......
}

ceph日志级别的载入

那么ceph的预定义日志级别在哪里,答案是config_opts.h,实际上,该文件定义了ceph集群所需要的所有配置项,一共包括三部分内容: OPTION, DEFAULT_SUBSYS, SUBSYS, 其中SUBSYS就是日志模块和级别的定义.如下所示:

// src/common/config_opts.h
// 配置文件里,包含三个部分:OPTION, DEFAULT_SUBSYS, SUBSYS
// include的时候,会根据不同情况选择不同部分,未被选择的项就会为空

......

OPTION(xio_portal_threads, OPT_INT, 2) // (选项名称,选项类型,选项值)

DEFAULT_SUBSYS(0, 5)
SUBSYS(lockdep, 0, 1)
SUBSYS(context, 0, 1) // (子系统名称,log级别,gather级别)
......

以下ceph配置相关的类m_config_t:

struct md_config_t {
	......
	// 这里需要取得option的值,并定义常量
	// 定义特殊类型的OPTION
	#define OPTION_OPT_INT(name) const int name;
	#define OPTION_OPT_LONGLONG(name) const long long name;
	#define OPTION_OPT_STR(name) const std::string name;
	#define OPTION_OPT_DOUBLE(name) const double name;
	#define OPTION_OPT_FLOAT(name) const float name;
	#define OPTION_OPT_BOOL(name) const bool name;
	#define OPTION_OPT_ADDR(name) const entity_addr_t name;
	#define OPTION_OPT_U32(name) const uint32_t name;
	#define OPTION_OPT_U64(name) const uint64_t name;
	#define OPTION_OPT_UUID(name) const uuid_d name;

	// 定义OPTION
	#define OPTION(name, ty, init) OPTION_##ty(name)

	// 定义其他不需要的两项 SUBSYS/DEFAULT_SUBSYS 为空
	#define SUBSYS(name, log, gather)
	#define DEFAULT_SUBSYS(log, gather)

	// 把config_opts.h文件include进来, 实际上获取了所有的OPTION
	#include "common/config_opts.h"

	// 取消以前所有的宏定义
	#undef OPTION_OPT_INT
	#undef OPTION_OPT_LONGLONG
	#undef OPTION_OPT_STR
	#undef OPTION_OPT_DOUBLE
	#undef OPTION_OPT_FLOAT
	#undef OPTION_OPT_BOOL
	#undef OPTION_OPT_ADDR
	#undef OPTION_OPT_U32
	#undef OPTION_OPT_U64
	#undef OPTION_OPT_UUID
	#undef OPTION
	#undef SUBSYS
	#undef DEFAULT_SUBSYS
	......
};

// 定义一个枚举,实际上就是生成了每个子系统的下标,这里借助了枚举类型的自增
// 所以在文件中的位置决定了子系统在vector中的下标
enum config_subsys_id {
	ceph_subsys_,   // default

	// 因为这里不需要OPTION字段,先定义为空 
	#define OPTION(a,b,c)

	// 生成枚举item的宏
	#define SUBSYS(name, log, gather) \
		ceph_subsys_##name,
	
	// DEFAULT_SUBSYS这里也不需要,定义为空 
	#define DEFAULT_SUBSYS(log, gather)

	// 把config_iopts.h文件include进来, 实际上获取了所有的SUBSYS
	#include "common/config_opts.h"

	// 取消以前所有的宏定义
	#undef SUBSYS
	#undef OPTION
	#undef DEFAULT_SUBSYS

	ceph_subsys_max
};

以下是ceph配置类m_config_t的实现文件

md_config_t::md_config_t()
  : cluster("ceph"),

// 头文件定义了name 常量,这里对常量进行初始化
#define OPTION_OPT_INT(name, def_val) name(def_val),
#define OPTION_OPT_LONGLONG(name, def_val) name((1LL) * def_val),
#define OPTION_OPT_STR(name, def_val) name(def_val),
#define OPTION_OPT_DOUBLE(name, def_val) name(def_val),
#define OPTION_OPT_FLOAT(name, def_val) name(def_val),
#define OPTION_OPT_BOOL(name, def_val) name(def_val),
#define OPTION_OPT_ADDR(name, def_val) name(def_val),
#define OPTION_OPT_U32(name, def_val) name(def_val),
#define OPTION_OPT_U64(name, def_val) name(((uint64_t)1) * def_val),
#define OPTION_OPT_UUID(name, def_val) name(def_val),

#define OPTION(name, type, def_val) OPTION_##type(name, def_val)

// 以下两项不需要
#define SUBSYS(name, log, gather)
#define DEFAULT_SUBSYS(log, gather)

// include文件进来
#include "common/config_opts.h"

// 取消所有的宏
#undef OPTION_OPT_INT
#undef OPTION_OPT_LONGLONG
#undef OPTION_OPT_STR
#undef OPTION_OPT_DOUBLE
#undef OPTION_OPT_FLOAT
#undef OPTION_OPT_BOOL
#undef OPTION_OPT_ADDR
#undef OPTION_OPT_U32
#undef OPTION_OPT_U64
#undef OPTION_OPT_UUID
#undef OPTION
#undef SUBSYS
#undef DEFAULT_SUBSYS

  lock("md_config_t", true, false)
{
  init_subsys();
}

void md_config_t::init_subsys()
{
// 将子系统加入vector,第一个参数就是头文件中定义的宏,就是下标
#define SUBSYS(name, log, gather) \
  subsys.add(ceph_subsys_##name, STRINGIFY(name), log, gather);
#define DEFAULT_SUBSYS(log, gather) \
  subsys.add(ceph_subsys_, "none", log, gather);

// 过滤选项
#define OPTION(a, b, c)

// include 文件
#include "common/config_opts.h"

// 取消所有的宏
#undef OPTION
#undef SUBSYS
#undef DEFAULT_SUBSYS
}

参考

Ceph Dynamic Log/Option Mechanism

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Ceph中,stripe是一种将数据分片存储的概念。当进行文件读取操作时,需要通过一系列的计算来确定数据所在的具体位置。本文以CephFS的文件读取流程为例进行分析。 首先,在文件读取过程中,Ceph会将文件划分为若干个条带(stripe),每个条带由多个对象分片(stripe unit)组成。条带可以看作是逻辑上连续的一维地址空间。 接下来,通过file_to_extent函数将一维坐标转化为三维坐标(objectset,stripeno,stripepos),来确定具体的位置。其中,objectset表示所在的对象集,stripeno表示条带号,stripepos表示条带内的偏移位置。 具体的计算过程如下:假设需要读取的数据的偏移量为offset,每个对象分片的大小为su(stripe unit),每个条带中包含的对象分片数为stripe_count。 首先,计算块号blockno = offset / su,表示数据所在的分片号。 然后,计算条带号stripeno = blockno / stripe_count,表示数据所在的条带号。 接着,计算条带内偏移stripepos = blockno % stripe_count,表示数据在条带内的偏移位置。 接下来,计算对象集号objectsetno = stripeno / stripes_per_object,表示数据所在的对象集号。 最后,计算对象号objectno = objectsetno * stripe_count + stripepos,表示数据所在的对象号。 通过以上计算,可以确定数据在Ceph中的具体位置,从而完成文件读取操作。 需要注意的是,以上分析是基于Ceph版本10.2.2(jewel)进行的,尽管版本跨度较大,但是该部分代码在12.2.10(luminous)版本中仍然比较稳定,基本的框架没有发生变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值