从一次RPC调用流程看各场景下gRPC框架的解决方案(上)
阿巩
古人有云“gRPC是目前最常用也是性能最好的RPC框架之一”,本周阿巩将从一次RPC调用流程看在各场景下gRPC框架的解决方案,直击gRPC优秀的本质。上一篇中我们提到了HTTP/2和ProtoBuf 协议,gRPC便是结合了 HTTP/2 与 Protobuf 的优点,在应用层提供方便而高效的 RPC 远程调用协议。那空口无凭,先来简单总结回顾下HTTP/2和ProtoBuf 协议分别是如何提升性能的,再来展开讨论。日拱一卒,让我们开始吧!
在HTTP/2 中采用了静态表和动态表结合来降低HTTP头部体积,并通过Stream流实现并发传输;ProtoBuf 通过在 proto 文件中为每个字段预分配 1 个数字,编码时省去了完整字段名占用的空间。
RPC——像调用本地一样发起远程调用
那这里RPC既然是工作在应用层,我们是否可以使用现成的HTTP协议来替代呢?不能,RPC更多负责应用间通信,对于性能要求更高,我们在之前的文章中说过HTTP笨重的头部甚至是比传输的数据体积还大;另外由于HTTP协议它是无状态的,无法关联请求和响应,所以这里无法使用HTTP来做替代。
考虑到高并发调用场景,RPC调用更倾向于同步模型中的IO多路复用。我们先从宏观视角来看一次RPC调用的大体流程:
图中的动态代理是指通过对字节码进行增强,在方法调用的时候进行拦截,以便于在方法调用前后,增加我们需要的额外处理逻辑。其目的简言之就是为了屏蔽RPC调用细节,让使用者面向接口编程。流程图如下:
RPC服务发现——获得某节点信息的“通信录”
在生产环境中,服务提供方通常以集群方式提供服务,由于集群中的IP随时可能变化,需要一本“通信录”及时获取到对应的服务节点,这个过程就是服务发现。对于RPC框架中的服务发现机制主要是由服务注册和服务订阅组成,服务注册是将服务节点的IP和接口注册到注册中心;服务订阅是去注册中心查找并订阅服务提供方IP,缓存到本地便于后续远程调用。
在这里注册中心的核心作用是完成服务提供方和服务调用方,两者的路径匹配,一般来说对于服务提供方需要的信息有:IP、端口、接口、方法+服务分组别名;对于服务调用方需要:IP、端口。
其中服务分组别名用于保证一致性,设置的别名和服务消费者需要的不一致流量也不会打过去,什么时候打过去可以通过修改配置中心来控制;分组别名相同的,组内的多个调用方可以利用相关的负载均衡策略来具体分发流量。
具体RPC框架的服务发现该如何实现呢?服务发现的本质即完成接口与服务提供者IP之间的映射,这个映射可以看作是一种支持变更推送的命名服务。
gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展,我们可以借助开源的 ZooKeeper、etcd实现。下面简单介绍下基于 ZooKeeper 的服务发现流程:
搭建一个 ZooKeeper 集群作为注册中心集群
在ZooKeeper 中创建服务根路径,并在根路径下创建服务提供方目录与服务调用方目录(例如:/service/com.demo.xxService/provider 或者consumer)
服务提供方注册服务,在provider 目录下创建临时节点存储提供方信息。
调用方订阅服务,在consumer目录下创建临时节点存储调用方信息,并watch 该服务的provider 目录中所有的服务节点数据。
provider 目录下节点发生变更,ZooKeeper 就会通知给发起订阅的调用方。
zookeeper担任了一个高度可用的分布式协调者的角色。由于Zookeeper集群的每个节点的数据每次发生更新操作,都会通知其它 ZooKeeper 节点同时执行更新,保证每个节点的数据能够实时的完全一致,这也就是它的强一致性(CP)特性。
心跳检测包括以下三种状态:
健康:连接建立成功,心跳探活成功。
亚健康:连接建立成功,心跳请求连续失败。
死亡:建立连接失败。
我们优先从健康列表中选,如果健康列表为空再从亚健康中选。注意这里的“死亡”状态如果在某个时间内重新连接成功,我们会认为它复活。
健康检测的手段通常采用心跳检测,如果超过3次(阈值可配置)未响应则认为服务节点挂掉。
不过这样操作会遇到其他问题场景,比如:服务方会出现心跳正常响应,但是服务间歇性响应超时(亚健康状态),导致调用方误判。这个问题可以采用可用率的思路来解决,通过支持的插件获取可用率指标。
另外如果是调用方心跳机制出现问题导致误判服务方挂掉;可以用调用方集群部署,把检测程序部署在多个机器,其中任一台调用显示正常则认为正常的办法来减少误判。
在gRPC框架中我们可以通过在proto中定义服务及在K8S中使用工具来配置健康检测服务:
在proto定义服务方法:
syntax = "proto3";
package grpc.health.v1;
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3; // Used only by the Watch method.
}
ServingStatus status = 1;
}
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
客户端可以通过调用该Check方法来查询服务器的健康状态,并且应该在rpc上设置一个截止日期。客户端可以选择设置它想要查询健康状态的服务名称。建议的服务名称格式为package_names.ServiceName,例如grpc.health.v1.Health。
在Kubernetes上对gRPC服务器进行健康检查方法如下:
1. 先选择对应语言,然后找到gRPC“health”模块。
2.将grpc_health_probe二进制文件打到容器中。
3.配置Kubernetes“exec”探针以调用容器中的“grpc_health_probe”工具。
RPC路由策略——让请求按照设定的规则发到不同的节点上
RPC路由策略在一次RPC调用流程中的位置如下图:
RPC路由策略常用在灰度发布应用的场景,比如要求新上线的某个节点只允许某个IP可以调用。RPC路由策略通过在上线前限制调用方来源,达到将试错成本降到最低的目的。
这里假设一个实际场景:现在需要改造商品应用,来保证每个商品ID的所有操作(请求的响应)都是在老应用或者新应用上。由于单使用IP路由无法做到老新应用间的平滑过度,这里我们的解决方案是给服务提供方节点打上新/老应用的标签。这样的话在调用方发起请求时携带商品ID,在注册中心中根据商品ID来判断属于新/老应用,最终找到对应节点,实现流量隔离的效果。
当然灰度发布功能作为 RPC 路由功能的一个典型应用场景,我们还可以通过路由功能完成像定点调用、黑白名单等一些高级服务治理功能。
RPC负载均衡——集群下的多个服务节点共同分担请求压力
我们常提到的负载均衡包括软负载和硬负载,软负载指软件,像是LVS、Nginx等;硬负载指硬件,如F5服务器等。通过媒介加上常用的负载均衡算法(随机、轮询、最小链接等)来解决服务器性能相差较大的情况。
这里我们谈到的是RPC框架自身实现的负载均衡,它主要流程如下:
调用方与注册中心下发的节点建立长连接
调用方通过RPC负载均衡插件自主选择一个节点
发起RPC调用
RPC 负载均衡策略一般包括随机权重、Hash、轮询。当然,这还是主要看 RPC 框架自身的实现。
由于负载均衡机制完全是由 RPC 框架自身实现的,不需要依赖负载均衡设备,也就不会发生负载均衡设备的单点问题,同时我们可以通过控制权重的方式配置调用方的负载均衡策略。
type Balancer interface {
Start(target string, config BalancerConfig) error
Up(addr Address) (down func(error))
Get(ctx context.Context, opts BalancerGetOptions) (addr Address, put func(), err error)
Notify() <-chan []Address
// Close shuts down the balancer.
Close() error
}
参考:
《RPC实战与核心原理》 何小锋
《在Kubernetes上对gRPC服务器进行健康检查》 Ahmet Alp Balkan (Google)
https://github.com/grpc/grpc/blob/master/doc/health-checking.md
https://blog.csdn.net/kevin_tech/article/details/109281835