1️⃣ 概述

  在 sylar 项目中,**DNS 模块** 并不是 “只负责把域名解析成 IP” ,它的定位更接近于一个轻量级的 服务地址管理模块

  • 负责把 服务名 解析成 可用的后端地址
  • 承担 地址可用性判断连接复用 以及 周期刷新 等职责。

从架构上看,这个模块实际上站在 “服务发现”“连接管理” 的交界处,而不仅仅是传统意义上的 DNS 查询层。

2️⃣ AddressItem

  如果说 Dns 负责管理一个服务对应的多个可选节点,那么 AddressItem 负责管理其中某一个具体节点(ip:port)的运行状态、健康检查结果以及连接池。
  它的核心作用不是保存一个 ip:port,而是将 地址 抽象成一个可被调度、可被探活、可被复用的后端节点
  也就是说,在整个 DNS 模块中,真正参与 可用性判断连接复用 的最小单位,不是 Address,而是 AddressItem

设计思想

  • 服务中的每个地址必须有自己的运行时状态

    同一个服务名可能解析出针对该服务的多个 远端服务器地址,这些地址的健康状态连接存活情况可复用连接数量 都不一样。

  • 连接池必须按地址隔离

    连接是建立在 远端地址 上的,不能混用。

    连接池不能挂在整个 Dns 上,而必须挂在 “某个具体地址节点” 上。AddressItem 正好承担这个角色。

  • 健康检查和连接复用应该结合

    先尝试 复用已有连接 判断可用性,必要时再新建连接探测

整体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct AddressItem : public std::enable_shared_from_this<AddressItem> {
typedef std::shared_ptr<AddressItem> ptr;
~AddressItem();
sylar::Address::ptr addr;
std::list<Socket*> socks;
sylar::Spinlock m_mutex;
bool valid = false;
uint32_t pool_size = 0;
std::string check_path;

bool isValid();
bool checkValid(uint32_t timeout_ms);

void push(Socket* sock);
Socket::ptr pop();
Socket::ptr getSock();

std::string toString();
};

成员解析

成员变量

  • addr
    • 含义: 当前服务节点的实际地址;
    • 意义: 作为 AddressItem 的身份标识,代表 “这个对象管理的是哪个远端节点”
  • socks
    • 含义: 当前服务节点对应的连接池;
    • 意义: 连接池里的连接都是已经连到 addrsocket
  • m_mutex
    • 含义: 保护连接池 socks
    • 意义: 这里用 自旋锁 而不是 读写锁,说明临界区很短,锁竞争预期不高,主要是追求低开销;
  • valid
    • 含义: 当前节点是否被认为健康可用;
    • 意义: Dns::get() 之所以能快速返回一个 可用地址,就是依赖这个状态位;
  • pool_size
    • 含义: 当前节点的连接池配置;
    • 意义:
      • pool_size == 0 表示不做连接池,只做即时建连。
      • pool_size > 0 表示允许缓存连接并优先复用。
  • check_path
    • 含义: HTTP 健康检查路径;
    • 意义: 如果配置了它,探活就不只看 TCP connect 成功,还会进一步发 HTTP 请求,并要求返回 200

