外刊评论|Rust周期性服务发现

1,497 阅读5分钟

编辑:张汉东/ 王江桐(华为)

编者按:国外的 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的过程广义上可以被形容为:

  1. 客户端传参;
  2. 服务器(函数)运行;
  3. 服务器返回结果。

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增加了此功能来解决问题。

总体思路大致如下:

  1. 服务名字和IP相关联:通过trait LookupService来解析得到IP并存入socketaddr set;

    • 不限制解决方案,在例中truelayer使用Kubernetes' DNS或者API做
    • Kubernetes是一个开源的 Linux 容器自动化运维平台,它消除了容器化应用程序在部署、伸缩时涉及到的许多手动操作。换句话说,你可以将多台主机组合成集群来运行 Linux 容器,而 Kubernetes 可以帮助你简单高效地管理那些集群。
  2. 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服务,它并不通用。

引用