合作机构:阿里云 / 腾讯云 / 亚马逊云 / DreamHost / NameSilo / INWX / GODADDY / 百度统计
我们在介绍开源Zuul 2[2]的文章中简单提到了负载均衡方面的一些改进,本文将更详细介绍这项工作的原因、方式和结果。
Netflix的云网关团队一直致力于帮助系统减少错误,获得更高的可用性,并提高故障恢复能力。因为Netflix每秒有超过一百万次请求,即使是很低的错误率也会影响到会员体验,所以每一点提升都有帮助。
因此,我们向Zuul和其他团队学习,改进负载均衡实现,以进一步减少由服务器过载引起的错误。
Zuul以前用基于轮询的Ribbon负载均衡器[3],并基于某些过滤机制将连接失败率高的服务器列入黑名单。
过去几年里,我们做了一些改进和定制,比如向最近上线的服务器发送较少流量,以避免过载。这些改进已经取得了显著效果,但对于某些问题比较多的原始集群,还是会看到与负载相关的错误率远高于预期。
如果集群中所有服务器都过载,那选择哪一台服务器几乎没有什么区别,不过现实中我们经常看到只有某个服务器子集过载的情况。例如:
在开始一个项目时,需要记住一些原则,从而帮助指导在设计软件时需要做出的大大小小的决定,这个项目基于的原则如下。
我们已经将之前定制的负载均衡器集成到了Zuul代码库中,从而使得无法与Netflix的其他团队共享这些定制。因此,我们决定这次基于约束条件并做出额外投资,从一开始就考虑复用,从而能够直接在其他系统中使用,减少重新发明轮子的代价。
尝试在他人的想法和实现基础上构建,例如之前在Netflix其他IPC栈中试用的"二选一(choice-of-2)"和"试用期(probation)"算法。
选择本地决策,避免跨集群协调状态的弹性问题、复杂性和滞后。
多年来基于Zuul的操作经验表明,将服务配置的部分置于不属于同一团队的客户服务中会导致问题。
一个问题是,客户端配置往往与服务端不断变化的现实不同步,或者在不同团队拥有的服务之间引入耦合的变更管理。
例如,用于服务X的EC2实例类型升级,导致该集群所需节点减少。因此,现在服务Y中的"每台主机最大连接数"客户端配置应该增加,以反映新增加的容量。应该先对客户端进行更改,还是先对服务端进行更改,还是同时对两者进行更改?更有可能的是,完全忘了要改配置,从而导致更多问题。
尽可能不要配置静态阈值,而是采用基于当前流量、性能和环境变化的自适应机制。
当需要静态阈值时,与其让服务团队将阈值配置协调到每个客户端,不如让服务在运行时进行通信,以避免跨团队边界推动更改的问题。
主要的想法是,虽然服务器延迟的最佳数据来源是客户端视图,但服务器利用率的最佳数据来源是服务器本身。结合这两种数据源,可以得到最有效的负载均衡。
我们基于一组互补机制,其中大多数已经被其他人开发和使用过,只是以前可能没有以这种方式组合。
Join-the-Shortest-Queue和服务器报告利用率相结合
我们选择支持常用的Join-the-shortest-queue(JSQ) 算法,并将服务器报告的利用率作为第二算法,以尝试结合两者达到最佳效果。
Join-the-shortest-queue对于单个负载均衡器非常有效,但如果跨负载均衡器集群使用,则会出现严重问题。负载均衡器会倾向于在同一时间选择相同的低利用率服务器,从而造成超载,然后转移到下一个利用率最低的服务器并造成超载,以此类推……
通过结合使用JSQ和二选一算法,可以在很大程度上消除羊群问题,除了负载均衡器没有完整的服务器使用信息之外,其他方面都很好。
JSQ通常仅从本地负载均衡器计算到服务器的正在使用的连接数量来实现,但是当有10到100个负载均衡器节点时,本地视图可能会产生误导。
单个负载平衡器的观点可能与实际情况大不相同
例如,在上图中,负载均衡器A有一个到服务器X的请求和一个到服务器Z的请求,但没有到服务器Y的请求。所以当它收到新请求时,基于本地数据,选择利用率最小的服务器,会选择服务器Y,但这不是正确的选择。服务器Y实际上负载最重,其他两个负载均衡器目前都有请求发送到服务器Y上,但负载均衡器A没有办法知道。
这说明单个负载均衡器的观点与实际情况完全不同。
在只依赖客户端视图时遇到的另一个问题是,对于大型集群(特别是与低流量相结合时),负载均衡器通常只有几个活跃连接,和集群中的某个子集交互。因此,当它选择哪个服务器负载最少时,通常只是在若干个它认为负载都是0的服务器之间进行选择,而并没有关于所选服务器的利用率的数据,所以只能盲猜。
这个问题的解决方案是与所有其他负载均衡器共享所有活跃连接数状态……但这样就需要解决分布式状态问题。
考虑到获得的好处要大于付出的成本,因此我们通常只将分布式可变状态作为最后手段:
另一种更简单的解决方案(也是我们选择的),是依赖于服务器向每个负载均衡器报告资源使用情况……
服务器主动上报其使用率的好处是可以提供所有使用了该服务器的负载均衡器的完整信息,从而避免JSQ的不完整问题。
对此有两种实现方式:
我们选择第二种方式,其实现简单,可以频繁更新数据,避免了N个负载均衡器每隔几秒钟轮询M个服务器所带来的额外开销。
被动策略的影响是,负载均衡器向一台服务器发送请求的频率越高,获得的该服务器的利用率数据就越新。因此RPS越高,负载均衡的有效性就越高。但反过来,RPS越低,负载均衡的效果就越差。
这对我们来说不是问题,但对于通过特定负载均衡器处理低RPS(同时通过另一个负载均衡器处理高RPS)的服务来说,主动轮询运行状况检查可能更有效。临界点是负载均衡器向每个服务器发送的RPS低于运行状况检查的轮询频率。
我们在服务端通过简单跟踪活跃请求计数来实现,将其转换为该服务器配置的最大百分比,并将其作为HTTP响应报头:
X-Netflix.server.utilization: <current-utilization>[, target=<target-utilization>]
TOP