gRPC扩展研究
naco提供服务健康和负载均衡
nacos的心跳机制解决了服务的可用性,通过一些参数自行调整健康检查的条件。
当前nacos能支持gRPC的负载策略只有"轮询"。
nacos配置属性表:
|
配置
|
默认值
|
描述
|
|
spring.cloud.nacos.discovery.server-addr
|
127.0.0.1:8848
|
注册中心服务器地址
|
|
spring.cloud.nacos.discovery.namespace
|
命名空间
|
|
|
spring.cloud.nacos.discovery.service
|
${spring.application.name}
|
当前服务名称
|
|
spring.cloud.nacos.discovery.weight
|
1
|
权重, 1到100,值越大,权重越大
|
|
spring.cloud.nacos.discovery.network-interface
|
当IP未配置时,注册的IP为此网卡所对应的IP地址,如果此项也未配置,则默认取第一块网卡的地址
|
|
|
spring.cloud.nacos.discovery.ip
|
注册ip , 当前服务注册到nacos的IP,最高优先级
|
|
|
spring.cloud.nacos.discovery.port
|
${server.port}
|
注册端口 , 当前服务注册到nacos的端,默认情况下将自动检测,不需要配置
|
|
spring.cloud,nacos.discovery.metadata
|
元数据 , 可以使用key-value格式定义一些元数据
|
|
|
spring.cloud.nacos.discovery.cluster-name
|
Default
|
集群名称
|
|
ribbon.nacos.enabled
|
true
|
是否集成ribbon
|
|
spring.cloud.nacos.discovery.naming-load-cache-at-star
|
false
|
是否客户端启动时读取本地配置,如果是则也会持久化服务列表到本地
|
|
spring.cloud.nacos.discovery.heart-beat-interval
|
5s
|
客户端向服务端发送心跳间隔时间
|
|
spring.cloud.nacos.discovery.heart-beat-timeout
|
15s
|
nacos客户端向服务端发送⼼跳的时间间隔,默认5s注:客户端向服务端每隔5s向服务端发送⼼跳请求,进⾏服务续租,告诉服务端该实例IP健康。若在3次⼼跳的间隔时间(默认15s)内服务端没有接受到该实例的⼼跳请求,则认为该实例不健康,该实例将⽆法被消费。如果再次经历3次⼼跳的间隔时间,服务端接受到该实例的请求,那么会⽴刻将其设置外健康,并可以被消费,若未接受到,则删除该实例的注册信息。推荐配置为5s,如果有的业务线希望服务下线或者出故障时希望尽快被发现,可以适当减少该值
|
|
spring.cloud.nacos.discovery.watch.enabled
|
true
|
是否观察服务列表
|
|
spring.cloud.nacos.discovery.watch-delay
|
30000
|
观察服务列表变化
|
|
spring.cloud.nacos.config.server.addr
|
配置中心地址
|
|
|
spring.cloud.nacos.config.name
|
${spring.application.name}
|
配置名称
|
|
spring.cloud.nacos.config.prefix
|
配置名称前缀
|
|
|
spring.cloud.nacos.config.group
|
DEFAULT_GROUP
|
nacos的组配置
|
|
spring.cloud.nacos.config.file-extension
|
properties data id的后缀
|
|
|
spring.cloud.nacos.config.timeout
|
3000
|
从nacos获取配置超时时间
|
|
spring.cloud.nacos.config.namespace
|
命名空间
|
|
|
spring.cloud.nacos.config.contentPath
|
Nacos server的上下文路径
|
|
|
spring.cloud.nacos.config.sharedDataids
|
共享配置的dataid , 共享配置的数据标记,用","分隔
|
|
|
spring.cloud.nacos.config.refreshableDataids
|
共享配置的动态刷新dataid , 共享配置的动态刷新数据标记, 用","分隔
|
|
|
com.alibaba.nacos.naming.log.level
|
info
|
nacos日志级别
|
负载均衡策略
从一堆服务提供者(Provicer)中选择一个为当前请求服务的策略.
负载均衡实现方式:
1.Proxy Model存在一个 Load Balancer (LB) proxy 的中间层, 它会追踪Provider的状态.客户端不用只需要知道 Load Balancer (LB) proxy.通常是基于软件如 LVS,HAproxy等实现. LB一般具备健康检查能力,能自动摘除不健康的服务实例.
2.Client客户端知道每个Provicer, 每次服务客户端负载从中选择一个为本次请求服务.通常是基于注册中心实现. 服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查.服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。
gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展。
其基本实现原理:
1.服务启动后gRPC客户端向命名服务器发出名称解析请求,名称将解析为一个或多个IP地址,每个IP地址标示它是服务器地址还是负载均衡器地址,以及标示要使用那个客户端负载均衡策略或服务配置。
2.客户端实例化负载均衡策略,如果解析返回的地址是负载均衡器地址,则客户端将使用grpclb策略,否则客户端使用服务配置请求的负载均衡策略。
3.负载均衡策略为每个服务器地址创建一个子通道(channel)。
4.当有rpc请求时,负载均衡策略决定那个子通道即grpc服务器将接收请求,当可用服务器为空时客户端的请求将被阻塞。
根据gRPC官方提供的设计思路,基于进程内LB方案(即第2个案,阿里开源的服务框架Dubbo 也是采用类似机制)结合分布式一致的组件(如Zookeeper、Consul、Etcd),可找到gRPC服务发现和负载均衡的可行解决方案
XXX实现的是基于 ZK 的客户端负载均衡, 核心源码参见:
Grpc.grpcEx() --> GrpcClient.grpc() --> GrpcCore.getClient() -->RpcClientProxy //GrpcCore.java private final class RpcClientProxy implements InvocationHandler { private ChannelInfo getChannel(Failover<ChannelInfo> failover) { ... ChannelInfo channel = locateStrategy.apply(failover); ... return channel; } }
其中的 locateStrategy 就包含负载均衡的逻辑, 默认的是 WeightFailover 的getOneAvailable() 方法:
//GrpcClient.java public <C> C grpc(Class<C> iface, Duration deadlineAfter) { return holder.getClient(iface, Failover::getOneAvailable, null,deadlineAfter); } //WeightFailover.java @Override public T getOneAvailable() { // TODO better using a snapshot current Weight<T> or a newstateful Weight<T> List<T> available = getAvailable(1); return available.isEmpty() ? null : available.get(0); } @Override public List<T> getAvailable(int n) { return getAvailable(n, emptySet()); } //根据节点权重选择 private List<T> getAvailable(int n, Collection<T> exclusions) { int sum = 0; //总权重 for (Entry<T, Integer> entry : currentWeightMap.entrySet()) { ... sum += thisWeight; } List<T> result = new ArrayList<>(); if (sum > 0) { int size = snapshot.size(); for (int i = 0; i < size; i++) { if (sum == 0) { break; } if (result.size() == n) { break; } //主要的逻辑还是随机, 随机取 [0, sum-1] 之间的一个数 int left = ThreadLocalRandom.current().nextInt(sum); Iterator<TwoTuple<T, Integer>> iterator =snapshot.iterator(); while (iterator.hasNext()) { TwoTuple<T, Integer> candidate = iterator.next(); int entryWeight = candidate.getSecond(); if (left < entryWeight) { T obj = candidate.getFirst(); if (!exclusions.contains(obj) &&filter.test(obj)) { result.add(obj); } if (result.size() == n) { break; } iterator.remove(); sum -= entryWeight; break;
} left -= entryWeight; } } } return result; }
简单来说, 我们默认的策略就是从所有服务 Provider 中随机选择一个. 只是带上权重信息了, 每个节点有初始权重(100), 每次失败会降低权重, 每次成功会增加权重
容错(Fault tolerance)
Fault tolerance 解决的问题是, 当 client 某一次请求 server 失败的时候, 如何处理本次请求.
常见的 Failover 策略:
(1).Failover失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。
(2).Failfast快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
(3).Failsafe失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
(4).Failback失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
(5).Forking并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。
(6).Broadcast广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
XXX的gRPC 实现, 没有 Failover 策略, 失败了就失败了(框架层什么也不错). 但是我们是server 节点都带权重, 每次失败, 会降低改节点的权重, 减少后续请求打到该节点的几率。
重试策略完全依赖 client 自己做。
监控
GrpcCore 中维护了一堆 GrpcCallListener. GrpcCallListener 定义如下
public interface GrpcCallListener<T> { /** * will invoked when there is no available node. */ default void noAvailableNode() throws Exception { } /** * will invoked on caller's thread. */ @Nullable default T before(ChannelInfo channel, Method method) throwsException { return null; } void onSuccess(@Nullable T before, ChannelInfo channel, Methodmethod, long costInMs) throws Exception; /** * @param t possible instance: * {@link java.util.concurrent.TimeoutException} fromcaller thread. * {@link io.grpc.StatusRuntimeException} from gRPC workerthread or caller thread. */ void onError(@Nullable T before, ChannelInfo channel, Method method,long costInMs, @Nonnull Throwable t) throws Exception; void onCancel(@Nullable T before, ChannelInfo channel, Methodmethod, long costInMs, @Nonnull CancelType cancelType) throws Exception;}
这些 GrpcCallListener 是怎么起作用的呢?
GrpcCallListener 定义了再方法调用前后(成功/失败/取消/超时)的回调行为. 有了这个机制, 我们就可以做很多事情. 我们的 RPCMonitor 就是通过 GrpcCallListener 机制实现的. 每个 GrpcClient 默认都会带上 RPCMonitor 相关的 GrpcCallListener 来达到收集调用信息的目的
浙公网安备 33010602011771号