3、DNS优化实战:DNS缓存、HTTPDNS、预解析技术

DNS 解析,说白了就是域名转 IP 的那一跳。很多 SDK 接入慢,根因往往不在带宽,而在 DNS。我接手过一个直播推流 SDK,用户首帧延迟动不动就 3 秒以上,抓包一看,光 DNS 解析就占了 1.2 秒。你想想看,这还只是开始。

咱们这一章就把 DNS 优化的三把刀——DNS 缓存、HTTPDNS、预解析——拆开了讲。每把刀怎么用,在什么场景下用,有什么坑,我都会结合项目里的真实案例说清楚。

3.1 DNS 缓存:最直接的优化手段

DNS 缓存,本质就是拿空间换时间。把第一次解析结果存到本地,下次直接用,避免重复走网络查询。

我个人习惯在 SDK 启动时初始化一个轻量缓存池,结构很简单:

class DNSCache {
    private:
        struct Entry {
            std::string ip;
            time_t expire_time;
        };
        std::unordered_map<std::string, Entry> cache_;
        std::mutex mtx_;
    
    public:
        std::string get(const std::string& host) {
            std::lock_guard<std::mutex> lock(mtx_);
            auto it = cache_.find(host);
            if (it != cache_.end()) {
                if (time(nullptr) < it->second.expire_time) {
                    return it->second.ip;
                }
                cache_.erase(it);
            }
            return "";
        }
        
        void put(const std::string& host, const std::string& ip, int ttl) {
            std::lock_guard<std::mutex> lock(mtx_);
            cache_[host] = {ip, time(nullptr) + ttl};
        }
};
关键点:TTL 不是越大越好。太大的 TTL 会导致域名切换 IP 后客户端解析滞后,太小又失去缓存意义。我一般控制在 60~300 秒,具体视业务容忍度调整。

我在项目中遇到过一个问题:缓存命中了,但 IP 已经不可用,导致连接超时。解决方案是加入失败降级机制——连续 2 次连接失败就清除缓存,重新解析。

注意:DNS 缓存不是万能的。对于频繁变动的域名(如 CDN 调度域名),缓存反而可能引起解析滞后。这种情况下,建议结合 HTTPDNS 使用。

3.2 HTTPDNS:根治 DNS 劫持与调度问题

传统 DNS 走 UDP 53 端口,中间链路容易被劫持或篡改。HTTPDNS 直接通过 HTTPS 请求解析服务,绕过本地 DNS,从源头保证数据准确。

我曾经在一家海外电商 SDK 里遇到过:部分地区用户解析到错误的 CDN 节点,下载速度惨不忍睹。换成 HTTPDNS 后,解析准确率从 87% 提升到 99.5%,首包延迟降低 40%。

接入方式也不复杂,一般就是发一个 HTTP GET:

// 伪代码示例
std::string httpdns_resolve(const std::string& host) {
    std::string url = "https://dns.aliyun.com/resolve?host=" + host;
    HttpResponse resp = http_client->get(url);
    if (resp.code == 200) {
        Json json = Json::parse(resp.body);
        return json["ip"].as_string();  // 直接拿到最优 IP
    }
    return "";  // 降级走系统 DNS
}
核心优势:HTTPDNS 不仅能防劫持,还能做到智能调度。解析服务可以根据请求来源 IP,返回距离最近的节点,这是传统 DNS 做不到的。

不过 HTTPDNS 也不是银弹。它额外多了一次 HTTPS 请求,如果解析服务本身延迟高,反而拖慢整体速度。我建议只对核心域名开启 HTTPDNS,普通域名走缓存就够了。

3.3 预解析技术:把等待变成准备

预解析,就是在用户真正需要之前,先把 DNS 解析做完。这样等到实际请求时,解析结果已经在缓存里了,延迟直接归零。

我记得在做一个短视频 SDK 时,用户滑动 Feed 流,每刷到新视频都要解析 CDN 域名。虽然单个解析只要 50ms,但滑动场景下累积起来就很可观。我们在空闲线程里提前解析下一个可能用到的域名,效果立竿见影。

实现也很轻量:

// 预解析管理器
class PrefetchResolver {
    private:
        DNSCache* cache_;
        std::vector<std::string> prefetch_list_;
    
    public:
        void add_prefetch_domain(const std::string& domain) {
            prefetch_list_.push_back(domain);
        }
        
        void do_prefetch() {
            for (const auto& domain : prefetch_list_) {
                // 检查缓存是否已有且未过期
                if (cache_->get(domain).empty()) {
                    std::string ip = system_dns_resolve(domain);
                    if (!ip.empty()) {
                        cache_->put(domain, ip, 120);  // 预解析结果 TTL 可短一些
                    }
                }
            }
        }
};
策略建议:预解析触发时机很关键。我一般在 onPageStart 或 onIdle 时触发,而不是在构造函数里一次全做。避免抢占主线程资源。

这里有一个我曾经踩过的坑:预解析所有可能用到的域名,结果列表太大,线程池被占满,反而拖慢了主流程。后来我改成只预解析Top 3 高频域名,效果最好。

3.4 三种技术对比与选型建议

这三种技术不是互斥的,我一般会组合使用。下面这张表帮你快速决策:

技术方案 原理 优势 劣势 适用场景
DNS 缓存 本地存储 + TTL 控制 实现简单,零额外延迟 无法防劫持,调度不精准 低频域名、非核心链路
HTTPDNS HTTPS 直连解析服务 防劫持、智能调度、高准确率 增加一次 HTTP 请求,有成本 核心域名、跨国调度、劫持严重地区
预解析 提前触发 DNS 查询 消除解析等待,对用户无感 可能浪费资源,需要预测域名 滑动场景、首屏加载、预加载链路

我个人推荐的分层策略是:

  • 第一层:通用域名走 DNS 缓存,TTL 设在 120 秒左右。
  • 第二层:核心域名(如 CDN、API 网关)叠加 HTTPDNS,保证解析质量和安全。
  • 第三层:对用户即将访问的域名做预解析,提前把结果塞进缓存。

三层叠加之后,绝大多数场景下 DNS 解析都能在 1ms 内返回,几乎感知不到。

一句话总结:缓存兜底,HTTPDNS 保质量,预解析消灭等待。三者配合,才是 DNS 优化的完整答卷。
DNS 优化技术协同架构 客户端 SDK DNS 缓存 本地存储 | TTL 管理 HTTPDNS 直连解析 | 防劫持 预解析 提前触发 | 结果复用 DNS 解析结果汇聚 目标服务器 缓存兜底 · HTTPDNS保质量 · 预解析消灭等待

最后说一句,DNS 优化没有标准答案。我见过只靠缓存就搞定 90% 场景的团队,也见过四层调度全上结果过度设计的。关键是把你的域名列表拉出来,按访问频次和重要性排个序,然后对症下药。

快速自检清单:
✅ 核心域名是否用了 HTTPDNS?
✅ 缓存 TTL 是否与业务刷新频率匹配?
✅ 预解析是否在空闲时机触发?
✅ 是否有失败降级和缓存淘汰策略?