阿里 DNS 限速后还能用吗?测试阿里 DNS、DNSPod 的 DoH 速度限制

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> = LazyLock::new(|| {

// 读取 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 请求只是连接开始前的前奏,这个步骤应该尽可能快甚至不需要时间(本地缓存)。当延迟大到可以感知时,体验就会显著下降。

【每日一问】棒棒糖棍为什么有个豁口?
手指小太阳越来越小怎么了