どうも ryo_grid です。

昨年はRustを覚えたいと思い、題材としてRESTインタフェースを持った分散KVS(実質はいわゆる分散ハッシュテーブル)を書いたりしました。

FunnelKVS: Rust implementation of autonomous distributed key-value store which has REST interfaces

この記事では、他言語を使ってきた私が、経験のないRustを用いてそこそこのコード規模・複雑さのシステムソフトウェアを書いてみた上で、Rustについて感じたことを、独断と偏見で述べます。

Rustってなんか流行りそうな雰囲気あるけど難しいとも聞くし、どうなんだろ?と考えている方や、Rustガチ勢の方々に、「初学者はこう感じるんだな」「ここらへんに苦労するんだな」というところを伝えることで、Rustのスムーズな普及に少しでも寄与すればと思い記事にする次第です。

また、私が書いたRustなコードのRust的でないところや、こんな便利crate使って書いたら良くなるゾ、というところがあれば指摘いただけるとありがたやです。

前提: 私のプログラミング言語歴や職歴

    • とりあえず、以下など参照すると雑には伝わるかなと思います

業績(というか作ってみたものリスト的なものが主です)
LinkedInのプロフィール
この記事を書いているのが2022年1月ですが、プログラミングは高校2年生であった2002年半ばぐらいからそこそこ本格的に始めました
2010年からフルタイムのソフトウェアエンジニアとしてのキャリアをスタート

言語歴についてもう少し詳しく

趣味プログラミングや学生時代の研究活動

C、Golang、Swift、Java、C#、VB6.0、Ruby、Python(信号処理、機械学習、便利スクリプト等の広い用途で利用)、Kotlin、Google Apps Script、SQL
Scala(少し)、Scheme(少し)、Haskell(少し)、Dart(ほんの少し)、Matlab/Scilab(ほんの少し)、R(ほんの少し)

大学生・大学院生時代のアルバイト

トータル3年間いかない程度だったはず
Java、C#、Ruby(Ruby on Railsのバージョン1系)、Groovy、JS

職業プログラマとして

C、C++(ほとんどC、的な使い方しかしたことがない)、C#、Ruby、Python(バックエンド系が主)、JS、CoffeeScript、TypeScript、SQL
C++以外はまあまあ書けると思っています

Rustについての雑感(あくまで独断と偏見です)

    • 素晴らしい言語であるのは間違いない

 

    • 使いこなすのが難しいかどうかと言えば、まあ難しい言語だと思う

学習コストが高い

私はHaskellの場合を例外として、プログラミング言語を新たに学ぶために書籍を買うということはしたことがなかったがRustの場合は本能的に買った

エンジニアとしての基礎体力が比較的高くないとまともに扱えない感

極論、誰でも使える言語かと言うとそうではないように感じた

メモリ安全等々、堅牢なソフトウェアを開発しなければならないという要件が強くなければ、少なくとも商用ソフトウェアのチーム開発などで新規に採用するのは、敷居が高そう

メンバがRustの経験は無いまでも、皆つよつよとかだったら別

(定量的なエビデンス出せとか言われると困りますが、)現状、エコシステムの充実度については、システムプログラミング言語として立ち位置の近いC++やGolangと比べると弱い印象

上記雑感のいくつかについての詳細 + α

“素晴らしい言語であるのは間違いない”

使いこなせれば堅牢でパフォーマンスの高いプログラムが開発できる
マクロも強力で、それを活用したcrateを用いることで高い生産性を実現することも可能
コンパイルが通ればロジックバグを除いて大体大丈夫だろう的な安心感がある

“使いこなすのが難しいかどうかと言えば、まあ難しい言語だと思う”

CやC++等でメモリを意識したプログラミングを経験していないと、所有権の考え方や、Box型、Arc/Rc型、Cell/RefCell/UnsafeCell型などのデータ領域(メモリ領域)の割当・アクセス制限に関連する仕組みの考え方を理解するのは苦労しそう

そのあたりを抽象的な説明で理解させられる可能性も無いとは思わないがやはり大変そうな気はする
unsafeブロックの利用が必要な場面などもそうかも

とはいえ、シングルスレッドのプログラムならまだ何とかなりそう

コンパイラは結構親切だし

「こう修正するとうまくいくんじゃないかな?」 とか教えてくれる

問題は、スレッド間の共有データが存在するマルチスレッドなプログラムを書こうとした場合

(そもそも上記のようなマルチスレッドプログラミングが本質的に難しいという問題はもちろんあるとして)
Rustはマルチスレッドプログラミングを行う場合も、データの所有権同様、スレッド間で共有される可能性のあるデータへのアクセスに対して制約を強いる

(だからこそ、並行・並列処理を行うプログラムも堅牢性高く開発するのに向いている、と言われているわけではあるが)

ここでは、素直にスレッドを立ててマルチスレッドなプログラムを構成することを想定

チャネルを用いたプログラミングモデルなどもあるがスレッド間のデータ共有に関しては特別ラクになったりはしない認識
非同期プログラミングを導入しても同様

スレッド間でのデータ共有への制約と、データの所有権の制約が組み合わさるとただでさえ難しいコーディングがさらに難しくなる感はあった

Rustのパラダイムにフィットしたコード設計ができないと、クソコードを書かざるを得なくなる

分散KVSを開発するにあたっては、Chordなる分散ノードアドレス解決アルゴリズム(プロトコル)を用いたが、実装するとなると割とややこしいアルゴリズムである
いきなり実システムを、それもRustで作ってもうまくいかない自信があったのでシミュレータをPythonで書いた

マルチスレッドで共有データを触るプログラム

続いて、段階を踏んで進めようということで、例外処理のあたりのケアをした上で、シミュレータのコードをRustにポーティングした

