如何在短时间内向250万个主机发送5亿个不符合RFC的HTTP/1.1请求
任务背景
我需要在短时间内向250万个主机发送5亿个不符合RFC的HTTP/1.1请求,理想情况下要在几个小时内完成。为此,我深入研究了HTTP/1.1和Go语言的细节,并通过多种方法来优化这个过程,包括利用Kubernetes进行水平扩展,优化代码以充分利用每个CPU核心,甚至修改了Go的HTTP库来加速这一过程。
为什么选择Go?
我选择Go语言,因为它简单易用,具有出色的并发支持,而且速度快。此外,Go语言的学习曲线较低,即使是JS开发者也能轻松上手。这点对于我来说尤其重要,因为我曾尝试过用Rust来实现这一任务,但由于异步编程的复杂性,我选择了更易理解的Go。
5亿个HTTP/1.1请求有多少?
你可能想知道这是多还是少。实际上,这是一个非常庞大的数量级。假设每个请求耗时0.5秒,如果你使用curl从单台机器依次发送这些请求,需要7.9年才能完成。然而,现实情况会更慢,服务器会限制速率,且响应时间可能超过0.5秒。
从数据传输的角度来看,这个量并不算巨大:
- 5亿个请求 * 1 KB(平均请求大小)≈ 478 GB
- 5亿个响应 * 5 KB(平均响应大小)≈ 2.33 TB
问题不在于数据量,而在于如何高效地发送这些请求。
HTTP/1.1请求的复杂性
发送单个HTTP/1.1请求涉及多个步骤,例如DNS解析、TCP连接、TLS握手、请求构建与发送、响应处理等。每个步骤都可能失败,因此必须准备好处理重试。
即使是在高性能服务器上,解析DNS记录和打开TLS连接也可能耗时160ms左右。对于需要向不同网络中的多个主机发送大量请求的任务,这种延迟是不可接受的。
优化策略
移除不必要的步骤
通过提前解析DNS记录并手动构建HTTP/1.1请求,我消除了请求解析和DNS解析的延迟。此外,我使用了massdns工具来快速解析大量DNS记录,进一步加快了请求速度。
HTTP/1.1发送大炮的设计
我使用多个工作池来处理请求生成、发送和响应处理,每个工作池由并发安全的队列隔离。这样做的好处是,每个组件可以独立扩展,且易于调试和优化。
选择合适的HTTP库
Go语言中的net/http
库虽然功能强大,但在性能方面存在不足。我选择了fasthttp
库,它是为速度优化的低级HTTP库。通过一些自定义优化,例如跳过请求规范化步骤,我进一步提升了性能。
跳过DNS解析
为了避免每次请求都进行DNS解析,我使用自定义拨号器直接连接到已解析的IP地址。这一优化显著减少了请求的延迟。
优化TLS握手
由于TLS握手消耗了大量的CPU周期,我考虑硬编码密钥来加快这一过程,尽管最终我选择了更易实现的方案。
工作分块
我将250万个主机分成每块200个主机,每个块由一个工作线程处理。这一分块策略平衡了效率和可靠性,确保在请求失败时能够最小化重试的工作量。
Kubernetes扩展
为了完成这一庞大的任务,我使用DigitalOcean上的Kubernetes集群进行扩展。每个Pod可以达到每秒100-400个请求,通过扩展到60个Pod,我在几个小时内完成了向250万个主机发送5亿个HTTP/1.1请求的任务。
结论
最终结果相当令人满意:
- 每个Pod达到每秒100-400个请求
- 扩展到60个Pod
- 在几个小时内完成了任务
这次任务的成功归功于对Go语言和HTTP/1.1的深入理解,以及对工具和架构的有效优化。在下一篇文章中,我会分享更多关于任务结果的细节。