赞
踩
@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)]
通信模型示意图
原生分析
无此功能
定制分析
读取本地的配置文件信息 dfzq-grpc-config.properties应用程序启动时,按以下顺序依次找配置文件;如果没找到,则顺延到下一条:
* 从系统环境变量ORIENTSEC_GRPC_CONFIG读取
* 用户从启动目录下的config目录下查找grpc配置文件
* 用户从当前目录下查找grpc配置文件
框架第一次读取配置文件时,会将配置文件中的所有内容读取到内存。之后获取配置文件中的属性时,直接从内存中读取。
1.2 场景描述
1.3接口设计
1.3.1 关键文件
third_party/orientsec/orientsec_common/orientsec_grpc_properties_tools.h
third_party/orientsec/orientsec_common/orientsec_grpc_properties_tools.c
1.3.2关键接口
01: 名称:orientsec_grpc_properties_init 参数:无 功能:实现配置文件的正确读取 返回值:0 正确读取 -1,-2 读取失败 02: 名称:orientsec_grpc_properties_get_value 参数: key:关键字 prefix:前缀 value:值 功能:读取各个参数的具体值 返回值: 0 通过指针返回值 -1 读取失败
1.4流程图
无
1.5代码修改思路
1.6耦合度
与原生代码无耦合
2.1原理分析
2.2场 景描述
服务provider和服务consumer通过和zookeeper进行通信实现服务注册、服务发现、服务调度、配置同步。
2.3接口设计
2.3.1 关键文件
文件1:实现接口文件
third_party\orientsec\orientsec_provider\orientsec_provider_intf.c
文件2:调用接口文件
src\cpp\server\server_cc.cc
src\cpp\server\server_builder.cc
2.3.2 关键接口
01:
名称:provider_registry
参数:
port:服务端口
sIntf:服务名
sMethods:方法名
功能:Zookeeper Resolve插件注册入口
接口全称:
void provider_registry(int port, const char *sIntf, const char *sMethods)
返回值
无
2.4示意图
3.1原理分析:
注销服务提供者,就是将服务提供者的信息或者节点从zookeeper中删除。我们将注销代码嵌入到服务器对象析构的方法中,在调用服务器对象shutdown方法时,将服务提供者信息删除。
由于我们在注册时,向zookeeper中写的是临时节点,如果程序未正常关闭,利用zookeeper自身的定时检测长连接的机制,一旦发现提供服务的程序与zookeeper之间的连接意外断开,就自动将服务提供者删除。
3.2实现思路
在新增的 orientsec -provider 模块中实现注销服务信息。
3.3 相关代码:
third_party\orientsec\orientsec_provider\orientsec_provider_intf.h
void provider_unregistry();
在Server对象的析构中调用:
src\cpp\server\server_cc.cc
4.1原理分析
服务端可以采取两种手段进行服务流量控制,一种是并发请求数控制,另一种是并发连接数控制。
provider_configurators_callback()
中会调用update_provider_connection
方法,可以通过动态配置方法进行并发请求数的修改。init_provider()
方法 ,该方法定义了服务端建立连接的初始化方法,并设置了初始化的请求数和连接数。check_provider_connection()
来实现连接数控制。4.2实现思路
4.2.2. 连接数控制流量控制:
服务端有一个集合类记录着服务端与客户端建立的连接信息。当接收到客户端建立连接的请求时,检查当前的连接数是否达到最大的连接数。如果已经达到最大连接数,拒绝客户端建立连接的请求(客户端会接收到一个异常信息);如果未达到,建立连接,并将连接信息记录到集合类中。
4.3场景描述
并发请求数使用default.requests参数进行配置,连接数使用default.connections参数进行配置。
并发请求数参数和连接数参数可以同时配置,这是从两个不同的维度进行流量控制。前者控制的是服务者的并发处理能力,后者控制的是与之连接的客户端的个数。
默认情况下,并发请求数参数值为2000,连接数为20。即同一时刻最多可以有20个客户端与服务端建立连接,同时服务端处理客户端的请求最大并发数为2000。
如果因为客户端超过了默认连接数20的限制,从而导致客户端出现异常,可以通过服务治理平台配置该服务的最大连接数。
如果因为客户端调用频繁、并发程度高,或者服务端处理业务逻辑时间较长,可以通过服务治理平台配置该服务的最大并发请求数。
4.4相关代码
涉及到的模块与代码:
grpc-core 模块:
http2传输层
修改 on_accept()增加check()
orientsec-provider 模块:
新增 bool check_provider_connection(const char *intf)
5.1实现思路
在服务注册的最后一步,注册一个监听当前服务的监听器,用来监听注册中心上该服务的以下几种配置信息。
5.1.1服务并发请求数配置(default.requests)
当监听到注册中心上并发请求数参数配置发现改变后,将新的参数值更新到内存中;当监听到注册中心上并发请求参数配置信息被删除后,查询服务端初始的并发请求数配置,将内存中的并发请求数恢复到初始配置。
5.1.2服务连接数配置(default.connections)
当监听到注册中心上连接数参数配置发现改变后,将新的参数值更新到内存中;当监听到注册中心上连接数参数配置信息被删除后,查询服务端初始的连接数配置,将内存中的连接数恢复到初始配置。
5.1.3访问保护状态配置(access.protected)
当监听到注册中心上访问保护状态配置发现改变后,将新的参数值更新到内存中。当监听到注册中心上访问保护状态配置信息被删除后,查询服务端初始的访问保护状态,将内存中的访问保护状态恢复到初始配置。
然后根据当前的访问保护状态的参数值进行以下操作:
如果参数值为true,向注册中心写入一条“禁止所有客户端访问当前服务端的路由规则”;
如果参数值为false,将注册中心上“禁止所有客户端访问当前服务端的路由规则”删除。
5.1.4 服务是否有新版本配置(deprecated)
当监听到注册中心上deprecated参数配置发现改变后,将新的参数值更新到内存中;当监听到注册中心上deprecated参数配置信息被删除后,查询服务端初始的deprecated配置,将内存中的deprecated参数值恢复到初始配置。
5.2 相关代码
涉及到的模块与代码:
orientsec-provider 模块:
新增 void init_provider(provider_t* provider)
新增 void provider_configurators_callback(url_t *urls, int url_num)
新增 check_provider_request(const char *intf)
新增 update_provider_connection(const char *intf, int conns)
新增 update_provider_access_protected(const char* intf, bool access_protected)
6.1 原理分析
服务端初始化时或者在zookeeper中动态设置deprecated参数时,修改provider的属性。
6.2 实现思路
当服务端被调用时,如果当前服务的deprecated参数值为true,打印告警日志,1天只打印一次告警日志。
1天只打印一次日志可以通过增加一个变量存储“上一次记录deprecated日志的时间戳”来实现。
告警日志内容示例:当前服务[com.orientsec.grpc.examples.helloworld.Greeter]已经过时,请检查是否新服务上线替代了该服务。
6.3 相关代码
涉及到的模块与代码:
orientsec-grpc-provider 模块:
新增 void update_provider_deprecated(const char *intf, bool deprecated)
使用场景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()
1.1原理分析
框架第一次读取配置文件时,会将配置文件中的所有内容读取到内存。之后获取配置文件中的属性时,直接从内存中读取。
1.2实现思路
配置文件读取顺序
1、从系统环境变量ORIENTSEC_GRPC_CONFIG读取
2、从当前目录下config文件夹读取
3、从当前目录下读取
1.3相关代码:
orientsec_common\orientsec_grpc_properties_tools.h
int orientsec_grpc_properties_init();
int orientsec_grpc_properties_get_value(const char *key, char *prefix, char *value);
2.1原理分析
客户端启动时, 创建 Channel 对象。在创建 Channel 对象时,增加调用注册客户端信息的接口。
2.2实现思路
新增 orientsec-consumer 模块,在该模块中增加注册客户端信息的接口和实现。
2.3相关代码
涉及到的模块与代码:
grpc-core 模块:
修改 grpc_channel_create_with_builder()
新增 channel->reginfo = orientsec_grpc_consumer_register(sn)
orientsec-consumer 模块:
新增 char* orientsec_grpc_consumer_register(const char* fullmethod)
3.1原理分析
客户端关闭Channel时,会将ZookeeperNameResolver关闭,在关闭ZookeeperNameResolver时会注销客户端信息。
3.2实现思路
在 orientsec-consumer 模块中,增加注册客户端信息的接口方法。
3.3相关代码
涉及到的模块与代码:
orientsec-consumer 模块:
orientsec_consumer\orientsec_consumer_intf.h
int orientsec_grpc_consumer_unregister(char * reginfo);
orientsec-grpc 模块:
src\core\lib\surface\channel.cc
void grpc_channel_destroy()
orientsec_grpc_consumer_unregister(channel->reginfo)
4.1 原理分析
1) GreeterClient greeter(grpc::CreateChannel("localhost:55555",*);
2) GreeterClient greeter(grpc::CreateChannel("IP:PORT",*);
1) GreeterClient greeter(grpc::CreateChannel("zookeeper:///{serviceName}",*);
4.2 场景描述
Zookeeper NameResolve定制化开发,实现基于Zookeeper的NameResolve功能。
4.3接口设计
4.3.1关键文件
文件1:Zookeeper Resolver建立文件
grpc\src\core\ext\filters\client_channel\resolver\zookeeper\zookeeper_resolver.cpp
文件2:NameResolver功能文件
grpc\src\core\lib\iomgr\zk_resolve_address.c
文件3:插件注册入口文件
grpc\src\core\plugin_registry\grpc_unsecure_plugin_registry.cc
grpc\src\core\plugin_registry\grpc_plugin_registry.cc
4.3.2关键接口
01, 名称:consumer_query_providers 参数: service_name:serviceName传入 nums:provider数量传出 hasharg:一致性哈希算法传参 功能:Zookeeper Resolve插件注册入口 接口全称: provider_t* consumer_query_providers(const char *service_name, int *nums, char*hasharg) 02: 名称:zk_blocking_resolve_address 参数: name:服务名 default_port:默认端口(如果对应provider找不到端口,则使用默认端口) addresses:接收类 功能:通过解析provider从ServiceName中获取host 接口全称: static grpc_error* zk_blocking_resolve_address( const char* name, const char* default_port, grpc_resolved_addresses** addresses) 03, 名称:zk_resolve_address 参数: name:服务名 default_port:默认端口(如果对应provider找不到端口,则使用默认端口) interested_parties:略 on_done:回调 addresses:接收类 功能:Zookeeper address resolve接口入口 接口全称: void zk_resolve_address(const char* name, const char* default_port, grpc_pollset_set* interested_parties, grpc_closure* on_done, grpc_resolved_addresses** addresses) 04, 名称:grpc_resolver_zk_init 参数:无 功能:Zookeeper Resolve插件注册入口 接口全称: void grpc_resolver_zk_init()
4.4代码修改思路
A,将Zookeeper Resolve插件注册到plugin模块初始化中
B,通过DNS架构构建自己的ZookeeperResolve
C,将ServiceName解析加入到ZookeeperResolve中,将原来的DNS解析去除替换。
D,测试是否可以完成NameResolver全过程并完成调用。
5.1 原理分析
在客户端启动,注册客户端信息时,同时注册一个监听器,用来监听服务端列表的变化。同时,将服务端列表存储到内存中。
当监听到服务端上线时,更新缓存。当监听到有服务端下线时,更新缓存。
5.2实现思路
在orientsec-consumer模块中,修改客户端注册的方法,在注册客户端信息的同时,注册一个服务端列表的监听器。然后在监听器中实现服务端列表更新后的业务逻辑。
5.3相关代码
涉及到的模块与代码:
orientsec-consumer 模块:
消费者 providers目录订阅函数
新增 void consumer_providers_callback(url_t* urls, int url_num)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LIPmpPH6-1616151589085)(https://raw.githubusercontent.com/grpc-nebula/grpc-nebula/master/images/workflow.png “consumer workflow”)]
6.2实现思路
在grpc-core模块中,首先注册一个监听路由规则的目录订阅函数 consumer_routers_callback,然后新增一个路由规则解析器ConditionRouter,然后在获取服务端列表的模块 ZookeeperNameResolver调用路由规则解析器,实现服务端列表的过滤。
6.3相关代码
涉及到的模块与代码:
grpc-core 模块:
新增 zk_blocking_resolve_address()
orientsec_consumer模块:
新增class condition_router
新增void consumer_routers_callback(url_t* urls, int url_num)
7.1原理分析
路由规则由两个条件组成,分别用于对客户端和服务端进行匹配。比如有这样一条规则:
host = 192.168.1.* => host = 192.168.2.*
该条规则表示 IP 为 192.168.1.* 的客户端只可调用 IP 为 192.168.2.* 服务器上的服务,不可调用其他服务端上的服务。路由规则的格式如下:
[客户端匹配条件] => [服务端匹配条件]
如果客户端匹配条件为空,表示不对客户端进行限制。如果服务端匹配条件为空,表示对某些客户端禁用服务。
客户端匹配条件不仅可以限定到IP(或IP段),也可以限定到项目,例如这样一条规则:
project = grpc-test-apps => host = 192.168.2.*
表示项目名称为 grpc-test-apps 的客户端只可调用 IP 为 192.168.2.* 服务器上的服务。
需要特别注意的是:IP段条件(例如192.168.2.)中只能有一个星号()。星号既可以在开头,也可以在末尾,还可以在中间。
7.2示例
假设存在Ca,Cb,Cc 三个客户端,Sa,Sb,Sc三个服务端:
1.rule= => 结果:Ca,Cb,Cc都不能访问服务
2.rule=host=Ca=> 结果:Ca不能访问服务 Cb、Cc能访问服务
3.rule=host!=Ca=> 结果:Cb,Cc不能访问服务 Ca能访问服务
4.rule= =>host=Sa 结果:Ca,Cb,Cc只能访问服务Sa
5.rule= =>host!=Sa 结果:Ca,Cb,Cc只能访问服务Sb,Sc
6.rule= host=Ca => host=Sb 结果:Ca能访问Sb;Cb,Cc能访问Sa,Sb,Sc
7.rule=host!=Ca=>host=Sb 结果:Cb,Cc能访问Sb; Ca能访问Sa,Sb,Sc
8.rule=host=Cb,Cc=>host=Sb 结果:Cb,Cc能访问Sb; Ca能访问Sa,Sb,Sc
9.rule=host=Cb,Cc=>host!=Sb 结果:Cb,Cc能访问Sa,Sc;Ca能访问Sa,Sb,Sc
多条路由规则工作原理:
流程如上流程图所示, 如果多条规则,grpc-c会一条一条的匹配, 对于每一条,客户端先看 =>前面的匹配条件,是不是限制本身的,不是的话,跳过;
是的话,继续看=>后面的 服务端IP过滤条件,对provider 列表中的provider进行遍历过滤,如果是限制访问的,置黑名单标志位。
多条规则是 “与” 的工作模式,有一条限制了访问,就不能访问了。
7.3 实现思路
使用match_when()进行客户端条件匹配,如果适用规则,客户端返回true,不适用返回false。
使用match_then()进行服务端过滤,如果适用条件,服务端被过滤掉,不适用条件,服务端不被过滤掉,然后置黑名单flag。
7.4相关代码
涉及到的模块与代码:
orientsec-consumer 模块: condition_router.cc // 按照匹配条件进行匹配,客户端适用返回true,不适用返回false bool condition_router::match_when(url_t* url) { if (when_condition.size() == 0) { return true;// 如果匹配条件为空,表示对所有消费方应用 } return match_condition(when_condition, url, NULL); } bool condition_router::match_then(url_t* url, url_t* param) { return match_condition(then_condition, url, param); } bool condition_router::match_condition(std::map<std::string, match_pair> condition, url_t* url, url_t* param) { std::map<std::string, std::string> sample = url_to_map(url); std::map<std::string, std::string>::iterator samp_iter; std::map<std::string, match_pair>::iterator cond_iter; for (samp_iter = sample.begin(); samp_iter != sample.end(); samp_iter++) { cond_iter = condition.find(samp_iter->first); if (cond_iter != condition.end()) { match_pair &pair = cond_iter->second; if (!pair.is_match(samp_iter->second, param)) { return false; } } } return true;
}
8.1 原理分析
在客户端启动注册客户端信息时,同时注册一个监听器,用来监听路由规则。路由规则在内存中缓存一份。
当监听到路由规则发生变化时,更新内存中缓存的路由规则。路由规则发生变化后,还需要重新计算当前客户端的服务端列表,然后将最新的服务端列表缓存更新到内存中。
8.2 实现思路
首先,在模块注册客户端信息时,增加注册路由规则的监听器。然后实现监听到路由规则发生变化后,对内存中路由规则、客户端可能访问的服务端列表进行更新。
每一个服务客户端,对应着一个服务名称,每一个客户端需要监听的路由规则是隶属于该服务名称的路由规则。路由规则在内存中以列表的方式存储:
static std::map<std::string, std::vector<router*> > g_valid_routers;
遍历执行路由规则,按照指定服务逐条执行路由规则
revoker_providers_list_process()
8.3 相关代码
涉及到的模块与代码:
orientsec-consumer 模块:
新增 condition_router类
新增 revoker_providers_list_process()
新增 consumer_routers_callback()
9.1原理分析
服务端权重表示的是服务端提供服务的能力,也可以简单地理解为服务端所在服务器的配置。服务端的权重越高,表示服务器提供服务的能力越强。
如果配置的负载均衡算法为“加权轮询算法”,那么各服务端被调用次数的比例等于服务端权重的比例。
9.2实现思路
提供同一个服务的服务端可能有多个,因此一个客户端内存中存储的服务端权重信息在一个集合数据类型中。
当监听到注册中心上服务端权重参数配置值发生改变,客户端将内存中的服务权重更新为修改后的参数值;当监听到注册中心上服务端权重配置信息被删除后,查询服务端初始的权重,将内存中的服务权重恢复到初始配置。
9.3相关代码
涉及到的模块与代码:
orientsec-common模块:
新增 init_provider(provider_t* provider)
orientsec-consumer 模块:
新增 consumer_configurators_callback()
10.1原理分析
缺省情况下,服务端过时标志参数值为false。如果客户端就检测到服务端的过时标志被设置为true,那么客户端调用该服务端时会记录一条警告类型的日志信息。
10.2实现思路
当监听到注册中心上服务端过时标志参数配置值发生改变,客户端将内存中的服务端过时标志更新为修改后的参数值;当监听到注册中心上服务端过时标志配置信息被删除后,查询服务端初始的过时标志,将内存中的服务端过时标志恢复到初始配置。
10.3相关代码
涉及到的模块与代码:
orientsec-consumer 模块:
新增 consumer_check_provider_deprecated()
11.1 原理分析
客户端需要监听的配置信息有以下几种:
(1)客户端指定的服务端版本号 service.version
客户端指定了服务端版本号之后,程序会优先选择具有指定版本的服务端;如果注册中心没有该版本的服务端,则不限制版本重新选择服务提供者。(使用场景:灰度发布、A/B测试)
(2)客户端对服务端的每秒钟的请求次数参数 consumer.default.requests
用来限制客户端对服务端的请求频率。
11.2 实现思路
当监听到服务端版本号参数值发生变化后,将新的服务端版本号更新到内存中,同时根据服务端版本号重选服务端;当监听到服务端版本号配置信息被删除后,将服务端版本号恢复为默认值(默认值为:空字符串),同时根据服务端版本号重选服务端。
当监听到客户端对服务端的每秒钟的请求次数参数值发生变化后,将新的每秒钟的请求次数参数更新到内存中;当监听到每秒钟的请求次数参数配置信息被删除后,将客户端对服务端的每秒钟的请求次数参数恢复为默认值(默认值为:0,即不限制)。
11.3 相关代码
涉及到的模块与代码:
orientsec-consumer 模块:
新增 orientsec_grpc_consumer_control_requests.cc
新增 orientsec_grpc_consumer_control_version.cc
12.1原理分析
增加对客户端请求数控制的功能,限制客户端对某个服务每秒钟的请求次数(Requests Per Second)。
对客户端增加请求数控制的功能,通过在注册中心上动态增加参数配置。(不支持通过在本地文件配置,因为一个系统可能需要调用多个服务端)
为了和服务端区分,将参数的名称定为consumer.default.requests。程序中新增对注册中心上的consumer.default.requests参数进行监听,缺省情况下,不限制客户端对某个服务的请求次数。
12.2实现思路
客户端每次发起调用时总会调用 ClientCalls#startCall 方法,因此可以在这个方法中增加客户端每秒请求数的控制。
为了能够获取搭配客户端本次调用的方法属于哪个服务端,需要在抽象类 ClientCall 增加一个 getFullMethod 的方法,根据这个完成方法名,可以提取出服务端的服务名称。既然在抽象类中增加了方法,需要将继承该抽象类的子类也增加上 getFullMethod 的实现。
客户端每秒钟的请求次数的控制策略如下:
a. 首先判断当前客户端是否配置了每秒钟请求次数参数值,如果没有配置,直接退出流量控制代码段
b. 如果客户端配置了参数,取出每秒钟请求次数参数值,如果参数值小于或者等于0,直接退出流量控制代码段
c. 取出调用当前服务的客户端在一秒钟内的调用次数,判断调用次数是否达到每秒钟请求次数参数值,如果达到则拦截本次调用,给客户端返回一个异常信息;如果未达到每秒钟请求次数参数值,将客户端在一秒钟内的调用次数的计算增加1。这里的调用次数计数器使用的是带有并发控制的 AtomicLong 数据结构。
12.3相关代码
涉及到的模块与代码:
Orientsec-consumer:
添加orientsec_grpc_consumer_control_requests.cc
grpc-core 模块:
修改 client_unary_call.h 和 async_unary_call.h
调用 orientsec_grpc_consumer_control_requests
14.2.1 pick_first 随机数算法
顾名思义,就是在选取后端服务器的时候,采用随机的一个方法。随机算法是最常用的算法,绝大多数情况都使用他。首先,从概率上讲,它能保证我们的请求基本是分散的,从而达到我们想要的均衡效果;其次,他又是无状态的,不需要维持上一次的选择状态,也不需要均衡因子等等。总体上,方便实惠又好用。
算法细节:
int pickfirst_lb::choose_subchannel(const char* sn, provider_t *provider, const int*nums) {
if (!sn) {
return 0;
}
if (provider == NULL || *nums == 0) {
return 0;
}
int size = *nums;
srand(time(NULL));//设置随机数种子。
int index = rand() % size;
return index;
}
配置文件:
consumer.loadbalance.mode=request
consumer.default.loadbalance=pick_first
14.2.2 round_robin 轮询算法
轮询调度算法的原理是:每一次把来自用户的请求轮流分配给内部中的服务器。如:从1开始,一直到N(其中,N是内部服务器的总个数),然后重新开始循环。
算法细节:
int round_robin_lb::choose_subchannel(const char* sn, provider_t *provider, const int*nums) {//sn="service_name" if (!sn) { return 0; } if (provider == NULL || *nums == 0) { return 0; } int index = -1; int size = *nums; index = subchannlecursors; //初始游标 index = (index + 1) % size; //索引+1 subchannlecursors++; if (subchannlecursors > *nums - 1) subchannlecursors = 0; return index; }
配置文件:
consumer.loadbalance.mode=request
consumer.default.loadbalance=round_robin
14.2.3 weight_round_robin 带权重的轮询算法
该算法中,每个机器接受的连接数量是按权重比例分配的。这是对普通轮询算法的改进,比如你可以设定:第三台机器的处理能力是第一台机器的两倍,那么负载均衡器会把两倍的连接数量分配给第3台机器。
算法细节:
int weight_round_robin_lb::choose_subchannel(const char* sn, provider_t *provider, const int*nums) { if (!sn) { return 0; } if (provider == NULL || *nums == 0) { return 0; } int i; int index = -1; int total = 0; for (i = 0; i < *nums; i++) { provider[i].curr_weight += provider[i].weight; total += provider[i].weight; if (index == -1 || provider[index].curr_weight < provider[i].curr_weight) { index = i; } } provider[index].curr_weight -= total; return index; }
配置文件:
consumer.loadbalance.mode=request
consumer.default.loadbalance=weight_round_robin
//可选,类型int,缺省值100,说明:服务provider权重,是服务provider的容量,在负载均衡基于权重的选择算法中用到
provider.weight= 400
Note: 根据经验或者服务器性能对所有服务器进行权重估算,处理能力越强,权重(黑体加粗数值)越大。算法会根据配置的权重比进行任务分配,权重越大,被调用的次数越多。
14.2.4 consistent_hash 一致性hash算法
一致性hash, 相同参数的请求总是发到同一个提供者,当某一台提供者挂掉时,本来发往该提供者的要求,基于虚拟节点,平摊到其它服务提供者,不会引发剧烈变动。
算法细节:
//choose provider based on value of hash "arg" int conistent_hash_lb::choose_subchannel(const char * sn, provider_t * provider, const int * nums, const std::string &arg) { if (!sn) { return 0; } if (provider == NULL || *nums == 0) { return 0; } // 得到服务列表 int index = 0, index_valid = 0; int size = *nums; std::string factor; //bool rebuild_tree; //used for returning of provider node cnode_s * node; if (arg.empty()) factor = sn; else factor = arg; //判断服务列表是否发生变化,如果发生变化,需要重新构造红黑树 if (get_rebuild_flag() || !compare_provider_list(provider, size)) //两次不一样返回false { // rebuild hash tree gen_hash_rbtree(provider, size); // rebuild provider_list for comparing gen_provider_list(provider, nums); set_rebuild_flag(false); } //根据参数值选择指定的服务提供者 node = conhash->lookup_node_s(factor.c_str()); if (node != NULL) { if (prov_list.size() == 0) return index_valid; std::map<int, std::string>::iterator provider_iter = prov_list.begin(); while (provider_iter != prov_list.end()) // { if (strcmp(node->get_iden(), provider_iter->second.c_str()) == 0) { index_valid = index; break; } provider_iter++; index++; } } else { std::cout << "not find corresponding provider" << std::endl; } return index_valid; } void conistent_hash_lb::reset_cursor(const char * sn) { std::map<std::string, int>::iterator iter = cursors.find(sn); if (iter == cursors.end()) { cursors.insert(std::pair<std::string, int>(sn, -1)); return; } iter->second = -1; }
配置文件:
consumer.loadbalance.mode=request
consumer.default.loadbalance=consistent_hash
consumer.consistent.hash.arguments=name,no
14.3相关代码
涉及到的模块与代码:
orientsec-consumer module: consistent_hash.cc consistent_hash_lb.cc orientsec_consumer_intf.cc md5.cc pickfirst_lb.cc round_robin_lb.cc weight_round_robin_lb.cc grpc++/grpc core: src\core\ext\filters\client_channel\client_channel.cc include\grpcpp\impl\codegen\client_unary_call.h include\grpcpp\impl\codegen\call_op_set.h include\grpcpp\impl\codegen\proto_utils.h include\grpc\grpc.h src\core\lib\surface\call.cc src\core\ext\filters\client_channel\lb_policy\round_robin\round_robin.cc src\core\lib\iomgr\zk_resolve_address.c
consumer.backoff.maxsecond =120
15.3相关代码
涉及到的模块与代码:
orientsec-consumer 模块:
新增 failover_utils.cc文件
新增类 failover_utils{}
配置服务调用出错后自动重试次数后,可以启用服务容错功能,当调用某个服务端出错后,框架自动尝试切换到提供相同服务的服务端再次发起请求。
调用某个服务端,如果连续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框架,因此采用这种方案进行访问控制。
3.2实现思路
首先,在配置文件中增加zookeeper访问控制用户和密码的配置项。
zookeeper.acl.username=admin
zookeeper.acl.password=9b579c35ca6cc74230f1eed29064d10a
如果配置了zookeeper访问控制用户名和密码,那么在创建Zookeeper Client时,增加ACL验证数据。即客户端和服务端访问zookeeper时,需要进行ACL验证。验证失败的情况,无法正常访问服务。
3.3 ACL 使用场景
开发、测试环境分离,开发者无权操作测试库节点
生产环境控制指定 ip 的服务可以访问相关节点,防止混乱
3.4 相关代码
涉及到的模块与代码:
orientsec_registry 模块:
新增 base64.c,des.c,sha1.c
修改 zk_registry_service.c中zk_create_node() 方法,增加zoo_add_auth 验证,使用ZOO_CREATOR_ALL_ACL 设置ACL,后调用zoo_create()创建node.
修改zk_registry_service.c中start_zk_connect()方法,获得配置文件中的zk的认证的用户名和密码,并判断是否开启ACL验证。
添加一些static函数进行内部处理:
get_acl_info()
combine_name_pwd()
encrypt()
get_acl_param()
多个服务端提供服务的时候,能够区分主服务器和备服务器。当主服务器可用时客户端只能调用主服务器,不能调用备服务器;当所有主服务器不可用时,客户端自动切换到备服务器进行服务调用;当主服务器恢复时,客户端自动切换到主服务器进行服务调用。
给服务端添加一个master属性,用来标识服务端是主服务器还是备服务器,master等于true表示主服务器,master等于false表示备服务器,master缺省时为true。
当客户端启动时,首先根据服务名获取所有的服务端列表,然后根据每个服务端的master属性进行筛选操作:
(1) 当服务端列表中全部都是主服务器的时候,服务端列表不发生变化
(2) 当服务端列表中全部都是备服务器的时候,服务端列表不发生变化
(3) 当服务端列表中既有主服务器也有备服务器的时候,将备服务器从服务列表中移除出去,只保留主服务器
同时,客户端监听注册中心中服务端主备属性的变化,一旦监听到变化,重新获取服务端列表,并进行以上筛选操作。
涉及到的模块和代码:
orientsec-provider 模块:
增加provider的master属性
orientsec-consumer 模块:
修改 consumer_query_providers 函数
增加 master 和 online 属性的判断和解析
5.1 使用场景
场景1:服务分组。当服务集群非常大时,客户端不必与每个服务节点建立连接,通过对服务分组,一个客户端只与一个服务组连接。
场景2:业务隔离。例如服务端部署在三台服务器上,分别提供给三种业务的客户端。该场景下,可以将三台服务器配置不同的分组,然后不同业务的客户端配置各自的服务端分组。这样即使其中一种业务的客户端调用频繁导致服务端响应时间边长,也不会影响其它两种业务的客户端。
场景3:多机房支持。例如某证券公司在上海有两个机房A和B,在深圳有一个机房C,三个机房都对外提供服务;每个机房都划分为两个分组,即三个机房共有6个分组,分别为A1、A2、B1、B2、C1、C2。在上海地区的客户端要调用证券公司的服务时,可以优先调用A1、A2两个分组的服务端;如果A1、A2分组的服务端不可用,调用B1、B2两个分组的服务端;如果B1、B2分组的服务端也不可用,调用C1、C2两个分组的服务端。
5.2 实现思路
服务端添加一个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()
8.1功能描述
容灾:注册中心不可用时服务端和客户端可以正常启动,注册中心恢复后注册信息需要自动注册到注册中心
降级:客户端可以通过配置文件指定服务端地址,此时即使注册中心不可用,客户端也可以访问服务端;这种情况下,注册中心即使恢复,也不会再去访问注册中心获取最新的服务列表
8.2实现思路
(1)服务端启动时,将自动向zk注册Provider信息的任务代码提取到一个新的线程
(2)客户端启动时,将自动向zk注册Consumer信息的任务代码提取到一个新的线程
(3)获取配置文件中(service.server.list)提供服务的服务器地址列表:如果不为null,将服务提供者存入allProviders、serviceProviderMap中,客户端进行服务调用(忽略注册中心);否则客户端注册zk,获取zk的服务提供者列表进行服务调用
(4)特别注意:一旦配置service.server.list参数,客户端运行过程中,即使注册中心恢复可用,框架也不会访问注册中心。如果需要从配置中心查找服务端信息,需要注释掉该参数,并重启客户端应用。
8.3配置方法
在配置文件“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()
9.1 功能描述
v1.2.5版本前gprc log默认输出到标准输入输出,即stderr, 1.2.5版本增加了log输出到指定文件。
9.2 相关代码
/** Set global log target for windows */
GPRAPI void gpr_set_log_target(gpr_log_target target_to_print);
9.3 使用方法
在client端调用全局api进行设置。
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断开连接、服务注册信息丢失后,如果客户端与服务端连接正常,那么客户端与服务端依然可以正常通信。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。