libcurl 的 CURLOPT_DNS_SHUFFLE_ADDRESSES:打乱 DNS 解析结果实现连接负载分散
2026/9/10 4:11:29 网站建设 项目流程

libcurl 的 CURLOPT_DNS_SHUFFLE_ADDRESSES:打乱 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

导读

当 curl/libcurl 解析一个主机名并获得多个 IP 地址时,默认会按照系统解析器返回的顺序逐个尝试连接,这在多 IP(多 A/AAAA 记录)场景下容易造成流量始终集中在列表前部的服务器上。CURLOPT_DNS_SHUFFLE_ADDRESSES是 libcurl 从 7.60.0 起提供的一个布尔型选项,它让 libcurl 在拿到解析结果后随机打乱所有地址的使用顺序,从而在每次(重新)解析时实现连接目标的随机化。本文以 docs/libcurl/opts/CURLOPT_DNS_SHUFFLE_ADDRESSES.md 为骨架,结合本仓库lib/vdns/dnscache.c的实现与tests/unit/unit1608.c的单元测试,讲解该选项的用法、底层 Fisher-Yates 洗牌实现、与 DNS 缓存选项的配合方式及注意事项。读完本文,你将能正确地在自己的 libcurl 应用中启用地址打乱,并理解它对连接行为、IPv4/IPv6 优先级和性能的实际影响。

选项概览与原型

CURLOPT_DNS_SHUFFLE_ADDRESSES通过curl_easy_setopt设置,作用于整个 easy handle 的 DNS 解析流程,适用于所有协议(文档 Protocol 字段为 All)。

#include <curl/curl.h> CURLcode curl_easy_setopt(CURL *handle, CURLOPT_DNS_SHUFFLE_ADDRESSES, long onoff);
  • 参数类型:long
  • 取值:1L启用,0L禁用
  • 默认值:0(禁用)
  • 引入版本:7.60.0
  • 协议:所有协议
  • 错误返回:CURLE_OK,或在选项不被支持时返回CURLE_UNKNOWN_OPTION

在选项表中,该符号在 include/curl/curl.h 中定义为CURLOPT(CURLOPT_DNS_SHUFFLE_ADDRESSES, CURLOPTTYPE_LONG, 275),编号为 275,属于 LONG 类型选项。

功能说明:什么时候发生打乱

根据原文档,启用该选项后:当一个主机名被解析且返回了多个IP 地址时,libcurl 会打乱所有返回地址的顺序,使它们以随机顺序被使用。这与传统gethostbyname函数(多数现代平台上已不再使用)的历史行为类似。

从源码看,这个行为发生在 lib/vdns/dnscache.c 的dnsc_entry_assign_addr()函数中——当新的解析结果被写入 DNS 缓存条目之前:

#ifndef CURL_DISABLE_SHUFFLE_DNS /* shuffle addresses if requested */ if(data->set.dns_shuffle_addresses && dns->addr) { CURLcode result = dns_shuffle_addr(data, &dns->addr); if(result) { /* free without lock, we are the sole owner */ dnscache_entry_free(dns); dns = NULL; goto out; } } #else (void)data; #endif

需要注意的关键点:

  • 打乱发生在"新的解析结果生成并进入缓存"的时机,而不是每次从缓存命中时。
  • 选项状态存放在data->set.dns_shuffle_addresses中(lib/urldata.h 中以位域BIT(dns_shuffle_addresses)声明),由 lib/setopt.c 中的case CURLOPT_DNS_SHUFFLE_ADDRESSES分支写入。
  • 整个功能受编译宏CURL_DISABLE_SHUFFLE_DNS控制:禁用该宏编译时,setopt分支、打乱逻辑以及对应的单元测试都会被#ifndef屏蔽。

为什么"打乱只发生在解析时"很重要

因为打乱发生在解析结果写入缓存的那一刻,之后所有命中缓存的使用都会拿到同一次打乱后的顺序。也就是说:

  1. 第一次解析 example.com → 得到 4 个 IP → 打乱 → 缓存
  2. 后续连接如果在缓存有效期内,都会按这次打乱后的顺序使用

因此,如果你希望在多次连接之间频繁改变地址使用顺序,就必须配合缩短或禁用 DNS 缓存,这正是原文档强调的CURLOPT_DNS_CACHE_TIMEOUT的作用(详见下文"与 DNS 缓存选项的配合"一节)。

底层实现:Fisher-Yates 洗牌算法

打乱的核心实现在 lib/vdns/dnscache.c 的dns_shuffle_addr()函数中。函数注释说明它"通过重新链接链表来打乱Curl_addrinfo结构中地址的顺序",并且"特意不声明为 static,仅为便于单元测试使用(@unittest 1608)"。

实现分为三步:

  1. 计数并转为数组:先用num_addresses()遍历Curl_addrinfo链表统计地址个数;若num_addrs <= 1直接返回(单地址无需打乱)。随后把链表节点指针拷入一个临时数组nodes[]

  2. Fisher-Yates 洗牌:从随机源Curl_rand()获取足够的随机字节填充rnd[],然后从数组末尾向前遍历:

for(i = num_addrs - 1; i > 0; i--) { swap_tmp = nodes[rnd[i] % (unsigned int)(i + 1)]; nodes[rnd[i] % (unsigned int)(i + 1)] = nodes[i]; nodes[i] = swap_tmp; }

对每个位置i,从[0, i]范围内随机选一个下标并交换,保证每个排列等概率出现。随机数取自 libcurl 的随机子系统Curl_rand()(失败时返回非 OK 结果)。

  1. 重新链接链表:按洗牌后的数组顺序重建ai_next指针链,末节点指向NULL,并更新*addr指向新的头节点。

