客户端服务负载均衡

如何在客户端实现服务的负载均衡

在正式开始讨论之前,我们先来区分清楚几个容易混淆的概念,分别是前面两讲中我介绍过的服务发现、网关路由,以及这节课要探讨的负载均衡,还有在下一讲中将会介绍的调用容错。这几个技术名词都带有“从服务集群中寻找到一个合适的服务来调用”的含义,那么它们之间的差别都体现在哪呢?下面我就通过一个具体的案例场景来给你说明一下。

理解服务发现、网关路由、负载均衡、调用容错的具体区别

假设,你目前身处广东,要上 Fenix’s Bookstore 购买一本书。在程序业务逻辑里,购书的其中一个关键步骤是调用商品出库服务来完成货物准备,在代码中该服务的调用请求为:

PATCH https://warehouse:8080/restful/stockpile/3

{amount: -1}

假设 Fenix’s Bookstore 是个大书店,在北京、武汉、广州的机房均部署有服务集群,那么此时按顺序会发生以下事件:

1.首先是将 warehouse 这个服务名称转换为了恰当的服务地址。

注意,这里的“恰当”是个宽泛的描述,一种典型的“恰当”就是因为调用请求来自于广东,DNS 层面的负载均衡就会优先分配给传输距离最短的广州机房来应答。

其实按常理来说,这次出库服务的调用应该是集群内的流量,而不是用户浏览器直接发出的请求。所以尽管结果都一样,但更接近实际的情况应该是用户访问首页时,已经被 DNS 服务器分配到了广州机房,请求出库服务时,应优先选择同机房的服务进行调用,此时该服务的调用请求就变为:

PATCH https://guangzhou-ip-wan:8080/restful/stockpile/3

