这是一篇来自 jsDelivr 的 Dmitriy Akulov 的客座文章。
最近我写了关于 jsDelivr 及其独特之处 的文章,其中详细介绍了我们提供的功能以及我们的系统工作原理。从那时起,我们改进了很多东西,并发布了更多功能。但最大的一项是 开源我们的负载均衡算法。
正如您从之前的博客文章中所知,我们使用 Cedexis 来进行负载均衡。简而言之,我们收集来自世界各地的数百万个 RUM(真实用户指标)数据点。当用户访问 Cedexis 或我们网站合作伙伴的网站时,背景中会执行一个 JavaScript,它会对我们的核心 CDN、MaxCDN 和 CloudFlare 进行性能检查,并将这些数据发送回 Cedexis。然后我们可以使用这些数据,根据来自现实用户和 ISP 的实时性能信息进行负载均衡。这很重要,因为它使我们能够减轻 CDN 在非常局部区域(例如单个国家或甚至单个 ISP)而不是全球范围内可能遇到的中断。
开源负载均衡代码
现在,我们的 负载均衡代码对每个人开放,以便审查、测试,甚至发送自己的拉取请求,以进行改进和修改。
直到最近,该代码实际上是用 PHP 编写的,但由于性能问题和其他由此产生的问题,我们决定切换到 JavaScript。现在,DNS 应用程序完全用 js 编写,我将尝试解释它的工作原理。
这是一个在 DNS 级别运行的应用程序,并与 Cedexis 的 API 集成。对 cdn.jsdelivr.net 发出的每个 DNS 请求都由以下代码处理,然后根据所有变量,它返回一个 CNAME,客户端可以使用它获取请求的资产。
声明提供商
第一步是声明我们的提供商
providers: {
'cloudflare': 'cdn.jsdelivr.net.cdn.cloudflare.net',
'maxcdn': 'jsdelivr3.dak.netdna-cdn.com',
...
},
此数组包含我们所有提供商的别名以及如果选择了该提供商,我们可以返回的主机名。我们实际上使用了一些自定义服务器来提高 CDN 缺乏位置的性能,但我们目前正在逐步淘汰所有这些服务器,转而使用更多希望赞助我们的企业级 CDN。
在我解释下一个数组之前,我想跳到第 40 行
defaultProviders: [ 'maxcdn', 'cloudflare' ],
因为我们的 CDN 提供商比我们的自定义服务器获得了更多的 RUM 测试,所以它们的数据以及负载均衡结果更加可靠和更好。这就是为什么默认情况下,只有 MaxCDN 和 CloudFlare 被考虑用于任何用户请求。这实际上是我们想要淘汰自定义服务器的主要原因。
国家/地区映射
现在您知道了这一点,接下来是我们的下一个数组
countryMapping: {
'CN': [ 'exvm-sg', 'cloudflare' ],
'HK': [ 'exvm-sg', 'cloudflare' ],
'ID': [ 'exvm-sg', 'cloudflare' ],
'IT': [ 'prome-it', 'maxcdn', 'cloudflare' ],
'IN': [ 'exvm-sg', 'cloudflare' ],
'KR': [ 'exvm-sg', 'cloudflare' ],
'MY': [ 'exvm-sg', 'cloudflare' ],
'SG': [ 'exvm-sg', 'cloudflare' ],
'TH': [ 'exvm-sg', 'cloudflare' ],
'JP': [ 'exvm-sg', 'cloudflare', 'maxcdn' ],
'UA': [ 'leap-ua', 'maxcdn', 'cloudflare' ],
'RU': [ 'leap-ua', 'maxcdn' ],
'VN': [ 'exvm-sg', 'cloudflare' ],
'PT': [ 'leap-pt', 'maxcdn', 'cloudflare' ],
'MA': [ 'leap-pt', 'prome-it', 'maxcdn', 'cloudflare' ]
},
此数组包含覆盖“defaultProviders”参数的国家/地区映射。这是自定义服务器目前使用的地方。对于某些国家/地区,我们 100% 知道我们的自定义服务器比我们的 CDN 提供商快得多,因此我们手动指定它们。由于这些位置很少,我们只需要创建少量规则。
ASN 映射
asnMapping: {
'36114': [ 'maxcdn' ], // Las Vegas 2
'36351': [ 'maxcdn' ], // San Jose + Washington
'42473': [ 'prome-it' ], // Milan
'32489': [ 'cloudflare' ], // Canada
...
},
ASN 映射包含每个 ASN 的覆盖。目前,我们使用它们来改进 Pingdom 测试的结果。这样做的原因是我们依赖 RUM 结果来进行负载均衡,我们永远不会获得 Pingdom 租用服务器的公司等托管提供商使用的 ASN 的任何性能测试。因此,该代码被迫回退到国家/地区级别的性能数据,以选择最适合 Pingdom 和任何其他合成测试和服务器的提供商。这些数据并不总是可靠的,因为并非所有 ISP 在 CDN 提供商上的性能都与全国最快的 CDN 提供商相同。因此,我们调整了一些 ASN 以更好地与 jsDelivr 协同工作。
更多设置
lastResortProvider
设置了如果应用程序无法自行选择,我们想要使用的 CDN 提供商。这种情况应该很少见。defaultTtl: 20
是我们 DNS 记录的 TTL。我们进行了一些测试,并决定这是最优值。在最坏的情况下,如果出现停机,jsDelivr 最大的停机时间是 20 秒。此外,我们的 DNS 和 CDN 速度足够快,可以补偿每 20 秒的额外 DNS 延迟,而不会对性能产生任何影响。availabilityThresholds
是一个百分比值,它设置了提供商应被视为停机的正常运行时间。这基于 RUM 数据。同样,由于合成测试中的一些小问题,我们不得不降低 Pingdom 的阈值。Pingdom 值不会影响其他人。sonarThreshold
Sonar 是我们使用的辅助正常运行时间监控器,用于确保提供商的正常运行时间。它每 60 秒运行一次,并检查我们所有的提供商,包括它们的 SSL 证书。如果出现问题,我们的应用程序将检测到正常运行时间的变化,如果它降至此阈值以下,则该提供商将被视为已停机。- 最后,
minValidRtt
用于过滤掉所有无效的 RUM 测试。
初始化过程
接下来,我们的应用程序启动初始化过程。检查错误的配置和不符合我们标准的正常运行时间,然后从本次请求的潜在候选中删除所有不符合我们标准的提供商。
接下来,我们创建一个用于调试目的的 reasons
数组,并应用我们的覆盖设置。在这里,我们使用 Cedexis API 获取 sonar 正常运行时间、rum 更新和 HTTP 性能的最新实时数据。
sonar = request.getData('sonar');
candidates = filterObject(request.getProbe('avail'), filterCandidates);
//console.log('candidates: ' + JSON.stringify(candidates));
candidates = joinObjects(candidates, request.getProbe('http_rtt'), 'http_rtt');
//console.log('candidates (with rtt): ' + JSON.stringify(candidates));
candidateAliases = Object.keys(candidates);
在正常运行时间的情况下,我们还通过调用 filterCandidates
函数过滤掉不符合我们的正常运行时间标准的不良提供商。
function filterCandidates(candidate, alias) {
return (-1 < subpopulation.indexOf(alias))
&& (candidate.avail !== undefined)
&& (candidate.avail >= availabilityThreshold)
&& (sonar[alias] !== undefined)
&& (parseFloat(sonar[alias]) >= settings.sonarThreshold);
}
实际的决策过程由一小段代码执行
if (1 === candidateAliases.length) {
decisionAlias = candidateAliases[0];
decisionReasons.push(reasons.singleAvailableCandidate);
decisionTtl = decisionTtl || settings.defaultTtl;
} else if (0 === candidateAliases.length) {
decisionAlias = settings.lastResortProvider;
decisionReasons.push(reasons.noneAvailableOrNoRtt);
decisionTtl = decisionTtl || settings.defaultTtl;
} else {
candidates = filterObject(candidates, filterInvalidRtt);
//console.log('candidates (rtt filtered): ' + JSON.stringify(candidates));
candidateAliases = Object.keys(candidates);
if (!candidateAliases.length) {
decisionAlias = settings.lastResortProvider;
decisionReasons.push(reasons.missingRttForAvailableCandidates);
decisionTtl = decisionTtl || settings.defaultTtl;
} else {
decisionAlias = getLowest(candidates, 'http_rtt');
decisionReasons.push(reasons.rtt);
decisionTtl = decisionTtl || settings.defaultTtl;
}
}
response.respond(decisionAlias, settings.providers[decisionAlias]);
response.setReasonCode(decisionReasons.join(''));
response.setTTL(decisionTtl);
};
如果在检查后只剩下 1 个提供商,我们只需选择该提供商并输出 CNAME;如果剩下 0 个提供商,则使用 lastResortProvider
。否则,如果一切正常并且剩下 1 个以上的提供商,我们将进行更多检查。
一旦我们剩下当前在线且性能数据没有任何问题的提供商,我们就会根据 RUM HTTP 性能对其进行排序,并将 CNAME 推出,以便用户浏览器使用。
就是这样。大多数其他东西,比如回退到国家/地区级别的数据,都在后端自动完成,我们只获取应用程序中可以使用的数据。
结论
我希望您觉得它有趣,并了解到更多关于在进行负载均衡时,尤其是基于 RUM 数据时应该考虑哪些因素的信息。
查看 jsDelivr,并在您的项目中随意使用它。如果您有兴趣提供帮助,我们也在寻找 node.js 开发人员和设计师来帮助我们。
我们也在寻找公司赞助商来帮助我们更快地发展。
关于 Dmitriy Akulov
系统管理员。热爱科技、高性能和快速网页。有时假装自己是开发者。为 MaxCDN 工作。
关于 Robert Nyman [荣誉编辑]
Mozilla Hacks 的技术布道者和编辑。发表关于 HTML5、JavaScript 和开放 Web 的演讲和博客文章。Robert 是 HTML5 和开放 Web 的坚定支持者,自 1999 年以来一直从事 Web 前端开发工作 - 在瑞典和纽约市。他还经常在 http://robertnyman.com 上发表博客文章,热爱旅行和结识新朋友。
2 条评论