如您需要技术咨询、解决方案定制、故障排除、运维监控等服务,可联系ericwcn#at#163.com。

前端服务器的负载均衡 (SRE Google运维解密)

运维 立杰 323℃

前端服务器的负载均衡

作者:Piotr Lewandowski 编辑:Sarah Chavis

Google每秒要处理数以百万计的请求,与你猜想的一样,我们是使用多台服务器同时承担这些负载的。即使我们真的有一个台非常强大的超级计算机,可以处理所有这些请求(想想这个模式下光是网络带宽的要求吧!),我们还是不会采取这种受单点故障影响的策略:运维大型系统时,将所有鸡蛋放在一个篮子里面是引来灾难的最好办法。

本章关注高层次的负载均衡——google是如何在数据中心之间调节用户流量的。下章会更深入地探讨我们是如何在一个数据中心内进行流量分发和负载均衡的。

有时候硬件并不能解决问题

假设,这里仅仅是假设——我们有一台非常强大的服务器,和与之匹配的用不出故障、带宽充足的网络连接。这是否能满足Google的需求呢?并不是。就算拥有这样的一套配置,它仍然将会受到一些物理条件的限制。例如,光速是通过光纤通讯制约性因素,这限制了远距离传输数据的速度。就算在一个理想的情况下,采用这样一种受单点故障影响的基础设施也是一个糟糕的注意。

在现实中,Google拥有数据千计的服务器,也同时有比这个数量更多的用户在发送请求。很多用户甚至同时发送好几个请求。用户流量负载均衡(Tranffic load balancing)系统是来决定数据中心的这些机器中哪一个用来处理某个请求。理想情况下,用户流量应该最优地分布于多个网络链路上、多个数据中心中,以及多台服务器上。但是这里的“最优”是如何定义呢?其实这里并没有一个独立答案,因为这里的最优严重依赖于一下几个因素:

  • 逻辑层级(是在全局还是在局部)
  • 技术层面(硬件层面与软件层面)
  • 用户流量的天然属性

我们先来讨论一下两个常见的用户流量场景:一个简单的搜索请求和一个视频上传请求。用户想要很快的获取搜索结果,所以对搜索请求来说最重要的变量是延时(latency)。而对于视频上传请求来说,用户以及预期该请求将要花费一定的时间,但是同时希望该请求能够一次成功,所以这里最重要的变量是吞吐量(throughput)。两种请求用户的需求不同,使我们在全局层面觉定“最优”分配方案的重要条件。

  • 搜索请求将会被发往最近的,可用的数据中心——评价条件是数据包往返时间(RTT)因为我们想要最小化该请求的延迟。
  • 视频上传流将会采取另外一条路径——也许是一条目前带宽没有占满的链路——来最大化吞吐量,同时也会牺牲一定程度的延迟。

但是在局部层面,在一个数据中心内部,我们经常假设同一个物理建筑物内的所有物理服务器在同一个网络中,对用户来说都是等距离的。因此在这个层面上的“最优”分配往往关注与优化资源的利用率, 避免某个服务器负载过高。

当然这里例子中使用了简化场景。在现实中,很多其他因素也都在“最优”方案的考虑范围之内:有些请求可能会被指派到某个稍远一点的数据中心,以保障该数据中心的缓存处理有效状态。或者某些费交互式请求会被发往另外一个地理区域,以避免网络拥塞。负载均衡,尤其大型系统的负载均衡,是非常复杂和非常动态变化的。Google在多个层面上使用负载均衡策略来解决这些问题:下面一节会讨论到其中两个。为了更切实地展开讨论,我们这里主要讨论基于TCP的HTTP请求。对无状态(stateless)服务(如基于UDP的DNS)的负载均衡和这个有所不同,但这里讨论大部分方式都仍然适用。

使用DNS进行负载均衡

在某个客户端发送HTTP请求之前,经常需要通过DNS查询IP地址。这就为我们第一层的负载均衡机制提供了一个良好基础:DNS负载均衡。最简单的方案是使用DNS回复中提供多个A记录或AAAA记录,有客户端任意选择一个IP地址使用。这种方案虽然看起来简单并且容易实现,但是存在很多问题。

第一个问题是这种机制对客户端行为的约束力很弱:记录是随机选择的,也是每条记录都会引来有基本相同数量的请求流量。如何避免这个请求了?理论上我们可以使用SRV记录来指明每个IP地址的优先级和比重,但是HTTP协议目前还没有采用SRV记录。

