rados tools

rados bench 是 Ceph 自带的用来测试存储池性能的压测工具,其 main 函数在 src/tools/rados/rados.cc 中。rados.cc 集成了 rados 所有 bash 命令,可以通过 rados -h 查看帮助。开篇点题,让我们看看执行 rados bench 时,哪些线程被偷偷启动了。

#在一个终端开启 rados bench
[root@localhost build]# ./bin/rados -p rbd bench 60 write

#在另一个终端监控 rados 启动哪些线程
[root@localhost ~]# ps -ef | grep rados
root      17796  10743  0 16:08 pts/0    00:00:00 ./bin/rados bench -p rbd 60 write
root      17808  10765  0 16:09 pts/1    00:00:00 grep --color=auto rados
[root@localhost ~]# top -Hp 17796 -d 0.5
top - 16:19:51 up  6:36,  2 users,  load average: 2.54, 0.73, 0.35
Threads:  16 total,   0 running,  16 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.3 sy,  0.0 ni, 87.4 id, 12.3 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  3861288 total,   201816 free,  1232584 used,  2426888 buff/cache
KiB Swap:  4063228 total,  4063228 free,        0 used.  2327116 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                        
 20207 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.28 rados                          
 20208 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 msgr-worker-0                  
 20209 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.49 msgr-worker-1                  
 20210 root      20   0 1328848  95972  20544 S  0.0  2.5   0:01.60 msgr-worker-2                  
 20215 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 log                            
 20216 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 service                        
 20217 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 admin_socket                   
 20222 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 rados                          
 20223 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 ms_dispatch                    
 20224 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 ms_local                       
 20225 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 safe_timer                     
 20226 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 fn_anonymous                   
 20227 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 safe_timer                     
 20228 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 safe_timer                     
 20229 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.01 fn-radosclient                 
 20230 root      20   0 1328848  95972  20544 S  0.0  2.5   0:00.00 write_stat 

可以看到除了本身执行 rados bench 命令的线程外,还有十几个线程被创建出来。简单介绍下,线程号为20207的 rados 线程是 rados bench 命令的主线程,msgr-worker-0~3 是用来网络通信的三个工作线程,log 是用来记录日志的日志线程,service 是监控本地性能参数的线程,admin_socket 是对外提供查询接口的线程,ms_dispatch 和 ms_local 是用来处理消息分发的线程,safer_timer 是执行定时任务的线程,fn_anonymous 和 fn-radosclient 则是处理所有模块收尾工作的线程。

1. rados main()

以下给出了 rados 模块的 CMakeLists.txt 文件,可以看到 rados 一共包含了哪些源文件和链接了什么库。

# rados 源文件
set(rados_srcs 
 rados/rados.cc
 RadosDump.cc
 rados/RadosImport.cc
 rados/PoolDump.cc
 ${PROJECT_SOURCE_DIR}/src/common/util.cc
 ${PROJECT_SOURCE_DIR}/src/common/obj_bencher.cc
 ${PROJECT_SOURCE_DIR}/src/osd/ECUtil.cc)
add_executable(rados ${rados_srcs})
# 链接库
target_link_libraries(rados librados global ${BLKID_LIBRARIES} ${CMAKE_DL_LIBS})
if(WITH_LIBRADOSSTRIPER)
 target_link_libraries(rados radosstriper)
else()
 target_link_libraries(rados cls_lock_client)
endif()
install(TARGETS rados DESTINATION bin)

/src/tools/rados.cc 中提供了 rados 工具的入口 main() 函数。其实,main()函数主要进行了4步工作:参数解析、全局初始化、初始化 CephContest和调用 rados_tool_common() 函数。在传入 main() 的参数中:argc 表示传入的参数个数,argv 以二维数组的方式记录了参数值。函数argv_to_vec(argc, argc, args)功能是把参数解析,并存入 args vector 中。例如,输入命令:rados bench -p rbd 10 write。agrs解析得到后的结果[“bench”, “-p”, “rbd”, “10”, “write”]。后续的的 if-else 循环中,依然是参数解析,这一步的作用是解析参数名和参数值,把结果放在 opts map 中。例如“-p rbd”被解析成“pool : rbd”。

global_init(),即全局初始化,参数 CEPH_ENTITY_TYPE_CLIENT 和 CODE_ENVIRONMENT_UTILITY 分别表示该模块是 CLIENT 和 代码环境为应用程序。此外还有 MON 、MDS、OSD等模块,DEAMON、LIBRARY 等代码环境。

后文将逐步介绍 global_init()、common_init_finish()、rados_tool_common()。

//rados.cc

int main(int argc, const char **argv){
    ...;
    argv_to_vec(argc, argv, args); //参数解析
    ...;
    auto cct = global_init(NULL, args, CEPH_ENTITY_TYPE_CLIENT, CODE_ENVIRONMENT_UTILITY, 0);
    common_init_finish(g_ceph_context);//初始化 CephContext
    ...;
    else if (ceph_argparse_flag(args, i, "--force-full", (char*)NULL)) //参数解析,放入 map 中
        opts["force-full"] = "true";
    ...;
    return rados_tool_common(opts, args);//根据 args 调用相关 rados 命令
}

2. global_init()

global_init() 是全局初始化函数,所有的 ceph 相关进程(rados,ceph,rbd等)都需要执行该操作,msgr-worker 和 log 线程都是在这一步创建的。该函数主要目的是进行参数的初始化解析工作、创建 CephContext、开启一些基础线程以及执行一些通用的预处理工作。

run_pre_init 标志位默认是 true,所以通常情况下,都会执行 global_pre_init()。下文会详细介绍。

block_signals() ,此函数屏蔽了 siglist[] 中的信号,这里是 SIGPIPE 信号。SIGPIPE 信号是服务端断开链接后发送给客户端的信号,一般来说客户端收到 SIGPIPE 信号会立刻中断进程。Ceph 不希望出现客户端突然中断,所以这里屏蔽了该信号。install_standard_sighandlers() 则是制定了某些信号的处理程序,包括:SIGSEGV、SIGABRT、SIGBUS、SIGILL、SIGFPE、SIGXCPU、SIGCFSZ、SIGSYS,具体处理方法见 handle_fatal_signal() 函数(本文这里不做分析)。

g_ceph_context->_log->set_flush_on_exit() 向退出函数注册 log_on_exit() 回调函数,该函数旨在退出时清空 log 指针,避免内存泄漏。

在 getuid() 循环体中,如果当前是 root 用户,则更改配置文件中的 userid 和 gruopid 属性为0(最高权限),并设置环境变量 HOME 为当前家目录。