2.然后,广州机房的服务网关将该请求与配置中的特征进行比对,由 URL 中的“/restful/stockpile/**”得知该请求访问的是商品出库服务。因此,将请求的 IP 地址转换为内网中 warehouse 服务集群的入口地址:

PATCH https://warehouse-gz-lan:8080/restful/stockpile/3

3.由于集群中部署有多个 warehouse 服务,所以在收到调用请求后,负载均衡器要在多个服务中根据某种标准或均衡策略(可能是随机挑选,也可能是按顺序轮询,或者是选择此前调用次数最少那个,等等),找出要响应本次调用的服务,我们称其为 warehouse-gz-lan-node1。

PATCH https://warehouse-gz-lan-node1:8080/restful/stockpile/3

4.接着,访问 warehouse-gz-lan-node1 服务,没有返回需要的结果,而是抛出了 500 错。

HTTP/1.1 500 Internal Server Error

5.那么根据预置的故障转移(Failover)策略,负载均衡器会进行重试,把调用分配给能够提供该服务的其他节点,我们称其为 warehouse-gz-lan-node2。

PATCH https://warehouse-gz-lan-node2:8080/restful/stockpile/3

6.最后,warehouse-gz-lan-node2 服务返回商品出库成功。

HTTP/1.1 200 OK

所以,从整体上看,前面步骤中的

1(找到对应的服务)、

2(服务转内网url地址)、

3(找个可以用的节点)、

5(节点挂了换个节点),

就分别对应了服务发现、网关路由、负载均衡和调用容错;

而从细节上看,其中的部分职责又是有交叉的,并不是注册中心就只关心服务发现,网关只关心路由,均衡器只关心负载均衡。

比如,步骤 1 服务发现的过程中,“根据请求来源的物理位置来分配机房”这个操作,本质上是根据请求中的特征(地理位置)来进行流量分发,这其实是一种路由行为。在实际的系统中,DNS 服务器(DNS 智能线路)、服务注册中心(如 Eureka 等框架中的 Region、Zone 概念)或者负载均衡器(可用区负载均衡,如 AWS 的 NLB,或 Envoy 的 Region、Zone、Sub-zone)里都有可能实现路由功能。

而且除此之外,你有没有感觉到这个网络调用的过程好像过于繁琐了,一个从广州机房内网发出的服务请求,绕到了网络边缘的网关、负载均衡器这些设施上,然后再被分配回内网中另外一个服务去响应。这不仅消耗了带宽,降低了性能,也增加了链路上的风险和运维的复杂度。

可是,如果流量不经过这些设施,它们相应的职责就无法发挥作用了:如果不经过负载均衡器的话,甚至连请求应该具体交给哪一个服务去处理都无法确定。

所以既然如此,我们有什么办法可以简化这个调用过程吗?这就需要用到客户端负载均衡器了。

客户端负载均衡器的工作原理

以前,负载均衡器大多只部署在整个服务集群的前端,将用户的请求分流到各个服务进行处理,这种经典的部署形式现在被称为集中式的负载均衡。

而随着微服务的日渐流行,服务集群收到的请求来源就不再局限于外部了,越来越多的访问请求是由集群内部的某个服务发起,由集群内部的另一个服务进行响应的。对于这类流量的负载均衡,既有的方案依然是可行的,但针对内部流量的特点,直接在服务集群内部消化掉,肯定是更合理、更受开发者青睐的办法。

由此,一种全新的、独立位于每个服务前端的、分散式的负载均衡方式正逐渐变得流行起来,这就是本节课我们要讨论的主角:客户端负载均衡器(Client-Side Load Balancer)。

1

客户端负载均衡器的理念提出以后,此前的集中式负载均衡器也有了一个方便与它对比的名字,“服务端负载均衡器”(Server-Side Load Balancer)。

从上图中,你其实能够清晰地看到这两种均衡器的关键差别所在:服务端负载均衡器是集中式的,同时为多个节点提供服务,而客户端负载均衡器是和服务实例一一对应的,而且与服务实例并存于同一个进程之内。这能为它带来很多好处,比如说:

  • 均衡器与服务之间的信息交换是进程内的方法调用,不存在任何额外的网络开销。
  • 客户端均衡器不依赖集群边缘的设施,所有内部流量都仅在服务集群的内部循环,避免出现前面提到的,集群内部流量要“绕场一周”的尴尬局面。
  • 分散式的均衡器意味着它天然避免了集中式的单点问题,它的带宽资源将不会像集中式均衡器那样敏感,这在以七层均衡器为绝对主流、不能通过 IP 隧道和三角传输这样的方式来节省带宽的微服务环境中,显得更具优势。
  • 客户端均衡器要更加灵活,能够针对每一个服务实例单独设置均衡策略等参数。比如访问某个服务是否需要具备亲和性,选择服务的策略是随机、轮询、加权还是最小连接,等等,都可以单独设置而不影响其他服务。
  • ……

但是你也要清楚,客户端均衡器并不是银弹,它的缺点同样是不少的:

  • 它与服务运行于同一个进程之内,就意味着它的选型要受到服务所使用的编程语言的限制。比如,用 Golang 开发的微服务,就不太可能搭配 Spring Cloud Load Balancer 来一起使用,因为要为每种语言都实现对应的能够支持复杂网络情况的均衡器是非常难的。客户端均衡器的这个缺陷其实有违于微服务中,技术异构不应受到限制的原则。
  • 从个体服务来看,由于客户端均衡器与服务实例是共用一个进程,均衡器的稳定性会直接影响整个服务进程的稳定性,而消耗的 CPU、内存等资源也同样会影响到服务的可用资源。
  • 由于请求的来源可能是来自集群中任意一个服务节点,而不再是统一来自集中式均衡器,这就会导致内部网络安全和信任关系变得复杂,当入侵者攻破任何一个服务时,都会更容易通过该服务突破集群中的其他部分。
  • 我们知道,服务集群的拓扑关系是动态的,每一个客户端均衡器必须持续跟踪其他服务的健康状况,以实现上线新服务、下线旧服务、自动剔除失败的服务、自动重连恢复的服务等均衡器必须具备的功能。由于这些操作都需要通过访问服务注册中心来完成,因此数量庞大的客户端均衡器需要一直持续轮询服务注册中心,这也会为它带来不小的负担。
  • ……

代理客户端负载均衡器

在 Java 领域,客户端负载均衡器中最具代表性的产品,就是 Netflix Ribbon 和 Spring Cloud Load Balancer 了,随着微服务的流行,它们在 Java 微服务中已经积聚了相当可观的使用者。

而到了最近两三年,服务网格(Service Mesh)开始盛行,另一种被称为“代理客户端负载均衡器”(Proxy Client-Side Load Balancer,后面就简称“代理均衡器”)的客户端均衡器变体形式,开始引起不同编程语言的微服务开发者的共同关注,因为它解决了此前客户端均衡器的大部分缺陷。

代理均衡器对此前的客户端负载均衡器的改进,其实就是将原本嵌入在服务进程中的均衡器提取出来,放到边车代理中去实现,它的流量关系如下图所示:

这里你可以发现,虽然代理均衡器与服务实例不再是进程内通讯,而是通过虚拟化网络进行数据交换的,数据要经过操作系统的协议栈,要进行打包拆包、计算校验和、维护序列号等网络数据的收发等步骤,流量比起之前的客户端均衡器确实多经历了一次代理过程。

不过,Kubernetes 严格保证了同一个 Pod 中的容器不会跨越不同的节点,相同 Pod 中的容器共享同一个网络和Linux UTS 名称空间,因此代理均衡器与服务实例的交互,仍然要比真正的网络交互高效且稳定得多,代价很小。但它从服务进程中分离出来的收益则是非常明显的:

  • 代理均衡器不再受编程语言的限制。比如说,要发展一个支持 Java、Golang、Python 等所有微服务应用的通用代理均衡器,就具有很高的性价比。集中不同编程语言的使用者的力量,也更容易打造出能面对复杂网络情况的、高效健壮的均衡器。即使退一步说,独立于服务进程的均衡器,也不会因为自身的稳定性而影响到服务进程的稳定。
  • 在服务拓扑感知方面,代理均衡器也要更有优势。由于边车代理接受控制平面的统一管理,当服务节点拓扑关系发生变化时,控制平面就会主动向边车代理发送更新服务清单的控制指令,这避免了此前客户端均衡器必须长期主动轮询服务注册中心所造成的浪费。
  • 在安全性、可观测性上,由于边车代理都是一致的实现,有利于在服务间建立双向 TLS 通讯,也有利于对整个调用链路给出更详细的统计信息。
  • ……

所以总体而言,边车代理这种通过同一个 Pod 的独立容器实现的负载均衡器,就是目前处理微服务集群内部流量最理想的方式。只是服务网格本身还不够成熟,拭目以待。

小结

这节课,我们从“如何将流量转发到最恰当的服务”开始,讨论了客户端负载均衡器出现的背景、它与服务端负载均衡器之间的差异,以及通过代理来实现客户端负载均衡器的差别。最后,借助本节课建立的上下文场景,我还给你介绍了地域与可用区域的概念。这些概念不仅在购买、使用云计算服务时会用到,还直接影响到了应用程序中路由、负载均衡的请求分发形式。

猜你喜欢

转载自blog.csdn.net/wdays83892469/article/details/129743560
今日推荐