原文作者:mattklein123
我最近注意到,介绍现代网络的负载均衡及代理方面的有教育意义的介绍资料存在着一定的欠缺。我就在想:怎么会这样呢?作为构建可靠的分布式系统所需的核心要素,负载均衡肯定有很多高质量的资料才对吧?我搜索了一下,却发现这样的资料真的很少见。维基百科上介绍负载均衡和代理服务器的文章对一些概念的阐述,尤其是在涉及到现代微服务架构时,都并不能流畅地介绍这个主题。用谷歌搜索负载均衡更是只找到一些服务商的页面,而它们充斥着各种时髦词语,罕见具体细节。
在本文里面,我会尝试把现在的网络负载均衡以及代理服务器方面的技术慢慢地讲解一遍,以此来填补这方面的资料的欠缺。这确实是一个内容很宽泛的主题,甚至能写本书来说。为了精简这篇文章的篇幅,我会试着去将各种各样的复杂概念用一个简述来讲解。如果反馈的效果不错,我也会考虑一下在后续的文章里面继续详细地介绍这一主题。
那么这就是对我撰写这篇文章的背景的一个简述 - 下面开始正片吧!
维基百科对负载均衡的定义如下:
In computing, load balancing improves the distribution of workloads across multiple computing resources, such as computers, a computer cluster, network links, central processing units, or disk drives. Load balancing aims to optimize resource use, maximize throughput, minimize response time, and avoid overload of any single resource. Using multiple components with load balancing instead of a single component may increase reliability and availability through redundancy. Load balancing usually involves dedicated software or hardware, such as a multilayer switch or a Domain Name System server process. 在计算领域里,负载均衡改善了在多个计算资源(比如计算机、计算机集群、网络链路、CPU,还有磁盘驱动器)之间分配工作负载的方式。负载均衡的目标在于最优化资源利用,最大化吞吐量,最小化响应时间,并规避任一计算资源的过载。使用处于负载均衡下的多重组件能取得比使用单一组件更高的可靠性,且通过冗余还能取得更高的可用性。负载均衡通常会用一些专门的软件或硬件来实现,比如多层交换机,或者一个域名系统服务器的进程。
上述定义引入了计算领域的全部概念,而并没有局限于计算机网络。操作系统可以基于负载均衡来在若干物理的处理器之间调度作业,容器编排器(如 Kubernetes)会基于负载均衡在计算机集群里面调度作业,网络负载均衡器也会基于负载均衡来在若干可用的服务端之间调度作业。本文只讨论网络上的负载均衡。
图 1 便是对网络负载均衡的一个高度概括。有些客户端会向一些服务端请求资源,而处于它们之间的负载均衡器就会执行这些关键任务:
若是分布式系统里面的负载均衡用得好,那就会带来这些好处:
其实在业界里面,在谈起网络负载均衡的时候,负载均衡器和代理服务器这两个词的用法是大致等同的(严格地说,并非所有代理服务器都是负载均衡器,但是多数代理服务器的主要功能里面都有负载均衡)。
可能有人会这样反驳,有些负载均衡流程是通过一个在客户端内置的类库来完成的,这种负载均衡器就不能说是代理服务器。不过我认为区分这种特例会给对这一本来含混的话题带来不必要的复杂性。我们会在后面讨论一下这种类型的负载均衡器,但本文只会把这类内置的负载均衡拓扑结构看作代理服务器的一个特例 - 从整体抽象的视角来看,一个应用进程使用内置的能起到代理作用的类库,跟使用外部的负载均衡器是一样的。
现在业界的负载均衡方案可以分为两种:L4 和 L7,分别所指的是 OSI 模型里面的第 4 层和第 7 层。不过我觉得使用这两个分类其实有点不对,其原因在介绍 L7 负载均衡的时候会是显然的。OSI 模型其实并不能很好地描述那些既涉及到第 4 层协议(如 TCP、UDP)又使用了其他层的协议的复杂负载均衡方案。比如说,一个 L4 的 TCP 负载均衡在给 TLS 终端(TLS termination)提供了支持的情况下,是否又应该算是 L7 负载均衡器呢?
图 2 展示了一个传统的 L4 TCP 负载均衡器。在这种情况下,客户端会向负载均衡器建立 TCP 连接,然后负载均衡器会截掉这个连接(也就是直接响应 SYN 报文),选择一个服务端,然后跟相应服务端建立新的 TCP 连接(即发送一个新的 SYN 报文)。 这张图的一些的细节现在并不重要,我们会在后续专门讲 L4 负载均衡的部分再详细讨论。
L4 负载均衡器通常只会操纵第 4 层里面的 TCP/ UDP 的连接或会话。粗略地说,这种负载均衡器所做的只是把一些字节转到另外一个地方,并确保同一会话的字节数据会转到同一个服务端。L4 负载均衡器对它所操纵的字节的应用细节是一无所知的。这里所说的字节数据可以是 HTTP 的、或者是 Redis、MongoDB,甚至是其他应用层协议的。
L4 负载均衡的技术简单,目前仍有广泛用途。那么 L4 负载均衡有什么局限性使得我们必须关注 L7(应用)负载均衡呢?下面就介绍一个 L4 负载均衡的使用场景:
在这个情景里面,被选定处理客户端 A 的请求的服务端就所承担的负载就会是客户端 B 所对应的服务端的 3000 倍!这个问题很大,从根本上破坏了负载均衡的初衷。不得不提,这一问题会经常出现在一些引入了复用(Multiplexing)和持久化连接(Kept-alive)的协议里面(复用指的是并行地向往一个第 4 层的连接发送多个应用请求,持久化连接指的就是在没有正进行的请求时也不关闭连接)。出于效率上的考虑,所有现代的网络协议都引入了复用和持久化连接的特性(建立连接的性能开销通常很大,尤其是在连接要被 TLS 加密的时候),因此 L4 负载均衡器的负载不均衡问题也就会变得越来越显著。L7 负载均衡器则可以解决这个问题。
图 3 展现了一个 L7 HTTP/2 的负载均衡器。此时,客户端会向负载均衡器建立一个 HTTP/2 TCP 连接,然后负载均衡器会再跟两个服务端建立连接。在客户端向负载均衡器发送两个 HTTP/2 数据流的时候,1 号流会被发送到 1 号服务端,2 号流会被发到 2 号服务端。这样,即便一个复用客户端发来了大量不同的请求,这些请求带来的负载也最终会高效地分发给各个服务端。这也是 L7 负载均衡对现代网络协议的一大意义(L7 负载均衡器还可以检查应用层流量的具体内容。这会额外带来不少好处,不过我们之后再说)。
正如我在介绍 L4 负载均衡时所说的,用 OSI 模型来描述负载均衡会有很多问题。这原因就在于 OSI 模型的第 7 层本身在负载均衡的视角来看就又会分为好几个抽象层。比如对 HTTP 协议就又可以分为以下子层:
一个成熟的 L7 负载均衡器会为每一个子层提供对应的功能。另外一些 L7 负载均衡器则可能只会有一小部分能算在第 7 层的功能。总而言之,L7 负载均衡器比 L4 的会具有更多更复杂的功能(这里只提到了 HTTP 协议,实际上 L7 负载均衡还给很多其他应用层的协议带来了好处,比如 Redis、Kafka,还有 MongoDB 等)。
这一部分是对负载均衡器所提供的高层功能的一个总结。不过并不是所有负载均衡器都会提供全部功能。
服务发现指的是负载均衡器确认一个可用的服务端集合的流程。实现的具体方法有很多,比如:
健康检查指的是负载均衡器确认某个服务端是否能正常接受传入流量的流程。这一流程通常又分为两种执行方式:
/healthcheck
端点发一个 HTTP 请求),以此确认服务端的可用状态。负载均衡器必须脚踏实地地实现负载均衡,没毛病!不过,给定一个可用的服务端集合,又如何挑选一个服务端来接受连接或者请求呢?负载均衡的算法是个活跃的研究领域,具体例子有简单的随机选择或轮转调度,乃至十分复杂的,会考量时延和服务端当前负载的算法。现在最流行的算法是 power of 2 least request load balancing,兼顾了性能和简洁方面的需要。
在一些应用里面,让同一会话的请求能到达相同的服务端是很重要的。这会影响到对缓存以及复杂的临时状态的维护。对会话的定义又可能会写在 HTTP cookies、客户端连接属性或者别的参数上。很多 L7 负载均衡器都对会话的粘附提供了一定的支持。不过,我想指出会话粘附其实是很难维持的(主管特定会话的服务器会随时挂掉),因此在设计面向会话的系统时应该多加谨慎。
TLS,以及 TLS 在边缘服务,还有保护服务间通信中的角色这些话题,都值得再写一篇文章来探讨。很多 L7 负载均衡器都为 TLS 做了很多处理,包括实现连接截止、整数校验及绑定,使用 SNI 提供证书等等。
互联网自古以来就是不可靠的,而负载均衡器经常要负责输出状态、跟踪还有日志以便运维人员能观测系统,定位错误,并解决问题。我时常会 “可观测性,可观测性,可观测性” 地念叨,而负载均衡器的输出里面可以观测到的东西是各种各样的。最先进的负载均衡器会提供多种输出,包括数字状态、分布式系统的跟踪,还有自定义的日志等。我想指出这种可观测性的改善并非一蹴而就的,负载均衡器需要额外付出一些代价才能做到这点。不过,可观测数据带来的好处大大地超出了其在性能上的开销。
负载均衡器常常会采取一些安全功能,比如流量限制、身份认证,以及防 Dos (拒绝服务攻击)措施(比如记录并识别 IP 地址,以及过滤等技术)等。在边缘代理拓扑里面安全性会更受重视,后面会提到。
负载均衡器需要配置。在一个大的部署体系里面,配置更是极其重要的部分。通常,配置负载均衡器的系统会被叫做 “控制层”,且有很多种实现形式。我在服务网格数据层 vs 控制层这篇博客里面谈到了关于这话题的更多细节。
这一部分只提了负载均衡器功能的一点皮毛。在详细介绍 L7 负载均衡器的部分还会谈到更多种类的功能。
现在我们已在较高层面上大致见识了负载均衡器是什么、L4 和 L7 负载均衡器的区别,以及对其功能的一个总结。接下来我会介绍各种分布式系统里面负载均衡器部署的拓扑结构。(下面每个拓扑结构都适用于 L4 和 L7 负载均衡器的情景)
图 4 所给出的中介代理服务器拓扑结构应该会是最为读者所熟知的负载均衡方案。这一分类涵盖了如 Cisco、Juniper、F5 等硬件设备;还有像亚马逊的 ALB 和 NLB 以及谷歌的 Cloud Load Balancer 等这些云端的软件解决方案;还有像 HAProxy、NGINX 以及 Envoy 这样的纯粹的通过软件来自行搭建并主管的解决方案。中介代理拓扑的好处便是其对用户的简洁性。总的来说,用户通过 DNS 连接到负载均衡器即可使用,不需理会其他细节。不过这一方案有不少坏处,便是代理服务器(即便做成一个集群)不仅会造成单点故障的情形,还会成为扩展的瓶颈。同时,中介代理这一黑盒的存在也会让操作变得困难,使得我们很难查清发生的问题是否有出现在客户端、物理网络、中介代理,还是服务端。
图 5 所给出的边缘代理服务器拓扑结构其实只是中介代理拓扑的一种变体。这一模式下的负载均衡器可以通过互联网访问。在这种情况下,负载均衡器通常必须提供一些 “API 网关” 的功能,比如说 TLS 终端、流量限制、身份认证,以及复杂的流量路由功能。边缘代理拓扑的好处和坏处跟中介代理拓扑一样。顺便一提,在构建一个面向互联网的分布式系统时,边缘代理会是一个绕不开的部分,而客户端通常要使用服务提供者不能干预的网络库来通过 DNS 访问这一系统(使得在客户端上直接运行客户端集成类库或者边车代理拓扑显得很不实用)。除此之外,出于安全考虑,我们应该准备一个接收所有从互联网进入到系统里面的流量的网关。
为了规避中介代理拓扑的单点故障,以及扩展瓶颈这些问题,很多复杂的底层结构都以类库的形式直接迁移到了一个嵌入到客户端里面的负载均衡器里面,如图 6 所示。目前有很多类库支持各种各样的功能,不过最为熟知、功能最丰富的还是 Finagle、Eureka / Ribbon / Hystrix,以及 gRPC(对一个内部的叫 Stubby 的谷歌系统有着松散的依赖)。这一方式的好处主要是其将负载均衡器的职能完全分发给了每个客户端,消灭了上面提到的单点故障以及扩展方面的问题。这一方式的坏处主要是这一基于类库的解决方案其实必须在每一种可能用到的编程语言里面都实现一遍。分布式系统的架构也会因此 “通晓” 越来越多门编程语言。在这种环境里,在多种编程语言里面实现网络库的成本也会变得让人望而却步。最终,在一个大型服务架构里面部署一个类库的更新会经受极大的痛苦,很可能会让生产环境同时运行好几个不同版本的类库,并让开发及运维成本激增。
总而言之,上述提到的类库方案在能限制编程语言扩展,并克服类库更新的阵痛的企业里面还是很成功的。
如图 7 所示的边车代理服务器(Sidecar proxy)就是客户端嵌入库的一种变体。最近,这种拓扑结构又因 “服务网格(service mesh)” 的出现而流行。边车代理背后的原理是以牺牲跳转到别的进程的一点延迟为代价,以实现一种无需适配特定编程语言的客户端集成类库方案。目前最流行的边车代理有 Envoy、NGINX、HAProxy 还有 Linkerd。若想了解边车代理方案的更多细节,不妨看看我的这篇介绍 Envoy 的博客,以及这篇主题为服务网格面板 vs 控制层的博客。
总的来说,我认为在服务间的通信里面,边车代理拓扑(服务网格)会逐渐地取代其他的拓扑结构。另外,为了处理传入服务网格的互联网流量,边缘代理拓扑会长期存在。
本文已讨论了 L7 负载均衡器对现代网络协议的不少好处,并且也会在之后的部分详细讲解 L7 负载均衡器的各种功能。这是否在说 L4 负载均衡器没用了呢?不是!尽管我认为 L7 负载均衡器会最终在服务间通信里面完全取代 L4 负载均衡器,但 L4 负载均衡器在边缘部分依然极其重要,因为几乎所有现代的大型分布式架构都会为处理互联网流量而使用一种双层式的 L4 / L7 负载均衡架构,而在边缘部署部分的 L7 负载均衡器前面专门部署 L4 负载均衡器有着这些好处:
在本章接下来的部分,我会介绍一些关于基于中介 / 边缘代理的 L4 负载均衡器的一些不同设计。这些设计是没办法套用到客户端集成类库还有边车代理拓扑里面的。
第一种仍在使用的 L4 负载均衡器便是图 8 所示的终端负载均衡器。这跟在此前介绍 L4 负载均衡器的部分所介绍的负载均衡器是一样的。这种负载均衡器会用到两种类型的 TCP 连接:一个连接了客户端,另一个连接了服务端。
L4 终端负载均衡器现在仍在使用的原因有两点:
第二种要介绍的 L4 负载均衡器便是如图 9 所示的透传负载均衡器。这种负载均衡器不会截止 TCP 连接,而会在进行连接追踪以及网络地址转换(NAT)之后,将每个连接的数据包转发到一个选定的服务端。我们首先来定义一下连接追踪和 NAT:
同时用上连接追踪还有 NAT,负载均衡器就可以把客户端发来的原始 TCP 流量转移到对应的服务端里去。比方说客户端跟 1.2.3.4:80
这一负载均衡器交互,而对应的服务端在 10.0.0.2:9000
。那么,客户端发出的 TCP 报文就会首先抵达 1.2.3.4:80
的负载均衡器。负载均衡器就会把 TCP 报文的目标地址还有端口号改成 10.0.0.2:9000
,并且还会把源地址和端口号换成 1.2.3.4:80
。这样在服务端响应 TCP 连接的时候,服务端发出的报文会返回到负载均衡器,负载均衡器就可以进行连接追踪还有 NAT 了。
为什么这一更复杂的负载均衡器能取代之前提到的终端负载均衡器呢?有几点原因:
图 10 展示了服务器直接返回(DSR)负载均衡器。DSR 建立在上面提到透传负载均衡器之上,对经过负载均衡器的传入 / 请求数据包的处理做了优化。采取 DSR 这一模式的主要原因在于很多响应的流量都比请求的流量要大得多(比如典型的 HTTP 请求 / 响应模式)。不妨假设总流量的 10% 是请求流量,90% 是响应流量,如果负载均衡器引入了 DSR,那么负载均衡器的理论容量只需要达到总流量的 1 / 10 便可以满足系统需求了。这一优化会给系统成本及可靠性方面(多一事不如少一事)带来持久的好处,毕竟负载均衡器在以往看来都是极其昂贵的。
DSR 负载均衡器基于以下方式扩展了透传负载均衡器的概念:
不论是透传的还是 DSR 的负载均衡器,都有多种多样的在负载均衡器和服务端间实现连接追踪、NAT、GRE 等要素的方法。不过这一话题的讨论就超出了本文的范围了。
目前为止,我们已经分别探讨了一些 L4 负载均衡器的设计。其中透传的和 DSR 的负载均衡器都需要保存一些连接追踪及其自身的状态。那要是负载均衡器瘫痪了该怎么办?在这种情况下,经过这个负载均衡器的所有连接都会受到严重影响,同时视具体应用而定也会对应用性能造成持续的冲击。
从前,L4 负载均衡器其实是一些熟知的供应商(Cisco、Juniper、F5 等)出售的硬件设备。这些设备可以处理大额的流量,价格也极其高昂。为了避免一个负载均衡器的瘫痪影响到所有的连接并导致应用持续停运,负载均衡器通常都部署在一个如图 11 所示的高可用配对里面。在其中较典型的 HA 负载均衡器体系的设计如下:
上述体系便是很多现在的高流量互联网应用都在使用的一种模式。不过,上述体系也存在这些固有的缺点:
上一部分介绍了使用 HA 配对的 L4 负载均衡器的差错容忍模式以及它的一些设计上的局限。大概在 2000 年之后,一种如图 12 所示的大型并行 L4 负载均衡系统开始了设计,并在一些大型的互联网底层设施里面得到了部署。这些系统的目标在于:
这种 L4 负载均衡器的设计便是本部分所说的 “通过分布式一致性散列算法实现容错及扩展”。它的工作模式如下:
我们再来看看上述设计是怎么解决 HA 配对方案所带来的问题的。
针对这一设计有一个经常会问到的问题:“边缘路由器为什么不能直接使用 ECMP 来跟服务端交互呢?我们还要负载均衡器干嘛?”。这其中的原因主要围绕着 Dos 防御以及服务端的易操作性两个方面。如果没了负载均衡器,那么每个服务端都要参与到 BGP 协议的体系里来,这样在执行滚动部署的时候就会很让人难堪了。
所有现代的 L4 负载均衡系统其实都在朝着这种设计(或其变体)发展。有两个最为突出并熟知的例子是谷歌的 Maglev 还有亚马逊的 Network Load Balancer。目前还没有哪个开源的负载均衡器实现了这种设计,不过据我所知有间公司正在计划在 2018 年发布一个这样的负载均衡器。我很热切地期待着它的发布。它将会成为现代 L4 负载均衡器在开源领域的最后一块拼图。
The proxy wars in tech currently is quite literally the proxy wars. Or the "war of the proxies". Nginx plus, HAProxy, linkerd, Envoy all quite literally killing it. And proxy-as-a-service / routing-as-a-service SaaS vendors raising the bar as well. Very interesting times! 目前在技术领域的代理服务器之战(proxy wars in tech)就如同其名字 “代理人战争(proxy wars)” 一样。Nginx plus、HAProxy、Linkerd,还有 Envoy 都身处这一战场,还有代理即服务 / 路由即服务的 SaaS 服务商也拉高了这一战局的水准。这时代太有意思了!
确实是这样。在近几年里,L7 负载均衡器 / 代理服务器的发展迎来了一次复苏。微服务架构还有分布式系统的发展更是进一步推动了这一发展。从根本上来说,网络的不可靠本质让其在被频繁使用的时候降低了不少操作效率。在另一方面,自动扩展、容器调度器等新事物的发展使得在静态配置文件里面规定静态 IP 的配置方式也已经落后于时代了。系统不仅需要更高效地利用网络,还需要更高的动态性,更向负载均衡器提出了更高的功能性需求。在这一部分里面我会简单地总结现代 L7 负载均衡器所发展出来的一些功能。
现代的 L7 负载均衡器给很多不同的网络协议都提供了显式的支持。负载均衡器对应用层流量了解的越多,它能在可观测数据的输出、高级负载均衡操作以及路由等方面执行更为复杂的操作。比如目前 Envoy 就对 HTTP/1、HTTP2、gRPC、Redis、MongoDB 还有 DynamoDB 这些 L7 的协议的解析以及路由提供了显式的支持。将来 Envoy 可能会支持更多的协议,比如 MySQL 还有 Kakfa。
如上文所述,分布式系统中的动态性需求与日倍增,需要开发一个动态的、响应式的控制系统来配合工作。Istio 就是其中的一个典例。不放阅读一下我的服务网格面板 vs 控制层这篇文章来了解关于这话题的更多信息。
现在 L7 负载均衡器基本都内置了对一些高级的负载均衡方法的支持,比如超时、重试、限流、断路、投影(shadowing)、缓冲、基于内容的路由等等。
在介绍负载均衡器通用功能也提过,动态系统的与日俱增也增加了调试的难度。稳健的、可观测性强的协议细节输出大概也是了现代 L7 负载均衡器所提供的最重要的一个功能。所有的 L7 负载均衡解决方案都在某种程度上要求了生成数据统计、分布式追踪,还有自定义日志这些功能。
现代 L7 负载均衡器的用户时常会想要给负载均衡器增加一些自定义的功能。这可以通过编写一个插件式的过滤器并加载到负载均衡器里面来实现。很多负载均衡器都支持这一编写脚本(通常用 Lua 来编写)的特性。
我在上面提到了很多有关 L7 负载均衡器进行差错容忍的方式。那么 L7 负载均衡器又如何进行差错容忍呢?总的来说,我们可以认为 L7 负载均衡器具有可扩展性和无状态性。商业化软件的使用让 L7 负载均衡器能更容易地进行水平扩展。另外,L7 负载均衡器所执行的处理还有状态追踪要比 L4 的要复杂得多,给 L7 负载均衡器构建一个 HA 配对在技术上是可行的,但任务会非常艰巨。
总而言之,在 L4 和 L7 负载均衡技术的领域里面,整个业界都在逐渐脱离 HA 配对的模式,并朝着使用可水平扩展并能用一致性散列算法来聚合的系统的方向发展。
L7 负载均衡器现仍在以惊人速度演变着。不妨在 Envoy 的架构概览里面看一看 Envoy 所提供的一些新特性。
谈及负载均衡的未来,单个负载均衡器有着成为商业化设备的趋势。在我看来,其中所有的创新和商业机遇都埋藏在控制层里面。图 13 就展示了一个全局负载均衡系统的例子。此例有这些不一样的要素:
另外,全局负载均衡器也会逐渐能做一些单个负载均衡器无法完成的复杂任务。比如:
为了让全局负载均衡成为可能,用作控制层的负载均衡器必须具有能进行复杂动态配置的能力。不妨也查阅一下我的 Envoy 的通用数据层 API 还有服务网格数据层 vs 控制层来了解关于这话题的更多信息。
目前本文只是主要在介绍老式的 L4 负载均衡器 HA 配对模式的时候提了一下硬件 vs 软件这一话题,而这一方面的业界趋势又是怎么样的呢?
I've seen the new OSI stack of eight layers of software. I think it's more like this:
我已经看过新的 OSI 八层模型了。
我觉得它更像是这样:
—— @infosecdad
上面这条推文虽然是一种夸张式的幽默,但其实也是对趋势的一个很好的总结,那就是:
本文要点如下:
总之我认为,现在正是计算机网络发展的一个精彩时期!面向多数系统及开源的软件正以极快步伐进行着发展,并且随着分布式系统不断通过 “无服务器化” 规范向动态化前进,底层网络还有负载均衡系统的复杂度还会继续增加。