globla_init() 最终返回一个 g_ceph_context 指针,该指针实际指向一个 CephContext 对象。CephContext表示由单个库用户持有的上下文。在同一个进程中可以有多个CephContexts。对于守护进程和实际程序(如本文最开始的 rados bench…),只有一个CephContext。CephContext包含配置、dout对象以及您可能希望在每次函数调用时传递给libcommon的任何其他内容。

boost::intrusive_ptr<CephContext>
global_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,
	    const char *data_dir_option, bool run_pre_init)
{
    //标志位,避免一个进程中多次执行 global_init()
    static bool first_run = true;
    //执行 global_pre_init()...
    if (run_pre_init) {
        ceph_assert(!g_ceph_context && first_run);
        global_pre_init(defaults, args, module_type, code_env, flags);
    } else {
        ceph_assert(g_ceph_context && first_run);
    }
    //更改标志位,避免重复执行global_init()
    first_run = false;
    //屏蔽 SIGPIPE 信号
    int siglist[] = { SIGPIPE, 0 };
    block_signals(siglist, NULL);
    //加载信号处理器
    if (g_conf()->fatal_signal_handlers) {
        install_standard_sighandlers();
    }
    //退出时删除 log 指针,注意是内存中的 log 指针,不是日志文件
    if (g_conf()->log_flush_on_exit)
        g_ceph_context->_log->set_flush_on_exit();
    
    //读取 --setuser 配置参数,如果当前不是 root 则什么也不做,如果时 root,则根据配置参数更改
    if (getuid() != 0) {
        ...
    }
    else if (g_conf()->setgroup.length() ||
             g_conf()->setuser.length()) {
        ...
        g_ceph_context->set_uid_gid(uid, gid);
        g_ceph_context->set_uid_gid_strings(uid_string, gid_string);
        ...
        setenv("HOME", home_directory.c_str(), 1)
        ...
    }
    //生效更改的配置
    // Expand metavariables. Invoke configuration observers. Open log file.
    g_conf().apply_changes(nullptr);

    // call all observers now.  this has the side-effect of configuring
    // and opening the log file immediately.
    g_conf().call_all_observers();
    //打印先前发生的错误到日志文件中
    g_conf().complain_about_parse_errors(g_ceph_context);
    ...
    //内存泄漏检测
    if (g_conf()->debug_deliberately_leak_memory) {
        derr << "deliberately leaking some memory" << dendl;
        char *s = new char[1234567];
        (void)s;
        // cppcheck-suppress memleak
    } 
    ...
    return boost::intrusive_ptr<CephContext>{g_ceph_context, false};
}

2.1 global_pre_init()

global_pre_init() 是 global_init() 的预执行步骤,主要解析配置、启动 MonClient、启动 log 线程。这里的解析配置的顺序是:部分命令行参数、默认配置、配置文件 ceph.conf、环境变量、命令行参数。后一步的配置可以覆盖前一步获得的配置参数。因此命令行参数的优先级最高,默认配置的优先级最低。

ceph_argparse_early_args() 就是用来预解释命令含参数的,包括:conf、cluster、id、name。此外遇到“–version”则会直接打印版本号,然后退出程序;遇到“–show_args”则会打印出所有的命令行参数。

common_preinit() 返回了一个 CephContext 实例,对于一个实例进程来说,贯穿一生的唯一 CephContext 其实就是在这里创建的,上文 global_init() 返回的 g_ceph_context 指针实际就是这个 CephContext 对象。其中包括了 log、配置参数、网络通信等等一个 Ceph 进程所包含的各种模块的信息或者实例指针。

conf.parse_config_files() 读取配置文件的参数。默认使用 /etc/ceph/ceph.conf。conf.parse_env() 从环境读取“CEPH_ARGS”参数。conf.parse_argv() 再从命令行读取配置参数。

cct->_log->start() 启动 log 线程,开启日志。

mc_bootstrap.get_monmap_and_config() 配置网络通信,并开启3个 msgr_worker 线程。

do_argv_commands() 执行 --show-config[-val] 命令,打印指定参数值。 g_conf().complain_about_parse_errors() 则是向日志中写入错误信息。因为日志刚刚开启,所以没有在一开始发生错误的时候就写日志,而是收集起来,最后一起写入日志文件。

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)
{
    ...
    //预读取命令行参数
    CephInitParameters iparams = ceph_argparse_early_args(
    args, module_type,
    &cluster, &conf_file_list);
    ...
    //创建 CephContext
    CephContext *cct = common_preinit(iparams, code_env, flags);
    cct->_conf->cluster = cluster;
    global_init_set_globals(cct);
    auto& conf = cct->_conf;
    ...
    //读取配置文件参数
    int ret = conf.parse_config_files(c_str_or_null(conf_file_list),&cerr, flags);
    ...
    //读取环境变量参数
    conf.parse_env(cct->get_module_type());
    //读取命令行参数
    conf.parse_argv(args);
    //开启 log
    if (conf->log_early &&!cct->_log->is_started()) {
        cct->_log->start();
    }
    //开启网络通信
    if (!conf->no_mon_config) {
        conf.apply_changes(nullptr);
        MonClient mc_bootstrap(g_ceph_context);
        if (mc_bootstrap.get_monmap_and_config() < 0) {
            cct->_log->flush();
            cerr << "failed to fetch mon config (--no-mon-config to skip)"
                << std::endl;
            _exit(1);
        }
    }
    // do the --show-config[-val], if present in argv
    conf.do_argv_commands();

    // Now we're ready to complain about config file parse errors
   g_conf().complain_about_parse_errors(g_ceph_context); 
}

2.1.1 log

Ceph 对每个子系统的日志都预先定义了日志级别,支持动态修改。每条 log 都带有日志级别,低于预先定义的级别才会被打印,预定义级别可以在配置文件中修改,并且可以针对某个模块,如 osd、rbd 等。log 模块有单独的线程,只需要向该线程提交 job 即可,是异步的过程,对系统的性能影响很小。

下面给出了 log 类的头文件,里面定义了 log 所需的变量以及方法。DEFAULT_MAX_NEW =100: 表示新建日志的队列深度为100条,DEFAULT_MAX_RECENT=10000 表示最多记录最近写的10000条日志记录。

log 类其实可以简单的通过三个队列来理解。m_new 储存新加入的待写日志,m_recent 存储最近写完的日志,m_flush 表示即将写入日志文件的日志。就像是3个杯子,首先向 new 杯子倒水,如果不把 new 杯子里的水倒掉,最多倒100次 new 杯子就满了。new 杯子的水则倒向向 flush 杯子,flush杯子像是一个临时的中转杯,水在这里短暂停留,一旦用小本本记下了这杯水,就立刻把 flush 杯子的水倒入 recent 杯子。看起来 recent 杯子好像是个垃圾桶,明明都已经记载在小本上了,却还要留着这杯水,这是为了方便我们后续可以直接通过 recent 杯子查看倒了多少水、什么水。但是 recent 杯子也不是无底洞,它最多可以存10000次,满了就把杯底的开关打开,把水从底部放掉,这样可以优先把最不新鲜的水倒掉。

log 线程则通过 start() 来创建,具体调用链:cct->_log->start() -> Thread::create() -> Thread::try_create() -> pthread_create()。线程具体方法则是 *Log::entry()。

class Log : private Thread
{
  ...
  static const std::size_t DEFAULT_MAX_NEW = 100;
  static const std::size_t DEFAULT_MAX_RECENT = 10000;
  ...
  //三个队列,分别是最新的entries(待写日志)队列,最近写完的日志队列和即将写的日志队列(m_new flush后得到)
  EntryVector m_new;    ///< new entries
  EntryRing m_recent; ///< recent (less new) entries we've already written at low detail
  EntryVector m_flush; ///< entries to be flushed (here to optimize heap allocations)
  ...  

  //线程入口
  void *entry() override;
  ...

public:
  using Thread::is_started;

  Log(const SubsystemMap *s);
  ~Log() override;
  //退出时 flush 一次,清空 m_new,保证写入所有日志
  void set_flush_on_exit();
  ...
  //刷新
  void flush();
  //打印最近的日志
  void dump_recent();

  ...
  //提交日志
  void submit_entry(Entry&& e);
  //开启线程和终止线程
  void start();
  void stop();
  ...
}

注意上面 log 的构造函数: Log(const SubsystemMap *s),创建 Log 实例需要传入 SubsystemMap 。这个 map 实际也是在 CephContext 构造函数初始化列表中创建的。主要用来描述单个子系统的日志信息(即子模块,类似 rbd、rbd bench)。此处的日志级别区别与每条日志的级别,只有当日志级别小于等于该子系统日志级别时才会被打印,可在配置文件中更改子系统日志级别。

struct Subsystem {
  int log_level; // 日志级别
  int gather_level; // gather级别
  std::string name; // 子系统名称
  
  Subsystem() : log_level(0), gather_level(0) {}     
};

下面给出了日志写入文件操作的部分源代码。简单叙述整个过程,就是把 m_new 队列数据转移到 m_flush,读取 m_flush 队列中的每条记录,判断是否满足日志级别小于子系统级别的要求,同时把数据复制到 m_recent 队列,然后经过层层调用,最终通过 write() 写入日志文件。

void *Log::entry()
{
  reopen_log_file(); //打开日志文件,返回 fd 文件号,记录在 Log.cc 中的 m_fd 局部变量中
  {
    ...
    //循环刷新日志,m_new 转移到 m_flush,具体在 flush() 函数实现。
    while (!m_stop) {
      if (!m_new.empty()) {
        ...
        flush();
        ...
        continue;
      }
      ...
    }
    ...
  }
  //线程结束时,再次刷新
  flush();
  return NULL;
}


void Log::flush()
{
  {
    ...
    m_flush.swap(m_new);//把 m_new 队列数据转移到 m_flush
    m_cond_loggers.notify_all();//唤醒所有线程
    ...
  }
  _flush(m_flush, true, false);//实例化 m_flush 队列,数据记录在 m_recent
 ...
}


void Log::_flush(EntryVector& t, bool requeue, bool crash)
{
  ...
  for (auto& e : t) {
    //判断本条日志级别是否小于等于子系统日志级别,小于则为true
    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;

    if (do_fd || do_syslog || do_stderr) {
      ...
      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;
      }
      ...
      if (do_fd) {
      ...
      if (m_log_buf.size() > MAX_LOG_BUF) {
        _flush_logbuf();
      }
    }
    //把数据存储压入m_recent
    if (requeue) {
      m_recent.push_back(std::move(e));
    }
  }
  t.clear();

  _flush_logbuf();
}


//再经过几次调用,最终通过 write() 函数把日志序列化到文件中。
void Log::_flush_logbuf()
{
  if (m_log_buf.size()) {
    _log_safe_write(std::string_view(m_log_buf.data(), m_log_buf.size()));
    m_log_buf.resize(0);
  }
}

大致了解了 log 线程的工作模式后,让我们来看看如何通过代码的方式记录日志,以及它是如何实现的?

随机挑选一条日志记录代码,具体如下。

ldout(cct, 20) << "Log depth=" << depth << " x=" << x << dendl;

可以看到和我们通常使用的 cout 命令有点像。<< 拼接字符串,dendl 结尾,ldout() 中传入两个参数,第一个是固定的 cct (CephContext),第二个代表日志级别:error = -1、warn = 0、info = 1、debug = 5、20 = trace。了解这些,已经能够完成记录日志的操作了。

ldout 主要在dout.h 文件中通过宏定义来实现,如下给出了3个日志输出方式:lsubdout、ldout、lderr,都通过 dout_impl() 方法实现。should_gather 标志位,判定是否满足输出级别要求。创建 MutableEntry 实例,通过submit_entr() 方法提交到 m_new 队列,交由 log 线程处理。

#define lsubdout(cct, sub, v)  dout_impl(cct, ceph_subsys_##sub, v) dout_prefix
#define ldout(cct, v)  dout_impl(cct, dout_subsys, v) dout_prefix
#define lderr(cct) dout_impl(cct, ceph_subsys_, -1) dout_prefix

#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)

2.1.2 msgr-worker-x

说到 msgr-worker,它实际是 Messenger 模块创建的通信线程,用来监听和发送对象。在《Ceph 源码分析》一书中有简单介绍 SimpleMessenger,通过 pipe 传递消息。但是在目前的N版中,主要使用 AsyncMessenger,AsynsMessenger 使用 epoll 来监听端口发送消息,有异步、多路IO复用等功能,能够提升端口监听的数量以及降低资源消耗。

msgr-worker 创建过程的具体调用链如下:
global_init() -> global_pre_init() -> mc_bootstrap.get_monmap_and_config() -> Messenger::create_client_messenger() -> Messenger::create() -> new AsyncMessenger -> Stack->start()。

网络通信模块实在是纷繁复杂,想要顺畅、完整地介绍它,本人还做不到。这里就简单的介绍下 AsyncMessenger,关于网络通信模块详细介绍留待后续。

就从 Messenger::create() 开始吧。Meseenger 支持三种模式:simple、async、xio,可以通过指定 --ms_type=XX 参数来选择模式。AsyncMessenger 除了支持 posix 传输模式外,还支持 rdma 和 dpdk,通过 --ms_type=async+XX来配置。AsyncMessenger 首先创建一个 StackSingleton 单例,再调用 single->ready(),再到 NetworkStack::create() 函数创建 NetworkStack。

