norpc: Tokioランタイム上で動くマイクロサービスを作るフレームワーク

microservices

マイクロサービスは現代的なシステムの設計技法の一つであり、 コンテナ技術との相性が良いため、広く使われるようになった。

GoFによる古典的なデザインパターンはオブジェクト指向のコード設計に関するものだが、 マイクロサービスを前提とした場合にもやはりデザインパターンがある。 サイドカー、アダプタ、アンバサダ、こういったパターンを知っていると、 設計をより柔軟に思考することが出来る。

マイクロサービスでは、gRPCが広く使われる。 gRPCは言語非依存のフレームワークであり、あるサービスはRustで、 別のサービスはPythonでなどという実装が可能になる。

Rustの武器は、非同期ランタイムシステムだ。 Rustを使えば、非同期アプリケーションの開発は簡単にはなるが、 おれはさらにそこにマイクロサービスの技術を導入したい。 つまり、非同期ランタイムの上でマイクロサービスを実現したい。 こうすることで、単一のランタイム内においてマイクロサービスの技術を使用可能になり、 開発がさらに容易になる。

tarpc

当然、そのようなことを考える人間はおり、 先行技術としてはtarpcが存在する。 しかし、tarpcは古くから(リリースノートによると、5年も前から)あるソフトウェアであり、 設計が現在のエコシステムを活用していない。 そのため、コードが意味もなくでかい。

Towerは、リクエストからレスポンスを生成する関数を中心としたフレームワークであるが(Scala業界でいえばfinagleのようなものだ)、 もしtarpcがTowerを活用していれば、多くのコードは不要になる。 これについては、tarpc#356にて議論を行った。 おそらくだが、tarpcがこの問題を修正の積み重ねによって解決するのは現実的ではない。

tarpcに関わる問題のもう一つは、プロセス内マイクロサービスと、ネットワーク越しのマイクロサービス(シリアライザにserdeを使っている) を同じ抽象の下に実現しようとしており、 プロセス内マイクロサービス側は、ネットワーク越しのマイクロサービスの特殊なバージョンとして実装されていることだ。 これにより、実装は割を食う。 この点についていうとおれは、gRPCを実装したTonicが存在する今に至っては、 ネットワーク越しのバージョンをサポートする意味は実用上は皆無であり、 意味もなく損をしているだけだという考えを持っている。

norpc

だから、おれはゼロからnorpcを作ることにした。

https://github.com/akiradeveloper/norpc

norpcは、コード生成にマクロを使わず、Tonicのようにコンパイラを提供している。

クライアントやサーバの実装はTowerのServiceを実装しており、 timeoutやrate limitingやbufferingなどといった機能を追加したければ、 Towerのエコシステムにあるデコレータをスタックすればよい。

サービスの定義はほぼRustのように書くことが可能で、

service HelloWorld {
    fn read(id: u64) -> Option<String>;
    fn write(id: u64, s: String) -> ();
    fn write_many(kv: HashSet<(u64, String)>) -> ();
    fn noop() -> ();
}

クレートのコンパイル時に実行されるbuild.rs内に

fn main() {
    norpc::build::Compiler::default().compile("hello_world.norpc");
}

のように書くことで、OUT_DIRが指すtarget以下のどこかにコードが生成される。 あとは、コード内でこのように書けば、生成されたコードがインクルードされる。

norpc::include_code!("hello_world");

開発は20時間ほどぶっ通しで行った。 さすがの余も疲れたぞ。