构建高可用、高性能的通信服务,通常采用服务注册与发现、负载均衡和容错处理等机制实现。根据负载均衡实现所在的位置不同,通常可分为以下三种解决方案:
1、集中式LB(Proxy Model)
在服务消费者和服务提供者之间有一个独立的LB,通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。 该方案主要问题:
单点问题,所有服务调用流量都经过LB,当服务数量和调用量大的时候,LB容易成为瓶颈,且一旦LB发生故障影响整个系统;
服务消费方、提供方之间增加了一级,有一定性能开销。
2、进程内LB(Balancing-aware Client)
针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。该方案主要问题:
开发成本,该方案将服务调用方集成到客户端的进程里头,如果有多种不同的语言栈,就要配合开发多种不同的客户端,有一定的研发和维护成本;
另外生产环境中,后续如果要对客户库进行升级,势必要求服务调用方修改代码并重新发布,升级较复杂。
3、独立 LB 进程(External Load Balancing Service)
该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。
不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,一个LB进程挂了只影响该主机上的服务调用方,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库,LB的升级不需要服务调用方改代码。
该方案主要问题:部署较复杂,环节多,出错调试排查问题不方便。
构建高可用、高性能的通信服务,通常采用服务注册与发现、负载均衡和容错处理等机制实现。根据负载均衡实现所在的位置不同,通常可分为以下三种解决方案
负载均衡选择
代理还是客户端?
注意:在某些文献中,代理负载平衡也称为服务器端负载平衡。
在代理与客户端负载平衡之间进行选择是主要的架构选择。 在代理负载平衡中,客户端向负载均衡器(LB)代理发出RPC。 LB将RPC调用分配给可用的后端服务器之一,后端服务器实现服务调用的实际逻辑。 LB跟踪每个后端的负载,并实现公平分配负载的算法。 客户端自己不了解后端服务器。 客户可能不受信任。 此体系结构通常用于面向用户的服务,其中来自开放式Internet的客户端可以连接到数据中心中的服务器
在这种情况下,客户端向LB发出请求(#1)。 LB将请求传递给其中一个后端(#2),后端报告加载到LB(#3)
在客户端负载平衡中,客户端知道多个后端服务器,并选择一个用于每个RPC。 客户端从后端服务器获取负载报告,客户端实现负载平衡算法。 在更简单的配置中,不考虑服务器负载,客户端只能在可用服务器之间进行循环。 如下图所示。 如您所见,客户端向特定后端发出请求(#1)。 后端响应负载信息(#2),通常在执行客户端RPC的同一连接上。 然后客户端更新其内部状态。
代理负载均衡选项
代理负载平衡可以是L3 / L4(传输级别)或L7(应用级别)。在传输级负载平衡中,服务器终止TCP连接并打开与所选后端的另一个连接。应用程序数据(HTTP / 2和gRPC帧)只是在客户端连接到后端连接之间复制。 L3 / L4 LB设计的处理非常少,与L7 LB相比延迟更少,并且因为它消耗更少的资源而更便宜。
在L7(应用程序级别)负载平衡中,LB终止并解析HTTP / 2协议。 LB可以检查每个请求并根据请求内容分配后端。例如,作为HTTP标头的一部分发送的会话cookie可用于与特定后端关联,因此该会话的所有请求都由同一后端提供。一旦LB选择了适当的后端,它就会为该后端创建一个新的HTTP / 2连接。然后,它将从客户端接收的HTTP / 2流转发到所选择的后端。使用HTTP / 2,LB可以在多个后端之间分配来自一个客户端的流。
客户端负载均衡选项
重客户端
胖客户端方法意味着在客户端中实现负载平衡智能。 客户端负责跟踪可用服务器,其工作负载以及用于选择服务器的算法。 客户端通常集成与其他基础结构通信的库,例如服务发现,名称解析,配额管理等。
grpc客户端负载均衡
gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展。
其基本实现原理:
服务启动后gRPC客户端向命名服务器发出名称解析请求,名称将解析为一个或多个IP地址,每个IP地址标示它是服务器地址还是负载均衡器地址,以及标示要使用那个客户端负载均衡策略或服务配置。
客户端实例化负载均衡策略,如果解析返回的地址是负载均衡器地址,则客户端将使用grpclb策略,否则客户端使用服务配置请求的负载均衡策略。
负载均衡策略为每个服务器地址创建一个子通道(channel)。
当有rpc请求时,负载均衡策略决定那个子通道即grpc服务器将接收请求,当可用服务器为空时客户端的请求将被阻塞。
根据gRPC官方提供的设计思路,基于进程内LB方案(即第2个案,阿里开源的服务框架 Dubbo 也是采用类似机制),结合分布式一致的组件(如Zookeeper、Consul、Etcd),可找到gRPC服务发现和负载均衡的可行解决方案
当服务A需要与服务B通信时,必须为服务A提供至少一个适当的IP地址或主机名(客户端负载均衡), 或者服务A必须能够委托地址解析和路由到第三方给一个已知的服务B的逻辑名(服务端负载均衡). 在微服务领域不断变化的上下文中,这两种方式都需要出现服务发现。在最简单的形式中,服务发现只是为一个或多个服务注册运行实例。
如果这对你来说听起来像DNS服务, 它确实如此。区别在于服务发现用于集群内部,让微服务互相能找到对方,而DNS一般是更加静态的、适用于外部路由,因此外部方可以请求路由到你的服务。此外,DNS服务和DNS协议通常不适合处理具有不断变化微服务环境的拓扑结构,容器和节点来来往往,客户端通常也不遵循TTL值、失败监测等等。
大多数微服务框架为服务发现提供一个或多个选项。 默认情况下,Spring Cloud/Netflix OSS使用Netflix Eureka(支持Consul, etcd和ZooKeeper), 服务使用已知的Eureka实例来注册自己,然后间歇性的发送心跳来确保Eureka实例知道它们依然活跃着。Consul提供了一个包含DNS集成的丰富的特征集的选项已经变得越来越流行。 其他流行的选项是分布式和可复制key-value存储的使用, 例如etcd中服务可以注册自己。Apache ZooKeeper也将会意识到这样需求的一群人。
客户端负载均衡
由客户端查询发现服务来获取它们要调用服务的实际地址信息(IP, 主机名, 端口号), 找到之后,它们可以使用一种负载均衡策略(比如轮询或随机)来选择一个服务。此外,为了不必要让每个即将到来的调用都查询发现服务,每个客户端通常都保持一份端点的本地缓存,这些端点必须与来自发现服务的主信息保持合理同步。 Spring Cloud中客户端负载均衡的一个例子是Netflix Ribbon。类似的东西在etcd支持的go-kit生态中也存在。客户端负载均衡的一些优点是具有弹性、分散性以及没有中心瓶颈,因为每个服务消费者都自己保持有生产端的注册。 缺点就是具有较高的内部服务复杂性,以及本地注册可能会包含过时条目的风险。
服务端负载均衡
这个模型中,客户端依赖负载均衡,提供服务逻辑名来查询它要调用服务的合适实例。这种操作模式通常称为代理, 因为它既充当负载均衡器又充当反向代理。我认为它的主要优点就是简单。 负载均衡器和服务发现机制一般都内置于你的容器编排器中,你无需关心安装和管理这些组件。另外,客户端(e.g. 我们的服务)不需要知道服务注册 - 负载均衡器为我们负责这些。 依赖负载均衡器来路由所有呼叫可能降低弹性,并且负载均衡器在理论上来说可能成为性能的瓶颈。
注意:当我们使用swarm模式的Docker的服务抽象时, 例如上面的服务端的生产服务注册实际上对作为开发者的你来说是完全透明的。也就是说,我们的生产服务甚至不会意识到它们在操作服务端负载均衡的上下文(或者甚至在容器编排的上下文中). Swarm模式的Docker负责我们全部的注册、心跳、取消注册。
使用另外一个单独的服务发现机制 - 即运行Netflix Eureka, Consul或类似的东西,并确保除了Docker Swarm模式的机制外,在这些服务发现机制中也可以发现可注册/取消注册的微服务。然后我们只需要使用发现服务的注册/查询/心跳等API即可。我不喜欢这个选项,因为它引入了更多复杂的东西到服务中,当Swarm模式的Docker可以或多或少透明的为我们处理这些里边的大部分的事情。我几乎认为这是一种饭模式,如果除非你必须要这么做,否则还是不要这样了。
应用特定的发现令牌 - 在这种方式中,服务想要广播它们的存在,可以周期性的在一个消息话题上post一个带有IP, 服务名等等的发现令牌。消费者需要了解实例以及它们的IP, 可以订阅这个话题(Topic), 并保持它自己的服务实例注册即时更新。当我们在稍后的文章中看不使用Eureka的Netflix Turbine, 我们就会使用这个机制来向一个定制的Turbine发现插件提供信息。这种方式有点不同,因为它们不需要充分利用完整的服务注册表 - 毕竟,在这个特定的用例中,我们只关心特定的一组服务。
etcd的简单介绍:
因为前面说的问题我们就需要etcd 这类产品了,etcd 是一个 分布式 的高一致性的 键值存储系统。我们每次网关 后面加一个服务,只需要向etcd 注册 该服务(其实就是 存一个值)然后向etcd 发送心跳,当etcd 没有检测到心跳就会 把这个键值对 删了(这整个动作是etcd里的租约模式),网关那边 就只需要 watch 这个 key ,就能够知道 所有服务的所有动态了。
etcd的 租约模式:客户端申请 一个租约 并设置 过期时间,每隔一段时间 就要 请求 etcd 申请续租。客户端可以通过租约存key。如果不续租 ,过期了,etcd 会删除这个租约上的 所有key-value。类似于心跳模式。
一般相同的服务存的 key 的前缀是一样的 比如 “server/001”=> “127.0.0.1:1212” 和 ”server/002”=>”127.0.0.1:1313” 这种模式,然后 客户端 就直接 匹配 “server/” 这个key。
1.创建一个client 连到etcd。
2.匹配到所有相同前缀的 key。把值存到 serverList 这个map里面。
3 watch这个 key前缀,当有增加或者删除的时候 就 修改这个map。
4所以这个map就是 实时的 服务列表
https://github.com/mistaker/etcdTool