LINUX平台的开源多层负载均衡

原标题《Multi-tier load-balancing with Linux》作者:Vincent Bernat 发布时间:2018年5月23日

要提供高可用性和可扩展性服务的常见解决方案是接入负载平衡层,以将用户的请求分配、转发到后端服务器。原文注1我们通常对负载均衡层有几个目标:

  • 可扩展性
    允许通过将流量推送到新配置的后端服务器来扩展服务。当性能成为瓶颈时,能够自动扩展。
  • 可用性
    它为服务提供高可用性。如果一台服务器不可用,则应迅速将流量转移至另一台服务器。负载均衡层本身也应具有高可用性。
  • 灵活性
    能够处理短连接和长连接。有足够的灵活性来提供后端的所有特性,一般希望从负载均衡器中得到TLS或HTTP路由等功能。
  • 可操作性
    接入负载均衡层后,如在后端推出一个新软件,添加或删除后端,或者扩展或缩小负载平衡层本身,这些预期的变化都应该让服务保持连续性。

作者注1:在本文中,“ 后端服务器 ”是负载平衡层背后的服务器。为避免混淆,我们不会使用“ 前端 ” 一词。

问题及其解决方案众所周知。参见本作者在本博客翻译的文章《 网络负载均衡和代理技术 》提供了对现有技术的概述。谷歌发布了《Maglev:快速可靠的软件网络负载均衡器 》,详细描述了他们的内部解决方案。基本上,使用商用服务器构建负载平衡解决方案包括组装三个组件:

  • ECMP路由
  • 无状态L4负载平衡
  • 有状态的L7负载平衡

    在本文中,我将描述并使用Linux和开源组件的多层解决方案。它是您构建生产环境负载均衡层的基础。
    由于原文注2中引入另一篇文章,所以在翻译期间,未保留原文注2
    译注:文章作者的负载架构模型视角为从上而下。负载均衡模型分别为0层:DNS负载,第1层:ECMP路由;第二层:L4负载层;第三层:L7负载层,参考如下图,今后将不再赘述。

LINUX平台的开源多层负载均衡

第3层:L7负载均衡

它的作用是通过将请求转发到健康的后端来提供高可用性,以及在它们之间均衡请求来提供可扩展性。L7在OSI 模型的最高层工作,它还可以提供其他服务,如TLS -termination,HTTP 路由,标头重写,未经身份验证的用户的速率限制等。作为有状态层,它可以利用复杂的负载平衡算法。作为与后端服务器的直连(译注:逻辑上的直连),它能够减轻维护成本并最大限度地减少日常变化中的影响。
LINUX平台的开源多层负载均衡
负载均衡解决方案第3层是一组L7负载均衡器,用于接收用户连接并将其转发到后端。L7:七层负载;www:表示后端服务

L7负责终止与客户端的TCP连接。这引入了负载均衡组件和后端之间的松耦合,具有以下优点:

  • 与后端创建连接并保持打开状态,以降低资源使用率和延迟,
  • 如果连接失败或中断,请求可以透明地重试
  • 客户端与后端使用不同的IP协议
  • 后端不必关心路径MTU发现,TCP拥塞控制算法,避免TIME - WAIT状态和各种其他底层(译注:应用层以下)细节。
    许多开源软件都适合这一层,并且有大量文章提到如何配置。你可以看看HAProxy,Envoy或Træfik。以下是HAProxy的配置示例:
# L7 load-balancer endpoint
frontend l7lb
  # Listen on both IPv4 and IPv6
  bind :80 v4v6
  # Redirect everything to a default backend
  default_backend servers
  # Healthchecking
  acl dead nbsrv(servers) lt 1
  acl disabled nbsrv(enabler) lt 1
  monitor-uri /healthcheck
  monitor fail if dead || disabled