最終的に実システム化する際にメソッド呼び出しをRPCに置き換えれば良いという感じの設計にしてあったこともあり、ひとまずシミュレータの状態で、コード設計はなるべくいじらないようにしてRustにポーティングした

=> クソコードができあがって絶望した

Rust版のシミュレータ(アドレス解決までのみ、かつ故障ノード発生の考慮無し)

あまりにひどかったので、シミュレータから実システムに落とし込む際は、自身が開発している分散KVSと大体同じようなものである、RustなChord実装の rust-chord のコード設計を参考に、ある程度Rustのお作法に寄せた

利用するMutexをサードパティのリエントラントなものから、Rustの標準ライブラリのMutexに変えた(非リエントラント)ことも大きな変化要因であるが、私としては、お作法を寄せたことで大分マシになった、と思っている

シミュレータのポーティング時に利用したMutex実装である parking_lot を利用していた際は、Rust標準のMutexより保護する対象のデータを多くの型でラップしなければならない等の理由から、コードが冗長になっていた
具体的にはラップしたデータの皮むきのコードが冗長になっていた
ちなみに、Python標準のMutexはリエントラント

親ポチの記述を言い換えると、Rustつよつよパーソンが詳細設計なりクラスモデリングする体制が作れないと破綻したコードベースが生まれるように思われた

私はもちろんつよつよパーソンではないので、いくらかマシになったかな、という程度

余談(1)

    • 注: 以下は私の知識不足によるものが原因である可能性も大なので鵜呑みにはしないで下さい

 

    • gRPC や REST を RPC として用いようとcrateを探すとデファクトのものは多くが非同期プログラミングを導入していることが前提となっていた

tokio(実質) をランタイムとして導入している前提と言い換えても良い

非同期プログラミングの有用性は理解しているつもり

確かにスレッドプールを用いて省リソースでの並行・並列処理が実現できるのは良い
いちいち明にスレッドを立てずとも、並行・並列処理ができるのも便利ではある

ただ、今回は同期でやりたかった。が、gRPCを実装しようとすると非同期プログラミング(というかtokio)の導入は避けられなかった

ノード間通信をRESTからgRPCに置き換えて高速化しようと思ってトライして、一応動くようになったが、ほとんど高速化が成されなかった、などもろもろあって、全部RESTという実装に戻した
gRPCを実装してみたブランチはこちら

https://github.com/ryogrid/FunnelKVS/tree/dkvs-grpc-with-tonic-avoid-self-call/src

ちなみに、非同期プログラミング(≒tokio)を導入したところ…

awaitを使うためには基本的には呼び出し元の関数が async でないとダメルールによって、今回のプログラムの構成上、ほとんどの関数を非同期関数化(async関数化)するはめになった

つらい

tokioを導入していなかった時点(RPCとしてRESTを用いていた時点)と比べて、パフォーマンスのばらつきが大きくなった

スレッドプール内のスレッド数や、キューに投げ込む際の非同期タスクの優先度などが関係していそうに思ったが、そのあたりを自前でチューニングする(ためのインタフェースに触る)ことは、調べた限り簡単にはできそうになかった

main関数の定義の上にアノテーションを置く以外にランタイムを起動する方法があることを知り、試してみるも、どう起動すればアノテーションを置いた場合と同様にランタイムが動いてくれるのか分からずプログラムがクラッシュ

実装を読めば良いのかもしれないが、そこまでの気合は無く・・・

main関数へのアノテーションにより起動したランタイムのインスタンスを得る関数があれば良さそうなものだけれど、私には見つけることができず

困った

非同期プログラミングを導入すると、借用やMutexのロック保持に(詳細は省略するが、)追加で制約が加わる

つらい

Mutexのロック保持の制約は tokio の非同期プログラミングなコード用のMutexを用いることで回避できるが、公式のドキュメントには低速なので出来る限り使わないことが望ましいとあった

使ってみると本当に低速で、目に見えるレベルでパフォーマンスが落ちた
困った

つまるところ、先進的な?非同期プログラミングというモデルの導入に積極的なのは良いことなのかもしれないが、サードのcrateの開発者の方々は、非同期プログラミング用ランタイム無しで使えるものも提供してくれるとありがたいな、と私のようなよわよわRustコーダーは思ったのでした

余談(2)

    • 開発したKVSのテストや簡単な評価のために、RESTやgRPCのリクエストを行うためのツールをGolangで書いてみたが、思いのほか良い感じであった

 

    • 聞くところによると、JavaなどのGCあり言語を採用しようとする際に課題として挙げられがちな、GC実行時の(短時間の)システム停止の問題も、Golangでは処理系の工夫のおかげなのか、比較的軽減されているらしい

 

    • そんなわけで、ガチアンドガチのシステムプログラミングでなければ、Golangあたりを採用するのが無難な感じがしている今日この頃

ちなみに、一度作ってみたいということで開発を始めたRDBの開発では Golangを採用していたりする

SamehadaDB: A Simple Relational Database implemented in Golang
まだRDB実装のお勉強中でコードは書けていないけれど…

関連記事(過去に書いたもの)

    • Pythonで書いた分散KVSのシミュレータをRustに書き換えようとして苦労している話 – Qiita

 

    Pythonで書いた分散KVSのシミュレータをRustに書き換えようとして苦労している話(2) – Qiita

参考(Rustを書く際のアンチパターン等)

    • Things you can’t do in Rust (and what to do instead) – LogRocket

Anti-patterns – Rust Design Patterns

アンチパターンのところより、このサイト全体をRustのお作法を習得するための参考として通読した方が良い気もします

“3.3 Structual” のあたりが大事な感

他でRust的なコード構成のコツ的な情報がまとまったページがあったらコメント欄によろしくおねしゃす!

enjoy!

bannerAds