成员函数

  • isValid

    • 含义: 返回当前服务节点的健康状态;

    • 意义: 这是一个快路径接口,不做探测,只读状态;

    • 代码:

      1
      bool Dns::AddressItem::isValid() { return valid; }
  • checkValid

    • 含义: 检测服务节点是否可用;

    • 意义: 对当前节点做一次健康探测,并更新 valid 状态;

    • 逻辑:

      • 如果启用了连接池,先检查池中是否还有活连接;
      • 如果没有可用连接,则新建一个 socket,尝试 TCP connect
      • 如果配置了 check_path,再补做一次 HTTP 健康检查;
      • 如果 节点健康启用了连接池,会把 这次探测时创建的连接 放回池中,供后续 复用
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      bool Dns::AddressItem::checkValid(uint32_t timeout_ms) {

      /*---------------------------------------------------------------------------------------
      | 使用连接池 |
      ---------------------------------------------------------------------------------------*/
      if(pool_size > 0) {
      std::vector<Socket*> tmp; // 暂存失效连接
      sylar::Spinlock::Lock lock(m_mutex);
      for(auto it = socks.begin(); // 遍历池中连接
      it != socks.end();) {
      if((*it)->checkConnected()) { // 有任一连接仍可用
      return true; // 直接认为服务节点有效
      } else { //
      tmp.push_back(*it); // 连接失效,则存放失效连接
      socks.erase(it++); // 从连接池中移除失效连接
      }
      }
      lock.unlock();
      for(auto& i : tmp) { // 清理失效连接
      delete i; // 释放内存
      }
      }

      /*---------------------------------------------------------------------------------------
      | 不使用连接池 |
      ---------------------------------------------------------------------------------------*/
      sylar::Socket* sock = new sylar::Socket(addr->getFamily(), sylar::Socket::TCP, 0); // 新建探活连接
      valid = sock->connect(addr, timeout_ms); // TCP 连接探测服务节点是否可达

      if(valid) { // TCP 连通成功
      if(!check_path.empty()) { // 若配置了 HTTP 健康检查
      sylar::http::HttpRequest::ptr req = std::make_shared<sylar::http::HttpRequest>(); // 创建 HTTP 请求
      req->setPath(check_path); // 设置检查路径
      req->setHeader("host", addr->toString()); // 设置 Host 头
      sylar::Socket::ptr sock_ptr(sock, sylar::nop<sylar::Socket>); // 包装为 shared_ptr 但不接管删除
      auto rt = sylar::http::HttpConnection::DoRequest(req, sock_ptr, timeout_ms); // 发 HTTP 请求检查
      if(!rt->response || (int)rt->response->getStatus() != 200) { // 无响应或状态码非200
      valid = false; // 判定不健康
      SYLAR_LOG_ERROR(g_logger) << "health_check fail result=" << rt->result
      << " rsp.status=" << (rt->response ? (int)rt->response->getStatus() : -1)
      << " check_path=" << check_path
      << " addr=" << addr->toString(); // 记录失败详情
      }
      }
      if(valid && pool_size > 0) { // 健康且启用连接池
      sylar::Spinlock::Lock lock(m_mutex);
      socks.push_back(sock); // 把探活连接放入池

      } else {
      delete sock; // 直接释放连接
      }
      } else { // TCP探活失败
      delete sock; // 释放连接
      }
      return valid; // 返回最终健康状态
      }
  • push

    • 含义: 将连接了服务节点的 socket 放入到连接池中;

    • 意义: 通常在上层使用完连接后被调用,用于把连接重新放回 socks 中,以便下次继续复用;

    • 逻辑:

      • 如果 socket 处于连接状态,则放回池中;
      • 如果 socket 已经断开,则直接释放;
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      void Dns::AddressItem::push(Socket* sock) {                     
      if(sock->checkConnected()) { // 只回收 Established 的 sock
      sylar::Spinlock::Lock lock(m_mutex);
      socks.push_back(sock); // 放回池尾
      } else {
      delete sock;
      }
      }
  • pop()

    • 含义: 从连接池中取出一个连接;

    • 意义: 如果启用了连接池,并且池中还有连接,就取出一个 socket 给调用方使用;

    • 逻辑:

      • 并不是简单返回一个 裸连接,而是会把取出的裸指针包装成一个 shared_ptr<Socket>,并绑定一个 自定义删除器
      • 删除器 在连接对象生命周期结束时,会自动把连接归还给当前 AddressItem
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      static void ReleaseSock(Socket* sock, Dns::AddressItem::ptr ai) {
      ai->push(sock);
      }

      // 从池中取一个连接
      Socket::ptr Dns::AddressItem::pop() {
      if(pool_size == 0) { // 未启用连接池
      return nullptr; // 不提供连接
      }
      sylar::Spinlock::Lock lock(m_mutex);
      if(socks.empty()) { // 池为空
      return nullptr; // 不提供连接
      }
      auto rt = socks.front(); // 取队首连接
      socks.pop_front(); // 从池中弹出
      lock.unlock();

      Socket::ptr v(rt, std::bind(ReleaseSock, std::placeholders::_1, shared_from_this()));
      return v;
      }
  • getSock()

    • 含义: 获取一个当前可用的 socket

    • 意义: 让上层在访问某个地址节点时,可以直接拿到一个 “已经可用” 的连接;

    • 逻辑:

      • 如果启用了连接池,优先尝试从池中复用连接;
      • 如果池中没有可用连接,则在节点健康的前提下尝试新建连接;
      • 如果节点不健康或者连接失败,则返回空指针;
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      Socket::ptr Dns::AddressItem::getSock() {
      if(pool_size > 0) { // 连接池模式
      do {
      auto sock = pop(); // 从池里取
      if(!sock) { // 池空
      break; // 跳出循环
      }
      if(sock->checkConnected()) { // 连接仍有效
      return sock; // 直接复用
      }
      } while(true); // 直到拿到有效连接或池空
      }
      else if(valid) { // 非连接池模式且地址健康
      sylar::Socket* sock = new sylar::Socket(addr->getFamily(), sylar::Socket::TCP, 0);
      if(sock->connect(addr, 20)) { // 快速连接(20ms超时)
      if(pool_size > 0) {
      Socket::ptr v(sock, std::bind(ReleaseSock, std::placeholders::_1, shared_from_this()));
      } else {
      return sylar::Socket::ptr(sock);
      }
      } else {
      delete sock;
      }
      }
      return nullptr;
      }


  当我们真正了解 AddressItem ,就不再会把它当成一个普通的 “地址封装对象” 来看待了。它实际上代表的是 Dns 模块内部的一个 “后端节点实例”,里面既保存了 节点本身的地址,也保存了节点当前是否健康、是否还有可复用连接等运行时状态
  理解了这一点,再回头去看 Dns 类中的其他成员,就会发现它们并不是零散堆在一起的字段和函数,而是在围绕 “如何管理一组后端节点并把可用节点交给上层使用” 这一目标协同工作。
  正因为如此,只有先搞清楚 AddressItem 的定位和作用,我们后面分析 Dns 的整体工作流程时,才能真正看懂它是如何完成 地址维护健康筛选连接复用 以及 节点选择 这一整套逻辑的。

