不能直接用getaddrinfo做高并发DNS解析,因其同步阻塞、不可取消、不支持事件循环且无法复用socket;应使用全局复用的c-ares channel,接入epoll/kqueue,并谨慎处理回调线程安全与内存生命周期。

为什么不能直接用 getaddrinfo 做高并发 DNS 解析
因为 getaddrinfo 是同步阻塞的,每次调用都会让线程卡住直到 DNS 响应返回或超时。在爬虫系统里,你可能同时发起数万次域名解析请求(比如解析百万级 URL 的 host 部分),如果每个都走系统 resolver,线程池会迅速被占满,CPU 空转等 IO,吞吐直接崩盘。更糟的是:它无法取消、不支持事件循环集成、也不能复用 socket 连接 —— 这些对高并发爬虫都是硬伤。
ares_channel_t 必须全局复用,不能 per-request 创建
c-ares 的核心不是“一次一查”,而是一个可长期运行的状态机。每个 ares_channel_t 内部维护着 socket 句柄池、pending 查询队列、nameserver 列表和重试状态。频繁 ares_init/ares_destroy 会触发 socket 重建、DNS 配置重载、缓存清空,性能断崖式下跌。
- 初始化一次,在程序启动时调用
ares_library_init和ares_init_options,传入ARES_OPT_TIMEOUTMS、ARES_OPT_TRIES、ARES_OPT_NDOTS等关键参数 - 所有解析请求共用同一个 channel,哪怕跨线程也要加锁(c-ares 本身线程安全仅限于单 channel 多线程调用,但回调执行不在你的控制线程里)
- 避免在回调函数里调用
ares_cancel或ares_destroy—— 它们不是线程安全的,且会破坏 channel 状态
如何把 c-ares 接入 epoll/kqueue 事件循环
c-ares 不自己轮询 socket,而是靠你告诉它“哪个 fd 就绪了”。你需要注册 sock_state_cb 回调,在里面把 socket 状态变化通知给事件循环;反过来,当 epoll 返回可读/可写时,你要调用 ares_process_fd 让 c-ares 消费数据。
- 创建 channel 时必须设置
ARES_OPT_SOCK_STATE_CB,否则 c-ares 不会告诉你 socket 状态变化 - 在
sock_state_cb中,对readable或writable为 true 的 fd,调用epoll_ctl(EPOLL_CTL_ADD/MOD)注册对应事件 - 在事件循环主干中,对每个就绪 fd 调用
ares_process_fd(channel, fd, ARES_SOCKET_BAD);若 fd 可读可写,传fd两次;若只读,第二个参数传ARES_SOCKET_BAD - 别漏掉
ARES_SOCKET_BAD—— 它是 c-ares 的约定值,不是 -1,硬写 -1 会导致静默失败
回调里最容易崩的三件事
c-ares 回调看似简单,但实际是整个系统最脆弱的一环:它在任意线程(取决于 socket 就绪时机)中执行,且生命周期完全脱离你的控制。
- 回调参数里的
struct hostent*是栈上临时结构,只在回调内有效 —— 不能保存指针、不能跨线程传递、不能延迟解析。要留数据,必须深拷贝h_addr_list和h_name - 回调中禁止调用任何阻塞操作:
printf、malloc(尤其 jemalloc/tcmalloc 在多线程下有锁)、std::mutex::lock(除非你确认不会递归或死锁)。推荐用 lock-free ringbuffer 或原子计数器暂存结果 - status 为
ARES_SUCCESS不代表一定有 IP:要检查host && host->h_addr_list && host->h_addr_list[0],否则inet_ntop会 segfault
超高并发下,真正难的不是发请求,而是让数万个回调不互相踩内存、不争抢锁、不触发 malloc 竞态 —— 这些细节没压测过,上线后第一波流量就会暴露。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











