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};
}
};
我在项目中遇到过一个问题:缓存命中了,但 IP 已经不可用,导致连接超时。解决方案是加入失败降级机制——连续 2 次连接失败就清除缓存,重新解析。
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 也不是银弹。它额外多了一次 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 可短一些
}
}
}
}
};
这里有一个我曾经踩过的坑:预解析所有可能用到的域名,结果列表太大,线程池被占满,反而拖慢了主流程。后来我改成只预解析Top 3 高频域名,效果最好。
3.4 三种技术对比与选型建议
这三种技术不是互斥的,我一般会组合使用。下面这张表帮你快速决策:
| 技术方案 | 原理 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| DNS 缓存 | 本地存储 + TTL 控制 | 实现简单,零额外延迟 | 无法防劫持,调度不精准 | 低频域名、非核心链路 |
| HTTPDNS | HTTPS 直连解析服务 | 防劫持、智能调度、高准确率 | 增加一次 HTTP 请求,有成本 | 核心域名、跨国调度、劫持严重地区 |
| 预解析 | 提前触发 DNS 查询 | 消除解析等待,对用户无感 | 可能浪费资源,需要预测域名 | 滑动场景、首屏加载、预加载链路 |
我个人推荐的分层策略是:
- 第一层:通用域名走 DNS 缓存,TTL 设在 120 秒左右。
- 第二层:核心域名(如 CDN、API 网关)叠加 HTTPDNS,保证解析质量和安全。
- 第三层:对用户即将访问的域名做预解析,提前把结果塞进缓存。
三层叠加之后,绝大多数场景下 DNS 解析都能在 1ms 内返回,几乎感知不到。
最后说一句,DNS 优化没有标准答案。我见过只靠缓存就搞定 90% 场景的团队,也见过四层调度全上结果过度设计的。关键是把你的域名列表拉出来,按访问频次和重要性排个序,然后对症下药。
✅ 核心域名是否用了 HTTPDNS?
✅ 缓存 TTL 是否与业务刷新频率匹配?
✅ 预解析是否在空闲时机触发?
✅ 是否有失败降级和缓存淘汰策略?