服务器路由
分布式服务框架上线运行时都是集群组网,这意味着集群中存在着某个服务的多实例部署,消费者如何从服务列表中选择合适的服务提供者进行调用,这就涉及到服务路由。分布式服务框架要能够满足用户灵活
的路由需求
透明化路由
很多开源的RPC框架调用者需要配置服务提供者的地址信息,尽管可以通过读取数据库的服务地址列表避免硬编码地址信息,但是消费者依然要感知服务提供者的地址信息,这违反了透明化路由的原则
基于注册中心的订阅发布
在分布式服务框架中,服务注册中心用于存储服务提供者地址信息,服务发布相关的属性信息,消费者通过主动查询和被动通知的方式获取提供者的地址信息,而不需要像之前那样在代码中硬编码服务提供者地址信息。消费者只需要知道当前系统发布了哪些服务,而不需要知道服务具体存在什么位置,这就是透明化路由。他的工作原理就是基于服务注册中心(例如zookeeper)的订阅发布
服务注册中心原理图
服务消费者和服务提供者通过注册中心提供的SDK与注册中心建立链路(例如ZOOKEEPER采用长连接),服务提供者将需要腹部的服务地址属性列表写入注册中心。服务消费者根据本地引用的接口名等信息,从服务注册中心获取服务提供者列表,缓存到本地
由于消费者可能小于服务提供者启动,或者系统运行过程中新增的服务提供者,或者某些歌服务提供者当即退出,就出导致注册中心发生服务提供者地址变更。注册中心检测到服务提供者列表地址变更,注册中心检测到服务提供者列表变更之后,就将变更内容主动推送给服务消费者,消费者根据变更列表,动态刷新本地缓存的的服务提供者地址
消费者缓存服务提供者地址列表
消费者调用服务提供者时不需要每次调用都去服务注册中心查询服务提供者列表,消费者客户端直接从本地缓存的服务提供者路由表查询地址信息,根据路由策略进行服务选路
当服务提供者发生变更时,注册中心主动将变更内容推送给消费者,由后者动态刷新本地缓存的服务路由表,保证服务路由信息的实时准确性
采用客户端缓存服务提供者地址的方案不仅能提升服务调用性能,还能保证系统的可靠性,当注册中心全部当掉后,服务提供者和服务消费者仍能通过本地缓存的地址信息进行通信,只是影响服务的注册和老服务的下线,不影响已经发布和运行的服务
负载均衡
负载均衡是服务重要的属性,分布式服务框架通常会提供多种负载均衡策略,同时支持用户扩展负载均衡算法
随机
采用随机算法进行负载均衡,通常在对等集群组网中,随机路由算法消息分发还是比较均匀的,他的主要却吊事
1)在一个界面上碰撞的概率较高
2)非对等集群组网,或者硬件配置差异较大,会导致各节点负载不均匀
通常实现上会采用JDK提供的java.util.Random或者java.security.SecureRandom在指定服务提供者地址列表中生成随机地址,消费者基于随机生成的服务提供者地址进行远程调用
轮询
轮询,按公约后的权重轮询比率,到达边界之后,继续绕接他的主要缺点是存在慢的提供者累积请求问题。比如第二台机器很慢,但没挂,当请求调到第二台就卡在那,久而久之,所有的请求都卡在第二台上
轮询策略的实现非常的简单,他的原理就是按照权重,顺序遍历服务提供者列表,到达上限之后重新归零,继续顺序循环
服务调用延时
消费者缓存所有服务提供者的服务调用时延,周期性的计算服务调用平均时延,然后计算每个服务提供者调用时延与平均时延的差值,根据差值大小动态调整权重,保证服务时延大的服务提供者接受更少的消息,防止消息堆积
该策略的特点就是保证处理能力强的服务提供者接收到更多的消息,通过动态自动权重调整消除服务调用时延的震荡范围,使所有服务提供者服务调用时延接近平均值,实现负载均衡
一致性哈希
相同参数的请求总是发到同一个服务提供者,当某一台提供者宕机时,原本发往该提供者的请求,基于虚拟节点,平摊到其他提供者,不会引起剧烈的变动。平台提供默认的虚拟节点数,可以通过配置参数进行修改
路由规则
负载均衡,本地路由优先策略等路由策略通常可以满足大部分业务的线上需求,但是在一些场景中需要对路由策略设置一些过滤条件,比较常用的是基于表达式的条件路由和脚本路由
条件路由规则
1. 排除预发布机:
|
2. 白名单:(注意:一个服务只能有一条白名单规则,否则两条规则交叉,就都被筛选掉了)
|
3. 黑名单:
|
4. 服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉:
|
5. 为重要应用提供额外的机器:
|
6. 读写分离:
|
|
7. 前后台分离:
|
|
8. 隔离不同机房网段:
|
9. 提供者与消费者部署在同集群内,本机只访问本机的服务:
|
=> :之前的为消费者匹配条件,suo所有所有参数和消费者的URL进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则,分隔符之后为提供者地址列表的过滤条件,suo所有参数和提供者的URL进行对比,消费者最终只拿到过滤后的地址列表
表达式由以下三部分组成
参数:主要包括服务提供者的属性信息,消费者的消费信息 例如 method,IP等
条件:就是java表达式 例如等于(=) 不等于(!=) 或(|)与(&)
值:包括常量 例如192.168.。1.1 模糊匹配* 变量¥+变量 例如$parameter.
路由规则定制
灰度升级
服务故障
最佳实践 多机房路由
随着业务的不断发展,单个机房容量已经不足以支撑未来业务的发展,业务开始跨机房部署
跨机房服务调用会带来时延增加,网络故障概率变大等问题,如何避免跨机房服务调用,需要从路由策略上进行考虑
为了能够相互发现对方的服务,不同机房会在公用同一个服务注册中心集群,例如机房1发布了服务A,机房2发布额服务A,此时服务注册中心就会将2个不同机房的服务A地址信息推送给消费者,无论是机房1还是机房2的消费者,都讲看到两个不同机房的服务
如果仅仅依靠随机,轮询等负载均衡策略,消息将会被路由到两个机房,达不到跨机房调用的目标,如何解决跨机房服务调用的问题?
当然在上面的条件路由规则,可以非常便捷的解决问题
在原有的负载均衡策略基础上,配置条件路由规则,由于不同机房的网段通常不同,可以根据网段条件匹配来实现地址过滤,具体配置策略如下:
app=web*,consumerIp=192.168.1.*=>providerIP=192.168.1.*.
对于机房A中所有的Web前台应用,只访问本机房内部的服务提供者,不夸机房调用
集群容错
集群服务调用失败后,服务框架需要能够在底层自动容错,容错策略很多,分别适用于不同场景
在分布式服务框架中,业务消费者不需要了解服务提供者的具体位置,它发起的服务调用请求也不包含具体地址信息,因此,某个服务提供者是否可用对消费者而言无关紧要,最终的服务调用成功才是最重要的
经过服务路由之后,选定某个服务提供者进行远程服务调用,但是服务调用可能会出错,小面我们就对可能的故障场景进行分析
1. 通信链路故障
这里的通信链路故障是指消费者和服务提供者之间的链路(通常为长连接),可能导致链路中断的原因有
1)通信过程中,对方突然当即导致链路中断
2)通信过程中,对方因为解码失败等原因Rest掉连接,导致链路中断
3)通信过程中,消费者Write SocketChannel发生IOException导致链路中断
4)通信过程中,消费者read SocketChannel发生IOException导致链路发生中断
5)通信双方因为心跳超时,主动close SocketChannel导致链路中断
6)通信过程中,网络发生闪断故障
7)通信过程中,交换机异常导致链路中断
8)通信过程中,消费者或者服务提供者因为长时间Full GC导致链路中断
无论何种原因导致的链路中断,最终都会导致本次服务调用失败
2.服务端超时
当服务端无法在指定的时间内返回应答给客户端,就会发生超时,导致超时的原因主要有:
1)服务端的IO线程没有及时从网络中读取客户端请求消息,导致该问题的原因通常是I/O线程被意外阻塞或者执行长周期操作
2)服务端业务处理缓慢,或者被长时间阻塞,例如查询数据库,由于没有索引导致全表查询,耗时较长
3)服务端发生长时间Full GC.导致所有业务线程暂停运行,无法及时返回应答给客户端
3 .服务端调用失败
有时会发生服务端调用失败,导致服务端调用失败的原因主要有如下几种
1)服务端解码失败,会返回消息解码失败异常
2)服务端消息队列挤压率超过最大阀值
容错策略
需要指出的是服务调用异常不包括业务层面的处理异常,例如数据库操作异常,用户记录不存在异常
消费者根据配置的路由策略选择某个目标地址之后,发起远程服务调用,在此期间如果发生了远程服务调用异常,则需要服务框架进行集群容错,重新进行选路和调用。集群容错是系统自动执行的,上层用户并不关心底层的服务调用过程
失败自动切换(Failover)
服务调用失败自动切换策略指的是当发生RPC调用异常时,重新选路,查找下一个可用的服务提供者
服务发布的时候,可以指定服务的集群容错策略,消费者可以覆盖服务提供者的通信配置,实现个性化的容错策略
failover策略的设计思路如下:消费者路由操作完成之后,获得目标地址,调用通信框架的消息发送接口发送请求,监听服务端应答,如果返回的结果是RPC调用异常(超时,流控,解码失败等系统异常),根据消费者集群容错的策略进行容错路由,如果是failover,则重新返回到路由Handler的入口,从路由节点继续执行,选路完成之后,对目标地址进行比对,防止重新路由到故障服务节点,过滤掉上次故障服务提供者之后,调用通信框架的消息发送接口发送请求消息
分布式服务框架提供failover容错策略,但是用户在使用时需要自己保证用对地方,下面对Failover策略的应用场景进行总结:
读操作,因为通常它是幂等的
幂等醒服务,保证用户调用1次与N次效果相同
需要特别指出的是,失败重试会增加服务调用时延,因此框架必须对失败重试的最大次数做限制,通常默认为3,防止无限制重试导致服务调用时延不可控
失败通知(Failback)
在很多业务场景中,消费者需要能够获取到服务调用失败的具体信息,通过对失败错误码等信息的判断,决定后续的执行策略,例如非幂等性的服务调用
failback的设计方案如下:服务框架获取到服务提供者返回的RPC异常响应之后,根据策略进行容错,如果是Failback模式,则不再重试其他服务提供者,而是将RPC异常通知给消费者,由消费者捕获异常进行后续处理
失败缓存(failcache)
快速失败(failfast)
在业务高峰期,对于一些非核心的服务,希望只调用一次,失败也不再重试,为重要的核心服务节约宝贵的运行资源,此时,快速失败是个不错的选择
快速失败策略的设计比较简单,获取到服务调用异常之后,直接忽略异常,记录异常日志
dubbo官方文档里
Failover Cluster
- 失败自动切换,当出现失败,重试其它服务器。(缺省)
- 通常用于读操作,但重试会带来更长延迟。
- 可通过retries="2"来设置重试次数(不含第一次)。
Failfast Cluster
- 快速失败,只发起一次调用,失败立即报错。
- 通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster
- 失败安全,出现异常时,直接忽略。
- 通常用于写入审计日志等操作。
Failback Cluster
- 失败自动恢复,后台记录失败请求,定时重发。
- 通常用于消息通知操作。
Forking Cluster
- 并行调用多个服务器,只要一个成功即返回。
- 通常用于实时性要求较高的读操作,但需要浪费更多服务资源。
- 可通过forks="2"来设置最大并行数。
Broadcast Cluster
- 广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0开始支持)
- 通常用于通知所有提供者更新缓存或日志等本地资源信息。
重试次数配置如:(failover集群模式生效)
<dubbo:serviceretries="2"/> |
或:
<dubbo:referenceretries="2"/> |
或:
<dubbo:reference> <dubbo:methodname="findFoo"retries="2"/> </dubbo:reference> |
集群模式配置如:
<dubbo:servicecluster="failsafe"/> |
或:
<dubbo:referencecluster="failsafe"/> |
服务注册中心
对于服务提供者,他需要发布服务,由于应用系统的复杂性,服务的数量,类型不断膨胀,对于服务消费者,他最关心的事如何获取它需要的服务。对于服务提供方和服务消费者来说,他们还有可能兼具这两种角色:既需要提供服务,又需要消费服务
如何有效的管理服务订阅/发布,避免硬编码地址信息分布式服务框架需要解决的一个问题。通过奖服务统一管理起来,可以有效的优化内部应用对服务发布/使用的流程管理,服务注册中心就是专门用来管理服务订阅/发布的配置管理节点
服务提供者:
服务提供者就是发布服务的服务提供方,它通常就是一个普通的java实现类
服务消费者:
服务消费者就是调用远程服务的消费方,它可能就是哥简单的客户端,web前台,也可能嵌套在某个服务内部
服务注册中心:
服务注册中心就是分布式服务框架的目录服务器,相比于传统的目录服务器,他有如下几个特点:
1)高HA:支持数据持久化,支持集群
2)数据一致性问题:集群中所有的客户端应该看到同一份数据,不能出现读或者写数据不一致
3)服务变更主动推送:当注册中心的数据发生变更时(增加,删除,修改)需要能勾及时将变化的数据通知给客户端
当服务越来越多时,服务URL配置管理变得越俩越困难,F等和硬件负载均衡的单点压力也越来越大,此时需要一个服务注册中心,动态的注册和发现服务,使服务位置透明。
1)服务提供者在启动时,根据服务发布文件中配置的服务发布信息向注册中心注册自己提供的服务
2)服务消费者在启动时,根据消费者配置文件中配置的服务消费信息向注册中心订阅自己所需的服务,消费者刷新本地缓存路由表
3)注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心主动推送变更给消费者,消费者刷新本地缓存的路由表
4)服务消费者从本地缓存的服务提供者列表地址中,给予负载均衡算法选择一台服务提供者进行调用
支持对等集群
服务注册中心需要支持对等集群,其中某一个或多个服务注册中心进程当机,不会导致服务注册中心集群功能不可用
对于客户端,无论服务注册中心集群配置多少个进程,客户端只需要连接其中某一个即可(服务端之间自己进行数据同步)
订阅发布机制
服务注册中心需要支持服务的订阅发布,对付服务提供者,可以根据服务名等信息动态发布服务;对于消费者,可以根据订阅关系主动获得服务发布者的地址信息等。
订阅发布机制还有一个比较重要的机制就是对变化的监听和主动推送能力
消费者可以监听一个活着多个目录服务,当目录服务名称,内容发生变更时,消费者可以时实时的获得变更的数据活着变更后的结果信息
服务提供者可以发布一个活着多个服务,动态修改服务名称,服务内容等,可以主动讲修改后的数据活着修改后的结果推送给所有坚挺此服务目录的消费者
订阅发布优点
透明化路由:服务提供者何消费者解耦。服务提供者位置透明,消费者不需要再硬编码服务提供者地址
服务健康状态检查:服务注册中心可以实时检查发布服务的质量,如果服务提供者宕机,由服务注册中心实时通知给消费者
弹性伸缩能力(动态发现)
应用在云端部署之后,由于VM资源占用率过高,动态伸展出一个新的服务提供者,服务注册中心将新增的服务提供者地址信息推送给消费者,消费者刷新本地路由表之后可以访问新的服务提供者,实现服务的弹性伸缩
可靠性:
服务注册中心需要支持对等集群,任意一台宕机后,服务都能自动切换到其他正常的服务注册中心,腐国服务中心全部宕机,只影响新的服务注册,已发布服务的下线,不影响服务的正常运行和调用,消费者可以依靠本地缓存的服务路由表进行路由
除了服务注册中心自身可靠性,服务提供者的健康状态检查也由服务注册中心负责监测。服务注册中心通过长连接心跳检测服务提供者的存在,服务提供者宕机,注册中心将立即推送服务下线时间通知给消费者,消费者将下线的服务提供者地址从缓存的路由表中删除,新接入的消息将不再路由到故障节点,实现实时故障隔离
基于Zookeeper的服务注册中心设计
服务订阅发布流程
Zookeeper是Apache Hadoop的一个子项目,它主要用来解决分布式应用中经常遇到的一些数据管理问题,如统一命名服务、状态同步服务、集群管理、分布式应用同意配置等
基于zookeeper的服务订阅发布流程设计如图
第一步:zookeeper客户端通过创建Zookeeper的一个实例对象连接Zookeeper服务器,然后调用这个接口来和服务器交互
第二步:根据服务提供者发布的服务列表寻环调用create(String path,byte[]data,List<ACL>all,createMode)接口,创建目录节点,同时将服务属性写入目录节点的内容中,create方法用于创建一 个给定路径的目录节点,并 . 给他设置数据。createMode标识有四种形式的目录节点,分别如下:
PERSISTENT:持久化目录节点,这个节点的存储数据不会丢失
PERSISTENT_SEQUENTIAL:顺序自动编号的目录节点,这个目录节点会根据当前已经存在的节点自动加一,然后向客户端返回已经成功创建的目录节点
EPHEMERAL:临时目录节点,一旦创建这个节点的客户端与服务端口回话超时,这种节点就会自动删除
EPHEMERAL_SEQUENTIAL:临时自动编号节点
第三步:与第一步相同
第四步:消费者根据消费的服务名,从zookeeper server的服务发布目录中查询已经发布的服务地址和属性信息。getData(String path,boolean watch,Stat stat)方法用于获取这个path对应的目录节点存储的数据,数据的版本等信息可以通过stat指定,同时还可以射者是否监控这个目录节点数据的状态。获得服务提供者信息之后,将其更新到本地缓存中,避免每次服务调用都要实时的去Zookeeper Server 查询
第五步: 消费者监听服务提供者列表,当有服务下线时,可以获取zookeeper server通知消息
第六步:服务提供者调用create(String path, byte[]data,List<ACL>all,createMode createMode)方法动态注册新的服务
第七步:消费者通过监听器Wather的process(WatchedEvent event)方法获取变更的路径信息,然后调用getData(String path, boolean watch, Stat stat)接口获取变更的数据,最后更新本地缓存的服务路由列表
第八步:服务提供者取消已发布的服务,调用delete(String path,int version)方法删除path对应的目录节点,version为-1可以匹配任何版本,也就删除了这个目录节点的所有数据
第九步: 消费者接受到变更通知推送消息后,根据删除的路径获取删除的具体服务信息,跟新本地缓存的服务路由表,将已经下线的服务从路由表中删除
第十步:ZK客户端与服务端的连接绘画终止,存储在ZK上的所有临时数据与注册的订阅者都会自动移除
第十一步:消费者获取服务列表被删除的通知消息后,更新本地缓存的路由表,将发生会话超时的服务提供者从路由表中清除掉,直到对方回复于Zookeeper server连接
在实际项目中,消费者和服务提供者的启动顺序是无法控制的,所以步骤一,二和步骤三,四可以互换,对功能无影响
服务健康状态检测
基于ZooKeepr客户端和服务端的长连接和会话超时控制机制,可以非常方便的实现服务健康状态检测
在Zookpper中,客户端和服务端建立连接后,会话随之建立,生成一个全局唯一的Session ID。服务器和客户端之间维护的是一个长连接,在SESSION_TIMEOUT周期内,服务端回检测与客户端的链路是否正常(客户端定时向服务器发送心跳消息,服务重制下次SESSION_TIMEOUT时间),因此在正常情况下,Session会一直有效,并且ZK集群所有机器上都会保存这个session信息,在出现网络或者其他问题情况下(客户端宕机、网络闪断),如果客户端与之前连接的Zookeeper Server断连了,此时客户端会主动在地址列表中选择新的地址进行连接
如果ZK客户端宕机,或者网络出现故障,超过了SESSION_TIMEOUT后服务端仍然没有受到客户端的心跳消息,则服务器认为这个session已经结束了,在ZK中很多数据状态和会话绑定的,一旦会话失效,那么zk就开始清除和这个会话相关的信息,包括这个会话创建的临时节点和所有Wather。消费者获取到相关通知消息后,根据时间类型进行判断就可以得到哪些服务提供者一经宕机或者下线,通过服务名等地址信息更新路由表,即可防止新接入的消息路由到故障节点
对等集群防止单点故障
Zookeeper集群通过2n+1台server组成,每台server都知道彼此的存在,每台server都维护内存状态镜像以及持久化存储的事务日志和快照,对于2n+1server,只要n+1台(大多数)server可用,整个集群就保持可用
系统启动时,集群中的server会选举出一台server为Leader,其他的就作为Follower。接着由Follower来服务client的请求,对于不改变系统一致性状态的读操作,由Follower的本地内存数据库直接给Client返回结果,对于会改变系统状态的更新操作,则交由Leader进行提议投票,超过半数通过后返回结果给Client
Zookeeper集群管理的核心是原子广播,这个机制保证了各个server之间的数据同步。实现这个机制的协议叫做ZAB协议,ZAB协议由两种模式,分别是恢复模式和广播模式。当服务启动或者在Leader崩溃后,Zab就进入了恢复模式。当Leader被选举出来,且大多数Server完成了呵Leader的状态同步后,恢复模式就结束了。状态同步保证了Leader和Server具有相同的系统状态
一旦Leader已经和多数的Follower进行了状态同步后,他就可以开始广播消息了,即进入了广播状态。这时候当一台Server加入Zookeeper服务中时,它会在恢复模式下启动,发现Leader并和leader进行状态同步。待到同步结束,他也参与消息广播。Zookeeper服务一致维持在广播状态,直到Leader崩溃了或者Leader失去了大部分的Follower的支持
广播模式:
变更通知机制
服务提供者将服务注册测心保存在Zookeeper的某个服务目录节点中,消费者监控服务配置信息的状态。一旦配置信息发生变化,集群中的每个消费者实例就会收到Zookeeper的通知,然后从Zookeeper server获取新的服务注册信息应用到系统中