AsyncMessenger::AsyncMessenger(CephContext *cct, entity_name_t name,
                               const std::string &type, string mname, uint64_t _nonce)
  : SimplePolicyMessenger(cct, name,mname, _nonce),
    dispatch_queue(cct, this, mname),
    lock("AsyncMessenger::lock"),
    nonce(_nonce), need_addr(true), did_bind(false),
    global_seq(0), deleted_lock("AsyncMessenger::deleted_lock"),
    cluster_protocol(0), stopped(true)
{
  std::string transport_type = "posix";
  //支持 rdma 和 dpdk 传输模式
  if (type.find("rdma") != std::string::npos)
    transport_type = "rdma";
  else if (type.find("dpdk") != std::string::npos)
    transport_type = "dpdk";
        
  //创建 StackSingleton 单例
  auto single = &cct->lookup_or_create_singleton_object<StackSingleton>(
    "AsyncMessenger::NetworkStack::" + transport_type, true, cct);
  //创建 NetworkStack
  single->ready(transport_type);
  //创建 PosixWorker
  stack = single->stack.get();
  //开启 msgr-worker 线程
  stack->start();
  ...
}

struct StackSingleton {
  ...
  void ready(std::string &type) {
    if (!stack)
      stack = NetworkStack::create(cct, type);
  }
  ...
};

NetworkStack 构造函数如下。NetworkStack 中的 workers 用来保存多个 worker,每个 worker 都会创建一个 epoll(大多的网络编程中,都会使用基于事件通知的异步网络 IO 方式来实现,比如 Epoll 和 Kqueue,ceph 的网络模块使用的是Epoll)。worker 的作用主要就是监听端口和建立连接,使用 epoll 来进行 socket 事件驱动。3个 worker 保证了负载均衡,每次调用 connection 都会寻找连接数最少的 worker 线程。

NetworkStack::NetworkStack(CephContext *c, const string &t): type(t), started(false), cct(c)
{
  //读取 msger_worker 线程的数量配置
  num_workers = cct->_conf->ms_async_op_threads;
  ...
 //循环创建 woker,压入 workers 队列,在事件中心初始化
  for (unsigned i = 0; i < num_workers; ++i) {
    Worker *w = create_worker(cct, type, i);
    w->center.init(InitEventNumber, i, type);
    workers.push_back(w);
  }
}

class PosixWorker : public Worker {
  //监听端口
  int listen(entity_addr_t &sa,
	     unsigned addr_slot,
	     const SocketOptions &opt,
	     ServerSocket *socks) override;
  //连接端口
  int connect(const entity_addr_t &addr, const SocketOptions &opts, ConnectedSocket *socket) override;
};

在 stack->start() 方法中,add_thread() 创建了线程的方法体(线程开启时,执行该方法),spawn_worker() 中通过 std::thread() 方法创建线程。线程的数量 num_workers 根据配置中的 ms.async.op.threads 来决定,默认为3。

 void NetworkStack::start(){
     ...
      for (unsigned i = 0; i < num_workers; ++i) { 
          if (workers[i]->is_init())
              continue;
          //创建线程主体
          std::function<void ()> thread = add_thread(i);
          //开启线程
          spawn_worker(i, std::move(thread));
      }
     ...
 }

add_thread() 中设置了当前线程的名称:msgr-worker-{num},在事件中心注册 worker 线程,进行初始化工作。while 循环中则一直轮询处理 EventCenter 中的事件。EventCenter 是一个存储事件的容器,它通过注册的回调函数 EventCallbackRef 来针对的处理事件。事件类型有三种:time_events(定时事件)、external_events(外部事件,发送消息)、可读事件(epoll 监听的事件,接收消息)。

std::function<void ()> NetworkStack::add_thread(unsigned i)
{
  Worker *w = workers[i];
  return [this, w]() {
      char tp_name[16];
      sprintf(tp_name, "msgr-worker-%u", w->id);
      //线程改名:msgr-worker-x
      ceph_pthread_setname(pthread_self(), tp_name);
      const unsigned EventMaxWaitUs = 30000000;
      w->center.set_owner();
      ldout(cct, 10) << __func__ << " starting" << dendl;
      w->initialize();
      w->init_done();
      //轮询事件中心,处理消息
      while (!w->done) {
        ldout(cct, 30) << __func__ << " calling event process" << dendl;

        ceph::timespan dur;
        int r = w->center.process_events(EventMaxWaitUs, &dur);
        if (r < 0) {
          ldout(cct, 20) << __func__ << " process events failed: "
                         << cpp_strerror(errno) << dendl;
          // TODO do something?
        }
        w->perf_logger->tinc(l_msgr_running_total_time, dur);
      }
      w->reset();
      w->destroy();
  };
}

至此,建立了网络通信模块的基础,后续需要连接某个 ip 端口时,需要从 stack 中申请一个负载最小的 msgr_worker 线程,通过 new AsyncConnection() 方式建立连接。可以在上述代码中看到消息处理的蛛丝马迹:w->center.process_events(),消息的传递都经由 EventCenter 事件中心,我们在实际操作中,只需要把消息封装成 message ,通过 dispatch_event_external() 提交给事件中心并注册为外部事件即可,然后由 connection 建立连接时指定的 msgr_worker 线程去处理消息的传递。

总结:global_init() 函数是通用了全局初始化函数,无论是客户端还是服务端,Ceph 的任意子系统都需要进行该初始化。主要内容包括:解析配置(默认配置、命令行参数、配置文件)、开启 log、开启网络通信、加载信号处理器,创建 CephContext。

3. common_init_finish()

common_init_finish() 是在 rados.cc 的 main 函数中,紧接 global_init() 函数出现的。主要作用是开启 service 线程和 admin_socket 线程,具体调用过程:common_init_finish() => cct->start_service_thread() => _service_thread->create(),_admin_socket->init()。其中 service 是一个定时任务,通过 _refresh_perf_values() 方法定时刷新 workers 和 un_healthy_workers 线程的数量,监控 mempool 内存池的容量。admin_socket 则提供对外接口,用于查看当前配置、进程状态、获取 log 等。

void common_init_finish(CephContext *cct)
{
  // only do this once per cct
  if (cct->_finished) {
    return;
  }
  cct->_finished = true;
  //初始化加密设置
  cct->init_crypto();
  ZTracer::ztrace_init();
  //开启日志线程
  if (!cct->_log->is_started()) {
    cct->_log->start();
  }

  int flags = cct->get_init_flags();
  if (!(flags & CINIT_FLAG_NO_DAEMON_ACTIONS))
    //开启 service、admin_socket 线程
    cct->start_service_thread();
  ...
}

void CephContext::start_service_thread()
{
  {
    //开启 service 线程
    _service_thread = new CephContextServiceThread(this);
    _service_thread->create("service");
  }

  // make logs flush on_exit()
  if (_conf->log_flush_on_exit)
    _log->set_flush_on_exit();

  // Trigger callbacks on any config observers that were waiting for
  // it to become safe to start threads.
  _conf.set_safe_to_start_threads();
  _conf.call_all_observers();

  // start admin socket
  if (_conf->admin_socket.length())
    _admin_socket->init(_conf->admin_socket);
}

