vlambda博客
学习文章列表

从一次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——像调用本地一样发起远程调用



在进入gRPC框架前先来简单看一下RPC是什么。
RPC全称是 Remote Procedure Call,即远程过程调用,用于网络间的进程通信。RPC以编程语言中本地函数调用形式,封装了跨网络、跨平台、跨语言的服务访问,使得开发者专注于业务代码,将网络协议及消息编解码交由框架处理。
RPC技术通常用在分布式系统中应用程序之间或者应用程序与中间件通信,如etcd统一配置服务,它是通过gRPC框架和服务端进行通信的;另外在K8S的kube-apiserver和集群中每个组件的通信也是通过gRPC框架进行。可以说RPC 对应的是整个分布式应用系统,就像是“经络”一样的存在。

从一次RPC调用流程看各场景下gRPC框架的解决方案(上)

那这里RPC既然是工作在应用层,我们是否可以使用现成的HTTP协议来替代呢?不能,RPC更多负责应用间通信,对于性能要求更高,我们在之前的文章中说过HTTP笨重的头部甚至是比传输的数据体积还大;另外由于HTTP协议它是无状态的,无法关联请求和响应,所以这里无法使用HTTP来做替代。

考虑到高并发调用场景,RPC调用更倾向于同步模型中的IO多路复用。我们先从宏观视角来看一次RPC调用的大体流程:

从一次RPC调用流程看各场景下gRPC框架的解决方案(上)

图中的动态代理是指通过对字节码进行增强,在方法调用的时候进行拦截,以便于在方法调用前后,增加我们需要的额外处理逻辑。其目的简言之就是为了屏蔽RPC调用细节,让使用者面向接口编程。流程图如下:

从一次RPC调用流程看各场景下gRPC框架的解决方案(上)



RPC服务发现——获得某节点信息的“通信录”



在生产环境中,服务提供方通常以集群方式提供服务,由于集群中的IP随时可能变化,需要一本“通信录”及时获取到对应的服务节点,这个过程就是服务发现。对于RPC框架中的服务发现机制主要是由服务注册和服务订阅组成,服务注册是将服务节点的IP和接口注册到注册中心;服务订阅是去注册中心查找并订阅服务提供方IP,缓存到本地便于后续远程调用。

在这里注册中心的核心作用是完成服务提供方和服务调用方,两者的路径匹配,一般来说对于服务提供方需要的信息有:IP、端口、接口、方法+服务分组别名;对于服务调用方需要:IP、端口。

其中服务分组别名用于保证一致性,设置的别名和服务消费者需要的不一致流量也不会打过去,什么时候打过去可以通过修改配置中心来控制;分组别名相同的,组内的多个调用方可以利用相关的负载均衡策略来具体分发流量。

具体RPC框架的服务发现该如何实现呢?服务发现的本质即完成接口与服务提供者IP之间的映射,这个映射可以看作是一种支持变更推送的命名服务。

gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展,我们可以借助开源的 ZooKeeper、etcd实现。下面简单介绍下基于 ZooKeeper 的服务发现流程:

  1. 搭建一个 ZooKeeper 集群作为注册中心集群

  2. ZooKeeper 中创建服务根路径,并在根路径下创建服务提供方目录与服务调用方目录(例如:/service/com.demo.xxService/provider 或者consumer)

  3. 服务提供方注册服务,在provider 目录下创建临时节点存储提供方信息。

  4. 调用方订阅服务,在consumer目录下创建临时节点存储调用方信息,并watch 该服务的provider 目录中所有的服务节点数据。

  5. 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框架自身实现的负载均衡,它主要流程如下:

  1. 调用方与注册中心下发的节点建立长连接

  2. 调用方通过RPC负载均衡插件自主选择一个节点

  3. 发起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



点个“在看”鼓励下快要秃头的小阿巩~