编辑:张汉东/ 王江桐(华为)
编者按:国外的 Rust 资源非常丰富,将其都翻译过来也不现实,所以想到这样一个栏目。外刊评论,主要是收集优秀的 Rust 相关文章,取其精华,浓缩为一篇简单评论。
欢迎大家贡献:github.com/RustMagazin…
周期性服务发现
来自《This Week in Rust 》第404篇博文
ginepro是由truelayer公司开发的一个Rust crate,在tonic的gRPC channel基础之上增加了周期性服务发现的功能来更好地实现负载均衡。
什么是gRPC
要知道gRPC,首先我们要知道RPC。
RPC(Remote Procedure Call)称为远程过程调用。当项目较小时,项目或可以完全部署在本地,调用本地服务并且获得结果。当项目规模逐渐增大,本地部署不再能完全满足需求,服务或部署至远程服务器,新的问题由此产生:部署在A服务器上的应用需要调用B服务器上应用提供的函数或方法,让B服务器应用进行计算,并得到其返回结果。
RPC框架是这个问题的一个解法。通过远程过程调用协议,不需要了解底层细节,客户端应用可以向服务端请求服务,并且像访问本地资源一样访问服务器资源。
RPC的过程广义上可以被形容为:
- 客户端传参;
- 服务器(函数)运行;
- 服务器返回结果。
RPC涉及到的组件通常包含:
- 客户端;
- 客户端存根/打桩(Client Stub):存放服务端地址信息,请求参数打包成网络信息,etc.;
- 服务器存根/打桩(Server Stub):类同客户端存根/打桩;
- 服务器。
gRPC是谷歌开发的一个开源通用高性能的RPC框架,有严格的接口约束条件,支持流式通讯,并可以将数据序列化为二进制编码,大幅减少传输的数据量,提高性能。然而,gRPC没有针对分布式系统数据传输的必要组件,比如负载均衡,限流熔断,服务注册,等等。
什么是tonic
toinc是基于HTTP/2的gRPC的rust实现。相比于rust的其他gRPC库,tonic已经实现了负载均衡并且可以动态更新节点,而其他的Rust库还没有实现这一点。
tonic涉及到的其他相关模块:
- hyper(异步,底层http)
- tokio(异步)
- prost(a Protocol Buffers:读写序列化结构数据 e.g. XML)
问题:负载均衡
gRPC的负载均衡问题来自于HTTP/2协议的稳定性。gRPC使用HTTP/2协议在单个TCP连接中实现请求和响应的多路复用(一个信道同时传输多路信号)。由于连接是持久的,在已有连接建立以后,即使有新的服务器,连接也不会向新服务器倾斜,可能造成原有的服务器过载。
我们需要负载均衡来使得针对于每一个新请求,客户端可以合理选择一个服务器并且通过现有链接发出请求。
#### 解决方案
分析问题,可知总体解决方案如下:
- 服务器周期性强制客户端重连(解决连接不发现问题,使得连接不过于持久)
- 客户端周期性执行服务发现(解决服务不发现问题)
- 引入应用级负载均衡(在应用层面监控负载均衡相关)
应用级负载均衡通常可以分为两类,客户端侧维护,以及代理或外部线程维护,也就是边车模式。前者便于测试,不会过于复杂,但没有语言或平台兼容;后者相对比较复杂,或导致系统延迟,并且难于维护和监控,或是需要额外的投入去监控,比如service mesh。
由于流量和项目要求,truelayer希望能实现负载均衡。当前Rust关于gRPC的crate有三个:grpc-rs,grpc-rust,tonic,但是它们没有完全满足要求。出于性能和维护原因,truelayer也不考虑使用service mesh。这使得truelayer最终选择在三个库中最贴近需求的tonic基础之上编写了一个crate: ginepro。
ginepro
tonic已经实现了大部分解决方案,但是它缺少周期性服务发现的功能。ginepro为tonic的channel增加了此功能来解决问题。
总体思路大致如下:
-
服务名字和IP相关联:通过trait LookupService来解析得到IP并存入socketaddr set;
- 不限制解决方案,在例中truelayer使用Kubernetes' DNS或者API做
- Kubernetes是一个开源的 Linux 容器自动化运维平台,它消除了容器化应用程序在部署、伸缩时涉及到的许多手动操作。换句话说,你可以将多台主机组合成集群来运行 Linux 容器,而 Kubernetes 可以帮助你简单高效地管理那些集群。
-
LoadBalancedChannel将使用tonic的Channel,并进行事件循环,在循环中实现服务发现功能,并且维护endpoints列表。
具体代码如下:
/// Interface that provides functionality to
/// acquire a list of ips given a valid host name.
#[async_trait::async_trait]
pub trait LookupService {
/// Return a list of unique `SockAddr` associated with the provided
/// `ServiceDefinition` containing the `hostname` `port` of the service.
/// If no ip addresses were resolved, an empty `HashSet` is returned.
async fn resolve_service_endpoints(
&self,
definition: &ServiceDefinition,
) -> Result<HashSet<SocketAddr>, anyhow::Error>;
}
loop {
let discovered_endpoints = self
.lookup_service
.resolve_service_endpoints(service_definition).await;
let changeset = self.create_changeset(&discovered_endpoints).await;
// Report the changeset to `tonic` to update the list of available server IPs.
self.report_and_commit(changeset, endpoints).await?;
// Wait until the next interval.
tokio::time::sleep(self.probe_interval).await;
}
评估
ginepro可以很好地解决truelayer的问题,但是同样,没有银弹,这个解决方案仍然不是完美的。如果endpoint失效,除非客户端收到显式指令(e.g. Kubernets删除了这个端点),不然客户端不会将其从端点列表中删除;其次,ginepro是客户端侧解决方案,只适用于truelayer的Rust服务,它并不通用。