3.1 service

以下给出了 service 线程的入口方法 entry() 。每经过 heartbeat_interval 内部心跳时间,就刷新一次性能参数:l_cct_total_workers、l_cct_unhealthy_workers、mempool中参数,通过 CephContext::_refresh_perf_values() 方法。

class CephContextServiceThread : public Thread
{
  void *entry() override
  {
    while (1) {
      
       //定时触发
      if (_cct->_conf->heartbeat_interval) {
        auto interval = ceph::make_timespan(_cct->_conf->heartbeat_interval);
        _cond.wait_for(l, interval);
      } else
        _cond.wait(l);
      //是否重打开日志文件
      if (_reopen_logs) {
        _cct->_log->reopen_log_file();
        _reopen_logs = false;
      }
      _cct->_heartbeat_map->check_touch_file();

      //刷新性能计数器。
      // refresh the perf coutners
      _cct->_refresh_perf_values();
    }
    return NULL;
  }
}

void CephContext::_refresh_perf_values()
{
  if (_cct_perf) {
    _cct_perf->set(l_cct_total_workers, _heartbeat_map->get_total_workers());
    _cct_perf->set(l_cct_unhealthy_workers, _heartbeat_map->get_unhealthy_workers());
  }
  unsigned l = l_mempool_first + 1;
  for (unsigned i = 0; i < mempool::num_pools; ++i) {
    mempool::pool_t& p = mempool::get_pool(mempool::pool_index_t(i));
    //byte 和 items 为原子变量,本身具有锁得特性,所以读写无需上锁。
    _mempool_perf->set(l++, p.allocated_bytes());
    _mempool_perf->set(l++, p.allocated_items());
  }
}

以下给出了 mempool 中性能参数列表,可以通过 ceph daemon osd.0 perf dump mempool 查询指定模块的性能参数。

"mempool": {
        "bloom_filter_bytes": 0,
        "bloom_filter_items": 0,
        "bluestore_alloc_bytes": 98720,
        "bluestore_alloc_items": 12340,
        "bluestore_cache_data_bytes": 0,
        "bluestore_cache_data_items": 0,
        "bluestore_cache_onode_bytes": 48600,
        "bluestore_cache_onode_items": 81,
        "bluestore_cache_other_bytes": 66492,
        "bluestore_cache_other_items": 8132,
        "bluestore_fsck_bytes": 0,
        "bluestore_fsck_items": 0,
        "bluestore_txc_bytes": 17664,
        "bluestore_txc_items": 24,
        "bluestore_writing_deferred_bytes": 425088,
        "bluestore_writing_deferred_items": 93,
        "bluestore_writing_bytes": 0,
        "bluestore_writing_items": 0,
        "bluefs_bytes": 4936,
        "bluefs_items": 84,
        "buffer_anon_bytes": 2232173,
        "buffer_anon_items": 112,
        "buffer_meta_bytes": 3784,
        "buffer_meta_items": 43,
        "osd_bytes": 286656,
        "osd_items": 24,
        "osd_mapbl_bytes": 0,
        "osd_mapbl_items": 0,
        "osd_pglog_bytes": 17600,
        "osd_pglog_items": 40,
        "osdmap_bytes": 35292,
        "osdmap_items": 290,
        "osdmap_mapping_bytes": 0,
        "osdmap_mapping_items": 0,
        "pgmap_bytes": 0,
        "pgmap_items": 0,
        "mds_co_bytes": 0,
        "mds_co_items": 0,
        "unittest_1_bytes": 0,
        "unittest_1_items": 0,
        "unittest_2_bytes": 0,
        "unittest_2_items": 0
    },

_cct_perf 和 _mempool_pref 都是 PerfCounter 类的实例化对象,PerfCounter 是一个容器,用来记录某种性能参数,根据注释所示,它可以追踪记录四种参数:

* 1) integer values & counters              //整数
* 2) floating-point values & counters       //浮点数
* 3) floating-point averages                //浮点数
* 4) 2D histograms of quantized value pairs //二维柱状图

此外还可以记录时间。以下给出了修改和获取参数的函数:

  //idx 为参数索引
  void inc(int idx, uint64_t v = 1); //加1
  void dec(int idx, uint64_t v = 1);//减1
  void set(int idx, uint64_t v);//设置为v值
  uint64_t get(int idx) const;//获取
  //修改时间的函数
  void tset(int idx, utime_t v);
  void tinc(int idx, utime_t v);
  void tinc(int idx, ceph::timespan v);
  utime_t tget(int idx) const;

注意的是,service 线程中,只定时维护了_cct_perf 和 _mempool_pref 中的参数更新,对于一个 ceph 模块来说还有很多其他的 PerfCounter。用户可以通过自定义的方式,添加性能监控,并手动维护参数更新。这里给出简单的使用方法。

//PerfCountersBuilder 是构造 PerfCounters 对象的类,其构造函数中创建 PerfCounters 对象。
PerfCountersBuilder plb(this, "cct", l_cct_first, l_cct_last);

//添加要监控的性能参数名称、描述
plb.add_u64(l_cct_total_workers, "total_workers", "Total workers");
plb.add_u64(l_cct_unhealthy_workers, "unhealthy_workers", "Unhealthy workers");

//获取动态分配的指针
_cct_perf = plb.create_perf_counters();

//将 PerfCounters 对象加入 collection
_perf_counters_collection->add(_cct_perf);

//在程序中手动设置监控参数的值,还有 inc()、dec() 方法。
_cct_perf->set(l_cct_total_workers, _heartbeat_map->get_total_workers());
_cct_perf->set(l_cct_unhealthy_workers, _heartbeat_map->get_unhealthy_workers());

3.2 admin_socket

admin_socket 线程是用来处理 ceph daemon 命令的线程,它和 service 线程一起提供了性能监控服务,service 线程更新各个模块性能参数,admin_socket 线程提供对外查询接口。

其对象 _admin_socket 在 CephContext 中进行初始化。同时,还新建了 _admin_hook,并向 _admin_socket 对象注册了很多个命令。这里简单介绍下 register_command,该函数的作用就是把命令参数和对应的方法关联起来,例如:config show 对应 _conf->show_config(),具体映射在钩子函数对象中 CephContextHook -> call() -> m_cct->do_command()。

  _admin_socket = new AdminSocket(this);
  _admin_hook = new CephContextHook(this);
  _admin_socket->register_command("assert", "assert", _admin_hook, "");
  _admin_socket->register_command("abort", "abort", _admin_hook, "");
  _admin_socket->register_command("perfcounters_dump", "perfcounters_dump", _admin_hook, "");
  _admin_socket->register_command("1", "1", _admin_hook, "");
  _admin_socket->register_command("perf dump", "perf dump name=logger,type=CephString,req=false name=counter,type=CephString,req=false", _admin_hook, "dump perfcounters value");
  ...