# IPv6-only servers with HTTP healthchecking and remote agent checks
backend servers
  balance roundrobin
  option httpchk
  server web1 [2001:db8:1:0:2::1]:80 send-proxy check agent-check agent-port 5555
  server web2 [2001:db8:1:0:2::2]:80 send-proxy check agent-check agent-port 5555
  server web3 [2001:db8:1:0:2::3]:80 send-proxy check agent-check agent-port 5555
  server web4 [2001:db8:1:0:2::4]:80 send-proxy check agent-check agent-port 5555

# Fake backend: if the local agent check fails, we assume we are dead
backend enabler
  server enabler [::1]:0 agent-check agent-port 5555

此配置是本指南中最不完整的一部分。但是,它说明了可操作(译注:既服务连续)性的两个关键概念:

  • Web服务器的健康检查在HTTP -level(with check 和option httpchk)和使用辅助代理检查(with agent-check)完成。后者使服务器易于维护或编排逐步提交。在每个后端,需要一个进程侦听端口5555和报告服务的状态(UP在线, DOWN离线,MAINT维护)。一个简单的socat过程可以做到这一点:原文注3
    socat -ly \ 
    TCP6-LISTEN:5555,ipv6only = 0,reuseaddr,fork \ 
    OPEN:/ etc / lb / agent-check,rdonly

    当服务处于额定模式时,执行/etc/lb/agent-check检查.如果常规健康检查后端也正常,HAProxy将向此节点发送请求。当您需要维护时,请写入MAINT(维护)并等待现有连接关闭。使用READY取消此模式。

  • 负载均衡器本身应该为上层提供运行状况健康检查点(/healthcheck)。如果没有可用的后端服务器或通过代理检查来设置启用器后端,它将返回503错误。可以使用与常规后端相同的机制来表示该负载均衡器的不可用性。

    作者注 3:如果您觉得这个解决方案很脆弱,请自行设计自己的代理方案。它可以与键值存储协调以确定服务器的所需状态。可以将代理集中在一个位置,但是您可能会遇到鸡蛋共存问题以确保其可用性。

此外,该send-proxy指令使代理协议能够传输真实客户端的IP地址。此协议也适用于非HTTP 连接,并受各种服务器支持,包括nginx:

http {
  server {
    listen [::]:80 default ipv6only=off proxy_protocol;
    root /var/www;
    set_real_ip_from ::/0;
    real_ip_header proxy_protocol;
  }
}

也就是说,这个解决方案并不完整。我们刚刚将可用性和可伸缩性问题转移到其他地方。我们如何在负载均衡器之间对请求进行负载均衡?

第1层:ECMP路由

在大多数现代IP路由网络上,客户端和服务器之间存在冗余路径。对于每个数据包,路由器必须选择路径。当关联路径成本相等时,输入流原文注4在可用目的地之间进行负载均衡(链路负载)。此特性可用于均衡健康的负载均衡器之间的连接:
LINUX平台的开源多层负载均衡
ECMP路由用作第1层。流量分布在可用的L7负载均衡器中。路由是无状态和非对称的。后端服务器未表示。

作者注:4:流通常由源和目标IP以及L4协议确定。或者,也可以使用源端口和目标端口。路由器对这些信息进行哈希处理以选择目的地。对于Linux,您可以在“ Celebrating ECMP in Linux.”中找到有关此主题的更多信息。

ECMP路由对负载均衡的控制很少,但可以带来了两层(L7和ECMP)上水平扩展。实现这种解决方案的常用方法是使用BGP,一种路由协议来在网络设备之间交换路由。每个负载均衡器向其连接的路由器通告它所持有的服务IP地址。
假设您已经拥有支持BGP的路由器,ExaBGP是一种灵活的解决方案,可让负载均衡器发布其可用性。以下是其中一个负载均衡器的配置:

# Healthcheck for IPv6
process service-v6 {
  run python -m exabgp healthcheck -s --interval 10 --increase 0 --cmd "test -f /etc/lb/v6-ready -a ! -f /etc/lb/disable";
  encoder text;
}

template {
  # Template for IPv6 neighbors
  neighbor v6 {
    router-id 192.0.2.132;
    local-address 2001:db8::192.0.2.132;
    local-as 65000;
    peer-as 65000;
    hold-time 6;
    family {
      ipv6 unicast;
    }
    api services-v6 {
      processes [ service-v6 ];
    }
  }
}

# First router
neighbor 2001:db8::192.0.2.254 {
  inherit v6;
}

# Second router
neighbor 2001:db8::192.0.2.253 {
  inherit v6;
}

如果/etc/lb/v6-ready存在而/etc/lb/disable不存在,则将在两个路由器上通知接口配置的所有IP地址lo。如果其他负载均衡器使用类似的配置,则路由器将在它们之间分配输入流。一些外部进程应该通过检查负载平衡器的健康状况(例如,使用/healthcheck)来管理/etc/lb/v6-ready文件的存在。通过创建/etc/lb/disable文件。运维人员可以从轮询中删除负载均衡器。
要了解有关此部分的更多详细信息,请查看“ 使用ExaBGP实现高可用性 ” 。如果您位于云中,则此层通常由您的云提供商实施,使用任播IP地址或基本L4负载平衡器。
值得注意的是,一旦负载层或ECMP路由层计划性或意外的变化发生时,此解决方案不具有弹性。在添加或删除负载均衡器时,目标的可用路由数会发生变化。路由器使用的散列算法不一致,流量在可用的负载均衡器之间重新分配,破坏了现有的连接:
LINUX平台的开源多层负载均衡
发生变更时,ECMP路由不稳定。将额外的负载均衡器添加到池中,并将数据包路由到不同的负载均衡器,这些负载均衡器在其路由表中没有相应的记录
而且,每个路由器可以选择自己的路由。当一个路由器变得不可用时,另一个路由器将相同的流路由到不同路径:
LINUX平台的开源多层负载均衡
部分路由器变得不可用,其余路由器负载均衡其流量。其中一个路由到不同的负载均衡器,其路由表中没有相应的记录。
如果您认为这不是可接受的结果,特别是如果您需要处理文件下载,视频流或websocket等长连接,则需要额外的层。继续阅读!

第2层:L4负载均衡

第2层是IP路由的无状态和L7负载均衡的有状态之间的粘合剂。它通过L4负载平衡实现。粘合剂这个术语可能有点令人困惑:此层IP路由数据报(无 TCP终止),但L4负载均衡这个调度使用目标IP和端口来选择可用的L7负载均衡器。此层的目的是确保所有成员对传入数据包采取相同的调度策略。
有两种选择:

  • 状态L4负载均衡与成员之间的状态同步
  • 具有一致哈希的无状态L4负载平衡
    第一种选择会增加复杂性并限制可扩展性。我们不会用它。原文注5第二种选择在某些变化期间弹性较差,但可以使用本地状态的混合方法进行增强。

    作者注五:在Linux上,它可以通过使用Netfilter实现负载平衡并使用conntrackd来实现同步状态。IPVS仅提供主动/备份同步。

我们使用IPVS,一个在Linux内核中运行的高性能L4负载均衡器,使用Keepalived,一个IPVS的前端,带有一组健康检查程序来启动一个不健康的组件。IPVS配置为使用Maglev调度程序,这是Google提供的一致哈希算法。在它的系列中,这是一个很好的算法,因为它可以均衡地传播连接,最大限度地减少更改期间的中断,并且在构建查找表时非常快。最后,为了提高性能,我们让第3层--L7负载均衡器 - 直接向客户端应答,而不涉及第2层--L4负载均衡器。这被称为直接服务器返回(DSR)或直接路由( DR)。
LINUX平台的开源多层负载均衡
IPVS和一致哈希的L4负载均衡作为第1层和第2层之间的粘合剂。后端服务器已被省略。虚线表示返回数据包的路径。
通过这样的设置,我们希望来自输入流的数据包能够在两个第1层(ECMP路由)之间自由移动,同时坚持使用相同的L7负载均衡器。

配置

假设已经按照上一节中的描述配置了ExaBGP,那么让我们从Keepalived的配置开始:

virtual_server_group VS_GROUP_MH_IPv6 {
  2001:db8::198.51.100.1 80
}
virtual_server group VS_GROUP_MH_IPv6 {
  lvs_method TUN  # Tunnel mode for DSR
  lvs_sched mh    # Scheduler: Maglev
  sh-port         # Use port information for scheduling
  protocol TCP
  delay_loop 5
  alpha           # All servers are down on start
  omega           # Execute quorum_down on shutdown
  quorum_up   "/bin/touch /etc/lb/v6-ready"
  quorum_down "/bin/rm -f /etc/lb/v6-ready"

  # First L7 load-balancer
  real_server 2001:db8::192.0.2.132 80 {
    weight 1
    HTTP_GET {
      url {
        path /healthcheck
        status_code 200
      }
      connect_timeout 2
    }
  }

  # Many others...
}

quorum_up和quorum_down语句定义了当服务分别变为可用和不可用要执行的命令。该 /etc/lb/v6-ready文件用作ExaBGP的信号,将服务IP地址通告给相邻路由器。
此外,IPVS需要配置为继续路由来自另一个L4负载均衡器的数据包。它还应该继续从不可用的目的地路由数据包,以确保我们可以正确地转包到L7负载均衡器。

# Schedule non-SYN packets
sysctl -qw net.ipv4.vs.sloppy_tcp=1
# Do NOT reschedule a connection when destination
# doesn't exist anymore
sysctl -qw net.ipv4.vs.expire_nodest_conn=0
sysctl -qw net.ipv4.vs.expire_quiescent_template=0

Maglev 调度算法将在Linux 4.18中可用,这要归功于 Inju Song。对于较旧的内核,我准备了一个backport。原文注6使用源哈希作为调度算法会破坏设置的弹性。

作者注6:backport并不完全等同于其原始版本。请务必检查README文件以了解其中的差异。

简而言之,在Keepalived配置中,您应该:

  • 不使用 inhibit_on_failure
  • 使用 sh-port
  • 不使用 sh-fallback
    DSR使用隧道模式实现。此方法与路由数据中心和云环境兼容。使用 IPIP封装将请求隧道传输到调度的对等方。它增加了一小部分开销,可能导致 MTU 问题。如果可能,请确保使用更大的 MTU进行第二层和第三层之间的通信。原文注7否则,最好明确允许 IP数据包碎片:
    sysctl -qw net.ipv4.vs.pmtu_disc = 0

    作者注7:IPv4至少1520,IPv6 1540。

您还需要配置L7负载平衡器来处理封装流量:原文注8

作者注8:同样的,这种配置是不安全的。您需要确保只有L4负载均衡器才能发送IPIP流量。

# Setup IPIP tunnel to accept packets from any source
ip tunnel add tunlv6 mode ip6ip6 local  2001:db8 :: 192.0.2.132 
ip link set dev tunlv6 
ip addr add 2001:db8 :: 198.51.100.1/128 dev tunlv6

评估弹性

根据配置,第2层增加了此设置的弹性,原因有两个:

  1. 调度算法使用一致的哈希来选择其目的地。这种算法通过最小化移动到新目的地的数据包数量来减少预期或意外变化的负面影响。“ Consistent Hashing:Algorithmic Tradeoffs ”提供了有关此主题的更多详细信息。
  2. IPVS为已知流保留本地连接表。当更改仅影响第3层时,将根据连接表正确定引导现有流。

如果我们添加或删除L4负载均衡器,则现有流量不会受到影响,因为每个负载均衡器都会采取相同的策略,只要存在同一组L7负载均衡器:

LINUX平台的开源多层负载均衡
丢失的L4负载均衡器对现有流量没有影响。每个箭头都是流程的一个例子。圆点表示绑定到关联负载均衡器的流端点。如果他们已经转移到另一个负载均衡器,则连接将丢失。
如果我们添加L7负载均衡器,则现有流量也不会受到影响,因为只会安排新连接。对于现有连接,IPVS将查看其本地连接表并继续将数据包转发到原始目标。同样,如果我们删除L7负载均衡器,则只会影响在此负载均衡器处终止的现有流。其他现有连接将正确转发:
LINUX平台的开源多层负载均衡
丢失L7负载平衡器只会影响绑定到它的流量。
只有在两层(L4和L7)上同时变更才能产生明显的影响。例如,当同时添加L4负载均衡器和L7负载均衡器时,只有连接到L4负载均衡器且没有状态并安排到新负载均衡器的连接将被中断。由于采用了一致的散列算法,其他连接将保持与正确的L7负载均衡器绑定。在有计划的变更期间,可以通过先添加新的L4负载平衡器,等待几分钟,然后添加新的L7负载平衡器来最小化连接中断。
LINUX平台的开源多层负载均衡
L4负载均衡器和L7负载均衡器都恢复了。一致的哈希算法确保只有五分之一的现有连接将被转移到传入的L7负载均衡器。其中一些继续通过其原始的L4负载平衡器进行路由,从而减轻了影响。
此外,IPVS正确地将ICMP消息路由到与关联连接相同的L7负载均衡器。这确保了路径MTU 发现的显着效果,并且不需要智能解决方法。

第0层:DNS负载均衡

这其实是一个可选层。您可以将DNS负载均衡添加到负载架构中。如果您的环境跨越多个数据中心或多个云区域,或者您希望将大型负载均衡集群分解为较小的集群,则此功能非常有用。但它不能替代第1层,因为它具有不同的特点:没有负载均衡的特性(它不是基于流的),并且从故障中恢复很慢。
LINUX平台的开源多层负载均衡
跨两个数据中心的完整负载平衡解决方案。
gdnsd是一个具有集成健康检查的权威DNS服务端。它可以使用RFC  1035区域格式从主文件服务区域:

@ SOA ns1 ns1.example.org。1 7200 1800 259200 900 
@ NS ns1.example.com。
@ NS ns1.example.net。
@ MX 10 smtp 
@ 60 DYNA multifo!web

www 60 DYNA multifo!web
 smtp A 198.51.100.99

查询制定的插件后,特殊的RR类型DYNA将返回A和AAAA记录。在这里,multifo插件实现了监控地址的所有活动故障转移:

service_types => {
  web => {
    plugin => http_status
    url_path => /healthcheck
    down_thresh => 5
    interval => 5
  }
  ext => {
    plugin => extfile
    file => /etc/lb/ext
    def_down => false
  }
}

plugins => {
  multifo => {
    web => {
      service_types => [ ext, web ]
      addrs_v4 => [ 198.51.100.1, 198.51.100.2 ]
      addrs_v6 => [ 2001:db8::198.51.100.1, 2001:db8::198.51.100.2 ]
    }
  }
}

在正常状态下,A请求将返回两个DNS解析198.51.100.1和 198.51.100.2。运行状况检查失败将相应地更新返回的值。还可以通过修改/etc/lb/ext文件删除记录 。例如,使用以下内容,198.51.100.2将不再进行发布:

198.51.100.1 => UP 
198.51.100.2 => DOWN 
2001:db8 :: c633:6401 => UP 
2001:db8 :: c633:6402 => UP

您可以在GitHub存储库中找到所有配置文件和每个层的设置 。如果要以较小的规模复制此设置,可以使用localnode或网络命名空间来整合第1层和第2层 。即使您不需要其花哨的负载均衡服务,您也应该保留最后一层(第3层):当后端服务器不断变化时,L7负载平衡器带来稳定性,这转化为弹性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章