3️⃣ Dns

  Dns 类并不是一个只负责 “域名转 IP 的简单工具,它更像是一个 面向服务访问 的地址管理器。

  • 支持两种模式:
    • TYPE_DOMAIN通过域名动态解析出一组服务节点
    • TYPE_ADDRESS:直接管理配置好的 固定地址列表
  • 维护地址状态:
    • 解析出地址后,Dns 会为每个地址 维护可用状态,并通过 轮询方式 选择健康节点;
  • 健康检查:
    • 当配置了 check_path,它还会进一步发起 HTTP 健康检查,确保返回的不是 “能连上但服务不可用” 的地址。
  • 连接池:
    • pool_size > 0 时,Dns 还会为节点维护连接池,优先复用已有连接,减少频繁建连带来的开销。

整体框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class Dns {
public:
typedef std::shared_ptr<Dns> ptr;
typedef sylar::RWMutex RWMutexType;

/**
* @brief Dns类型
*/
enum Type {
TYPE_DOMAIN = 1,
TYPE_ADDRESS = 2
};

/**
* @brief 构造函数
* @param[in] domain 域名或服务名(如 www.sylar.top:80)
* @param[in] type 类型(TYPE_DOMAIN / TYPE_ADDRESS)
* @param[in] pool_size 每个地址的连接池大小(0 表示不使用连接池)
*/
Dns(const std::string& domain, int type, uint32_t pool_size = 0);

/**
* @brief 设置固定地址集合(仅TYPE_ADDRESS使用)
*/
void set(const std::set<std::string>& addrs);

/**
* @brief 获取一个可用地址(轮询)
* @param[in] seed 可选随机/轮询种子
*/
sylar::Address::ptr get(uint32_t seed = -1);

/**
* @brief 获取一个可用Socket(可能来自连接池)
* @param[in] seed 可选随机/轮询种子
*/
sylar::Socket::ptr getSock(uint32_t seed = -1);

const std::string& getDomain() const { return m_domain;}
int getType() const { return m_type;}

std::string getCheckPath() const { return m_checkPath;}
void setCheckPath(const std::string& v) { m_checkPath = v;}

/**
* @brief 转字符串调试信息
*/
std::string toString();

/**
* @brief 刷新地址(重新解析或重建地址列表)
*/
void refresh();
public:
/**
* @brief 单个地址项
* @details
* 维护一个具体地址对应的健康状态 + 连接池
*/
struct AddressItem : public std::enable_shared_from_this<AddressItem> {
typedef std::shared_ptr<AddressItem> ptr;
~AddressItem();
sylar::Address::ptr addr; ///具体地址
std::list<Socket*> socks; ///裸Socket池(由AddressItem托管释放)
sylar::Spinlock m_mutex; ///连接池锁
bool valid = false; ///地址是否健康可用
uint32_t pool_size = 0; ///池大小
std::string check_path; ///HTTP健康检查路径

bool isValid();

bool isValid();
bool checkValid(uint32_t timeout_ms);

void push(Socket* sock);
Socket::ptr pop();
Socket::ptr getSock();

std::string toString();
};
private:
/**
* @brief TYPE_ADDRESS 模式下,按 m_addrs 初始化地址
*/
void init();

/**
* @brief 用解析结果更新内部地址列表
*/
void initAddress(const std::vector<Address::ptr>& result);
private:
std::string m_domain; ///域名/服务名
int m_type; ///解析类型
uint32_t m_idx; ///轮询下标(原子递增种子)
uint32_t m_poolSize = 0; ///连接池大小
std::string m_checkPath; ///健康检查路径
RWMutexType m_mutex; ///读写锁
std::vector<AddressItem::ptr> m_address; ///地址项列表
std::set<std::string> m_addrs; ///TYPE_ADDRESS 原始地址字符串集合
};