这里演示下 ceph daemon … 命令。admin_socket 线程作用就是监听这类查询命令,返回查询结果。

[root@localhost build]# ./bin/ceph daemon /tmp/ceph-asok.UU2i8r/client.admin.6783.asok help
*** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH ***
{
    "config diff": "dump diff of current config and default config",
    "config diff get": "dump diff get <field>: dump diff of current and default config setting <field>",
    "config get": "config get <field>: get the config value",
    "config help": "get config setting schema and descriptions",
    "config set": "config set <field> <val> [<val> ...]: set a config variable",
    "config show": "dump current config settings",
    "config unset": "config unset <field>: unset a config variable",
    "dump_mempools": "get mempool stats",
    "get_command_descriptions": "list available commands",
    "git_version": "get git sha1",
    "help": "list available commands",
    "log dump": "dump recent log entries to log file",
    "log flush": "flush log entries to log file",
    "log reopen": "reopen log file",
    "objecter_requests": "show in-progress osd requests",
    "perf dump": "dump perfcounters value",
    "perf histogram dump": "dump perf histogram values",
    "perf histogram schema": "dump perf histogram schema",
    "perf reset": "perf reset <name>: perf reset all or one perfcounter name",
    "perf schema": "dump perfcounters schema",
    "version": "get ceph version"
}

admin_socket 线程在 init() 函数中启动。首先是创建了管道,读取端的文件描述符记录在 m_shutdown_rd_fd 中,写入端的文件描述符记录在 m_shutdown_wr_fd 中。从变量名字也可以看出,该文件描述符的作用是收取关闭信息。退出的信号会写入管道的写入端,而线程会通过多路复用接口,监听读取端,一旦发现 m_shutdown_rd_fd 中读出内容,就关闭线程。线程的入口函数为 AdminSocket::entry(),功能就是循环监听端口,执行命令。

bool AdminSocket::init(const std::string& path)
{
  ldout(m_cct, 5) << "init " << path << dendl;

  /* Set up things for the new thread */
  //创建管道
  int pipe_rd = -1, pipe_wr = -1;
  err = create_shutdown_pipe(&pipe_rd, &pipe_wr);
  ...
  //绑定端口监听端口
  int sock_fd;
  err = bind_and_listen(path, &sock_fd);
  ...
  /* Create new thread */
  th = make_named_thread("admin_socket", &AdminSocket::entry, this);
 ...
  return true;
}

void AdminSocket::entry() noexcept
{
  ldout(m_cct, 5) << "entry start" << dendl;
  while (true) {
    ...
    if (fds[0].revents & POLLIN) {
      // Send out some data
      do_accept();
    }
    if (fds[1].revents & POLLIN) {
      // Parent wants us to shut down
      return;
    }
  }
}

common_init_finish() 中就创建了 service 和 admin_socket 线程,功能已经在上文介绍过了,可以看出,common_init_finish() 函数更侧重于一个进程的基础查询服务,包括对内的性能参数监控和对外的参数配置查询接口,它像是在 global_init() 功能之外的一层包装,global_init() 做的更多的是一个个 ceph 子系统或者子模块的基本构,类似操作系统的内核,而 commmon_init_finish() 类似内核之外的监控模块。

4. rados_tool_common()

rados_tool_common() 中封装了每个 rados 命令的具体处理方式。通过解析命令参数,来判断具体的处理方法。rados_tool_common() 中也做了一些基础通用操作,用于初始化 rados 和与集群建立连接。

rados.init_with_context() 主要工作就是根据配置文件中的参数,初始化 rados。具体调用链:rados.init_with_context() -> rados_create_with_context() -> _rados_create_with_context() -> librados::RadosClient::RadosClient()。

rados_connect() 作用是与集群建立连接,在 librados.h 中有提示注意:在调用其他相关通讯函数之前,必须调用此函数,否则会导致进程崩溃。在建立连接的过程中,还创建了6种线程:rados、ms_dispatcher、ms_local、safe_timer、fn_anonymous、fn_radosclient。后几小节将具体介绍这6个线程创建的时机与作用。

static int rados_tool_common(const std::map < std::string, std::string > &opts,
                             std::vector<const char*> &nargs)
{
    ...;
    Rados rados; 
    IoCtx io_ctx; //新建了 Rados 和 IoCtx 对象,方便后续对 librados 调用
    ...;
    //解析参数,opts 是在 main 中构造的 map,里面保留了命令中所有的有效参数
    i = opts.find("create");
    if (i != opts.end()) {
        create_pool = true;
    }
    i = opts.find("pool");
    if (i != opts.end()) {
        pool_name = i->second.c_str();
    }
    ...;
    //打开 rados
    ret = rados.init_with_context(g_ceph_context);
    ...;
    ret = rados.connect();
    ...;
    //创建新 pool
    if (create_pool) {
        ret = rados.pool_create(pool_name);
        if (ret < 0) {
            cerr << "error creating pool " << pool_name << ": " << cpp_strerror(ret) << std::endl;
            return 1;
        }
    }
    ...
    //打开io 上下文
        ret = pool_name ? rados.ioctx_create(pool_name, io_ctx) : rados.ioctx_create2(pgid->pool(), io_ctx);

    
    //通过判断传入的第一个参数命令,来决定调用什么方法,如果第一个参数为“bench”(rados bench ...),则进入一下代码模块。
    else if (strcmp(nargs[0], "bench") == 0) {
        ...;
    }
}

4.1 rados.connect()

先来解析下 rados_connect() 的调用链,找到具体的函数方法。rados.connect() -> client->connect() -> librados::RadosClient::connect()。实际上,connect() 进行网络通信需要做的一些预处理,尽管它的名字叫做 connnect “链接”,但是实际的网络链接是发生在后面的操作中:_op_submit() -> _get_session() -> messenger->get_connection()。

这里简单介绍下一个完整的 client 端发送消息的流程。
第一步:创建 messenger 并设置策略,Messenger::create() 和 messenger->set_default_policy()。
第二步:创建 objecter,new Object()。
第三步:设置消息分发器, messenger->add_dispatcher_head() 和 messenger->add_dispatcher_tail()。
第四步:启动消息,messenger->start()。
第五步:发送请求 objecter->op_submit(),在发送请求之前,要把数据封装成 Objecter::Op 对象。