另外一个潜在问题是客户端无法识别“最近”的地址。我们可以通过提供一个anycast DNS服务器地址,通过DNS请求一般会到达最近的地址这个方式来一定程度上缓解这个问题。服务器可以使用最近的数据中心地址来生成DNS回复。更进一步的优化方式是,将所有的网络地址和他们对于的大概物理位置建立一个对照表,按照这个对照表来发送一个数据更新流水线(pipeline)来保证位置信息的正确性。

当然,没有一个很简单的方案,因为这是由于DNS的基本特性决定的:最终用户很少直接跟权威域名服务器(authoritive nameserver)直接联系。在用户到权威服务器中间经常有一个递归解析器(recursive nameserver)代理请求。该递归解析器代理用户请求,同时经常提供一定程度的缓存机制。这样的DNS中间人机制在用户流量管理上有三个非常重要的影响:

  • 递归方式解析IP地址
  • 不确定的恢复路径
  • 额外的缓存问题

以递归方式解析IP地址会造成一定的问题,因为权威服务器接收到的不是用户地址,而是递归解析器的IP地址。这是一个严重的问题,因为这样DNS服务器只能根据递归解析器的IP地址返回一个最优方案。一个可能的解决方案是使用EDNS0扩展协议,该协议在递归解析器发送请求中包括了最终用户的子网网段。这样权威服务器可以返回一个队最终用户来讲最优地恢复。虽然这个协议还不是正式规范,但是这些明显的优势使得最大的DNS解析器(如OpenDNS和Google)已经开始使用了。

不仅要处理返回最优IP的这个难题,处理请求的域名服务器可能同时需要处理几千、几万个用户的请求,范围从一个小办公室到整个大楼。举例来说,某个大型的国家级电信服务商可能在数个大都市区域也有网络互联。该ISP的域名服务器返回的一个回复可能对他们当前的数据中心来讲是最优地,却没有考虑到可能对其他用户还有更优的网络路径。

最后,递归解析器一般根据接收到的回复中的时效(TTL)来缓存和发送这些回复。这样会造成预估某个DNS回复的用户流量影响很困难:因为一个回复可能会发送一个用户,或者几万个用户。我们利用两种方式来解决这个问题:

  • 分析流量变化,并且持续不断地更新已知的DNS解析器的用户数量,这样可以评估某个解析器的预期影响。
  • 根据数据评估每个已知解析器背后用户的地址位置分布,以便更好地将用户转向追加地址。

准确评估地理位置分布式非常困难的,尤其用户分布在很广的区域时。在这种情况下,我们只能针对大部分用户的情况来选择最优地位置进行优化。

但是“最优位置”在DNS负载均衡的语境中,到时是什么意思呢?最直接的答案是“离用户最近的位置”。但是(先不考虑确定用户位置有多难)还应该有其他的选择条件。DNS负载均衡还要保证选择的数据中心和网络目前都处于良好状态,否则将用户导向正在经受网络故障和供电故障的地点并不是一个很合理的做法。幸好,Google可以将权威DNS服务器和我们的全局负载均衡(GSLB)整合起来,该负载均衡系统负载跟踪我们的流量水平,可用容量和各种基础设施的状态。

DNS中间人带来的第三个问题是跟缓存有关的。因为权威服务器不能主动清除某个解析器的缓存,DNS记录需要保持衣蛾相对较低时效值(TTL)。这其实是为DNS回复的变化到达最终用户的速度设置一个下限。不幸的是,我们除了将这个因素包含在负载均衡计算之外,并没有什么其他的应对办法。

尽管有些不足,DNS仍然是最简单、最有效的负载均衡制度,他在用户发起连接之前就生效了。另一方面,我们清晰地看到仅仅靠DNS里做负载均衡是不够的。我们要记住RFC1035将DNS回复限制为512字节。

DNS的尺寸限制实际上为单个DNS回复能返回的地址数量设置了上限,这个上限明显远远小于我们服务器的数量。

要真正解决前端负载均衡的问题,我们需要在DNS负载均衡之后增加一层虚拟IP地址。

负载均衡:虚拟IP

虚拟IP(VIP)不是绑定在某一个特定的网络接口上,它是由很多设备共享的。但是,从用户视角来看,VIP仍然是一个独立的、普通的IP地址。理论上讲,这种实现可以让我们将底层实现细节隐藏起来(比如某一个VIP背后的机器数量),无缝进行维护工作。比如我们可以依次升级某些机器,或者资源池中增加更多的机器而不影响用户。在实践中,最重要的VIP实现部分是一个称为网络负载均衡器(network load balancer)的组件。该负载均衡器接收网络数据包,同时将他们转发给VIP背后的某一个服务器,这些后端服务器可以接下来处理该请求。

