Rust 1.39.0 でasync/.await
が安定化され、今後はサーバーサイドの用途で Rust が採用されるケースが増えそうですね(期待を込めて)。
そのなかでも今回は、マイクロサービス指向で作成されたプロジェジェクトで採用されることが多い gRPC にフォーカスし、Rust の gRPC ライブラリである hyperium/tonic について紹介します。
Rust の gRPC ライブラリ
Tonic
のことを紹介する前に、他の gRPC ライブラリについても軽く触れておきます。
2019 年 12 月時点で、grpc-ecosystem/awesome-grpc
リポジトリには以下のライブラリが紹介されています。
- stepancheg/grpc-rust - Rust implementation of gRPC
- tikv/grpc-rs - The gRPC library for Rust built on C Core library and futures
- tower-rs/tower-grpc - A client and server gRPC implementation based on Tower
- hyperium/tonic - A native gRPC client & server implementation with async/await support
もっとも古い stepancheg/grpc-rust
は 2015 年より開発が始まっています。私も長い間リポジトリをウォッチしていましたが、 継続的に開発が進められているものの、個人プロジェクトということもあり、リリース自体は 2018 年で止まっています。
tikv/grpc-rs
は、PingCAP 社が開発し、Bay Area Rust Meetup August 2017 で事例紹介がされたことから、早期の段階で本番環境に導入されたライブラリではないでしょうか。
Web framework である tower-rs/tower をベースにして作られたtower-rs/tower-grpc
は、Tonic の前身となるプロジェクトです。
そして今回紹介するhyperium/tonic
は、2019 年 8 月から開発が始まった、もっとも新しいプロジェクトで、 2019 年 12 月 15 日時点では、アルファ版のみのリリースとなります。
Tonic とは
Tonic を 1 行で説明すると、ハイパフォーマンスで、相互運用性と柔軟性にフォーカスした gRPC over HTTP/2 のライブラリとなります。
背景を交えた説明は、メイン開発者の一人である Timber.io の Lucio Franco さんのブログ Tonic: gRPC has come to async/await!に記載されていたので、そちらのブログから一部抜粋して紹介します。
もともと Tonic が開発される前には、前進のプロジェクトであるtower-grpc
がありました。
tower-grpc
はfutures 0.1
をベースにして開発され、本番環境でも導入できる gRPC ライブラリとして CNCF 傘下のプロジェクトである Service Mesh の Linkerd のニーズから始まりました。
実際に Linkerd のコンポーネントであるlinker-proxy
の中でtower-grpc
は使用されており、Linkerd 自体もKubecon NA 2019で、いくつかのセッションで事例が紹介されるなど、本番環境での採用も増えてきたライブラリとなります。
そして、async/await
の安定化により、この新しい構文をファーストクラスでサポートするために Tonic が作成されました。
Tonic には gRPC の実装だけでなく、hyper、tokio、tower などのライブラリを利用して HTTP/2 のクライアントとサーバーの実装も含まれています。
その他にも 相互運用性を保つため、コミット ごとに grpc-go の実装に対して gRPC interop test cases
のチェックを実施しています。
予定のものを含め Tonic で提供する機能は以下のとおりとなります。
- Implemented in pure Rust (minus openssl which is optional)
- Interoperability tested via tonic-interop
- Bi-directional streaming
- Custom metadata
- Trailing metadata
- Codegen via prost
- Exposes tracing diagnostics
- Fully featured HTTP/2 client and server based on hyper
- TLS backed by either openssl or rustls
- Load balancing powered by tower
- Reliability features such as timeouts, rate limiting, concurrency control, and more
- gRPC interceptors
サービスメッシュのライブラリとして利用されることを意識した機能も多いですね。
基本的な使い方
基本的な使い方は gRPC を利用された経験があれば、それほど難しくはありません。
Tonic の場合、plugin を利用して直接 protoc コマンドを呼び出すのではなく、prost-build に コード生成の処理を委譲し、prost-build の処理の中で protoc コマンドが実行されます。
またこの prost-build には protoc コマンドがバイナリとして埋め込まれているため、protoc コマンドのインストールも不要です。
最新の protoc バイナリや、特定バージョンのバイナリを利用したい場合でも、PROTOC
環境変数で利用するバイナリを上書きできる仕組みが用意されているようなので、複数言語で共通の protoc を使いたい場合でも問題なさそうです。
注意点としては prost-build で生成されたコードにいくつかの依存ライブラリがあるため、いくつかの依存ライブラリの追加が必要となります。
他にも生成されたコードは、デフォルトでは target ディレクトリ配下に作成されるため IDE などでコード補完ができない問題が発生します。
この問題は生成されたコードの出力先を変更することで解決できるかもしれませんが、cargo publish が出来ないなどの問題があるため、target ディレクトリ以外に生成先を変更するのは避けたほうが無難です。
それでは実際にプロジェクトを作成して動作を確認していきます。
# サンプルプロジェクトを作成
❯❯ cargo new --bin rust-grpc-web-server && cd rust-grpc-web-server
# helloworld の proto ファイルをダウンロード
❯❯ mkdir proto
❯❯ wget https://raw.githubusercontent.com/grpc/grpc/master/examples/protos/helloworld.proto -O ./proto/helloworld.proto
次に Tonic と prost-build で生成された Rust のコードに必要な依存ライブラリを追加します。
また、ここでビルドスクリプトに必要な tonic-build の依存も合わせて追加します。
...
[dependencies]
tonic = "0.1.0-alpha.6"
bytes = "0.4"
prost = "0.5"
prost-derive = "0.5"
tokio = "0.2.0-alpha.6"
[build-dependencies]
tonic-build = "0.1.0-alpha.6"
ビルドスクリプトを追加し、ビルドごとに proto ファイルから Rust コードが生成されるようにします。
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/helloworld.proto")?;
Ok(())
}
最後に Tonic と非同期処理の Runtime として tokio を使い、helloworld を実装します。
use tonic::{transport::Server, Request, Response, Status};
pub mod hello_world {
tonic::include_proto!("helloworld");
}
use hello_world::{
server::{Greeter, GreeterServer},
HelloReply, HelloRequest,
};
#[derive(Default)]
pub struct MyGreeter {}
#[tonic::async_trait]
impl Greeter for MyGreeter {
async fn say_hello(&self, request: Request<HelloRequest>) -> Result<Response<HelloReply>, Status> {
let reply = hello_world::HelloReply {
message: format!("Hello {}!", request.into_inner().name).into(),
};
Ok(Response::new(reply))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap();
let greeter = MyGreeter::default();
Server::builder()
.add_service(GreeterServer::new(greeter))
.serve(addr)
.await?;
Ok(())
}
このコードをビルドすると、以下のパスに helloworld.rs が生成されます。
❯❯ cargo build
❯❯ ls -al target/debug/build/rust-grpc-web-server-xxxxx/out/helloworld.rs
.rw-r--r-- 6.0k watawuwu 1 Dec 16:57 target/debug/build/rust-grpc-web-server-xxxx/out/helloworld.rs
/// The request message containing the user's name.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct HelloRequest {
#[prost(string, tag = "1")]
pub name: std::string::String,
}
/// The response message containing the greetings
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct HelloReply {
#[prost(string, tag = "1")]
pub message: std::string::String,
}
...
それでは grpc のクライアントの grpcurl を使って、通信の確認をします。
# サーバーを起動
❯❯ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 47.46s
Running `target/debug/rust-grpc-web-server`
# 別コンソールから実行
❯❯ grpcurl \
-proto proto/helloworld.proto \
-d '{"name":"watawuwu"}' \
-plaintext \
localhost:50051 \
helloworld.Greeter.SayHello
{
"message": "Hello watawuwu!"
}
バッチリ通信できました
パフォーマンス比較
この helloworld の実装を使って、性能比較をしたいと思います。
環境は手持ちの MacBook Pro で実行し、パラメーターチューニングも実行しないため、参考程度の情報として扱ってください。
また比較用として、stepancheg/grpc-rust
とtikv/grpc-rs
でも同じような helloworld の実装を用意しました。
ソースコードはこちらで確認できます。
負荷クライアントには ghz を利用し、結果は以下のとおりとなりました。
Requests/sec | Average(ms) | 99% tile(ms) | |
---|---|---|---|
stepancheg/grpc-rust(v0.6.1) | 18475 | 5.35 | 9.3 |
stepancheg/grpc-rust(latest) | 45352 | 2.15 | 3.93 |
tikv/grpc-rs(v0.4.6) | 30950 | 1.25 | 2.32 |
hyperium/tonic(v0.1.0-alpha.6) | 51898 | 1.81 | 4.12 |
Alpha 版にもかかわらず Tonic がパフォーマンスでは健闘しています。
全ての出力結果はこちらから参照できます。
Load Balancing
実は、機能一覧にあったこのLoad Balancing
がかなり気になっていました。
というのも gRPC で設計されているBalancing-aware Client
が実装されていると、プロキシを別途用意しなくてもクライアント側でバランシングできるのでとても便利です。小規模アプリケーションでは特に活躍する機能です。
ですが実装を見たところ、Load Balancing 機能は実装されていましたが、サーバーリストを動的に取得するような、DNS リゾルバの機能が提供されていませんでした
Discord を見ると trust-dns の作者に実装方法について質問していたので、もしかしたらリゾルバ実装の候補を探しているかもしれません。
今後の動向
最後に、絶賛開発中である Tonic の今後の動向を知るために、気になる Issue について紹介して終わりたいと思います。
(開発が活発なので、記事の公開時に解決されているIssueも多いかもしれません)
Support futures runtimes other than Tokio(#152)
futures runtime として async-std
などのサポートについて議論されている Issue です。
今の開発ステータスは Alpha バージョンであり、まずはすぐに使えるものが選択されたようです。
特に HTTP2 実装は hyper/h2 以外に実用的なものがなく、それらに依存したライブラリ群となっているようです。
現状のネットワーク系のライブラリは trust-dns を含め、tokio に依存しているものが多いので、当面の間は tokio runtime のサポートだけになりそうです。
Upgrade to tokio
0.2(#159)
tokio 0.2 へのアップグレードは、hyper 0.13 のリリースと、いくつかの tower ライブラリの対応待ちです。
=> 対応されてました https://github.com/hyperium/tonic/pull/163
Graceful shutdown docs/example/default-impl(#144)
実際の挙動で確認できていませんが、現状だと gRPC のhealth checking service(#135) に対応できていないため、Graceful shutdown に対応できていないようです。
=> 対応されてました https://github.com/hyperium/tonic/pull/169
Upstream to main gRPC repo(#149)
本家 gPRC リポジトリでの管理についての Issue です。
本家側での Issue でもやりとりされていますが、どのような基準で本家 gRPC 管理に採択されるのでしょうか。
どの gRPC リポジトリにせよ、本家で管理されるようになれば Rust でも gRPC 利用者の増加を見込めますしメリットも多いのかもしれません。
でも取りあえずは 0.1.0 リリースに向けて頑張ってほしいですね!