内存分配失败时返回CURLE_OUT_OF_MEMORY,并由调用方dnsc_entry_assign_addr()释放缓存条目后向上传播。

单元测试验证

仓库在 tests/unit/unit1608.c 中提供了专门的单元测试(@unittest 1608):

  • 构造一个 8 节点的Curl_addrinfo静态链表(addrs[8]ai_next依次串联);
  • 通过curl_easy_setopt(easy, CURLOPT_DNS_SHUFFLE_ADDRESSES, 1L)启用选项并断言返回CURLE_OK
  • 反复调用dns_shuffle_addr()最多 10 次,只要链表头节点发生变化(addrhead != addrs)即判定"地址被重排";
  • 若 10 次内始终未变化,则测试失败,断言信息为"addresses are not being reordered"

该测试同时验证了选项接口(setopt)与洗牌算法本身,是理解本功能行为的最直接的可运行证据。整个测试同样被CURL_DISABLE_SHUFFLE_DNS宏包裹,说明该功能可在编译期整体裁掉。

完整使用示例

原文档给出的最小示例:

int main(void) { CURL *curl = curl_easy_init(); if(curl) { CURLcode result; curl_easy_setopt(curl, CURLOPT_URL, "https://example.com"); curl_easy_setopt(curl, CURLOPT_DNS_SHUFFLE_ADDRESSES, 1L); result = curl_easy_perform(curl); /* always cleanup */ curl_easy_cleanup(curl); } }

一个更贴近实战的组合用法是:启用打乱的同时缩短 DNS 缓存,让每次请求都有较大概率重新解析并重新打乱地址:

CURL *curl = curl_easy_init(); if(curl) { curl_easy_setopt(curl, CURLOPT_URL, "https://example.com"); /* 启用地址打乱,使多次连接分散到不同 IP */ curl_easy_setopt(curl, CURLOPT_DNS_SHUFFLE_ADDRESSES, 1L); /* 缩短 DNS 缓存时间,让下一次 perform 尽快重新解析并重新打乱 */ curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, 2L); curl_easy_perform(curl); /* 两秒后再次 perform,将重新解析并得到新的随机地址顺序 */ curl_easy_cleanup(curl); }

编译时链接-lcurl即可运行。也可在命令行使用curl --dns-shuffle-addresses(仓库命令行工具对应的选项文档见 docs/cmdline-opts/dns-shuffle-addresses.md)。

与 DNS 缓存选项的配合

原文档明确说明:如果名称解析是通过 DNS 缓存完成的,地址不会被重新打乱。因为缓存命中直接复用已打乱(或未打乱)的旧结果,不会触发新的洗牌。

为此,文档建议配合CURLOPT_DNS_CACHE_TIMEOUT(docs/libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md)使用:

  • 该选项接受以秒为单位的long值,设置解析结果在内存中保留的时长;
  • 设为0完全禁用缓存——每次连接都重新解析、重新打乱;
  • 设为-1则缓存条目永久保留;
  • 默认值为 60 秒,即解析结果默认被缓存一分钟。

文档同时提醒:不建议随意调整缓存超时,过大的值在主机名很多时会让缓存体积显著增长。此外,缓存还有一个硬性上限:无论超时设置为何值,当缓存条目超过30,000条时 libcurl 都会进行剪枝(8.1.0 起)。从 8.16.0 起,失败的解析也会按超时时间的一半存入缓存;从 8.22.0 起,仅当解析器明确应答"名称不存在"时才缓存失败结果,瞬时或本地解析错误不再缓存。

影响与注意事项

原文档指出:由于返回的地址被随机重排,地址顺序不再符合 RFC 3484 或系统解析器产生的任何确定性排序。这带来两方面影响:

  1. IPv4/IPv6 优先级不再受控:可能造成 IPv4 在 IPv6 之前被使用,或反之。如果你的应用对地址族顺序有硬性要求(例如必须优先 IPv6),应改用CURLOPT_IPRESOLVE(docs/libcurl/opts/CURLOPT_IPRESOLVE.md)显式限定CURL_IPRESOLVE_V4CURL_IPRESOLVE_V6CURL_IPRESOLVE_WHATEVER,而不是依赖打乱后的随机顺序。

  2. 可能的性能影响:系统解析器返回的顺序通常是经过策略优化(如就近原则、权重)的,随机化可能使连接落到"更远"或质量较差的地址上。在负载均衡类场景中,这通常可以用随机化带来的分散收益抵消,但对于单主机连接,建议评估后再启用。

适用场景小结

场景建议
多 IP 主机(多个 A/AAAA 记录),希望连接流量分散到各地址启用本选项,并适当缩短/禁用 DNS 缓存以频繁重打乱
对 IPv4/IPv6 使用顺序有硬性要求不要依赖随机顺序,改用CURLOPT_IPRESOLVE显式指定
依赖解析器返回顺序做最优路由保持默认(禁用),因为随机化可能带来性能损失
编译期希望裁剪该功能定义CURL_DISABLE_SHUFFLE_DNS,对应代码路径与单元测试都会被移除

参考与延伸阅读

  • 选项官方文档:docs/libcurl/opts/CURLOPT_DNS_SHUFFLE_ADDRESSES.md
  • 洗牌实现与调用点:lib/vdns/dnscache.c
  • 选项解析:lib/setopt.c、选项定义 include/curl/curl.h
  • 状态存储:lib/urldata.h
  • 单元测试:tests/unit/unit1608.c
  • 关联选项:DNS 缓存超时 CURLOPT_DNS_CACHE_TIMEOUT、地址族选择 CURLOPT_IPRESOLVE

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询