Maglev
是Google内部数据中心使用的网络负载均衡系统,与基于硬件的负载均衡器相比,Maglev具有高的可伸缩性和易用性,支持快速迭代,易于升级。 实际上,Maglev是运行在Google商业服务器集群上的一个分布式系统。Maglev负责
- 流量均匀分配,
- 保持连接的一致性(同一连接的数据包会被转发到相同服务器端点),
- 具有对小数据包的高吞吐能力。
等价多路径(
ECMP
)策略满足第一个要求。 后两个要求需要
一致性哈希
和
连接跟踪
。
概览
每个Google服务都有一个或多个虚拟IP地址(VIP)。 VIP与物理IP的不同之处在于,它没有被分配给特定的网络接口,而是映射到Maglev后端的多个服务器端点。 Maglev将每个VIP与一组服务端点相关联,并通过BGP将其通告给路由器; 接下来路由器会把VIP当做谷歌的骨干网络节点。 VIP会在互联网上公开从而在全球范围内都可以访问。 Maglev同时支持IPv4和IPv6,二者在下文的所有讨论中等价…
Google的DNS服务器会给每个请求连接的用户分配一个附近的位置(location),会同时考虑用户的地理位置和location的当前负载。 DNS返回VIP后,用户将尝试与该VIP建立连接。 Google数据中心内部的路由器接收了VIP数据包后,通过ECMP将其转发到集群中的一台Maglev上(所有Maglev在其通告的VIP下具有相同的权重)。 Maglev机器负责选一个后端服务器端点,并使用GRE(通用路由封装,Generic Routing Encapsulation)封装数据包——数据包在后端服务器中解压缩并被处理。
完成处理后,响应将被放入IP数据包中,源地址为VIP,目标地址为用户的IP。 我们使用直接服务器返回(DSR, direct Server Return)将响应直接发送到路由器,这样Maglev不需要处理返回的数据包(通常比请求数据包更大)。
每台Maglev机器都包含一个控制器(controller)和一个转发器(forwarder)。 控制器负责监视转发器的运行状况,并决定是否通过BGP通告或撤销所有的VIP。 转发器负责完成具体的工作。
Maglev机器接收的所有VIP包都由forwarder处理。 在转发器中,每个VIP配置有一个或多个后端池(backend pool)。 除非另有说明,Maglev的后端指的是服务器端点。 后端池可以包含服务器的物理IP; 也可以递归地包含其他后端池,从而提高某些经常使用的后端池的复用性。
转发器的配置管理器(config manager)会从配置对象中获取要向外通告的VIP ——配置对象从文件中读取或通过RPC接收。 所有配置更新操作都是原子操作。
数据包转发(Packet Forwarding)
转发器直接从NIC(网卡)接收数据包,重写GRE/IP headers 后送回NIC。完全绕过了 Linux内核。
来自NIC的分组首先由引导(steering module)模块处理,使用散列函数将它们分发给多个分组接收队列之一。 每个队列由专用线程(数据包重写线程)提供服务,绑定线程到某一CPU内核上。 这些数据包重写线程再次计算数据包的哈希值(为避免跨线程同步)并在连接跟踪表(connection tracking table)中查找对应条目。 如果找到匹配的可用的后端,就重用之前的结果。 否则调用一致性散列模块(consistent hashing module)来为包选择新的后端,并向连接跟踪表添加一条条目。 为避免线程竞争,每个线程都有自己的连接跟踪表。 选择出后端以后,将使用适当的GRE / IP头封装数据包,并发送到线程的传输队列。 多路复用模块(muxing module )轮询所有传输队列并将数据包传递回NIC。
在两次散列计算中,Maglev使用5元组散列:源IP,源端口,目标IP,目标端口,IP协议号。
由于两个原因,引导模块会使用5元组散列而不是循环调度(round-robin)。 首先,它有助于降低由于不同线程的处理速度不一致而导致的同一连接数据包重新排序的可能性。 其次,通过连接跟踪,转发器只需要为每个连接执行一次后端选择,从而节省时钟周期,并消除
由于后端运行状况更新引起的竞态条件所导致的后端选择结果不同
的可能性。 在某一接收队列填满的极少数情况下,引导模块会降级到循环调度,将数据包转发到其他可用队列。 这种回退机制在处理具有大量相同5元组的数据包时特别有用。
为了在Maglev和NIC之间进行高效通信,Maglev会在启动时预先分配一个NIC和转发器之间共享的数据包池(packet pool)。 引导模块和多路复用模块各自维护一个由
指向数据包池中数据包的指针
组成的循环队列(简称环)。
引导和多路复用模块都保持三个指向环的指针。 在接收侧,NIC将新接收的分组放在
接收指针
(received pointer)处并向前移动。 引导模块将接收的分组分发给分组线程并使
已处理指针
(processed pointer)前进。 它还保留来自数据包池的未使用数据包,将它们放入环中(这是唯一更新环内容的方式)并使
预留指针
前进。 如箭头所示,三个指针互相追逐。 类似地,在发送侧,NIC发送由
发送指针
指向的分组并使其前进。 多路复用模块将由数据包线程重写的数据包放入环中并使
就绪指针
前进。 它还将已经由NIC发送的数据包返回到数据包池并使
循环指针
前进(看起来,两个环的容量和为pool的容量,循环指针好像也与预留指针有配合,具体没有搞明白)。 请注意,转发器不会将数据包复制到任何位置。
通常,数据包线程需要大约350ns来处理Google标准服务器上的每一个数据包。 如果数据包池大小为3000,转发器每秒处理10M数据包,则处理所有缓冲的数据包大约需要300μs。
后端选择的一致性哈希算法
对于面向连接的协议(如TCP),我们需要将同一连接的所有数据包发送到同一后端。 在正常情况下,连接跟踪表负责这一点——但是我们还需要在表没有对应条目的情况下分配后端。并且由于表不是跨线程共享的,更不用说跨Maglev实例了——例如Maglev群集的滚动发布可以导致连接在Maglev实例之间切换。
Maglev提供一致的散列方法,以确保在这种情况下数据包可靠传输。
事实上,Maglev的目标是“最少的中断”,这不是绝对的。 Maglev可以容忍少量中断。 总而言之,Maglev计划旨在:
- 公平负载均衡 – 每个后端应该接收几乎相同数量的连接,并且
- 最少的中断 – 当后端集合变更时,连接尽可能找到原来的后端
该方法的核心是一张查找表…
Maglev的基本思想是为每个后端分配一个所有查找表位置的首选项列表。 然后所有后端轮流填充仍然为空的他们最喜欢的表位置,直到查找表完全填满。因此,Maglev散列给每个后端提供了几乎相等的查找表份额。 可以通过改变后端填充轮次来实现异构后端权重…
设N是VIP后端池的大小。 一个表由M个条目构成,其中M是素数,M> 100 * N,这将确保分配给后端的散列空间最多有1%的差异。
每个后端节点都有一个0…M-1的随机排列。 Google依赖于每个后端服务器都具有唯一名称的事实构建这种随机排列。 使用两个不同的散列函数计算偏移量和步长:
offset = hash1(name) mod M
skip = hash2(name) mod (M-1) + 1
假设M是7,并且对于给定的后端i, offset = 3并且skip = 4 。我们将据此为后端生成随机排列,如下所示:
permutation[i][0] = 3
permutation[i][1] = 0 // (3 + 1
4) mod 7
permutation[i][2] = 4 // (3 + 2
4) mod 7
permutation[i][3] = 1 // (3 + 3
4) mod 7
permutation[i][4] = 5 //(3 + 4
4) mod 7
permutation[i][5] = 2 // (3 + 5
4) mod 7
permutation[i][6] = 6 // (3 + 6
4) mod 7
一旦我们对所有后端i完成了排列计算,我们就会通过允许每个后端轮流选择一个位置来填写查找表M,直到所有的位置都被填满(有点像领队为学校体育团队挑选球员)。 当后端选择一个位置时,它参照其随机排列,并从该列表中选择M中的第一个尚未填充的位置。
这些复杂计算的原因是维持添加或删除后端节点时映射的相对稳定性。 考虑如果在我们之前的计算中,从查找表中删除B1,然后我们重建查找表会发生什么:
我们建立了maglev,通过ECMP进行扩展,并在每台机器上可靠地以10Gbps线速运行,以便在快速增加的服务需求下实现经济高效的性能。 我们通过连接跟踪和maglev散列的组合将连接一致地映射到相同的后端。 这个软件系统的大规模运行让我们有效地运营我们的网站很多年,快速响应规模不断增长和新的功能需求。