在上一篇文章中,我们提到为了支持gRPC,TiKV造了一个轮子gRPC-rs。本文简单介绍一下这个库。首先,让我们谈谈什么是gRPC。gRPC是Google推出的基于HTTP2的开源RPC框架,希望能够在各种微服务之间实现统一的RPC基础设施。它不仅支持Linux和Windows等常规平台,还支持移动设备和物联网。有十几种语言实现,现在又多了一种语言Rust。gRPC之所以有这么多语言支持,是因为它有一个用C写的核心库(gRPCcore),所以只要是兼容CABI的语言,那么你就可以通过封装写一个该语言的gRPC库。Rust对C有很好的支持,gRPC-rs是gRPC核心ABI的Rust包装器。Core可以异步处理RPC请求。考虑到Rust中有一个比较成熟的异步框架Futures,我们决定将API设计为Future模式。gRPC-rs架构图我们按照架构图自下而上的讲。传输层和协议在上一篇已经讲过了,这里不再赘述。gRPCCoreCore中有几个重要的对象:Call和4种RPC:Call代表一个RPC,可以派生出4种RPC。一元:这是最简单的RPC模式,即一问一答,客户端发出请求,服务器返回回复,一轮RPC结束。客户端流:这种类型的RPC将创建一个客户端到服务器的流,客户端可以通过该流向服务器发送多个请求,而服务器只会返回一个回复。Serverstreaming:和上面类似,但是会创建一个从server到client的stream,server可以发送多个reply。Bidirectionalstreaming:如果上面两种类型是单工的,那么这种类型就是双工的,客户端和服务端可以同时向对方发送消息。值得一提的是,由于gRPC是基于HTTP2的,所以利用了HTTP2的多路复用特性,可以同时在一个TCP连接上进行多个RPC,一个RPC就是HTTP2中的一个Stream。Channel:是对底层链接的抽象。具体来说,Channel是连接到远程服务器的TCP链接。Server:顾名思义就是gRPC服务端的封装,可以在上面注册我们的服务。Completionqueue:就是gRPC的完成事件队列。事件可以是新的回复或新的请求。简单介绍一下Core库的实现。Core中有一个Combiner的概念,Combiner中有一个函数指针或者一个组合子队列。每个组合子都有特定的功能,通过不同的组合可以实现不同的功能。以下伪代码大致说明了Combiner的工作原理。Combiner中有一个mpsc的无锁队列q。由于q只能有一个消费者,所以要求只有一个线程可以同时调用队列中的每个函数。调用的入口点是run()方法,其中每个函数将按顺序执行。当获取q时,该回合结束。假设一个RPC由六个函数组成,这种设计让这组函数(RPC)运行在不同的线程上,这就是异步RPC的基础。Completionqueue(以下简称CQ)是一个Combiner,它暴露了一个next()借口,相当于Combiner的run()。由于接口简单,Core内部不需要额外开线程,只要从外部不断调用next()就可以驱动整个Core。所有的HTTP2处理,Client的RPC请求和Server的RPC连接都是由组合子的不同组合组成的。下面是一元的代码。它由6个combinator组成,这些combinator作为一个batch加上Call来记录状态,两者构成了这个RPC。用Rust封装Core介绍完Core,我们再来说说如何用Rust封装它。这层封装不会产生额外的开销,不像某些语言在调用C时有类型转换或运行时开销,Rust中的开销可以忽略不计,得益于Rust使用llvm作为编译器Backend,有很好的支持对于C,Rust调用CABI就像调用普通函数一样,可以做到零成本。同时,用Rust封装CABI是一件非常简单的事情,就像黑魔法一样简单。比如封装CQnext():C:Rust:那么我们看看如何封装C的类型。继续以next()为例:C:Rust:CQ在Core的ABI中以指针的形式传递,而RustWrapper不需要知道CQ的具体内部结构。对于这种情况,Rust推荐使用非成员枚举体来表达,具体有两个好处,***,因为没有成员,我们无法在Rust中构造枚举体的实例,第二,类型安全,当传递了错误类型的指针时,编译器会抛出错误。#[repr(C)]也是Rust的黑魔法之一。带有此标记的结构在内存中具有与C相同的布局和对齐方式,这样的结构可以安全地传递给CABI。gRPC-rs中的Futures经过上一节的打包,我们得到了一个可用但非常裸露的RustgRPC库grpc-sys。实际上,我们不建议直接使用grpc-sys。直接使用它就像在Rust中编写C。会事半功倍。Rust语言的很多特性无法使用,比如泛型、Trait、Ownership等,无法融入Rust社区。.上面说了Core可以异步处理RPC,那么如何使用Rust进行更好的封装呢?期货!它是一个成熟的异步编程库,拥有活跃的社区。Futures非常适合RPC等IO操作频繁的场景。Futures中还有一个combinator的概念,和Core中的类似,但是使用起来更方便,也更容易理解。举个栗子:你认为输出的答案是什么?是的,是42个。在Core部分,说过可以将不同的combinators组织在一起做不同的事情。在Future中,我们可以理解为一件事情可以分为多个步骤,每个步骤由一个combinator来完成。比如上面的例子,map完成了double的动作,and_then给输入加了40。下面我们来看看gRPC-rs封装的API。以helloworld.proto为例,GreeterClient::say_hello_async()向远程服务器发送请求(HelloRequest),服务器返回结果(HelloReply)。由于是异步操作,这个函数会立即返回,返回的ClientUnaryReceiver实现了Future,完成后会得到HelloReply。在一般的异步编程中,都会有Callback,用来处理异步的返回值。在这个RPC中,就是HelloReply。在Future中,可以写成组合子,比如and_then。再举个栗子,有完整的RPC逻辑,得到回复后打印到日志。下面是gRPC-rs的具体用法。一元RPCgRPC-rs根据proto文件中服务的定义生成相应的代码,包括RPC方法定义(Method)、客户端和服务端代码,生成的代码会使用gRPC-rsAPI。那么具体怎么做呢?本节仍然以helloworld.proto为例,讲一下UnaryRPC在客户端的具体实现。首先,SayHello的Method记录了RPC类型、全名以及序列化和反序列化函数。为什么序列化和反序列化函数?因为Core本身不涉及消息的序列化,所以这部分由封装层来处理。在生成的客户端中,可以调用gRPC-rs的API,根据Method的定义发起RPC。这篇写在***的文章简单介绍了gRPCCore的实现和gRPC-rs的封装,以及详细的使用方法。这里就不过多介绍了。如果您有兴趣,可以查看示例。gRPC-rs深入使用了Future,里面有很多神奇的用法,比如gRPC-rs部分Futures中的executor,gRPC-rs使用CQ实现了一个可以并发执行Future的executor(类似于futures-rs在futures-rs)Executer)中,上下文切换大大减少,性能得到显着提升。【本文为专栏组织《PingCAP》原创文章,转载请联系作者获得授权】点此阅读作者更多好文
