一见

多线程程序如何简单高效地读取配置中心上的配置?

本文限定为主动从配置中心读取配置方法,不限定配置中心类型,比如可使用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(),来减少锁的调用次数。

局限性:
本文介绍的方法仅适合配置比较少的场景,是多还是少,主要依据消耗的内存大小,当消耗的内存在可接受范围内则可定义为少。

此外,一个问题是:配置更新时间点不一致,但即使每次都去读取配置中心,仍然不一致问题。

posted on 2020-12-18 15:54  -见  阅读(126)  评论(0编辑  收藏  举报

导航