Dns 成员解析

  Dns 类是整个 DNS 模块的核心对象。

  • AddressItem 负责管理 “单个后端节点” 的运行时状态
  • Dns 负责管理的就是 “一个服务对应的一组后端节点”

  换句话说,Dns 是站在 服务级别 工作的,而不是站在 单节点级别 工作的。

  它要解决的问题不是 “某个地址能不能连”,而是:

  • 一个服务当前有哪些地址
  • 这些地址中哪些还可用
  • 该把请求分发到哪个节点
  • 是否可以直接拿到一个可用连接
  • 地址列表是否需要刷新

因此,Dns 可以理解为一个 “服务名到节点集合” 的管理器。

成员变量

  • m_domain
    • 含义: 表示当前 Dns 对象管理的服务标识;
    • 意义:
      • 标识当前管理的是 哪个服务
      • DnsManager 中作为查找 key
      • 在刷新时作为 域名解析 输入;
  • m_type
    • 含义: 表示当前 Dns 对象的地址来源类型;
    • 意义: 当前应该按 域名模式 刷新,还是按 固定地址模式 刷新
      • TYPE_DOMAIN:表示这个 Dns 对象管理的是一个动态域名,刷新时需要重新做域名解析
      • TYPE_ADDRESS:表示这个 Dns 对象管理的是一组固定地址,刷新时使用的是配置里的静态地址集合
  • m_idx
    • 含义: 它本质上是一个递增的选择种子,用于在多个可用节点之间做轮询分配
    • 意义: 避免所有请求都集中到第一个地址,在多个可用地址之间做基本均衡分发;
  • m_poolSize
    • 含义: 表示 每个地址节点连接池大小配置
    • 意义: 节点级连接池配置模板
      • Dns 负责把这个配置传给每个 AddressItem
      • 每个 AddressItem 再基于这个值管理自己的连接池;
  • m_checkPath
    • 含义: 表示 健康检查路径
    • 意义: 这个字段会在 地址初始化 时下发给每个 AddressItem
      • 提供服务级别的健康检查策略
      • 让该服务下 所有地址节点 共享同一套 应用层探活规则
  • m_mutex
  • 含义: 表示 Dns 对象自身的读写锁;
  • 意义: 保护的是服务级别的数据结构
    • m_address
    • m_addrs
  • m_address
  • 含义: 表示当前服务对应的 地址节点列表
  • 意义:
    • 它保存的不是裸 Address,而是一组 AddressItem,也就是一组 “带状态的地址节点”
    • Dns 后续的选择逻辑并不只是看地址本身:这个节点是否健康能不能直接拿到连接有没有连接池可复用
  • m_addrs
    • 含义: 表示 静态地址模式 下的 原始地址字符串集合
    • 意义: 这个字段只在 TYPE_ADDRESS 模式下真正有意义
      • 127.0.0.1:8080
      • 10.0.0.5:9000

