当前位置: 首页 > 科技观察

服务发现和负载均衡的来龙去脉

时间:2023-03-22 13:27:23 科技观察

问题源于单机时代,大多数传统软件都是单体架构(Monolithic)。每个人都将CODE提交到一个代码仓库,这会导致应用扩展、理解和修改困难、扩展受限、无法按需扩展等诸多问题。单体架构如何解决多人协作的问题?模块化,对,按功能拆分,定义模块之间的编程接口(API),关心彼此的功能而不是实现。随着时代的发展,单机程序遇到了计算能力和存储的双重瓶颈,分布式架构应运而生。单体应用可以通过函数名(标识)轻松完成本地函数调用。在分布式系统中,服务(RPC/RESTfulAPI)承担了类似的角色,但仅服务名称不足以请求服务。服务名称只是服务能力(服务类型)的标识,还需要指明该服务在网络中的位置,部署在云端的服务实例的IP是动态分配的。缩放、故障和更新使问题变得更加复杂。服务实例的静态配置无法适应新的变化,需要更精细化的服务治理能力。为了解决或简化这个问题,服务发现被抽象出来并作为一项基本能力提供。它试图使请求网络服务像调用本地函数一样简单和透明。一个服务就是一个功能(function)。只有当服务与网络紧密相连时,才会出现网络服务这个名词。服务提供者通过网络发布服务,服务使用者通过网络请求服务。分布式系统突破了单机计算能力和存储的限制,完善了系统。稳定性使得高并发、高可用的海量服务成为可能,但也增加了软件的复杂性,引入了软件分层、负载均衡、微服务、服务发现/治理、分布式一致性等新的问题和挑战。服务发现服务分为服务提供者(ServiceProvider)和服务消费者(ServiceConsumer)。如果要提供海量的服务能力,单一的服务实例显然是不够的。如果要提供成千上万的服务,就需要有一个地方记录从服务名称到服务实例列表的映射。因此,有必要引入一个新的角色:服务中介。服务中介维护着一个服务注册中心(ServiceRegistry)。注册中心可以理解为一个服务字典,key就是服务名。value是服务提供者实例的列表;服务注册中心是服务提供者和服务消费者之间的桥梁。它维护着服务提供者的最新网络位置等信息,也是服务发现的核心部分。服务启动时,将服务信息注册(放入)到服务注册中心;当服务终止时,从服务注册表中删除(移除)自己的服务信息。服务消费者请求服务时,首先到服务注册中心按名称查询(获取)服务提供者列表,然后从列表中选择一个服务实例,向该实例请求服务。从简单之路,这是最简单的服务发现模型,也是服务发现的基本原理。到此为止,看似一切正常,但实际上还有几个问题没有弄清楚。问题及解决方案第一个问题是如果服务没有正常停止而是被系统kill了,它就没有机会通知服务注册中心删除自己的服务信息,这样注册中心就会多出一条信息指向无效的服务实例,服务消费者不知道,怎么办?解决方法很简单:保活(keepalive),服务提供者周期性(比如每10秒)向服务中介发送保活消息,服务中介收到保活消息后更新保活消息的保活时间戳服务实例,服务中介定期检查时间戳,如果过期则从注册表中删除服务实例。第二个问题,服务实例列表发生变化,如何通知服务消费者?方法无外乎两种,轮询和pub-sub。轮询是指消费者主动询问服务中介服务列表是否发生变化,如果有变化则将新的服务列表发送给消费者。如果消费者太多,服务中介将面临处理轮询消息的压力。当服务类别很多,服务列表很大时,甚至会成为瓶颈。pub-sub是一种主动通知服务消费者的服务中介,时效性优于轮询。缺点是会占用单独的线程或连接资源。第三个问题,服务中介挂了怎么办?所以我们要解决单点的问题,通常使用集群来对抗这个漏洞。服务注册中心的开源方案有很多,比如etcd/zookeeper/consul,本质上是使用分布式一致性数据库来保存注册中心信息,既解决了读写性能的问题,又提高了服务的稳定性和可用性系统。第四个问题,如果服务消费者每次使用远程服务都需要查询服务中介获取实例列表,然后请求服务,这样效率低吗?服务中介的压力不小?通常,客户端会缓存服务Instance列表,这样多次请求同名服务就不需要重复查询,既减少了延迟也减轻了服务中介的访问压力。第五个问题就是前面提到的keepalive是有间隔的。如果服务实例在这个时间间隔内不可用,服务消费者仍然无法感知,所以仍然有可能向无法提供服务的远程网络机器发送请求。这样一来,自然是行不通的。我们不能从根本上消除这种情况。系统需要容忍这种错误,但也可以做一些改进,比如在一个服务请求失败后拉黑一个实例,避免多次请求同一个无效的服务实例。第六个问题,一个服务消费者如何在多个服务实例中选择一个?如何保证同一个服务消费者的多个服务请求都分配到一个固定的服务实例(有时需要)?这其实是一个负载均衡的问题,有很多策略,比如rr,优先级,比如加权随机,一致性哈希。服务发现模式服务发现主要有两种模式:客户端发现模式(client-sidediscovery)和服务端发现模式(server-sidediscovery)。客户端发现模式客户端负责查询服务实例列表,并决定从哪个实例请求服务,即负载均衡策略在客户端实现。该模式包括两部分,注册和发现。服务实例调用服务中介的注册接口注册实例,服务实例通过keepalive更新服务,服务中介通过健康检查淘汰不可用的服务实例。当服务消费者请求服务时,它首先查询服务注册表以获取服务实例列表。注册表是一个服务数据库。为了提高性能和可靠性,客户端通常会缓存服务列表(缓存用于确保注册中心挂起后仍然可用)。可以继续工作),客户端得到实例列表后,根据负载均衡策略选择一个实例发送服务请求。优点Direct,客户端可以灵活的实现负载均衡策略。去中心化、无网关,有效避免单点瓶颈和可靠性降低。服务发现直接将SDK集成到客户端。语言集成很好,程序执行性能也很好,排错也方便。缺点客户端与服务注册中心耦合,服务客户端使用的每种语言和框架都需要开发服务发现逻辑。这种侵入性的集成会导致任何服务发现的变化都需要重新编译和部署客户端应用程序,而强绑定则违反了独立性原则。当服务下线时,会影响到调用者,导致服务暂时不可用。服务器端发现模式发现:服务消费者通过负载均衡器发送服务请求,负载均衡器查询服务注册中心,选择一个服务实例,将请求转发给服务实例。注册:服务注册/注销可以与上述客户端发现方式一致,也可以通过部署平台内置的服务注册和发现机制完成,即容器化部署平台(docker/k8s)可以主动发现服务实例并帮助服务实例完成注册注销。与客户端发现模式相比,使用服务端发现模式的客户端不会在本地保存服务实例列表,客户端也不会进行负载均衡。这个负载均衡器既承担了服务发现的角色,又承担了网关的角色,所以常被称为API网关服务器。因为负载均衡器是中心的,所以它也必须是一个集群。单实例不足以支持高并发访问。负载均衡器本身的服务发现和负载均衡通常使用DNS。Http服务器、Nginx和NginxPlus是这种服务器端发现模式的负载均衡器。优点服务发现对服务消费者透明,服务消费者与注册中心解耦,服务发现功能的更新对客户端无感知。服务消费者只需要向负载均衡器发送请求,不需要为服务消费者的每一种编程语言和框架开发服务发现逻辑SDK。缺点由于所有请求都由负载均衡器转发,负载均衡器可能成为新的性能瓶颈。负载均衡器(服务网关)是中心的,中心架构会有稳定性隐患。因为负载均衡器转发请求,RT会比客户端直连方式高。微服务和服务发现服务网格服务网格是微服务应用程序的可配置基础设施层,旨在处理服务之间基于网络的繁重进程间通信。ServiceMesh服务网关将调用和通信解耦。在非网状环境中,协议的感知和服务发现方式的感知都需要由应用来完成。使用mesh后,只需要调用,mesh通过controlplane控制应用的数据流。Mesh服务发现实际上是客户端发现模式的升级版,是基于sidecars和pilots实现的。Sidecars,即DataPlane,负责发现目标服务实例的地址列表并转发请求。Pilots,即ControlPlane,负责管理服务注册中心中的所有服务注册信息。服务注册方式的一种选择是服务实例自注册,即自注册方式。另一种选择是由其他系统组件来管理服务实例的注册,即第三方注册模式。如前所述,自注册模式非常简单,不需要第三方组件,缺点是必须为服务中使用的每种编程语言和框架实现注册代码。第三方注册服务实例不会自行完成注册和注销。它负责另一个名为ServiceRegistrar的系统组件。该组件会轮询部署环境或跟踪订阅事件,感知服务实例的变化,帮助服务实例完成自动注册和注销。第三方注册模式的主要优点是它解耦了服务和服务注册中心。无需为每种语言和框架都实现服务注册逻辑。服务实例注册由专用服务集中实现。缺点是除了内置于部署环境之外,它本身就是一个高可用的系统组件,需要启动和管理。其他:如果某个服务的服务实例太多,比如在一些Top公司,一个服务名可能对应上万个服务实例,那么查询和比较服务变化会很慢,而且数量多IO会大到超乎想象,通常会用versionnum来解决这个问题。