curl/libcurl 的 CURLOPT_DNS_CACHE_TIMEOUT:DNS 缓存生命周期控制与底层实现剖析
【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl
导读
CURLOPT_DNS_CACHE_TIMEOUT是 libcurl 中用于控制主机名解析结果(DNS 缓存条目)在内存中保留时长的核心选项。它直接决定了一次域名解析结果能被多少个连接复用、在多少秒内不被重新查询,是调整连接建立速度、减少 DNS 查询压力、以及规避临时性解析故障的重要旋钮。读完本文,你将掌握该选项的完整语义(默认 60 秒、0禁用、-1永久缓存)、三种取值边界下的行为差异,并能够结合 lib/vdns/dnscache.c 与 lib/setopt.c 的源码理解其修剪、负缓存与容量上限等底层机制,进而在真实业务中做出正确的配置决策。
选项总览:作用、原型与适用协议
CURLOPT_DNS_CACHE_TIMEOUT在官方文档中的定义为 "life-time for DNS cache entries"(DNS 缓存条目的生存时间),其接口原型如下(见 docs/libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md):
#include <curl/curl.h> CURLcode curl_easy_setopt(CURL *handle, CURLOPT_DNS_CACHE_TIMEOUT, long age);- 参数类型:
long,单位为秒,设置的是缓存超时时间。 - 适用协议:全部(All)。无论走 HTTP、HTTPS、FTP、SMTP 还是其他协议,只要涉及主机名到地址的解析,该选项都生效。
- 引入版本:Added-in 7.9.3,即从 2001 年发布的 curl 7.9.3 起便存在,属于历史悠久的稳定选项。
- 默认值:60(秒),见文档
DEFAULT一节。
在选项注册表中,它被登记为CURLOT_LONG类型(见 lib/easyoptions.c),即接受一个长整型数值。
三种取值模式:默认、禁用与永久缓存
文档对参数的语义描述非常明确:传入一个 long 值表示缓存超时的秒数,名称解析结果会被保存在内存中并在该秒数内被复用。三种典型取值对应三种截然不同的行为模式:
| 取值 | 行为 | 适用场景(按文档建议引申) |
|---|---|---|
60(默认) | 解析结果在内存中保留 60 秒 | 绝大多数常规场景,默认即合理 |
0 | 完全禁用缓存,每次连接都重新解析 | 域名频繁变更、需要实时感知 DNS 变化的场景;代价是每次连接多一次解析开销 |
-1 | 缓存条目永久保留,永不因超时被修剪 | 长期运行且域名固定的应用,可显著减少重复解析;但要注意缓存可能长期持有过期地址 |
值得强调的是,-1只是"不因超时被修剪",并不代表条目永远不会被删除——文档明确指出,当缓存条目数超过 30,000 时,libcurl 无论如何都会执行修剪(该行为自 8.1.0 起加入)。
源码视角:选项如何进入 libcurl 内部
理解该选项的底层语义,需要从参数入口开始追踪。
1. setopt 入口:秒转毫秒与 -1 的特殊处理
在 lib/setopt.c 中,CURLOPT_DNS_CACHE_TIMEOUT的处理逻辑为:
case CURLOPT_DNS_CACHE_TIMEOUT: if(arg != -1) return setopt_set_timeout_sec(&s->dns_cache_timeout_ms, arg); s->dns_cache_timeout_ms = -1; break;这段代码揭示了两个关键事实:
-1是特例:只有精确传入-1才会将内部字段直接置为-1(表示永久缓存),而其他任何非负值都走setopt_set_timeout_sec路径。- 内部以毫秒存储:
setopt_set_timeout_sec(见 lib/setopt.c)会把传入的秒数乘以 1000 转换为毫秒后存入data->set.dns_cache_timeout_ms,并做了溢出保护(当秒数超过TIMEDIFF_T_MAX / 1000时直接钳制为TIMEDIFF_T_MAX)。因此,如果你传入一个负数(如-5)而不是-1,setopt_set_timeout_sec会返回CURLE_BAD_FUNCTION_ARGUMENT拒绝该参数——只有-1这个特定值才表示"永久"。
内部存储字段定义于 lib/urldata.h:timediff_t dns_cache_timeout_ms; /* DNS cache timeout (milliseconds) */。
2. 默认值的初始化
默认值 60 秒在 lib/url.c 中以毫秒形式初始化:
set->dns_cache_timeout_ms = 60000; /* Timeout every 60 seconds by default */这与文档中DEFAULT 60的声明完全一致,也再次印证了"选项对外是秒、对内是毫秒"的设计。
3. 缓存的实际存放位置
解析结果并非存在 easy handle 上,而是存放在共享层级结构中。从 lib/vdns/dnscache.c 的dnscache_get()可以看出优先顺序:
static struct Curl_dnscache *dnscache_get(struct Curl_easy *data) { if(data->share &&>void Curl_dnscache_prune(struct Curl_easy *data) { struct Curl_dnscache *dnscache = dnscache_get(data); /* the timeout may be set -1 (forever) */ timediff_t timeout_ms =>if(dns && (data->set.dns_cache_timeout_ms != -1)) { ... if(dnscache_entry_is_stale(&user, dns)) { infof(data, "Hostname in DNS cache was stale, zapped"); dns = NULL; Curl_hash_delete(&dnscache->entries, key.data, key.len); } }这确保了即使两次修剪之间间隔较长,应用也绝不会使用超龄的缓存地址——命中过期条目会被立即"zap"(删除)并触发一次新的解析。同时注意,-1模式下此检查同样被跳过,缓存条目会一直被信任。
负缓存:失败解析也进缓存
自 curl 8.16.0 起,失败的名称解析也会被写入 DNS 缓存,但只保留设定超时时间的一半;自 8.22.0 起,该行为进一步收紧——只有解析器明确回答"域名不存在"(NXDOMAIN)时才会缓存失败结果,瞬时性或本地解析器故障不会被缓存。这一点在源码中有两处呼应:
- 负缓存条目的写入入口为
Curl_dnscache_add_negative()(见 lib/vdns/dnscache.c); - 在过期判定
dnscache_entry_is_stale()中,无地址条目(负缓存)的年龄会被加倍计算:if(!dns->addr) age *= 2; /* negative entries age twice as fast */(见 lib/vdns/dnscache.c)。
"年龄翻倍"意味着负缓存条目会以正常条目两倍的速度"老化",配合"只保留一半超时时间"的策略,实际效果就是失败结果比成功结果更快失效,避免一次瞬时故障长时间阻塞后续请求。
为什么不建议随意修改:文档与源码共同给出的警告
文档在 DESCRIPTION 中给出了明确建议:除非绝对必要,否则不要修改该选项。其理由在源码与系统层面都能找到依据:
- 大值导致缓存膨胀:如果超时时间内访问了大量不同主机名,缓存条目会显著增多。虽然 30,000 条上限兜底,但在达到上限前,内存占用与哈希遍历开销都会上升。文档特别提醒"使用大值时要小心"。
- libc 解析函数不主动重读服务器信息:文档指出,多数 libc 的解析函数(如
res_init(3)涉及的底层机制)在被显式通知之前不会重新读取名称服务器配置。即使 DHCP 更新了 DNS 服务器信息,libcurl 可能仍在使用旧服务器——这在表象上很像"DNS 缓存问题",实则是系统解析层的行为,单纯调整本选项无法解决。 - TTL 无关性:DNS 记录本身带有 TTL(生存时间)属性,但 libcurl不使用 TTL。文档明确说明该缓存超时完全是一种"推测性"设计——假设一个名字在未来一小段时间内仍解析到相同地址。因此,把超时设得比权威 TTL 更长,也不会自动纠正,只会持有更久的旧地址。
实战示例:控制短生命周期域名的解析时效
官方文档给出的示例(见 docs/libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md)展示了如何在代码中把缓存窗口压到极短:
int main(void) { CURL *curl = curl_easy_init(); if(curl) { CURLcode result; curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/foo.bin"); /* only reuse addresses for a short time */ curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, 2L); result = curl_easy_perform(curl); /* in this second request, the cache is not be used if more than two seconds have passed since the previous name resolve */ result = curl_easy_perform(curl); curl_easy_cleanup(curl); } }该示例的两个要点:
- 将超时设为
2L秒后,第一次curl_easy_perform()解析的地址只会被复用约 2 秒; - 若两次请求间隔超过 2 秒,第二次请求会触发全新的名称解析(源码层面表现为
fetch_addr()中该条目被判 stale 并删除)。
这一模式适合域名记录变更频繁、希望客户端尽快感知新地址的场景。注意若curl_easy_init()失败(返回 NULL),示例中直接跳过设置——这也是 libcurl 编程的常规防御写法。
返回值与错误处理
文档的RETURN VALUE一节说明:curl_easy_setopt()返回CURLcode指示成功或失败,CURLE_OK(0)表示一切正常,非零表示发生错误,具体错误码可参考libcurl-errors(3)。
从源码看,该选项可能返回的错误包括:
CURLE_BAD_FUNCTION_ARGUMENT:传入-1以外的负数(例如-5),由setopt_set_timeout_sec()的if(secs < 0)分支触发(见 lib/setopt.c);- 其余合法取值(任意非负 long 或
-1)均返回CURLE_OK。
与相关选项的协同关系
该选项并非孤立存在,官方文档的 See-also 列表(见 docs/libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md 头部元数据)指向了完整的相关能力矩阵:
- CURLOPT_CONNECTTIMEOUT_MS:连接超时,与 DNS 缓存协同决定"建立连接的总耗时上限";
- CURLOPT_DNS_SERVERS:指定使用的 DNS 服务器,影响解析结果来源;
- CURLOPT_DNS_USE_GLOBAL_CACHE:全局共享缓存开关(注意其默认关闭),与本文所述的 multi/share 级缓存机制相关联;
- CURLOPT_MAXAGE_CONN:连接复用的最大存活时间,控制的是"连接"而非"解析结果"的复用窗口;
- CURLOPT_RESOLVE:手动指定主机名到地址的映射(可视为对缓存内容的直接注入);
- CURLMOPT_NETWORK_CHANGED:通知 multi 句柄网络环境已变化,配合
Curl_dnscache_clear()(见 lib/vdns/dnscache.c)可主动清空缓存。
实践中常见的组合思路是:对动态环境(移动网络、容器 IP 频繁漂移)将CURLOPT_DNS_CACHE_TIMEOUT调小或配合网络变更通知主动清缓存;对地址长期稳定的内部服务,则可适度放大缓存窗口以降低解析延迟与 DNS 服务器压力。但无论哪种取向,都应遵循文档的忠告——在明确需求的前提下再动这个选项,并在大值场景下持续观察缓存规模。
小结
CURLOPT_DNS_CACHE_TIMEOUT虽是一个单一 long 参数的简单选项,其背后却串联起 libcurl 的名称解析缓存体系的全部关键环节:秒级 API 与毫秒级内部存储的转换、-1永久模式与0禁用模式的特殊分支、multi/share 级的共享缓存、基于时间戳的修剪与惰性过期检查、30,000 条容量上限的强制收敛,以及 8.16.0/8.22.0 之后引入的负缓存半衰期策略。理解了这些源码层面的实现,你就能在"减少 DNS 查询"与"及时感知地址变化"之间做出有依据的权衡,而不是盲目调大或调小这个看似不起眼的超时值。
【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考