重要成员函数

  • set

    • 含义: 设置静态地址集合;

    • 意义:传入的字符串地址 集合保存到 m_addrs 中,然后调用 init() 基于这些地址重新构建内部地址列表;

    • 逻辑:

      • 为静态地址模式提供 配置入口
      • 把外部传入的地址集合转化为内部可管理的 AddressItem 列表;
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      void Dns::set(const std::set<std::string>& addrs) {         
      {
      RWMutexType::WriteLock lock(m_mutex);
      m_addrs = addrs; // 更新内部地址集合
      }
      init(); // 根据新地址集合重建 AddressItem
      }
  • get

    • 含义: 获取一个可用地址;

    • 意义: 它会在 当前地址列表 中按照 轮询方式 选择一个节点,并返回其中第一个健康可用的地址;

    • 逻辑:

      • 如果调用方没有提供 seed,则使用 m_idx 自动递增;
      • 轮询顺序 遍历 m_address
      • 找到第一个 valid == true 的地址并返回;
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      sylar::Address::ptr Dns::get(uint32_t seed) {               
      if(seed == (uint32_t)-1) { // 未指定 seed 时
      seed = sylar::Atomic::addFetch(m_idx); // 用原子自增索引作为seed
      }
      RWMutexType::ReadLock lock(m_mutex);
      for(size_t i = 0; i < m_address.size(); ++i) { // 最多遍历所有地址
      auto info = m_address[seed % m_address.size()]; // 按 seed 轮询定位地址项
      if(info->valid) { // 只返回健康地址
      return info->addr; // 返回Address
      }
      seed = sylar::Atomic::addFetch(m_idx); // 不健康则换seed继续尝试
      }
      return nullptr; // 都不健康时返回空
      }
  • getSock

    • 含义: 获取一个可用连接;

    • 意义: 它不只是选出一个可用地址,而是直接尝试从该地址对应的 AddressItem 中取出一个可用 socket

    • 逻辑:

      • 轮询顺序 遍历地址列表;
      • 调用每个 AddressItemgetSock()
      • 一旦拿到可用连接就直接返回;
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      sylar::Socket::ptr Dns::getSock(uint32_t seed) {        
      if(seed == (uint32_t)-1) { // 未指定seed时
      seed = sylar::Atomic::addFetch(m_idx); // 使用原子索引轮询
      }
      RWMutexType::ReadLock lock(m_mutex);
      for(size_t i = 0; i < m_address.size(); ++i) { // 遍历地址项
      auto info = m_address[(seed + i) % m_address.size()]; // 依次尝试不同地址
      auto sock = info->getSock(); // 从地址项取连接
      if(sock) { // 拿到可用连接
      return sock; // 直接返回
      }
      }
      return nullptr; // 无可用连接
      }
  • init

    • 含义: 静态地址模式初始化函数;

    • 意义:

      • 这个函数只服务于 TYPE_ADDRESS 模式;
      • 它会把 m_addrs 中保存的字符串地址逐个解析成 Address 对象,然后交给 initAddress() 构建 内部地址节点列表
    • 逻辑:

      • 静态地址配置 转换成 内部统一格式
      • 复用统一的地址更新逻辑;
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      void Dns::init() {                                          
      if(m_type != TYPE_ADDRESS) {
      SYLAR_LOG_ERROR(g_logger) << m_domain << " invalid type " << m_type;
      return;
      }

      RWMutexType::ReadLock lock2(m_mutex);
      auto addrs = m_addrs; // 拷贝一份,避免长时间持锁
      lock2.unlock(); // 提前释放锁

      std::vector<Address::ptr> result; // 解析后的地址列表
      for(auto& i : addrs) { // 遍历每个字符串地址
      if(!sylar::Address::Lookup(result, i, sylar::Socket::IPv4, sylar::Socket::TCP)) {
      SYLAR_LOG_ERROR(g_logger) << m_domain << " invalid address: " << i;
      }
      }
      initAddress(result); // 用解析结果更新 m_address
      }
  • initAddress

    • 含义: 根据 解析结果 更新 内部地址节点列表

    • 意义: 负责把外部得到的一组 Address 变成内部的 AddressItem 列表;

    • 逻辑:

      • 如果是 旧地址,则尽量 复用 旧的 AddressItem,保留其 健康状态连接池资源
      • 如果是 新地址,就创建新的 AddressItem,初始化其 连接池配置健康检查路径并立即做一次健康探测
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      void Dns::initAddress(const std::vector<Address::ptr>& result) {
      std::map<std::string, AddressItem::ptr> old_address; // 保存旧地址项映射(字符串->对象)
      {
      RWMutexType::ReadLock lock(m_mutex);
      auto tmp = m_address; // 拷贝一份旧地址列表
      lock.unlock();

      for(auto& i : tmp) { // 构建旧地址map
      old_address[i->addr->toString()] = i; // 以地址字符串为key
      }
      }

      std::vector<AddressItem::ptr> address; // 新地址项数组
      address.resize(result.size()); // 与解析结果等长
      std::map<std::string, AddressItem::ptr> m;
      for(size_t i = 0; i < result.size(); ++i) { // 遍历新解析地址
      auto it = old_address.find(result[i]->toString()); // 看旧列表中是否已有
      if(it != old_address.end()) { // 旧地址可复用
      it->second->checkValid(50); // 复检健康状态(50ms)
      address[i] = it->second; // 复用旧AddressItem
      continue;
      }
      auto info = std::make_shared<AddressItem>(); // 新建AddressItem
      info->addr = result[i]; // 绑定地址
      info->pool_size = m_poolSize; // 继承Dns连接池配置
      info->check_path = m_checkPath; // 继承健康检查路径
      info->checkValid(50); // 初始化时做一次探活
      address[i] = info; // 放入新数组
      }

      RWMutexType::WriteLock lock(m_mutex);
      m_address.swap(address); // 原子替换地址列表
      }
  • refresh

    • 含义: 刷新当前服务的地址列表;

    • 意义:Dns 具备 动态更新能力,使 节点变化地址变更服务恢复 可以被重新纳入系统状态中;

    • 逻辑:

      • 分两种情况
        • TYPE_DOMAIN
          • 重新做域名解析
          • 得到新的地址列表
          • 调用 initAddress() 更新内部节点列表
        • TYPE_ADDRESS
          • 不做域名解析
          • 直接基于 m_addrs 调用 init()
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      void Dns::refresh() {
      if(m_type == TYPE_DOMAIN) { // 域名解析模式
      std::vector<Address::ptr> result;
      if(!sylar::Address::Lookup(result, m_domain, sylar::Socket::IPv4, sylar::Socket::TCP)) { // DNS解析
      SYLAR_LOG_ERROR(g_logger) << m_domain << " invalid address: " << m_domain;
      }
      initAddress(result); // 刷新地址项
      } else {
      init(); // 固定地址模式直接init
      }
      }


