基于protobuf的配置系统

简介

本文介绍一种基于protobuf实现的配置文件体系,提升配置文件使用体验,同时支持分环境加载不同配置。
本配置系统相对于json,支持配置中添加注释,方便理解;支持自动反序列化,操作简单,同时减少DOM操作不当造成的coredump。
本配置系统相对于yml等配置文件,支持自动反序列化;不会因为缩进问题造成解析失败。

使用说明

基础使用

基础使用的步骤包括proto定义、配置文件编写、服务引入三个环节。
首先是proto定义,参考下面定义一个proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
syntax="proto3";

message LogConfig {
enum LogLevel {
DEBUG = 0;
NOTICE = 1;
WARNING = 2;
ERROR = 3;
FATAL = 4;
}
string path = 1;
LogLevel level = 2;
}

message ServiceConfig {
string config = 1;
}

message ServerConfig {
uint32 port = 1;
uint32 thread_num = 2;
LogConfig log_config = 3;
repeated ServiceConfig service_config = 4;
}

接着是服务配置编写,例如:

1
2
3
4
5
6
7
8
9
port : 8080
thread_num: 8
log_config: {
path: "./log"
level: NOTICE
}
service_config: {
config: "123"
}

接着是在服务中使用了,由于本配置体系依赖了protobuf、glog、gflags,使用时确保链接这三个库。

使用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <gflags/gflags.h>
#include <glog/logging.h>
#include "config_manager.h"
#include "configs/server_config.pb.h"

int main(int argc, char** argv) {
gflags::ParseCommandLineFlags(&argc, &argv, true);
google::InitGoogleLogging(argv[0]);
std::string file = "../conf/server";
ServerConfig serverConfig;
ConfigManager::parse(file, &serverConfig);
google::ShutdownGoogleLogging();
return 0;
}

其中配置路径不需要后缀,默认配置文件后缀为”*.conf”,也就是”../conf/server.conf”文件。
ServerConfig为目标配置反序列化之后的对象。

高级使用

这部分介绍用户可以根据自己的需要,针对不同环境使用不同的配置,例如prod、pre、test使用不同的配置文件,同时有一份基准配置,优先使用指定环境的配置,如果该配置未设置,使用基准配置覆盖。
使用gflag “env”确定加载哪份配置作为环境配置,默认是prod。
使用gflag “suffix”确定配置文件后缀,默认是”conf”。

例如,基准配置conf/server.conf是:

1
2
3
4
5
6
7
8
9
port : 8080
thread_num: 8
log_config: {
path: "./log"
level: NOTICE
}
service_config: {
config: "123"
}

环境配置conf/server-prod.conf是:

1
2
3
4
port : 80
log_config: {
level: WARNING
}

最终得到的合并配置为:

1
2
3
4
5
6
7
8
9
port: 80 
thread_num: 8
log_config {
path: "./log"
level: WARNING
}
service_config {
config: "123"
}

需要注意的是,如果一个字段是repeated时,如果环境配置没有设置,就使用基准配置的;如果环境配置设置了,就不会使用基准配置了。

实现原理

文本配置解析

基于protobuf的文本配置,利用的是google/protobuf/text_format.h文件中的google::protobuf::TextFormat::Parse函数,其功能是将文本内容反序列化为message内容。
这个函数其实是google::protobuf::MessageDebugString函数的逆操作。

解析的代码如下:

1
2
3
4
5
6
7
8
9
10
int file = open(path.c_str(), O_RDONLY);
if (file < 0) {
return false;
}
google::protobuf::io::FileInputStream reader{file};
reader.SetCloseOnDelete(true);
if (!google::protobuf::TextFormat::Parse(&reader, msg)) {
return false;
}
return true;

配置合并

protobuf提供了MergeFrom函数支持两个Message对象进行合并,对于repeated的结构,合并后的结果是这两个结构的并集。
这个情况是不符合预期的,例如Message里有a字段是repeated的,合并结果如下:

A 配置中a字段元素数量B配置中a字段元素数量合并结果
202(全部来自A)
022(全部来自B)
232(全部来自A)

针对这样的需求,不能直接使用MergeFrom函数,需要使用protobuf的反射能力进行逐一赋值。

代码有点长,可以参考代码