负载均衡器在决定哪个后端服务器应该接收请求,有如下几种方案:第一种(也可能是最直接的)方案是,永远邮箱目前负责最小的后端服务器。理论上来说,这个方案应该可以最小化用户体验,因为请求始终会被发往最不忙的机器。但是,这个逻辑对有状态协议就不适用了,因为在处理请求的过程中必须使用相同的后端服务器。这就需要负载均衡器跟踪所有经过它转发的连接,以确保同一个连接的数据包都会发往同一个后端服务器。一种替代的方案是使用数据包中的某些部分创建出一个连接标识符(connection id)(可能使用某些数据包内的信息和一个哈希算法),使用该连接标识符来选择后端服务器。举例来说,连接标识符可以用如下算式表达:

id(packet) mod N

这里的id()是一个函数,以数据包内容为输入,得出一个连接标识符,N是所有配置的后端服务器数量。

这样负载均衡器就不用再保存状态了,每个连接的数据包也都会发送往同一个后端服务器。这就成功了吗?还没有。当某个后端服务器出现问题了,需要从列表中去掉时怎么办呢?这里N突然变成N-1,而id(packet)mod N变成id(packet)mod N-1,基本上所有的请求都被指向了另外一个后端服务器。如果后端服务器之间不同步状态,这几乎意味着所有现存的连接都要中断。这样的场景即使出现得不太频繁,对用户来说也是很不友好的。幸运的是,的确有一种替代方案。既不需要在内存中保存所有连接状态,也不会再单台机器出现故障时重置所有连接,这就是一致性哈希(consistent hashing)算法。1997年提出的一致性哈希算法描述了一种映射算法,在新的后端服务器被添加/删除时保持相对稳定。这种算法在后端资源变化时,最小程度地减少了对现存连接的影响。最终结果是,我们平时可以使用简单的连接跟踪机制,但是在系统压力上升时却换为一致性哈希算法,例如在处理分布式拒绝服务攻击时(DDOS)。

那么回到更大的问题上来:负载均衡器究竟是如何将数据包发往某个特定的VIP后端的呢?一个解决方案是进行移植网络地址转换(NAT)。但是这样要求我们的在内存中跟踪每一个连接,也就是不能提供一个完全无状态的后背机制。

另外一个解决方案是修改数据链路层(OSI模型的第2层)的信息。通过修改转发数据包的目标MAC地址,负载均衡器可以保持全部上层信息不变,后端将会受到原始的来源和目标信息。后端服务器可以直接发送回应给用户——这被称为直接服务器响应(DSR)。如果用户请求很小,而回复很大(恰如大多数HTTP请求这样),DSR可以节约大量资源,因为仅仅一小部分用户流量需要穿过负载均衡器。更好地是,DSR不需要保持状态。但是使用2层信息进行内部负载均衡会导致在大规模部署下出现问题。所有的机器(也就是所有的负载均衡器和所有的后端服务器)必须可以在网络链路层相同。如果服务器数量不多,网络能够支撑这样的连接那就不是问题,但是所有的机器都需要在同一个广播域中。正如你先的那样,Google在一段时间后,由于规模原因,已经放弃这种方案了。

我们现在的VIP负载均衡解决方案使用的是包封装(encapsulation)模式。网络服务在负载均衡器将待转发的网络包采用通用路由封装协议(GRE)封装到另外一个IP包中,使用后端服务器的地址作为目标地址。后端服务器接收到网络包,将IP和GRE层拆除,直接处理内部的IP包,就想直接从网络接口接收到的那样。网络负载均衡器和后端不在需要共存在同一个广播域中,只要中间有路由器连接即可,它们甚至可以再不同的大路上。

包封装是一个强有力的手段,可以为我哦们的网络设计和改进提供只够的灵活性。不幸的是,封装机制通常也会带来成本问题:包尺寸增加。包封装需要在一定程度的成本(IPv4+GRE,封装需要24字节),这经常导致数据包超过可用的传输单元(MTU)大小,而需要碎片重组(Fragmentation)。

数据到达数据中心内部以后,我们可以在内部采用更大的MTU来避免碎片重组的发生,但是这种做法需要网络设备支持。就像很多东西一样,负载均衡表面上听起来简单——尽早进行负载均衡,以分级多次进行——但是不管在前端负载均衡方面,还是在数据中心内部都存在现实的问题。

原文链接:https://landing.google.com/sre/book/chapters/load-balancing-frontend.html

译文:SRE Google运维解密 孙宇聪译

转载请注明:知识库 » 前端服务器的负载均衡 (SRE Google运维解密)

喜欢 (0)