从整体上看,Dns 类的成员设计非常清晰,它围绕的始终是一个核心目标:

    把 一个服务名 维护成 “一组可用节点的集合”,并对外提供 统一的地址连接访问入口

4️⃣ DNSManager

作用与定位

如果说:

  • AddressItem 管的是 “单个节点”
  • Dns 管的是 “一个服务对应的一组节点”

那么 DnsManager 管的就是 “整个进程里所有正在被使用的服务地址集合”

DnsManager 既是 DNS 模块的最外层入口,也是整个模块的统一调度者。

业务层一般不会直接关心某个 AddressItem,甚至很多时候也不会自己构造 Dns 对象,而是通过 DnsManager 统一完成:

  • 服务名查找
  • 地址获取
  • 连接获取
  • 缓存注册
  • 周期刷新

因此,DnsManager 在架构中的意义,可以概括为一句话:

   它把分散的 Dns 对象收拢起来,对外提供统一的服务访问入口,并负责维护这些 Dns 对象的刷新生命周期。

换句话说,DnsManager 是整个 DNS 模块的 “全局管理层”

为什么要有 DnsManager

从架构上讲,如果没有 DnsManager,项目会面临几个很实际的问题。

  • Dns 对象没有统一入口
    每个业务模块都可能自己创建 Dns,导致:

    • 同一个 service 被重复构造
    • 重复解析
    • 重复探活
    • 重复维护连接池
  • DNS 刷新没有统一调度点,DNS

    • 当前系统里有哪些 Dns
    • DNS 该什么时候刷新
    • 是否已经有人在刷新
    • 这些调度职责必须交给更高一层来做。
  • 业务层需要一个稳定入口

    • 业务代码更想要写的是:

      1
      2
      DnsMgr::GetInstance()->getAddress("www.xxx.com:80", true);
      DnsMgr::GetInstance()->getSocket("www.xxx.com:80", true);
    • 而不是

      • 先判断有没有缓存
      • 没有就构造 Dns
      • 再调用 refresh
      • 再拿地址/连接

