多线程程序如何简单高效地读取配置中心上的配置?
本文限定为主动从配置中心读取配置方法,不限定配置中心类型,比如可使用DB作为配置中心。
程序和配置中心间存在网络开销,因此需避免每次业务处理或请求都从配置中心读取配置。
规避网络开销,可采取本地缓存配置,每隔指定时间只读取一次配置中心,比如每秒读取一次配置中心。
假设每秒读取一次配置中心,这样每次的开销减少到每秒只有一次网络开销,此时可观察到性能毛刺,这毛刺是因为每次读取配置中心时的性能抖动(下降)。
需要将读取配置中心从业务线程中移除,为此引入配置线程,由配置线程专门负责从配置中心读取配置,业务线程不直接从配置中心读取配置。
struct Config { // 配置
string a;
string b;
};
class ConfigThread { // 配置线程
public:
void run() {
while (!stop()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
read_config_from_config_center(); // 从配置中心读取配置
std::unique_lock<std::shared_mutex> lock(_mutex);
update_config(); // 更新本地的 _config
}
}
void get_config(struct Config* config) const {
std::shared_lock<std::shared_mutex> lock(_mutex);
*config = _config;
}
private:
// 注:C++17 才支持 shared_mutex 锁
mutable std::shared_mutex _mutex; // 用来保护 _config 的读取写锁
struct Config _config;
};
class WorkThread { // 业务线程
public:
void run() {
while (!stop()) {
struct Config config;
_config_thread->get_config(&config);
}
}
private:
std::shared_ptr<ConfigThread> _config_thread;
};
从上面代码可以看到,有了新的矛盾点,即读取锁碰撞问题(注:锁带来的性能问题主要是锁碰撞,而非一次系统调用)。
当配置线程在加写锁更新配置时,仍然会产生毛刺。简单点可使用读优先读写锁来降低影响,但不能根本解决问题。
可使用尝试锁替代读锁来根本性解决问题:
struct Config { // 配置
string a;
string b;
};
class ConfigThread { // 配置线程
public:
void run() {
while (!stop()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
read_config_from_config_center(); // 从配置中心读取配置
std::unique_lock<std::shared_mutex> lock(_mutex);
update_config(); // 更新本地的 _config
}
}
bool get_config(struct Config* config) const {
if (!_mutex.try_lock_shared())
return false;
*config = _config;
_mutex.unlock();
return true
}
private:
mutable std::shared_mutex _mutex; // 用来保护 _config 的读取写锁
struct Config _config;
};
class WorkThread { // 业务线程
public:
void run() {
while (!stop()) {
const struct Config& config = get_config();
}
}
private:
const struct Config& get_config() {
struct Config config;
if (_config_thread->get_config(&config))
_config = config;
// 如果没有读取到新的配置,则仍然使用老的配置
return _config;
}
private:
std::shared_ptr<ConfigThread> _config_thread;
struct Config _config; // 线程本地配置
};
上述 WorkThread::get_config() 每次都要加一次锁,因为加的是读尝试锁,所以对性能影响很少,大多数都可忽略。如果仍然对性能有影响,可采取每秒只调用一次 ConfigThread::get_config(),来减少锁的调用次数。
局限性:
本文介绍的方法仅适合配置比较少的场景,是多还是少,主要依据消耗的内存大小,当消耗的内存在可接受范围内则可定义为少。
此外,一个问题是:配置更新时间点不一致,但即使每次都去读取配置中心,仍然不一致问题。