在 connect() 方法中,完成了前四步的过程,相当于为发送消息做了预处理,后续再发送请求时只需要调用 op_submit() 即可。此外,connect() 中还做了 monclient、mgrclient 和 timer 的初始化工作。其中 monclient 的初始化函数 init() 中将 monclient 实例对象注册到 dispatcher 中心(通过 add_dispatcher_head() 方法,后文有介绍),创建路由密钥,启动定时器 timer 和开启 finisher 线程等。mgrclient 的初始化函数 init() 中做的工作就简单许多,只是启动了定时器。

int librados::RadosClient::connect()
{
  //获取monmap并配置
  {
    MonClient mc_bootstrap(cct);
    err = mc_bootstrap.get_monmap_and_config();
    if (err < 0)
      return err; 
  }

  // get monmap
  err = monclient.build_initial_monmap();
  if (err < 0)
    goto out;

  //新建 messenger
  messenger = Messenger::create_client_messenger(cct, "radosclient");

  //设置messenger策略
  // require OSDREPLYMUX feature.  this means we will fail to talk to
  // old servers.  this is necessary because otherwise we won't know
  // how to decompose the reply data into its constituent pieces.
  messenger->set_default_policy(Messenger::Policy::lossy_client(CEPH_FEATURE_OSDREPLYMUX));

  //创建object,用来处理发送的数据
  objecter = new (std::nothrow) Objecter(cct, messenger, &monclient,
			  &finisher,
			  cct->_conf->rados_mon_op_timeout,
			  cct->_conf->rados_osd_op_timeout);
  if (!objecter)
    goto out;
  // 根据配置,设置启用 throttle 来控制发出的op数
  objecter->set_balanced_budget();
  //为 monclient 和 mgrclient 提供消息通信层实例
  monclient.set_messenger(messenger);
  mgrclient.set_messenger(messenger);
  //初始化objecter perfcounter 并提供能够查询正在处理的op状况的钩子
  objecter->init();
  //为消息层实例添加Dispatcher
  messenger->add_dispatcher_head(&mgrclient);
  messenger->add_dispatcher_tail(objecter);
  messenger->add_dispatcher_tail(this);
  //启动messenger实例
  messenger->start();

  ldout(cct, 1) << "setting wanted keys" << dendl;
  monclient.set_want_keys(
      CEPH_ENTITY_TYPE_MON | CEPH_ENTITY_TYPE_OSD | CEPH_ENTITY_TYPE_MGR);
  ldout(cct, 1) << "calling monclient init" << dendl;
  //初始化 monclient
  err = monclient.init();
  if (err) {
    ldout(cct, 0) << conf->name << " initialization error " << cpp_strerror(-err) << dendl;
    shutdown();
    goto out;
  }
    
  //验证客户端的权限
  err = monclient.authenticate(conf->client_mount_timeout);
  if (err) {
    ldout(cct, 0) << conf->name << " authentication error " << cpp_strerror(-err) << dendl;
    shutdown();
    goto out;
  }
  messenger->set_myname(entity_name_t::CLIENT(monclient.get_global_id()));

  // Detect older cluster, put mgrclient into compatible mode
  mgrclient.set_mgr_optional(
      !get_required_monitor_features().contains_all(
        ceph::features::mon::FEATURE_LUMINOUS));

  // MgrClient needs this (it doesn't have MonClient reference itself)
  monclient.sub_want("mgrmap", 0, 0);
  monclient.renew_subs();

  if (service_daemon) {
    ldout(cct, 10) << __func__ << " registering as " << service_name << "."
		   << daemon_name << dendl;
    mgrclient.service_daemon_register(service_name, daemon_name,
				      daemon_metadata);
  }
    
  //初始化 mgrclient
  mgrclient.init();

  objecter->set_client_incarnation(0);
  objecter->start();
  lock.Lock();
  //初始化timer
  timer.init();

  finisher.start();

  //状态更新为已连接
  state = CONNECTED;
  instance_id = monclient.get_global_id();

  return err;
}

4.1.1 ms_dispatcher 与 ms_local

dispatcher 是消息分发中心,所有收到的消息都经由该模块,并由该模块转发给相应的处理模块(moncliet、mdsclient、osd等)。其实现方式比较简单,就是把所有的模块及其处理消息的方法 handle 注册到分发中心,具体函数为 add_dispatcher_head/tail(),这样就像 dispaatcher_queue 中添加了指定模块。后续在分发消息时,对 dispatcher_queue 进行轮询,直到有一个处理模块能够处理该消息,通过 message->get_type() 来指定消息的处理函数。所有的消息分发都在 dispatcher 线程中完成。

在 add_dispatcher_head() 和 add_dispatcher_tail() 函数中,都做了 dispatcher 队列是否为空的判断(通过 dispatchers.empty() == true)。如果判定结果为空,说明需要重新创建 dispatcher 线程并绑定服务端地址,加入事件中心监听端口,具体方法在 ready() 中。

  void add_dispatcher_head(Dispatcher *d) {
    bool first = dispatchers.empty();
    dispatchers.push_front(d);
    if (d->ms_can_fast_dispatch_any())
      fast_dispatchers.push_front(d);
    if (first)
      ready();
  }

这里给出了 AsyncMessenger::ready() 方法。p->start()(Processor::start())方法中监听 EVENT_READABLE 事件并把事件提交到 EventCenter 事件中心,由上文介绍的 msgr-worker-x 线程去轮询事件中心的队列,监听端口是否收到消息。收到的消息则由 dispatcher 线程分发给指定的处理程序,其分发消息的接口为 ms_dispatch() 和 ms_fast_dispatch()。

dispatch_queue.start() 中开启了消息分发线程,分别为处理外部消息的 ms_dispatch 线程和处理本地消息的 ms_local 线程。相应的,它们有各自的优先级队列(注意:分发消息的队列时有优先级的,优先级越高,发送时机越早),分别是存储外部消息的 mqueue 和本地消息队列的 local_messages。消息队列的添加方式也有两种:mqueue.enqueue() 和 local_queue.emplace()。

void AsyncMessenger::ready()
{
  ldout(cct,10) << __func__ << " " << get_myaddrs() << dendl;

  stack->ready();
  //绑定端口
  if (pending_bind) {
    int err = bindv(pending_bind_addrs);
    if (err) {
      lderr(cct) << __func__ << " postponed bind failed" << dendl;
      ceph_abort();
    }
  }

  Mutex::Locker l(lock);
  //调用 worker 线程,监听端口
  for (auto &&p : processors)  
    p->start();
  //开启 ms_dispatcher 和 ms_locla 线程
  dispatch_queue.start();
}

void DispatchQueue::start()
{
  ceph_assert(!stop);
  ceph_assert(!dispatch_thread.is_started());
  //开启 ms_dispatch 和 ms_local 线程
  dispatch_thread.create("ms_dispatch");
  local_delivery_thread.create("ms_local");
}

4.1.2 safe_timer

