如果你有一个服务并希望在全球范围内实现快速响应时间,你的初步想法是什么?CDN?GeoDNS?
让我们从一个简单的例子开始。假设你有一个服务,其目的是为访问者提供一个 UUID。想象你的服务部署在德国,你的服务器 IP 地址是 159.69.27.1
。然后,你配置 DNS 将 uuid.example.com
指向这个 IP 地址。因此,当有人访问 uuid.example.com
时,他们会收到一个 UUID。
这是第一步:当任何人访问 uuid.example.com
时,它会解析到相应的 IP 地址。然后,浏览器发起请求,经过几秒钟后,成功返回所需的结果。然而,来自世界不同地区的用户可能会经历不同的延迟。
例如,从中国大陆的延迟可能会是这样的:
PING 159.69.27.1 (159.69.27.1) 56(84) bytes of data.
64 bytes from 159.69.27.1: icmp_seq=1 ttl=37 time=373 ms
64 bytes from 159.69.27.1: icmp_seq=2 ttl=37 time=423 ms
64 bytes from 159.69.27.1: icmp_seq=6 ttl=37 time=388 ms
而从美国的延迟情况如下:
PING 159.69.27.1 (159.69.27.1) 56(84) bytes of data.
64 bytes from 159.69.27.1: icmp_seq=1 ttl=56 time=114 ms
64 bytes from 159.69.27.1: icmp_seq=2 ttl=56 time=113 ms
64 bytes from 159.69.27.1: icmp_seq=4 ttl=56 time=113 ms
为了防止暴露你的服务器 IP 地址,这可能会导致攻击,并且为了“加速访问”,许多人可能会选择使用 CDN,如 Cloudflare。当你将你的域名与 Cloudflare 集成时,你会注意到 uuid.example.com
不再解析到你的服务器 IP,而是可能会指向由 Cloudflare 拥有的 IP,如 104.16.132.229
。
在这个阶段,当其他请求访问你的服务时,你会观察到使用 ping
命令时的延迟大约为 10 毫秒:
PING 104.16.133.229 (104.16.133.229) 56(84) bytes of data.
64 bytes from 104.16.133.229: icmp_seq=1 ttl=62 time=10.8 ms
64 bytes from 104.16.133.229: icmp_seq=2 ttl=62 time=10.2 ms
64 bytes from 104.16.133.229: icmp_seq=3 ttl=62 time=10.3 ms
在这个阶段,根据 ping
的结果,延迟似乎已经降低了,这让许多人认为实际服务的延迟已经改善。然而,重要的是要注意,这种延迟只是从你的访问者到 Cloudflare 的 Anycast 节点。虽然 ping
时间很低,但实际的 HTTP 请求仍然需要通过公共互联网到达你的源服务器。要测量实际的延迟,你需要查看首字节时间(TTFB)。
KeyCDN 提供了一个 性能测试 ,可以轻松评估服务在各个主要地区的延迟。以 WebP Cloud Services—公共服务为例,由于您的服务器全部位于德国的 Hetzner,因此得益于 Cloudflare 的 CDN,CONNECT 延迟较低。
然而,在检查 TTFB 时,您会注意到只有德国地区的延迟较低,而其他地区的延迟超过 100 毫秒。
WebP Cloud Services—公共服务为 Gravatar 和 GitHub Avatars 提供反向代理,解决了两个主要问题:
- 中国大陆用户无法直接访问 Gravatar,例如这个地址: https://www.gravatar.com/avatar/09eba3a443a7ea91cf818f6b27607d66 。
- 在提供这些图像时,它提供了 WebP 转换,显著减小了图像文件大小,对图像质量的影响 minimal,从而加速了整个网站的加载速度。
此外,该服务是公开的且完全免费的,拥有大量用户,包括但不限于像 CNX Software 和 Indienova 这样的网站。
从 Cloudflare 的统计仪表板中,我们可以看到在过去 30 天内,该服务处理了超过 600 万次请求,其中大部分来自美国和中国:
值得注意的是,除了中国移动用户外,大多数中国访问者被路由到 Cloudflare 位于西部的美国节点,通常位于 SJC。
根据我们的理论,大约有一半的用户首先访问 Cloudflare 的美国西部节点,然后通过公共互联网到达我们在德国的源服务器,导致额外的延迟超过 110 毫秒。这给用户留下了服务响应时间缓慢的印象。
那么,我们应该如何解决这个问题?
在这种情况下,存在几个隐含条件:
- 我们需要继续使用 Cloudflare 来保护我们的源服务器地址,并在 Cloudflare 的边缘执行某些计算和 WAF 规则。
- 我们不能直接将服务“迁移”到美国,因为我们仍然有欧洲用户。因此,我们需要在美国和欧洲都设有服务器。
- 鉴于大多数访问者来自美国和中国,且中国用户被路由到美国节点,我们的优化重点应放在提高美国的访问速度上。
- 我们的目标是将美国和中国的用户引导至美国的服务器,将欧洲用户引导至德国的服务器,并将其他地区的用户引导至最近的服务器。
考虑到这些因素,我们提出了几种解决方案:
- 使用私有 ASN + IPv6: 在美国和欧洲使用像 Vultr 这样的服务部署节点,并使用 BGP Anycast 进行负载均衡。这种方法与 Nova 的博客文章中讨论的类似,文章标题为“ Simulate Argo——在 Cloudflare 背后构建 IPv6 AnyCast 网络 。”成本包括 ASN 费用、IPv6 成本、Vultr 费用以及维护网络的开销,使其有些复杂。
- 直接使用 BuyVM 的 Anycast 服务: 在三个地点(美国和欧洲)购买 VPS,并使用 BuyVM 的 Anycast 服务进行负载均衡。此选项涉及 BuyVM 的 VPS 费用,大约每月 10.5 美元。
- 使用 Cloudflare 负载均衡器进行地理负载均衡: 利用 Cloudflare 负载均衡器进行地理负载均衡。费用包括 Cloudflare 负载均衡器费用,大约每月 5 美元可处理 50 万个请求,超出 50 万个请求的每 50 万个请求额外收取 0.5 美元。
如果我们旨在根据我们的需求实现基于区域的路由,成本将是每月20.5美元,基于我们每月600万的请求。 - 使用 Cloudflare Workers: 利用 Cloudflare Workers,这是一项在 Cloudflare 所有数据中心部署的无服务器服务。费用包括 Cloudflare Workers 的费用,大约每月 5 美元(处理高达每月 1000 万次请求,超出我们的月请求量)。
从上述计划来看,使用 Cloudflare Workers 似乎是更无忧的解决方案。付费并让我们开始吧!
Cloudflare Workers
Cloudflare Workers 是 Cloudflare 提供的一种无服务器服务,允许我们在 Cloudflare 的所有数据中心部署代码。它还使我们能够使用 Cloudflare Workers KV 存储数据,让我们能够在全球范围内部署代码并在全球范围内读写数据。
对于我们的特定用例,使用 Cloudflare Workers 的主要逻辑如下:
- 在 Workers 平台上,根据源 IP 地址(该地址极有可能为执行 Workers 机器的位置)确定请求的来源国家。
- 根据预定义的映射,将请求路由到物理上最接近的服务器。
- 此外,处理各种类型的异常请求并实现自动故障转移逻辑。
从 https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-ipcountry ,我们可以了解到,对于每个请求,我们可以使用 CF-IPCountry
获取请求来源区域的代码。在我们的案例中,为了简化,我们计划根据大陆来分配流量。因此,我们可以快速创建如下简单的映射:
function getContinentByISOCode(isoCode) {
const continentMap = {
'AD': 'Europe',
'AE': 'Asia',
...
'CN': 'North America', // China should be Asia, but we're using North America because China users are routed to the North America Edge
...
'ZW': 'Africa',
};
const continent = continentMap[isoCode];
if (continent) {
return continent;
} else {
return 'Unknown';
}
}
下一步是在各个地区启动服务,规划服务的端点地址,并根据以下大陆在地理位置上映射我们的实际后端服务:
const BACKEND_MAP = {
...
'Europe': 'https://eu-west-2-entrance.webp.se',
'North America': 'https://us-west-2-entrance.webp.se',
...
'Unknown': 'https://eu-west-1-entrance.webp.se'
}
最后,我们的 Workers 代码可以看起来像这样:
handleProxy
函数如下:
这不是很简单吗?
请注意,
handleProxy(request, backend_url, path, url.hostname, CF_IP_COUNTRY);
函数有三个参数:backend_url
、path
和url.hostname
。这是因为我们的服务是通过像gravatar.webp.se
这样的地址从外部访问的,而不是通过eu-west-2-entrance.webp.se
。然而,当 Workers 使用fetch()
访问源时,它只能使用后者。因此,在这里我们需要在fetch()
中传递一个额外的头部信息,以告知后端服务实际请求的域名。例如,在一个 Fetch 请求中,
Host
头是eu-west-2-entrance.webp.se
,而some-secret-header-to-backend
头是gravatar.webp.se
。当我们的实际后端检测到some-secret-header-to-backend
头的存在时,它会将此头视为评估的 Host。
效果对比
在使用 Workers 之前,所有源服务器都在德国的 Hetzner。
在使用 Workers 后,源服务器位于 Hetzner 德国和 Hetzner Hillsboro。您可以看到在美国的两个测试点,TTFB 从 300+毫秒显著下降到 100+毫秒。
您可以通过 x-powered-by
头部确定哪个区域的节点正在处理请求:
Hetzner Germany
Hetzner Hillsboro
截至本文发布时,我们已经在这个架构上运行了将近 3 天。根据监控数据,HIO 节点在启动后立即接管了大约一半的流量,这与我们的预期相符。
Workers 的统计数据: