最近我注意到,关于当代网络负载均衡和代理的入门教材非常匮乏。我心想:为什么会这样?负载均衡是构建可靠的分布式系统所需要的核心概念之一。应当可以获取到一些有用的信息的吧?我在网上搜了搜,却没有得到多少有用的信息。维基百科上关于负载均衡和代理服务器的文章包含了一些概念,但是对于这个课题没有一个完美的解答,更别提与现代微服务架构的联系了。Google上关于负载均衡的搜索结果主要是一些供应商的网页,显示的主要是一些流行术语,细节方面的内容很少。
在本文中,我将尝试改变这种信息匮乏的情况,详细介绍现代网络负载均衡和代理。坦率地讲,这是一个非常大的话题,围绕它写一本书都不为过。为了在一定程度上控制本篇文章的篇幅,我试着化繁为简,对这一系列复杂的话题进行了提炼;根据读者的兴趣和反馈,我会考虑在后续帖子中详细介绍一些单独的话题。
介绍了一点我写作本文的背景之后,让我们开始正文吧!
什么是网络负载均衡与代理?
维基百科将负载均衡定义为:
在计算机领域,负载均衡优化了多个计算资源间的工作量分布。这些计算资源包括计算机、计算机集群、网络连接、CPU或者硬盘等。负载均衡的目的是优化资源利用、最大化吞吐量、最小化响应时间,并且避免任何单个资源过载。通过冗余机制,使用多个负载均衡组件而不是单一组件可以增加可靠性和可用性。负载均衡通常涉及专门的软件或硬件,例如多层交换机或者域名系统服务器进程。
上述定义适用于所有计算机领域,而不仅仅是网络。操作系统使用负载均衡来调度物理处理器间的任务,容器编排工具(例如Kubernetes)使用负载均衡来调度计算机集群内的任务,网络负载均衡器使用负载均衡来调度可用后端间的网络任务。本文剩余部分将只涵盖网络负载均衡相关的内容。
<small style="margin: 0px; border: 0px; padding: 0px;">图1: 网络负载均衡概览</small>
图1高度概括了网络负载均衡。一些客户端从一些后端请求资源。负载均衡器位于客户端和后端之间,执行一些非常重要的任务:
- 服务发现:系统中有哪些后端可用?它们的地址是什么?(例如,负载均衡器如何和它们通信?)
- 状态检查:哪些后端现在可以接受请求?
- 负载均衡:应该使用什么算法来均衡后端间的请求?
在分布式系统中合理使用负载均衡可以带来以下好处:
- 抽象命名:客户端可以通过一个预定义的机制寻址到负载均衡器,然后将名称解析委托给负载均衡器,而不是每一个客户端都需要知道每一个后端的(有关服务发现的)信息。预定义机制包括内置库和众所周知的DNS/IP/端口地址,这些稍后都会详细讨论。
- 容错性:通过状态检查和各种算法技术,负载均衡器可以有效绕开挂掉的或者过载的后端。这意味着,运维人员通常可以在他们空闲时修复挂掉的后端而不是必须将这作为一个紧急事件来处理。
- 成本和性能方面的好处:分布式系统网络复杂多变。系统可能会包含多个网络区域和行政区。在一个区域内,网络可能相对过载。而不同区域间,网络通常又会有空余。(这里,网络过载或空余是指通过网卡消耗的带宽数量占不同路由器间的可用带宽的百分比)。智能负载均衡可以尽可能多地维持一个区域内的请求数量,这提高了性能(延迟降低)并且减少了整个系统的成本(不同区域间需要更少的宽带和光纤)。
负载均衡vs代理
谈到网络负载均衡器的时候,“负载均衡器”和“代理”这两个术语通常在行业内是差不多相同的意思。本文通常也会将这两个术语作为相同意思的词。(严格来说,并不是所有的代理都是负载均衡器,但是大部分代理将负载均衡作为首要功能)。
有人也许会争论说,当用嵌入式客户端库来完成负载均衡时,负载均衡器并不是一个代理。然而,我认为这个话题本来就十分容易令人混淆,而这种区分会为这个话题增加更多不必要的复杂性。下面会详细介绍负载均衡器的各种拓扑结构,但是本文仍将嵌入式负载均衡器拓扑结构作为一种特殊的代理;应用程序通过嵌入式库进行代理,这个库提供了作为一个独立于应用进程外的负载均衡器的所有相同的抽象。
L4(连接/session)负载均衡
现在讨论行业内的负载均衡方案时,通常会归结为两类:L4和L7.这两种分类指的是OSI模型的第4层和第7层。当我讲到L7负载均衡的时候,会明显发现我们使用这些术语来分类是错误的。OSI模型是负载均衡方案的复杂性的一种非常粗劣的抽象,因为负载均衡方案中虽然包含传统的第4层协议,例如TCP和UDP,但是通常最后还会或多或少包含一些其它OSI层级的协议。例如,如果一个L4 TCP负载均衡器也支持TLS终端,那么它现在是不是一个L7负载均衡器呢?
<small style="margin: 0px; border: 0px; padding: 0px;">图2:TCP L4终端负载均衡</small>
图2展示了一个传统的L4 TCP负载均衡器。在这个例子中,客户端与负载均衡器建立TCP连接。负载均衡器终止连接(例如,直接响应SYN),选择一个后端,与这个后端建立一个新的TCP连接(例如,发送一个新的SYN)。示意图中的细节并不重要,会在后面关于L4负载均衡的章节详细讨论。
本章节的要点是,L4负载均衡器通常只操作L4层的TCP/UDP连接或session。因此,负载均衡器只是来回地搬运bytes,并确保来自同一个session的bytes发送到相同的后端。L4负载均衡器不清楚这些搬运的bytes组成的应用的任何细节。这些bytes可以是HTTP、Redis、MongoDB或者其他任何应用层协议。
L7(应用层)负载均衡
L4负载均衡很简单并且仍被广泛使用。L4负载均衡的哪些不足导致需要使用L7(应用层)负载均衡?可以参考下列L4具体例子:
- 两个gRPC/HTTP2客户端通过一个L4负载均衡器来和后端通信。
- L4负载均衡器针对每个收到的TCP连接都会建立一个单独外出的TCP连接,导致会有2个收到的连接和2个外出的连接。
- 然而,客户端A通过它建立的连接每分钟发送1个请求(1 RPM),而客户端B通过它建立的连接每秒发送50个请求(50 RPS)。
在上述情景中,处理客户端A请求的后端比处理客户端B请求的后端处理的请求少了大约3000倍!这是一个非常重大的问题,也是违背负载均衡目标的首要问题。另外要注意到,这个问题在任何多路复用的长连接的协议中都会发生。(多路复用的意思是,通过单个L4连接并行发送应用请求。长连接的意思是,即使没有活跃请求也不关闭连接。)所有现代网络协议为了效率都会采用多路复用和长连接(通常建立连接的成本非常大,特别是连接使用TLS加密的时候),因此,随着时间的推移,L4负载均衡器调节负载失败的现象变得越来越明显。这个问题可以被L7负载均衡器修复。
<small style="margin: 0px; border: 0px; padding: 0px;">图3:HTTP/2 L7终端负载均衡</small>
图3展示了一个L7 HTTP/2负载均衡器。在这个例子中,客户端与负载均衡器建立了单个HTTP/2 TCP连接。负载均衡器然后建立了两个后端连接。当客户端向负载均衡器发送了两个HTTP/2信息流时,信息流1被送到后端1,而信息流2被送到后端2。因此,即使多路复用的客户端会发送各种不同的请求,后端的负载也会被很高效地均衡分配。这就是为什么L7负载均衡对于现代网络协议是这么的重要。(L7负载均衡能够监测应用流量,由此产生了非常多额外的好处,这些会在后面讲到)。
L7负载均衡和OSI模型
正如我在上面关于L4负载均衡的章节讲到的,使用OSI模型来描述负载均衡的特性是有问题的。至少如OSI模型描述的那样,L7自身就包含了其它层的负载均衡。例如,对于HTTP流量需要考虑以下子层:
- 可选的传输层安全(Transport Layer Security,TLS)。网络领域人士对于TLS归属于OSI模型哪一层存在争议。在本文的讨论中,我们认为TLS属于L7。
- HTTP物理层协议(HTTP/1或HTTP/2)。
- HTTP逻辑层协议(协议头、数据体、协议尾)。
- 通信协议(gRPC、REST等等)。
一个复杂的L7负载均衡器会提供与上述每一个子层相关的功能。而另外一个L7负载均衡器可能只会包含L7范畴内的一小部分功能。总之,从功能比较的视角来看,L7负载均衡器比L4复杂得多。(当然,本章节只涉及了HTTP;Redis、Kafka、MongoDB等都是受益于L7负载均衡的L7应用层协议)。
负载均衡器的功能
本节中,我将简要总结负载均衡器提供的高级功能。不是所有的负载均衡器都提供所有这些功能。
服务发现
服务发现是负载均衡器判断可用后端集合的过程。服务发现的方法各不相同,包括以下一些例子:
健康检查
健康检查是负载均衡器判断后端是否服务可用的过程。健康检查通常分为两类:
- 主动的:负载均衡器以固定时间间隔向后端发送ping请求(例如,每个健康检查的端点发送一个HTTP请求),并以此来判断健康状态。
- 被动的:负载均衡器从主要的数据流中检测健康状态。例如,如果在一段时间内有三次连接错误,L4负载均衡器可能会认为这个后端是不健康的。如果在一段时间内有三次HTTP 503响应代码,L7负载均衡器可能会认为该后端是不健康的。
负载均衡
是的,负载均衡器要真的能够均衡负载!如果有一堆状态良好的后端,如何选中一个后端来服务一个连接或请求?负载均衡算法是一个热门的研究领域,从比较简化的算法,例如随机选择法和循环淘汰法,到复杂一些的考虑到延迟和后端负载变化的算法。最流行的负载均衡算法之一是power of 2 request load balancing,具有良好的性能和较低的复杂度。(这种算法的机制是:当请求到达负载均衡器时,负载均衡器会先从系统中随机选择2个后端,然后从这2个后端中选择负载最小的那个来响应请求。)
粘性session
在特定应用中,同一个session的请求到达同一个后端是非常重要的。这可能与缓存、临时创建的复杂状态等有关。session的定义很多,可能包括HTTP cookies、客户端连接属性或者其它一些属性。许多L7负载均衡器支持粘性session。另外,我要提醒你注意的是,session的粘性本质上是脆弱的(后端持有的session可能会消亡),因此建议在设计依赖粘性session的系统时保持谨慎。
TLS终端
关于TLS和它在服务和保障服务间通信中的角色的话题值得深入探讨。许多L7负载均衡器承担了许多TLS处理流程,包括终止请求、证书验证和绑定、使用SNI的证书服务等。
观测性
正如我常说的:“观测性、观测性、观测性。”网络本质上是不可靠的,负载均衡器通常有责任导出统计数据、跟踪数据和日志,这些信息可以帮助运维人员发现问题从而修复这些问题。负载均衡器在可观测性输出结果上有很大不同。最高级的负载均衡器提供丰富的输出结果,包括数值统计、分布式跟踪信息和自定义日志。我要指出的是,为了增强功能而增加的观测性不是没有代价的;负载均衡器不得不做一些额外的工作来产出观测结果。然而,这些数据带来的好处远远超过了相对较小的性能影响。
安全性和DoS防御
特别是在边缘部署拓扑结构中(见下文),负载均衡器通常实现各种安全功能,包括速率限制、认证和DoS防御(例如,IP地址标记、识别、过滤等)。
配置控制层
负载均衡需要进行配置。这对于大型部署来说,是一种非常重要的保证。通常,配置负载均衡器的系统被称为“控制层”,而且它有各种各样的实现。关于这个话题的更多信息,请查看我的关于《service mesh data plane vs. control plane》的博客。
更多内容
本节初步涉猎了负载均衡器提供的各种功能。其它的讨论可以查看下文中关于L7负载均衡器的章节。
负载均衡器拓扑结构类型
既然我已经高度概括了什么是负载均衡器、L4和L7负载均衡器的区别以及负载均衡器的功能总结,下面我将继续介绍部署了负载均衡器的各种分布式系统的拓扑结构。(下面的每种拓扑结构都适用于L4和L7负载均衡器。)
中间代理
<small style="margin: 0px; border: 0px; padding: 0px;">图4:中间代理型负载均衡拓扑结构</small>
图4所示的中间代理拓扑结构对于大部分读者来说可能是最熟悉的使用负载均衡的方式。这类负载均衡包括Cisco、Juniper、F5出产的硬件设备;云端软件解决方案,例如Amazon的ALB和NLB以及Google的云端负载均衡器(Cloud Load Balancer);自托管的纯软件解决方案,例如HAProxy、NGINX和Envoy。中间代理解决方案的优点是简单易用。通常,用户通过DNS连接到负载均衡器,然后就不需要再关心其它任何事情了。中间代理解决方案的缺点是,代理(即使使用集群)是一个故障单点,同时也是一个扩展瓶颈。中间代理通常也是一个黑盒子,运维人员难以维护。如果发生了故障,那么这是一个在客户端能够观察到的问题吗?是发生在物理网络吗?还是发生在中间代理?或者是发生在后端?这很难说的清楚。
边缘代理
<small style="margin: 0px; border: 0px; padding: 0px;">图5:边缘代理负载均衡拓扑结构</small>
图5所示的边缘代理拓扑结构只是中间代理拓扑结构的一个变种,其中负载均衡器通过Internet访问。在这个场景中,负载均衡器通常必须提供额外的“API网关”功能,例如TLS终端、速率限制、身份认证和复杂的流量路由。边缘代理的优点和缺点与中间代理一样。需要注意的是,通常在大型的面向互联网的分布式系统中部署专用的边缘代理是不可避免的。客户端通常会使用任意网络库经过DNS访问系统,但这些网络库不受客户端的控制(这使得嵌入式客户端库负载均衡器或下面章节会提到的sidecar代理拓扑结构负载均衡器直接在客户端上运行变得不切实际)。另外,为了安全原因设置一个单独的安全网关是值得的,所有面向互联网的流量都必须先经过这个安全网关才能进入系统。
嵌入式的客户端库
<small style="margin: 0px; border: 0px; padding: 0px;">图6:通过嵌入式客户端库的负载均衡</small>
为了避免中间代理拓扑结构天生的单点故障和扩展问题,更复杂的基础设施朝着将负载均衡器通过一个库直接嵌入到服务中的方向发展,正如图6所示的那样。这些库支持的功能各种各样,其中最知名的功能丰富的库有Finagle、Eureka/Ribbon/Hystrix和gRPC(基于一个Google内部称作Stubby的系统)。基于库的解决方案的主要优点是,它完全将负载均衡器的所有功能分布到每一个客户端,因此消除了前面提到的单点故障和扩展问题。基于库的解决方案的主要缺点是所用的库必须用一个机构使用的每一种语言实现。分布式架构变得越来越“通晓各种语言”(使用多种语言编写的)。在这种环境下,用多种不同的语言重新实现一个非常复杂的网络库的代价会非常大。最后,跨一个大型服务架构来升级库会非常痛苦,很可能会造成许多不同版本的库同时在生产环境中运行,从而增加运维时了解网络情况的负担。
尽管如此,对于那些能够限制使用的编程语言的种类并且克服升级库的痛苦的公司来说,上面提到的各种库还是非常成功的。
Sidecar代理
<small style="margin: 0px; border: 0px; padding: 0px;">图7:通过sidecar代理的负载均衡</small>
图7所示的sidecar代理拓扑结构是客户端嵌入式库负载均衡拓扑结构的一个变种。最近几年,这种拓扑结构被称作“服务网格”并且非常流行。sidecar代理背后的思路是,以切换到另外一个进程造成轻微延迟的代价,获得嵌入式库方案的所有好处,而且无需局限于任何编程语言。截至本文,最流行的sidecar代理负载均衡器有Envoy、NGINX、HAProxy和Linkerd。如果想要更详细地了解sidecar代理方案的优化方法,请查看我博客中介绍Envoy的帖子和关于《service mesh data plane vs. control plane》(服务网格数据层vs控制层)的帖子。
各种负载均衡器拓扑结构的优缺点
- 中间代理拓扑结构是最容易使用的负载均衡拓扑结构。它的弊端是单点故障、扩展限制和黑箱操作端。
- 边缘代理拓扑结构和中间代理类似,但通常还免不了会用到。
- 嵌入式客户端库拓扑结构提供了最好的性能和可扩展性,但是需要用各种语言来实现这个库而且需要跨所有服务来升级这个库。
- sidecar代理拓扑结构执行性能虽然不如嵌入式客户端库拓扑结构,但是不会受到其中的任何限制。
总之,我认为sidecar代理拓扑结构(服务网格)正逐渐取代所有其它用来进行服务间通信的拓扑结构。在流量进入服务网格之前需要使用边缘代理拓扑结构
查看英文原文:Introduction to modern network load balancing and proxying