在 rados.connect() 中,monclient、mgrclient、radsoclient 都创建了 safe_time 线程。这其实一个定时器,每个模块都有自己的定时器(例如 mgrclient 在类中声明了:SafeTime timer 对象),通过调用 SafeTime::init() 方法来开启线程启动定时器模块。SafeTimer::timer_thread() 是 safe_time 线程方法,可以看到内部采用while 死循环,重复轮询事件表 schedule,检查是否到达任务的执行事件。任务在 schedule 中按照事件升序排列。首先检查,如果第一任务没有到事件,后面的任务就不用检查,直接 break。如果任务到了事件,则执行 callback 任务,并在 schedule 中删除该定时任务,然后继续循环。

添加定时任务函数:add_event_after()、add_event_at()。

取消任务:cancel_event()、cancel_all_events()。

void SafeTimer::init()
{
  ldout(cct,10) << "init" << dendl;
  //创建并开启 timer,线程名为 safe_timer
  thread = new SafeTimerThread(this);
  thread->create("safe_timer");
}

//线程方法
void SafeTimer::timer_thread()
{
  lock.lock();
  ldout(cct,10) << "timer_thread starting" << dendl;
  while (!stopping) {
    utime_t now = ceph_clock_now();

    while (!schedule.empty()) {
      scheduled_map_t::iterator p = schedule.begin();

      // is the future now?
      if (p->first > now)
	break;

      Context *callback = p->second;
      events.erase(callback);
      schedule.erase(p);
      ldout(cct,10) << "timer_thread executing " << callback << dendl;
      
      if (!safe_callbacks)
	lock.unlock();
      callback->complete(0);
      if (!safe_callbacks)
	lock.lock();
    }

    // recheck stopping if we dropped the lock
    if (!safe_callbacks && stopping)
      break;

    ldout(cct,20) << "timer_thread going to sleep" << dendl;
    if (schedule.empty())
      cond.Wait(lock);
    else
      cond.WaitUntil(lock, schedule.begin()->first);
    ldout(cct,20) << "timer_thread awake" << dendl;
  }
  ldout(cct,10) << "timer_thread exiting" << dendl;
  lock.unlock();
}

4.1.3 fn_anonymous 与 fn-radosclient

fn_anonymous 线程是在 monclient() 构造函数中创建,在 monclient.init() 初始化函数中启动的线程。fn-radosclient 线程则是 radosclient() 构造函数中创建的线程,在 rados.connect() 中启动的线程。它们都归属于 finisher 线程,只是一个属于匿名对象,一个属于命名对象。finisher 类主要用来完成回调函数 context 的执行,通过开启新线程的方式,异步处理 context 的收尾工作。

MonClient::MonClient(CephContext *cct_) :
  ...
  //创建 fn_anonymous 线程对象 
  finisher(cct_),
  ...
{}

librados::RadosClient::RadosClient(CephContext *cct_)
  : Dispatcher(cct_->get()),
    ...
    //fn-radosclient 线程在此初始化,后续调用 finisher.start() 开启线程
    finisher(cct, "radosclient", "fn-radosclient")
{
}

finisher 线程的方法实体为 Finisher::finisher_thread_entry(),其主要功能为循环处理 finisher_queue 队列中的结束任务。注意:这里把 finisher_queue 中的任务提取到局部变量 ls 队列中,减少了锁的使用,提高了性能。通过调用 context 的 complete 处理方法,来执行 context 的收尾函数。可以设置多种 context 实例,并个性化它们的 finish() 函数。

添加一个 context 至完成队列:Finisher::queue()。

void *Finisher::finisher_thread_entry()
{
  ...
  while (!finisher_stop) {
    /// Every time we are woken up, we process the queue until it is empty.
    while (!finisher_queue.empty()) {
      // To reduce lock contention, we swap out the queue to process.
      // This way other threads can submit new contexts to complete
      // while we are working.
      //把 finisher_queue 中的任务提取到 ls。这样就不必锁住 finisher_queue,在finish 线程处理任务的同时,其他线程可以向 finisher_queue 提交任务,提高了性能。
      vector<pair<Context*,int>> ls;
      ls.swap(finisher_queue);
      ...
      // Now actually process the contexts.
      for (auto p : ls) {
     //执行 context 收尾函数
	p.first->complete(p.second);
      }
      ldout(cct, 10) << "finisher_thread done with " << ls << dendl;
      ls.clear();
      ...
    }
    ...
  }
  // If we are exiting, we signal the thread waiting in stop(),
  // otherwise it would never unblock
  finisher_empty_cond.notify_all();
  ...
  return 0;
}

4.2 radso.ioctx_create()

创建 ioctx (io 上下文)有两种方式,第一种是有 pool_name,可以使用 rados.ioctx_create(pool_name, io_ctx) 函数;第二种是有 pgid,可以使用 rados.ioctx_create2(pgid->pool(), io_ctx)。如果两个参数都不知晓,那么将无法创建 ioctx。在实际应用中,一般提前在集群中创建 pool 或者在程序中使用 rados.pool_create(pool_name) 创建 pool,来获取 pool 名称。

在 ioctx_create() 可以看到,创建 ioctx 实例,实际上是创建了 IoctxImpl 实例,并且 ioctx 类中的所有方法的具体实现也是调用了 IoctxImpl 中的函数。所以,可以这样认为 ioctx 是 IoctxImpl 的封装,放在 librados 库中,专门用于提供对外的 api 接口。

int librados::Rados::ioctx_create(const char *name, IoCtx &io)
{
  rados_ioctx_t p;
  //创建 IoctxImpl
  int ret = rados_ioctx_create((rados_t)client, name, &p);
  if (ret)
    return ret;
  io.close();
  //把 IoctxImpl 加入 ioctx
  io.io_ctx_impl = (IoCtxImpl*)p;
  return 0;
}

//librados::IoCtx::write() 调用 librados::IoCtxImpl::write()
int librados::IoCtx::write(const std::string& oid, bufferlist& bl, size_t len, uint64_t off)
{
  object_t obj(oid);
  return io_ctx_impl->write(obj, bl, len, off);
}

以下是 IoCtxImpl 的结构体,给出了部分参数。

struct librados::IoCtxImpl {
  std::atomic<uint64_t> ref_cnt = { 0 };
  RadosClient *client;
  int64_t poolid;
  snapid_t snap_seq;
  ::SnapContext snapc;
  uint64_t assert_ver;
  version_t last_objver;
  uint32_t notify_timeout;
  object_locator_t oloc;

  Mutex aio_write_list_lock;
  ceph_tid_t aio_write_seq;
  Cond aio_write_cond;
  xlist<AioCompletionImpl*> aio_write_list;
  map<ceph_tid_t, std::list<AioCompletionImpl*> > aio_write_waiters;

  Objecter *objecter;
  ...
}

ioctx 的功能有:读写数据、读写属性、快照 pool、读取快照等。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值