2024-11-19本文已三次更新统计代码和结果,请以本消息更新日期之后的统计结果为准。 前言
自 2024 年 9 月 30 日起,阿里公共 DNS 宣布对免费版用户进行限速。我一直想找个机会测试一下限速后的可用性,随着本文的发表这次测试已经完成了。我个人是很早就开始使用加密 DNS(DNSCrypt),那时候 DoH 还未发布。
后来随着部分公共 DNS 开始提供 DoH 服务,我便也切换到 DoH 上。所以对于阿里 DNS 的限速,我个人也是比较在意的。我将在本文揭开限速后的可用性,以及和其它 DNS 的对比。
加密 DNS
由于本文的重点并不是对加密 DNS 的介绍,所以我不会在本文做一些多余的科普。我有一些未发布的文章,里边详细讲解了有关加密 DNS 的很多方面,但它由于反复修改仍未到位,所以我一直没有发布它。
我将在未来将要发布的文章中介绍什么是加密 DNS,以及为什么使用它。
q
首先我们需要一个支持 DoH 的客户端,做基本的手动测试。本章节名为 q,因为这个工具就叫 q。q 是一个强大的命令行 DNS 客户端,支持很多协议,包括 DoH。
使用 q 做一次的阿里 DNS 的解析测试:
q A baidu.com @https://223.5.5.5/dns-query -S
响应如下:
baidu.com. 6m22s A 110.242.68.66
baidu.com. 6m22s A 39.156.66.10
Stats:
Received 77 B from https://223.5.5.5:443/dns-query in 40.4ms (22:33:39 11-17-2024 CST)
Opcode: QUERY Status: NOERROR ID 13918: Flags: qr rd (1 Q 2 A 0 N 0 E)
上面我使用 q 请求阿里的 223.5.5.5 IP 的 DoH 服务,解析 baidu.com 域名的 A 记录。响应时间为 40.4 毫秒。百度是国人每天都要访问无数次的域名,它的解析速度只能表示具有缓存时的响应时间。
我们使用一个国人几乎不会访问的域名,例如 spitsbergen-svalbard.com。这是一个极为冷门的介绍斯瓦尔巴群岛和斯匹次卑尔根的旅游网站。请不要问我去过没有。
响应如下:
spitsbergen-svalbard.com. 1h A 81.28.228.191
Stats:
Received 82 B from https://223.5.5.5:443/dns-query in 295.5ms (03:56:08 11-17-2024 CST)
Opcode: QUERY Status: NOERROR ID 59575: Flags: qr rd (1 Q 1 A 0 N 0 E)
花了接近 300 毫秒的时间才响应。这是因为公共 DNS 通常是作为递归解析器的角色,在它没有缓存时,它会继续向上级 DNS 请求(如权威 DNS 服务器),直到找到答案。
我进一步测试了更多冷门域名,基本确定阿里 DNS 在我这里解析无缓存的域名要 300 毫秒左右。已经排除掉少量时间特别长(例如 2 秒)的特例情况。
域名清单
接下来,我要准备一份尽可能在缓存内(或者说大部分在缓存内)的域名列表,模拟一般人的日常访问情况。在后续章节中,将以不同的参数并发请求解析这份名单中的域名,获得统计数据。
执行命令如下:
curl -s https://raw.githubusercontent.com/carrnot/china-domain-list/release/domain.txt \
-o domains.txt &&
sed -i '1201,$d' domains.txt
此命令的含义是下载 carrnot/china-domain-list 仓库发布的 domain.txt 并保存为 domains.txt,然后截断前 1200 行,保留 1200 个域名在文件中。
测试程序
使用 Rust 实现一个测试程序。它用于并发执行查询任务,并根据所有任务的返回值输出统计结果:
use futures::future::join_all;
use rand::prelude::SliceRandom;
use reqwest::Response;
use std::{collections::HashMap, sync::LazyLock, time::Duration};
use tokio::task;
// 阿里 DNS: https://223.5.5.5/resolve
// DNSPod: https://1.12.12.12/resolve
const DOH_RESOLVE_BASE: &str = "https://223.5.5.5/resolve";
// 查询域名的数量
const QUERY_COUNT: usize = 20;
// 统计区间大小(毫秒)
const STATS_INTERVAL: u128 = 50;
#[tokio::main]
async fn main() {
LazyLock::force(&DOMAINS);
let domains = (*DOMAINS)
.choose_multiple(&mut rand::thread_rng(), QUERY_COUNT)
.collect::
let mut handles = vec![];
for domain in domains {
let handle = task::spawn(query(domain));
handles.push(handle);
}
let results = join_all(handles).await;
let mut timeout_count = 0;
let mut linechart_inputs = HashMap::new();
for r in results {
let (elapsed, r) = r.unwrap();
match r {
Ok(_resp) => {
let elapsed_ms = elapsed.as_millis();
let begin = (elapsed_ms / STATS_INTERVAL) * STATS_INTERVAL;
let count = linechart_inputs.entry(begin).or_insert(0);
*count += 1;
}
Err(e) => {
if e.is_timeout() {
timeout_count += 1;
}
}
}
}
let mut keys: Vec<_> = linechart_inputs.keys().collect();
// 按大小顺序排列 `linechart_inputs` 的键,并打印`键:值``
keys.sort();
println!("Line Chart inputs:\n");
for key in keys {
println!("{}: {}", key, linechart_inputs.get(key).unwrap());
}
println!("\nTimeout count: {}", timeout_count);
}
static DOMAINS: LazyLock
// 读取 domains.txt 文件,每一行一个域名。
let content = std::fs::read_to_string("domains.txt")
.unwrap()
.trim()
.to_owned();
content.lines().map(|line| line.to_string()).collect()
});
async fn query(domain: &str) -> (Duration, Result
let url = format!("{}?name={}&type=A", DOH_RESOLVE_BASE, domain);
let start = std::time::Instant::now();
let r = reqwest::get(&url).await;
let elapsed = start.elapsed();
(elapsed, r)
}
注意高亮的几行,当我们想测试不同的并发请求数量时,将修改 QUERY_COUNT 变量的值。相应的,随着 QUERY_COUNT 的增大,可能要增加 STATS_INTERVAL 的值,以便以更合适的粒度地统计数据。
预热
上面说过,我会尽可能让被测试域名被 DNS 服务器缓存。这里我提取了 1200 个域名,那么我提前进行其中 1000 个域名的解析即可保证大多数域名在缓存内。执行结果:
Line Chart inputs:
0: 33
500: 184
1000: 275
1500: 18
2000: 191
2500: 11
3000: 186
3500: 26
4000: 59
4500: 7
5000: 6
6000: 2
6500: 1
7000: 1
Timeout count: 0
注意看,我们得到了一些统计结果。这些结果可以输入给折线图(或称 XY 表)生成器:
COUNT: 1000, STATS_INTEVAL: 500
可以看到,并发解析 1000 个域名,直到第 4 秒后都有 75 个域名才完成响应。数秒才能解析一个域名,其使用体验是极为痛苦的。在这个规模下,阿里 DNS 不存在可用性。当然正常来讲个人也不会有如此大规模的并发使用场景。我上面做 1000 个域名的并发解析,只是为了预热这些域名,让 1200 个域名中至少有 1000 个是在缓存内的。
阿里 DNS 测试
由于近期阿里 DNS 才开始实施限速,并且很多人也想知道限速后的可用性,所以我将重点测试它。从以上手动单次查询可以得出,在我这里阿里 DNS 的最佳延迟是 40 - 50 毫秒。我们将 50 这个数值作为统计间隔,当统计数据中的毫秒为 0 时,表示 50 以下的响应个数,即最佳延迟内的个数。
上面是宣布限速的早期文档的截图。后来阿里把 20QPS 这个数值模糊掉了,没有明确说明限速多少。目前的限速可能是根据服务器负载或其它情况来动态决定。
20QPS
这是早期阿里文档中提到的数值,即每秒允许 20 个请求。输出如下:
Line Chart inputs:
0: 8
50: 10
250: 1
800: 1
Timeout count: 0
我想我们不需要折线图了,因为结果很简单。接近一半的解析在 50 毫秒以内,剩余的主要在 100 毫秒以内。庆幸的是个别高延迟并未达到 1 秒。这个结果表示能用,但绝不算最佳体验。别忘了我们在手动测试单个域名解析时,缓存内延迟始终都在 50 毫秒以内。
50QPS
即每秒 50 个请求,输出如下:
Line Chart inputs:
0: 3
50: 26
100: 7
200: 3
250: 9
450: 1
1100: 1
Timeout count: 0
这次整体延迟有所增加,多数请求在 50 - 100 毫秒之间响应。可以发现随着并发的提高,最佳响应延迟的出现概率显著降低。且有概率出现秒级响应。
100QPS
即每秒 100 个请求,输出如下:
Line Chart inputs:
0: 2
50: 45
100: 13
150: 2
200: 2
250: 15
300: 6
450: 1
500: 2
750: 1
1050: 7
1100: 1
1300: 2
2900: 1
Timeout count: 0
折线图:
在这个规模下已经失去可用性了。只有不到一半的请求能在 100 毫秒内完成,并且有 10% 左右的请求会达到秒级响应(个别甚至接近 3 秒)。在最佳延迟方面和 50QPS 场景类似,只有极少量请求能在 50 毫秒内响应。
阿里 DNS 总结
我想后续的就不必测试了。在阿里 DNS 严格的并发限制下,略高(超过 50)的并发可用性就大幅度降低了。例如大内网环境,过高的解析延迟会导致显著的上网体验下降。如果你用于服务器领域,过高的延迟会显著降低例如爬虫这类程序的执行效率。
比较奇怪的是,即使我反复测试 20 个并发,也就是阿里早期文档中允许的数值。其响应延迟也会稳定增加。几乎总有一小半甚至一半在 50 毫秒以外。但如果我仅测试(缓存内的)单次解析,它总在 50 毫秒以内。
DNSPod 测试
DNSPod 是腾讯提供的公共 DNS 服务器,其免费版本相较于阿里更早实施限速。由于 DNSPod 在我这里延迟高于阿里 DNS,它的最佳延迟大概是 60 - 70 毫秒。所以我将以 70 作为统计间隔。
现在我进行一些快速测试,省略预热过程。
首先是 20QPS:
Line Chart inputs:
70: 13
140: 3
280: 2
350: 1
1610: 1
Timeout count: 0
然后是 50QPS:
Line Chart inputs:
0: 2
70: 27
140: 5
280: 8
350: 4
630: 1
1680: 1
1750: 2
Timeout count: 0
最后是 100QPS:
Line Chart inputs:
70: 24
140: 7
210: 4
280: 22
350: 16
490: 4
560: 2
630: 1
700: 1
1050: 3
1120: 5
1190: 3
1260: 1
1330: 2
1470: 1
1750: 1
2520: 1
2590: 1
2800: 1
Timeout count: 0
折线图:
差异清晰可见,DNSPod 在全部并发测试中响应延迟皆高于阿里 DNS。首先我们几乎没有看到最佳延迟以内的数值(经反复测试都是极少的),其次它的秒级响应更多。所以虽然同样作为限速的免费 DoH 服务器,DNSPod 的可用性低于阿里 DNS。
程序设计
我上面贴出的代码是非常简单的实现,它只是用 Tokio 并发调用查询函数,等待全部任务执行结束并将结果统计出来。它并不是严格的为并发测试而设计的。其次我的代码中没有统计多次结果,例如查询十次,统计平均值。但我在上面贴出来的数值也是经过多次运行人工筛选的。会排除掉过高的特例情况(例如高达 3 秒才响应),偏向中庸的结果。
弃用的统计
在本文刚发表的时候,我采取了另一种思路,并发调用外部 q 进程来统计。由于进程的启动开销太大,导致结果的整体延迟偏大。
这是因为我在前期不清楚阿里 DNS 是如何限速的,它是返回不同的 DoH 状态码?还是不同 HTTP 状态码?又或是让请求排队,或空响应?在我测试之前是未知的,所以我当时没有选择自行实现 DoH 的调用,而是利用外部工具快速实施测试。
目前我已经改写程序,并更新了测试结果。
结束语
这就是对阿里 DNS 以及 DNSPod 的一些简单测试了。从结果可以看出来,限速的 DNS 在应付稍高的并发时,延迟会明显升高。虽然严格来说可用性仍然存在,因为它并没有响应错误或空响应。
但我个人认为可用性已经很低了。毕竟 DNS 请求只是连接开始前的前奏,这个步骤应该尽可能快甚至不需要时间(本地缓存)。当延迟大到可以感知时,体验就会显著下降。