ceph 不管是客户端 lib 库、命令行,还是服务端守护进程都支持大量的配置参数,与大多数软件一致,ceph 的配置同样支持多个级别的配置:
- 代码内置的默认配置值
- 环境变量
- 配置文件
- 命令行参数
librados 提供了如下的 C/C++ 接口支持参数配置:
rados_conf_read_file / Rados::conf_read_file
rados_conf_parse_argv / Rados::conf_parse_argv
rados_conf_parse_argv_remainder / Rados::conf_parse_argv_remainder
rados_conf_parse_env / Rados::conf_parse_env
rados_conf_set / Rados::conf_set
rados_conf_get / Rados::conf_get
其实现主要调用了如下一些接口:
argv_to_vec
env_to_vec
conf->parse_config_files
conf->parse_argv
conf->set_val
conf->get_val
其中 conf
为 md_config_t *
实例。
服务端(mon/mgr/osd/mds)的配置参数处理流程如下:
- 调用
argv_to_vec
将 main 函数的命令行参数转成vector<const char*>
的形式; - 调用
env_to_vec
将CEPH_ARGS
环境变量转成vector<const char*>
的形式,并与argv_to_vec
得到的 vector 合并; - 调用
global_init
->global_pre_init
解析参数:
void global_pre_init(std::vector < const char * > *alt_def_args,
std::vector < const char* >& args,
uint32_t module_type, code_environment_t code_env,
int flags)
{
std::string conf_file_list;
std::string cluster = "";
// 调用 ceph_argparse_early_args 得到集群名字、id、配置文件路径,注意其中的 module_type 参数指定了 entity type,因此不应该
// 再通过 --name / -n 参数进行覆盖;
CephInitParameters iparams = ceph_argparse_early_args(args, module_type,
&cluster, &conf_file_list);
CephContext *cct = common_preinit(iparams, code_env, flags);
cct->_conf->cluster = cluster;
md_config_t *conf = cct->_conf;
// 调用 conf->parse_argv 解析 alt_def_args 指定的参数,alt_def_args 与 args 类似,但 args 是用户指定的参数,而 alt_def_args 是
// 进程实现代码设置的一些默认值;
if (alt_def_args)
conf->parse_argv(*alt_def_args); // alternative default args
// 调用 conf->parse_config_files 解析配置文件(可以是使用 ;,= \t 等字符分隔的一个列表),如果用户没有使用 --conf / -c 指定配置
// 文件,则会尝试读取 CEPH_CONF 环境变量,如果环境变量也没有定义,则会使用默认的配置文件;
int ret = conf->parse_config_files(c_str_or_null(conf_file_list),
&cerr, flags);
// 调用 conf->parse_env 读取 CEPH_KEYRING 环境变量定义的 keyring 路径,注意不要被它的名字所迷惑,它就解析这么一个环境
// 变量,真正的环境变量(即 CEPH_ARGS)定义的参数在前面通过 env_to_vec 已经与 argv_to_vec 得到的命令行参数合并至 args 了;
conf->parse_env(); // environment variables override
// 调用 conf->parse_argv 解析合并后的参数 args;
conf->parse_argv(args); // argv override
}
显然,除了通过调用 ceph_argparse_early_args
得到集群名字、id、配置文件路径,与 librados 客户端的接口类似,具体的配置参数都传递给内部的 md_config_t *conf
实例进行处理。
参数的处理依赖于 ceph_argparse 模块(common/ceph_argparse.cc)中定义的辅助函数,主要的辅助函数如下:
argv_to_vec
,将命令行参数(int argc, const char **argv
)转成vector<const char*>
的形式,由于它假设第一个参数是可执行文件的路径,在处理时会跳过第一个参数:
args.insert(args.end(), argv + 1, argv + argc);
由于 librados 接口 rados_conf_parse_argv
/ Rados::conf_parse_argv
在内部也调用了这个辅助函数,因此需要特别注意这个特殊处理;
env_to_vec
,将指定的环境变量(如果不指定则使用默认的CEPH_ARGS
环境变量)以空格作为分隔符转成vector<const char*>
的形式,同时它还会与传递进来的vector<const char*>
(通常是argv_to_vec
的处理结果)进行合并,且env_to_vec
在合并的时候会考虑对--
进行特殊处理:
args.clear();
args.insert(args.end(), options.begin(), options.end());
args.insert(args.end(), env_options.begin(), env_options.end());
if (dashdash)
args.push_back("--");
args.insert(args.end(), arguments.begin(), arguments.end());
args.insert(args.end(), env_arguments.begin(), env_arguments.end());
形成类似如下的排序:options
-> env_options
-> arguments
-> env_arguments
注意在命令行中指定 ceph 的环境命令时需要使用单引号,如:
export CEPH_ARGS='log_file=/var/log/ceph/$cluster-$name.$pid.$cctid.log admin_socket=$run_dir/clients/$cluster-$name.$pid.$cctid.asok'
因为在命令行中双引号中的 $
会被自动展开,导致得到的结果与预期完全不一致;
ceph_argparse_early_args
,遍历传入的vector<const char*>
参数,查找参数中是否指定了--version
/-v
/--conf
/-c
/--cluster
/-i
/--id
/--user
/--name
/-n
/--show-args
等 11 个选项:
for (std::vector<const char*>::iterator i = args.begin(); i != args.end(); ) {
if (strcmp(*i, "--") == 0) {
break;
} else if (ceph_argparse_flag(args, i, "--version", "-v", (char*)NULL)) {
cout << pretty_version_to_str() << std::endl;
_exit(0);
} else if (ceph_argparse_witharg(args, i, &val, "--conf", "-c", (char*)NULL)) {
*conf_file_list = val;
}
...
else {
// ignore
++i;
}
}
--version
/ -v
,打印版本信息并退出;
--show-args
,打印命令行以及环境变量指定的参数并退出;
--conf
/ -c
,指定配置文件路径;
--cluster
,指定集群名字;
-i
,指定 entity id,用于非 librados 客户端,即通常用于服务端守护进程(mon/mgr/osd/mds);
--id
/ --user
,指定 entity id,用于 librados 客户端;
--name
/ -n
,指定 entity type 和 id,由于与 -i
/ --id
一样都会设置 id,因此如果同时指定的话,后面的参数设置会覆盖前面的参数设置,由于对于守护进程也可以指定 --name
/ -n
参数,因此会覆盖进程实际的 entity type(此处存在 bug,对于守护进程不应该指定 --name
/ -n
);
其内部又调用了 ceph_argparse_xxx
等更底层的辅助函数;
ceph_argparse_flag
,假定命令行是类似--flag
/-f
这样的 flag 选项,将用户传递的命令行与指定的选项进行对比,如果返回 true 则表明命令行与指定 flag 选项匹配:
// 指定进行对比的选项可以同时指定长选项和短选项,所以使用可变参数
bool ceph_argparse_flag(std::vector<const char*> &args,
std::vector<const char*>::iterator &i, ...)
{
// 命令行
const char *first = *i;
char tmp[strlen(first)+1];
// 将命令行中的中划线转成下划线
dashes_to_underscores(first, tmp);
first = tmp;
va_list ap;
va_start(ap, i);
// 遍历可变参数
while (1) {
// 得到指定进行对比的选项
const char *a = va_arg(ap, char*);
if (a == NULL) { // 最后一个可变参数是 NULL,因此 while 循环能够正常结束
va_end(ap);
return false;
}
// 将指定的选项中的中划线转成下划线,实际上不需要,因为 ceph 支持的选项都是使用的下划线
char a2[strlen(a)+1];
dashes_to_underscores(a, a2);
if (strcmp(a2, first) == 0) { // 命令行与指定的选项匹配
i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 flag 选项:--flag / -f
va_end(ap);
return true;
}
}
}
ceph_argparse_binary_flag
,假定命令行是类似--flag=true
/-f=0
这样的带真值的 flag 选项,或者类似--flag
/-f
这样的 flag 选项,将用户传递的命令行与指定的选项进行对比,如果返回 true 则表明命令行与指定选项匹配,并设置 flag 的真值,其主要处理流程通过调用va_ceph_argparse_binary_flag
实现:
// 指定进行对比的选项可以同时指定长选项和短选项,所以使用可变参数,注意 va_start(ap, oss) / va_end(ap) 都在外层函数 ceph_argparse_binary_flag 中被调用
static bool va_ceph_argparse_binary_flag(std::vector<const char*> &args,
std::vector<const char*>::iterator &i, int *ret, std::ostream *oss,
va_list ap) {
// 命令行
const char *first = *i;
char tmp[strlen(first) + 1];
// 将命令行中的中划线转成下划线
dashes_to_underscores(first, tmp);
first = tmp;
// 遍历可变参数,注意 va_start(ap, oss) 在外层函数 ceph_argparse_binary_flag 中被调用
// does this argument match any of the possibilities?
while (1) {
// 得到指定进行对比的选项
const char *a = va_arg(ap, char*);
if (a == NULL) // 最后一个可变参数是 NULL,因此 while 循环能够正常结束,注意 va_end(ap) 在外层函数 ceph_argparse_binary_flag 中被调用
return false;
// 将指定的选项中的中划线转成下划线,实际上不需要,因为 ceph 支持的选项都是使用的下划线
int strlen_a = strlen(a);
char a2[strlen_a + 1];
dashes_to_underscores(a, a2);
if (strncmp(a2, first, strlen(a2)) == 0) { // 命令行与指定的选项匹配
if (first[strlen_a] == '=') {
i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 flag 选项: --flag=true / --flag=1 / -f=true / -f=1 / --flag=false / --flag=0 / -f=false / -f=0
const char *val = first + strlen_a + 1;
if ((strcmp(val, "true") == 0) || (strcmp(val, "1") == 0)) { // 命令行为 --flag=true / --flag=1 / -f=true / -f=1 的形式
*ret = 1;
return true;
} else if ((strcmp(val, "false") == 0) || (strcmp(val, "0") == 0)) { // 命令行为 --flag=false / --flag=0 / -f=false / -f=0 的形式
*ret = 0;
return true;
}
*ret = -EINVAL;
return true;
} else if (first[strlen_a] == '\0') { // 命令行为 --flag / -f 的形式
i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 flag 选项: --flag / -f
*ret = 1;
return true;
}
}
}
}
ceph_argparse_witharg
,假定命令行是类似--option=arg
/-o=arg
/--option arg
/-o arg
这样的这样的带参数的 option 选项,将用户传递的命令行与指定的选项进行对比,如果返回 true 则表明命令行与指定选项匹配,并设置 arg 的值,由于参数 arg 的类型有差异,因此针对字符串类型与数值型有不同的错误处理,但其内部主要处理流程一致,都是通过调用va_ceph_argparse_witharg
实现:
// 指定进行对比的选项可以同时指定长选项和短选项,所以使用可变参数,注意 va_start(ap, oss) / va_end(ap) 都在外层函数 ceph_argparse_witharg 中被调用
static int va_ceph_argparse_witharg(std::vector<const char*> &args,
std::vector<const char*>::iterator &i, std::string *ret, std::ostream &oss,
va_list ap) {
// 命令行
const char *first = *i;
char tmp[strlen(first) + 1];
// 将命令行中的中划线转成下划线
dashes_to_underscores(first, tmp);
first = tmp;
// 遍历可变参数,注意 va_start(ap, oss) 在外层函数 ceph_argparse_witharg 中被调用
// does this argument match any of the possibilities?
while (1) {
// 得到指定进行对比的选项
const char *a = va_arg(ap, char*);
if (a == NULL) // 最后一个可变参数是 NULL,因此 while 循环能够正常结束,注意 va_end(ap) 在外层函数 ceph_argparse_witharg 中被调用
return 0;
// 将指定的选项中的中划线转成下划线,实际上不需要,因为 ceph 支持的选项都是使用的下划线
int strlen_a = strlen(a);
char a2[strlen_a + 1];
dashes_to_underscores(a, a2);
if (strncmp(a2, first, strlen(a2)) == 0) { // 命令行与指定的选项匹配
if (first[strlen_a] == '=') { // 命令行为 --option=arg / -o=arg 的形式
*ret = first + strlen_a + 1;
i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 option 选项: --option=arg / -o=arg
return 1;
} else if (first[strlen_a] == '\0') { // 命令行参数可能为 --option arg / -o arg 的形式
// find second part (or not)
if (i + 1 == args.end()) {
oss << "Option " << *i << " requires an argument." << std::endl;
i = args.erase(i);
return -EINVAL;
}
i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 option 选项: --option / -o
*ret = *i;
i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 option 选项值:arg
return 1;
}
}
}
}
显然,几个 ceph_argparse_xxx
的处理都大同小异,如果与指定的选项匹配就返回 true,并且根据选项类型(即 flag 选项 或 option 选项)以不同的方式获取选项值。
同时需要注意的时,一旦选项匹配,命令行选项(如果是 option 选项,可能还包括 option 选项值)就会从命令行 vector<const char*>
容器中删除, 这在 librados 与主进程的其他命令行处理库协作时需要特别注意。
上面分析了 ceph_argparse 模块的底层实现细节,现在回到 md_config_t
,对于参数的解析,它主要包括 parse_config_files
和 parse_argv
两个主要的接口:
md_config_t::parse_config_files
如果用户没有使用 --conf
/ -c
指定配置文件,则会尝试读取 CEPH_CONF
环境变量,如果环境变量也没有定义,则会使用默认的配置文件($data_dir/config, /etc/ceph/$cluster.conf, ~/.ceph/$cluster.conf, $cluster.conf),最后得到配置文件是使用 ;,= \t
等字符分隔的一个列表。
然后调用 parse_config_files_impl
解析所有的配置文件:
int md_config_t::parse_config_files_impl(const std::list<std::string> &conf_files, std::ostream *warnings) {
assert(lock.is_locked());
// open new conf
list<string>::const_iterator c;
for (c = conf_files.begin(); c != conf_files.end(); ++c) { // 遍历配置文件
cf.clear();
string fn = *c;
expand_meta(fn, warnings);
// 调用 ConfFile::parse_file 逐行解析配置文件,并形成按节(section)组织的结构(以节名为 key):map<string, ConfSection>
// 其中 ConfSection 为:set<ConfLine>,而 ConfLine 是一组 kv 键值对
// 在 ConfFile::process_line 过程中 ConfLine 可能会用来保存节名,但在最终的 set<ConfLine> 结构中,ConfLine 只保存着键值对
int ret = cf.parse_file(fn.c_str(), &parse_errors, warnings);
if (ret == 0) // 任何一个配置文件解析成功,就不再遍历下一个配置文件了,因此用户在命令行指定多个配置文件没有意义
break;
else if (ret != -ENOENT)
return ret;
}
// it must have been all ENOENTs, that's the only way we got here
if (c == conf_files.end())
return -ENOENT;
if (cluster.size() == 0) { // 如果没有定义集群名,则截取找到的配置文件的文件名作为集群名,即 xxx.conf -> xxx
/*
* If cluster name is not set yet, use the prefix of the
* basename of configuration file as cluster name.
*/
auto start = c->rfind('/') + 1;
auto end = c->find(".conf", start);
if (end == c->npos) {
/*
* If the configuration file does not follow $cluster.conf
* convention, we do the last try and assign the cluster to
* 'ceph'.
*/
cluster = "ceph";
}
else {
cluster = c->substr(start, end - start);
}
}
std::vector<std::string> my_sections;
_get_my_sections(my_sections); // 与自身有关的配置节,依次为 [type.id], [type], [global]
// 遍历 ceph 配置参数选项,即 common/options.cc 中定义的所有配置参数
for (const auto &i : schema) {
const auto &opt = i.second;
std::string val;
// 遍历与自身有关的配置节,并从每一节的配置中查找是否定义了当前的配置参数选项,如果在某一节中找到了配置,则跳过剩下的节,
// 因此 [type.id], [type], [global] 的配置优先级为从高到低
int ret = _get_val_from_conf_file(my_sections, opt.name, val, false);
if (ret == 0) {
std::string error_message;
int r = set_val_impl(val, opt, &error_message); // 设置
if (warnings != nullptr && (r != 0 || !error_message.empty())) {
*warnings << "parse error setting '" << opt.name << "' to '" << val
<< "'";
if (!error_message.empty()) {
*warnings << " (" << error_message << ")";
}
*warnings << std::endl;
}
}
}
// 遍历日志子系统
// subsystems?
for (size_t o = 0; o < subsys.get_num(); o++) {
// 构建日志级别选项 debug_xxx,注意默认的日志子系统名称是 none
std::string as_option("debug_");
as_option += subsys.get_name(o);
std::string val;
// 与前面遍历配置参数类似,查找是否配置了日志子系统的日志打印级别
int ret = _get_val_from_conf_file(my_sections, as_option.c_str(), val,
false);
if (ret == 0) {
int log, gather;
int r = sscanf(val.c_str(), "%d/%d", &log, &gather);
if (r >= 1) {
if (r < 2)
gather = log;
// cout << "config subsys " << subsys.get_name(o) << " log " << log << " gather " << gather << std::endl;
subsys.set_log_level(o, log); // 设置
subsys.set_gather_level(o, gather);
}
}
}
...
return 0;
}
其处理流程比较简单,主要就是先读取配置文件,并以节的形式组织配置项,而每一节的内部又由一组 kv 键值对组成,然后遍历所有已知的配置项,从配置文件中查找是否定义了该配置项,如果找到了则用该配置项覆盖代码中定义的默认值。
从上面的处理流程显然可以看到,在配置文件中定义 ceph 不支持的配置项是没有任何问题的。
md_config_t::parse_argv
最核心的处理流程如下:
std::string val;
for (std::vector<const char*>::iterator i = args.begin(); i != args.end();) {
if (strcmp(*i, "--") == 0) {
/* Normally we would use ceph_argparse_double_dash. However, in this
* function we *don't* want to remove the double dash, because later
* argument parses will still need to see it. */
break;
}
else if (ceph_argparse_flag(args, i, "--show_conf", (char*) NULL)) { // 打印找到的配置文件中的内容
cerr << cf << std::endl;
_exit(0);
}
else if (ceph_argparse_flag(args, i, "--show_config", (char*) NULL)) { // 显示与指定 entity 相关的所有选项,会对 $变量 进行展开
show_config = true;
}
else if (ceph_argparse_witharg(args, i, &val, "--show_config_value", // 显示与指定 entity 的指定选项,会对 $变量 进行展开
(char*) NULL)) {
show_config_value = true;
show_config_value_arg = val;
}
else if (ceph_argparse_flag(args, i, "--foreground", "-f", (char*) NULL)) {
set_val_or_die("daemonize", "false");
}
else if (ceph_argparse_flag(args, i, "-d", (char*) NULL)) {
set_val_or_die("daemonize", "false");
set_val_or_die("log_file", "");
set_val_or_die("log_to_stderr", "true");
set_val_or_die("err_to_stderr", "true");
set_val_or_die("log_to_syslog", "false");
}
// Some stuff that we wanted to give universal single-character options for
// Careful: you can burn through the alphabet pretty quickly by adding
// to this list.
else if (ceph_argparse_witharg(args, i, &val, "--monmap", "-M",
(char*) NULL)) {
set_val_or_die("monmap", val.c_str());
}
else if (ceph_argparse_witharg(args, i, &val, "--mon_host", "-m",
(char*) NULL)) {
set_val_or_die("mon_host", val.c_str());
}
else if (ceph_argparse_witharg(args, i, &val, "--bind", (char*) NULL)) {
set_val_or_die("public_addr", val.c_str());
}
else if (ceph_argparse_witharg(args, i, &val, "--keyfile", "-K",
(char*) NULL)) {
set_val_or_die("keyfile", val.c_str());
}
else if (ceph_argparse_witharg(args, i, &val, "--keyring", "-k",
(char*) NULL)) {
set_val_or_die("keyring", val.c_str());
}
else if (ceph_argparse_witharg(args, i, &val, "--client_mountpoint", "-r",
(char*) NULL)) {
set_val_or_die("client_mountpoint", val.c_str());
}
else {
int r = parse_option(args, i, NULL);
if (r < 0) {
return r;
}
}
}
根据前面对 ceph_argparse_xxx
的分析,这里的逻辑显而易见,主要关注 parse_option
的处理:
int md_config_t::parse_option(std::vector<const char*>& args,
std::vector<const char*>::iterator& i, ostream *oss) {
int ret = 0;
size_t o = 0;
std::string val;
// 遍历日志子系统
// subsystems?
for (o = 0; o < subsys.get_num(); o++) {
// 构建待匹配的日志级别选项 --debug_xxx,注意默认的日志子系统名称是 none
std::string as_option("--");
as_option += "debug_";
as_option += subsys.get_name(o);
// 将命令行与对应的日志打印级别选项 --debug_xxx 进行匹配
ostringstream err;
if (ceph_argparse_witharg(args, i, &val, err, as_option.c_str(), (char*) NULL)) { // 命令行是一个 --debug_xxx 选项
if (err.tellp()) {
if (oss) {
*oss << err.str();
}
ret = -EINVAL;
break;
}
int log, gather;
int r = sscanf(val.c_str(), "%d/%d", &log, &gather); // --debug_xxx 是 option 选项类型,选项值 arg 保存在出参 val 中
if (r >= 1) {
if (r < 2)
gather = log;
// cout << "subsys " << subsys.get_name(o) << " log " << log << " gather " << gather << std::endl;
subsys.set_log_level(o, log);
subsys.set_gather_level(o, gather);
if (oss)
*oss << "debug_" << subsys.get_name(o) << "=" << log << "/" << gather
<< " ";
}
break;
}
}
if (o < subsys.get_num()) {
return ret;
}
// 命令行不是一个 --debug_xxx 选项,尝试匹配 ceph 配置参数选项
std::string option_name;
std::string error_message;
o = 0;
// 遍历 ceph 配置参数选项,即 common/options.cc 中定义的所有配置参数
for (const auto& opt_iter : schema) {
const Option &opt = opt_iter.second;
ostringstream err;
// 构建配置参数选项 --xxx
std::string as_option("--");
as_option += opt.name;
option_name = opt.name;
if (opt.type == Option::TYPE_BOOL) {
int res;
if (ceph_argparse_binary_flag(args, i, &res, oss, as_option.c_str(),
(char*) NULL)) { // 这是一个 flag 选项,注意前面分析 ceph_argparse_binary_flag 实现时提到的 flag 选项的用法,切记 --flag true 这种是错误的方式,必须是 --flag=true 或者 --flag
if (res == 0)
ret = set_val_impl("false", opt, &error_message);
else if (res == 1)
ret = set_val_impl("true", opt, &error_message);
else
ret = res;
break;
} else {
std::string no("--no-");
no += opt.name;
if (ceph_argparse_flag(args, i, no.c_str(), (char*) NULL)) { // 这是一个 --no-flag 选项
ret = set_val_impl("false", opt, &error_message);
break;
}
}
} else if (ceph_argparse_witharg(args, i, &val, err, as_option.c_str(),
(char*) NULL)) { // 这是一个 option 选项
if (!err.str().empty()) {
error_message = err.str();
ret = -EINVAL;
break;
}
if (oss && ((!opt.is_safe()) && (observers.find(opt.name) == observers.end()))) {
*oss << "You cannot change " << opt.name << " using injectargs.\n";
return -ENOSYS;
}
// option 选项的值记录在出参 val 中
ret = set_val_impl(val, opt, &error_message);
break;
}
++o;
}
...
if (o == schema.size()) { // 没有匹配到任何配置参数
// ignore
++i;
}
return ret;
}
parse_option
首先尝试遍历日志子系统选项,如果都匹配不上,则继续遍历并尝试匹配所有的 ceph 配置参数,如果还是没有匹配上,则说明这个命令行并不是一个 ceph 所认识的选项,直接跳过对它的处理(注意前面提到的,匹配上的命令行都会从 args
容器中删除)。
最后修改于 2019-02-23