@TOC
gRPC 是一款高性能、开源的 RPC 框架,产自 Google,基于 ProtoBuf 序列化协议进行开发,支持多种语言(C++、Golang、Python、Java等) gRPC 对 HTTP/2 协议的支持使其在 Android、IOS 等客户端后端服务的开发领域具有良好的前景。 gRPC 提供了一种简单的方法来定义服务,同时客户端可以充分利用 HTTP2 stream 的特性,从而有助于节省带宽、降低 TCP 的连接次数、节省CPU的使用等。
(1)服务端:服务端需要实现.proto中定义的方法,并启动一个gRPC服务器用于处理客户端请求。gRPC反序列化到达的请求,执行服务方法,序列化服务端响应并发送给客户端。
(2)客户端:客户端本地有一个实现了服务端一样方法的对象,gRPC中称为桩或者存根,其他语言中更习惯称为客户端。客户调用本地存根的方法,将参数按照合适的协议封装并将请求发送给服务端,并接收服务端的响应。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OnrdYDL7-1616151589083)(https://raw.githubusercontent.com/grpc-nebula/grpc-nebula/master/images/grpc_transport.png)]
通信模型示意图
使用场景1:针对利用Nginx做grpc反向代理的场景,服务提供者可以通过配置文件将Nginx的地址注册到Zookeeper
使用场景2:针对服务器跨网段调用时会被映射为另一个IP的场景,应允许服务将自身地址配置为一个映射的IP
在配置文件,增加自定义IP与端口的配置信息
# 可选,类型string,说明:服务注册时指定的IP,优先级高于common.localhost.ip参数
# common.service.ip=
# 可选,类型int,说明:服务注册时指定的端口
# common.service.port=
在服务端向注册中心进行注册时,会将服务真实的IP与端口添加到real.ip
和real.port
参数中,如果配置了自定义的IP与端口,则使用该配置的IP与端口对服务进行注册;如果未配置,则使用真实的ip与端口进行注册;无论是否有配置,真实的IP与端口都将添加到real.ip
与real.port
参数中。
涉及到的模块与代码:
orientsec-provider 模块:
修改 provider_registry()
添加 orientsec_add_property_into_url_inner()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LIPmpPH6-1616151589085)(https://raw.githubusercontent.com/grpc-nebula/grpc-nebula/master/images/workflow.png “consumer workflow”)]
多条路由规则工作原理:
流程如上流程图所示, 如果多条规则,grpc-c会一条一条的匹配, 对于每一条,客户端先看 =>前面的匹配条件,是不是限制本身的,不是的话,跳过;
是的话,继续看=>后面的 服务端IP过滤条件,对provider 列表中的provider进行遍历过滤,如果是限制访问的,置黑名单标志位。
多条规则是 “与” 的工作模式,有一条限制了访问,就不能访问了。
14.1原理分析 框架支持以下四种负载均衡算法: (1) 随机算法 pick_first 实现原理:数据集合下标随机数 (2) 轮询算法 round_robin 实现原理:数据集合下标加1,取余运算 (3) 加权轮询算法 weight_round_robin 实现原理:采用ngix的平滑加权轮询算法。 (4) 一致性Hash算法 consistent_hash 实现原理:采用MD5算法来将对应的key哈希到一个具有232)-1的数字空间中。同时,引入虚拟机器节点,解决数据分配不均衡的问题。
14.2实现思路
配置文件:
consumer.loadbalance.mode=request
consumer.default.loadbalance=pick_first
配置文件: consumer.loadbalance.mode=request consumer.default.loadbalance=round_robin
配置文件:
consumer.loadbalance.mode=request
consumer.default.loadbalance=weight_round_robin
//可选,类型int,缺省值100,说明:服务provider权重,是服务provider的容量,在负载均衡基于权重的选择算法中用到
provider.weight= 400
Note: 根据经验或者服务器性能对所有服务器进行权重估算,处理能力越强,权重(黑体加粗数值)越大。算法会根据配置的权重比进行任务分配,权重越大,被调用的次数越多。
配置文件:
consumer.loadbalance.mode=request
consumer.default.loadbalance=consistent_hash
consumer.consistent.hash.arguments=name,no
consumer.backoff.maxsecond =120
配置服务调用出错后自动重试次数后,可以启用服务容错功能,当调用某个服务端出错后,框架自动尝试切换到提供相同服务的服务端再次发起请求。
调用某个服务端,如果连续5次请求出错,自动切换到提供相同服务的新服务端。(5这个数值支持配置)
调用某个服务端,如果连续5次请求出错,如果此时没有其他服务端,增加一个惩罚连接时间(例如60s)。
定义一个客户端调用服务端出现错误的数据集合:
/**
* 各个【客户端对应服务提供者】服务调用失败次数
* key值为:consumerId@IP:port
* value值为: 失败次数
* 其中consumerId指的是客户端在zk上注册的URL的字符串形式,@是分隔符,IP:port指的是服务提供者的IP和端口
*/
ConcurrentHashMap<String, AtomicInteger> requestFailures = new ConcurrentHashMap<>();
当客户端调用服务端抛出异常时,将错误次数累加到以上的数据集合中。当客户端调用同一个服务端失败达到5次时,进行以下处理:
如果服务端个数大于1,将出错的服务端从客户端内存中的服务端候选列表中移除,然后重新选择一个服务端;
如果服务端个数为1,先记录一下当前的时间,然后出错的服务端从客户端内存中的服务端候选列表中移除。
如果服务端个数为0,但是注册中心上服务端个数大于0,并且当前时间与从内存中删除服务端的时间差大于惩罚时间时,将注册中心上服务端列表更新到客户端内存中,然后调用负载均衡算法重新选择服务端。
涉及到的模块与代码:
orientsec-consumer 模块:
新增 failover_utils类
新增 record_provider_failure()方法
修改 BlockingUnaryCallImpl()方法嵌入调用接口
调用某个服务端,如果连续出错5次(5次内有一次调用成功,会重置失败次数,以达到连续的效果;5这个数值支持配置),会把该服务从服务端列表中摘除该服务端节点,通过FATAL ERROR信息的日志记录服务调用失败的相关情况;被移除的服务在10分钟后(时间支持配置),自动恢复到服务端列表中。
在配置文件,增加服务恢复时间的配置
# 可选,类型integer,缺省值5,说明:连续多少次请求出错,自动切换到提供相同服务的新服务器
# consumer.switchover.threshold=5
# 可选,类型int,说明:服务端节点调用失败被移除请求列表后,经过多长时间将该服务端节点重新添加回服务端候选列表
# 单位毫秒,默认值600000,即600秒,即10分钟
# consumer.service.recoveryMilliseconds=600000
服务调用失败时,比较当前失败服务的调用次数,如果服务端失败达到5次时,进行以下处理:
(1)将该服务从服务端列表中移除,并通过FATAL ERROR信息的日志进行输出;
(2)通过一个延迟执行的线程,在10分钟后,将该服务恢复到服务端列表中;
(3)重置该服务的失败次数,并重选服务提供者。
涉及到的模块与代码:
orientsec-grpc-core 模块:
修改 com.orientsec.grpc.consumer.ErrorNumberUtil#recordInvokeInfo
修改 com.orientsec.grpc.consumer.ErrorNumberUtil#removeCurrentProvider
新增 com.orientsec.grpc.consumer.ErrorNumberUtil#resetFailTimes
当服务调用出错时,可通过配置的重试次数进行重试,调用重试次数的配置支持到服务级别以及服务方法级别;重试次数配置优先级如下:方法级别 > 服务级别 > 默认重试配置
在配置文件,增加服务调用重试次数的相关配置,具体如下:
# 可选,类型int,缺省值0,0表示不进行重试,说明:服务调用出错后自动重试次数
# consumer.default.retries=0
# 可选,类型int,说明:指定服务名称的服务调用出错后,自动重试次数,[]中配置指定的服务名称
# consumer.default.retries[helloworld.Greeter]=0
# 可选,类型int,说明:指定服务的方法调用出错后,自动重试次数,[]中配置指定服务名称及方法名
# 最小可到指定到方法名
# consumer.default.retries[helloworld.Greeter.sayHello]=0
当某一服务在调用出错时,框架会进行调用重试,重试的次数根据配置来确定。在进行重试时,会根据当前出错服务的方法、服务名、默认配置来选择重试次数;获取重试次数的优先级:方法级别 > 服务级别 > 默认重试配置,确认重试次数后,会进行服务调用重试。
例:当前服务名:helloworld.Greeter,方法名为sayHello。当sayHello方法调用出错时,优先从配置文件获取consumer.default.retries[helloworld.Greeter.sayHello]属性值作为重试次数进行调用重试;如果未配置,则获取consumer.default.retries[helloworld.Greeter]属性值,若该属性也未配置,则取consumer.default.retries的配置作为重试次数。
涉及到的模块与代码:
orientsec-consumer 模块:
修改 BlockingUnaryCall() 判断调用结果,失败的话重新调用
综合考虑,digest权限控制方案比较适合grpc框架,因此采用这种方案进行访问控制。
如果配置了zookeeper访问控制用户名和密码,那么在创建Zookeeper Client时,增加ACL验证数据。即客户端和服务端访问zookeeper时,需要进行ACL验证。验证失败的情况,无法正常访问服务。
多个服务端提供服务的时候,能够区分主服务器和备服务器。当主服务器可用时客户端只能调用主服务器,不能调用备服务器;当所有主服务器不可用时,客户端自动切换到备服务器进行服务调用;当主服务器恢复时,客户端自动切换到主服务器进行服务调用。
给服务端添加一个master属性,用来标识服务端是主服务器还是备服务器,master等于true表示主服务器,master等于false表示备服务器,master缺省时为true。
当客户端启动时,首先根据服务名获取所有的服务端列表,然后根据每个服务端的master属性进行筛选操作:
(1) 当服务端列表中全部都是主服务器的时候,服务端列表不发生变化
(2) 当服务端列表中全部都是备服务器的时候,服务端列表不发生变化
(3) 当服务端列表中既有主服务器也有备服务器的时候,将备服务器从服务列表中移除出去,只保留主服务器
同时,客户端监听注册中心中服务端主备属性的变化,一旦监听到变化,重新获取服务端列表,并进行以上筛选操作。
涉及到的模块和代码:
orientsec-provider 模块:
增加provider的master属性
orientsec-consumer 模块:
修改 consumer_query_providers 函数
增加 master 和 online 属性的判断和解析
服务端添加一个group属性,用来标识服务端的服务分组,group缺省值为空,表示没有服务分组。客户端也添加一个group属性,用来标识当前客户端可以调用的服务端分组。
当客户端启动时,首先根据服务名获取所有的服务端列表,然后根据客户端的group属性和每个服务端的group属性,对服务端列表进行筛选操作:
(1) 当客户端group属性为空的时候,服务列表不发生变化
(2) 当客户端group属性不为空的时候,首先获取高优先级分组的服务端,如果获取不到,再获取优先级低的服务端。只要某个优先级分组的服务端获取到,就将获取到的服务端作为客户端的服务端列表。如果所有的优先级的分组服务端都没有获取到,客户端报错,提示找不到服务端。
同时,客户端监听注册中心中服务端和客户端分组的变化,一旦监听到变化,重新获取服务端列表,并进行以上筛选操作。 客户端与服务端允许对指定服务名的分组进行单独配置,配置项如下所示:
# 客户端consumer 在中括号[]中配置指定服务的服务名
consumer.invoke.group[helloworld.Greeter]=A1
# 服务端provider 在中括号[]中配置指定服务的服务名
provider.group[helloworld.Greeter]=B1
指定服务名的配置方式优先级高于未指定服务名的配置方式。
例:服务名为A的服务进行注册时,如果同时配置了group与group[A]两个属性,优先取group[A]的属性值作为服务的分组信息,同时如果有服务名为B的服务进行注册时,因为没有配置group[B]这个属性,所以会取group的属性值作为服务的分组信息。
涉及到的模块和代码:
orientsec-consumer 模块:
新增 orientsec_grpc_consumer_control_group.cc
修改 consumer_query_providers 函数
增加group属性的比对和解析
支持同一项目不同类型的grpc服务具有不同的可见性。
项目中可能会包括两类grpc服务,对于内部项目组件间grpc调用服务,此类服务并不对外暴露,因此应该避免外部项目可见;对于项目对外提供的grpc服务则需要允许外部系统可见。
公共注册中心参数配置包括:注册中心集群地址、注册根路径、digest模式ACL的用户名、digest模式ACL的密码。参数名称如下:
zookeeper.host.server (zookeeper.host.server和zookeeper.private.host.server至少配置一个参数)
common.root (可选参数,默认值/Application/grpc)
zookeeper.acl.username(可选参数)
zookeeper.acl.password(可选参数)
私有注册中心参数配置包括:注册中心集群地址、注册根路径、digest模式ACL的用户名、digest模式ACL的密码。参数名称如下:
zookeeper.private.host.server (zookeeper.host.server和zookeeper.private.host.server至少配置一个参数)
zookeeper.private.root (可选参数,默认值/Application/grpc)
zookeeper.private.acl.username(可选参数)
zookeeper.private.acl.password(可选参数)
如果服务端所有的服务都是公共服务(外部服务),只需要配置“公共注册中心参数”。
如果服务端所有的服务都是私有服务(内部服务),只需要配置“私有注册中心参数”。
如果服务端同时存在公共服务、私有服务,“公共注册中心参数”和“私有注册中心参数”都需要配置。
如果客户端只调用公共服务(外部服务),只需要配置“公共注册中心参数”。
如果客户端只调用私有服务(内部服务),只需要配置“私有注册中心参数”。
如果客户端同时调用公共服务、私有服务,“公共注册中心参数”和“私有注册中心参数”都需要配置。
如果系统存在私有服务(内部服务),需要配置哪些服务属于私有服务、哪些服务属于公共服务。
参数public.service.list
表示公共服务名称列表,多个服务名称之间以英文逗号分隔。该参数可选,如果不配置,表示所有服务都是公共服务。
参数private.service.list
表示私有服务名称列表,多个服务名称之间以英文逗号分隔。该参数可选,如果不配置,将公共服务名称列表之外的服务都视为私有服务。
公共服务向公共注册中心上注册,私有服务向私有注册中心上注册。
给服务端增加一个服务类型(service.type
)(公共/私有)的属性,服务类型根据服务所在的注册中心来判断,在公共注册中心上的服务为共有服务,在私有注册中心上的服务为私有服务。
公共注册中心集群、私有注册中心集群分开管理。
公共注册中心集群服务注册路径统一为/Application/grpc
,并且不设置访问控制权限。
私有注册中心集群服务注册路径为/Application/grpc/private/xxx
,xxx表示应用(或开发团队)。区分内外部服务的应用首先需要向zookeeper管理员申请私有注册中心的服务注册路径。
对于私有注册中心集群,不同应用(或开发团队)申请不同的注册路径,zookeeper管理员给不同注册路径设置不同访问控制权限(digest模式)。
对于私有注册中心集群,为了方便服务治理平台管理注册中心,zookeeper管理员将服务治理平台服务器的IP地址列表配置到每个私有注册路径节点的ACL中,实现服务治理平台可以免密访问私有注册中心集群。
涉及到的模块和代码:
orientsec-registry 模块:
修改 orientsec_grpc_registry_zk_intf_init()
修改 registry(url_t* url)
新增 zk_prov_reg_init()
新增 zk_cons_reg_init()
控制zookeeper的断线重连时间
配置文件中增加zookeeper断线重连最长时间配置项。
# 可选,类型int,缺省值30,单位天,即缺省值30天,说明:ZK断线重连最长时间
# zookeeper.retry.time=30
修改创建Zookeeper Client的代码,根据配置的重连最长时间计算重连的次数,创建重试指定次数的重试策略(RetryNTimes),在创建Zookeeper Client选用该重试策略并启用。
涉及到的模块与代码:
orientsec-registry 模块:
修改 zk_conn_watcher_g()
在配置文件“dfzq-grpc-config.properties”增加如下配置:
# 可选,类型string,说明:该参数用来手动指定提供服务的服务器地址列表。
# 使用场合: 在zookeeper注册中心不可用时,通过该参数指定服务器的地址;如果有多个服务,需要配置多个参数。
# 特别注意: 一旦配置该参数,客户端运行过程中,即使注册中心恢复可用,框架也不会访问注册中心。
# 如果需要从配置中心查找服务端信息,需要注释掉该参数,并重启客户端应用。
# xxx表示客户端调用的服务名称
# service.server.list[xxx]=10.45.0.100:50051
service.server.list[xxx]=10.45.0.100:50051,10.45.0.101:50051,10.45.0.102:50051
涉及到的模块和代码:
新增 obtain_appointed_provider_list()
新增 init_provider_from_appointed_list()
新增 init_provider_by_host_port()
修改 orientsec_grpc_consumer_register()
demo-sync-client.cpp:
int main(int argc, char** argv) {
// Instantiate the client. It requires a channel, out of which the actual RPCs
// are created. This channel models a connection to an endpoint (in this case,
// localhost at port 50051). We indicate that the channel isn't authenticated
// (use of InsecureChannelCredentials()).
//gpr_set_log_verbosity(GPR_LOG_SEVERITY_INFO);
//gpr_set_log_verbosity(GPR_LOG_SEVERITY_ERROR);
//gpr_set_log_verbosity(GPR_LOG_SEVERITY_DEBUG);
// set log output target
gpr_set_log_target(GPR_LOG_WIN_TO_FILE);
gpr_thd_id thd_id;
//开启n线程并发调用
for (int i = 0; i < 1; i++) {
gpr_thd_new(&thd_id, multiple, NULL, NULL);
}
getchar();
return 0;
}
默认输出到std,即不调用上述API(gpr_set_log_target).
调用如上API,相当于打开log开关,默认会将error log 输出到 client.exe 同目录下 //logs//nebula.log 文件中,方便调查问题。
当服务端与zookeeper断开连接、服务注册信息丢失后,如果客户端与服务端连接正常,那么客户端与服务端依然可以正常通信。