成员解析

成员变量

  • m_mutex
    • 含义: DnsManager 的读写锁;
    • 意义: 它保护的是整个管理器内部的全局状态
  • m_dns
    • 含义: 每一个服务名都对应一个 Dns 实例;
    • 意义:
      • 做全局注册表
      • 避免重复创建相同 serviceDns
      • 让所有模块共享同一份服务地址状态
  • m_timer
    • 含义: 用于周期刷新 DNS 的定时器对象;
    • 意义:
      • 让服务地址不是 “一次解析永久不变”
      • 让节点恢复、节点失效、解析结果变化,都能持续反映到系统状态中;
  • m_refresh
    • 含义: 刷新防重入标志
    • 意义: 它用来防止 init() 在尚未执行完成时被再次进入
      • 保证同一时刻只有一轮全局刷新在执行
      • 避免并发刷新导致状态覆盖或重复工作
  • m_lastUpdateTime
    • 含义: 表示最近一次刷新完成的时间
    • 意义:dump() 中会被输出,帮助开发者判断当前 DNS 缓存是否长期未刷新

成员函数

  • init()

    • 含义: 执行一次全局刷新;

    • 意义: 遍历当前管理器里的所有 Dns 对象,并逐个调用它们的 refresh()

    • 逻辑:

      • 检查 m_refresh,避免重复进入;
      • 复制当前 m_dns
      • 遍历所有 Dns
      • 调用每个 Dnsrefresh()
      • 更新 m_lastUpdateTime
    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      void DnsManager::init() {
      if(m_refresh) { // 正在刷新则跳过(防重入)
      return;
      }
      m_refresh = true; // 标记刷新开始
      RWMutexType::ReadLock lock(m_mutex); // 读锁读取m_dns
      std::map<std::string, Dns::ptr> dns = m_dns; // 拷贝一份,减少锁持有时间
      lock.unlock(); // 提前释放锁
      for(auto& i : dns) { // 遍历每个Dns
      i.second->refresh(); // 执行刷新
      }
      m_refresh = false; // 标记刷新结束
      m_lastUpdateTime = time(0); // 记录最后刷新时间
      }
  • add

    • 含义: 向管理器中添加一个 Dns 实例;

    • 意义: DnsManager“注册入口”

    • 逻辑:

      • 把某个服务对应的 Dns 纳入全局管理
      • 让这个 Dns 后续参与统一刷新
      • 其他模块可以通过 service 名称共享它
    • 代码:

      1
      2
      3
      4
      void DnsManager::add(Dns::ptr v) {
      RWMutexType::WriteLock lock(m_mutex);
      m_dns[v->getDomain()] = v;
      }
  • get

    • 含义: 根据服务名获取对应的 Dns 实例;

    • 意义:

      • 从全局缓存中查找 service
      • 如果存在,就复用已有的 Dns
      • 如果不存在,返回空
      1
      2
      3
      4
      5
      Dns::ptr DnsManager::get(const std::string& domain) {
      RWMutexType::WriteLock lock(m_mutex);
      auto it = m_dns.find(domain);
      return it == m_dns.end() ? nullptr : it->second;
      }
  • getAddress

    • 含义: 获取某个服务当前可用的地址;

    • 意义: 面向业务层的主要入口;

    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      sylar::Address::ptr DnsManager::getAddress(const std::string& service, bool cache, uint32_t seed) {
      auto dns = get(service); // 先查缓存Dns
      if(dns) { // 若缓存命中
      return dns->get(seed); // 直接从Dns拿地址
      }

      if(cache) { // 若要求缓存
      sylar::IOManager::GetThis()->schedule([service, this](){ // 异步初始化缓存Dns
      Dns::ptr dns = std::make_shared<Dns>(service, Dns::TYPE_DOMAIN); // 创建DOMAIN类型Dns
      dns->refresh(); // 立即解析刷新
      add(dns); // 放入管理器
      });
      }

      return sylar::Address::LookupAny(service, sylar::Socket::IPv4, sylar::Socket::TCP); // 兜底同步解析
      }
  • getSocket

    • 含义: 获取某个服务当前可用的连接;

    • 意义: 返回已经可用的 Socket

    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      sylar::Socket::ptr DnsManager::getSocket(const std::string& service, bool cache, uint32_t seed) {
      auto dns = get(service); // 先查缓存Dns
      if(dns) { // 命中缓存
      return dns->getSock(seed); // 直接取可用连接
      }

      if(cache) { // 要求缓存
      sylar::IOManager::GetThis()->schedule([service, this](){ // 异步创建并缓存Dns
      Dns::ptr dns = std::make_shared<Dns>(service, Dns::TYPE_DOMAIN, 1);
      dns->refresh(); // 刷新解析
      add(dns); // 放入管理器
      });
      }
      auto addr = sylar::Address::LookupAny(service, sylar::Socket::IPv4, sylar::Socket::TCP); // 同步解析地址
      sylar::Socket::ptr sock = sylar::Socket::CreateTCP(addr); // 创建TCP socket
      if(sock->connect(addr, 20)) { // 尝试连接(20ms)
      return sock;
      }
      return nullptr;
      }
  • start()

    • 含义: 启动定时刷新任务;

    • 意义:

      • DNS 缓存持续保持新鲜
      • 让整个服务节点集合具备后台自维护能力
      • 让业务层不用手动调用 refresh()
    • 逻辑: 如果还没有启动过定时器,就注册一个周期任务,周期性调用 init()

    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      void DnsManager::start() {
      // 已启动过则不重复启动
      if(m_timer) {
      return;
      }

      // 每秒刷新
      m_timer = sylar::IOManager::GetThis()->addTimer(1000, std::bind(&DnsManager::init, this), true);
      }