Rust言語の紹介、主要なテクノロジー、および実践的な経験
編集者注:高可用性アーキテクチャの共有と、アーキテクチャの分野で典型的な重要性を持つ記事の配布。この記事は、高可用性アーキテクチャグループのTangLiuによって共有されています。高可用性フレームワークのパブリックアカウント「ArchNotes」からのものであることを示してください。
PingCAPのチーフアーキテクトであるTangLiuは現在、次世代の分散データベースTiDBと分散ストレージTiKVの開発に取り組んでいます。オープンソース愛好家、言語愛好家、GoやRustなどの実務家。
みなさん、こんにちは。PingCAPのTangLiuです。Rust関連の知識とRustの使用に関するチームの実践的な経験を今日皆さんと共有できることを大変光栄に思います。
Rustを選ぶ理由
まず、最近Goコミュニティで議論されていることについて話しましょう。Dropboxは基盤となるS3のようなサービスをRustに変更して書き直しました。これにより、Rustの人気が急上昇しました。このような会社であるDropboxと呼ばれる人は、通常、技術アーキテクチャに基づいています。ウィンドベーン。共通の懸念事項:
DropboxがGoを使用せず、代わりに開発に急な学習曲線を持つRustを使用するのはなぜですか?
実際、この質問は私たちにも当てはまります。Goでの経験が豊富だと考えているチームは、なぜGoではなくRustを選択するのでしょうか。
私たちがしていることを紹介します。私たちのチームは、一般にNewSQLと呼ばれる次世代の分散データベースの開発に取り組んでいます。理論的根拠全体はGoogleのF1とSpannerに基づいているため、当然、NewSQLも2つの部分に分かれています。1つはステートレスSQLレイヤーです。この段階で、TiDB(http://dwz.cn/2XZkSm)はオープンソースであり、もう1つはTiKVと呼ばれる分散KVレイヤーです。4月にオープンソースになる予定です。
TiDBはGoで作成されています。Goに精通している学生は、Goで分散アプリケーションを作成するのが非常に高速で便利であり、チームメンバーは非常に深いGoプログラミング経験を持っていることを知っておく必要がありますが、TiKVを実行することを決定しています。当時、Goを使用したり、C ++やJavaなどのより一般的な静的言語を使用したりする代わりに、私たちはまったくなじみのない言語、Rust、Why?を選択しました。
Goを使用するときに発生する問題について説明します。
- GC、GoのGCは1.6以降非常によく改善されており、将来的にはさらに良くなると思いますが、パフォーマンス要件が非常に高い分散アプリケーションの場合、GCのない言語を選択する傾向があります。メモリはより制御可能です。
- CgoとTiKVは、基盤となるストレージエンジンとしてRocksDBを使用します。Goを使用する場合は、Cgoを使用してRocksDBを呼び出す必要がありますが、Cgoのパフォーマンスは比較的大きく低下します。
C ++やJavaを選ばないことについて話しましょう。C++は、大規模な開発にはチーム全体の要件が非常に高いことが主な理由です。また、C ++では短期間で製品を保持することは不可能だと感じているため、当然諦めます。Javaに関しては、私たちのチームの中でJavaに堪能な人はほとんどいないので、彼らはただあきらめました。
したがって、Rustには、タイプの安全性、メモリの安全性、スレッドの安全性など、いくつかの優れた機能があるため、最終的にRustに目を向けました。これについては後で詳しく説明します。
Rustの基本
Rustの公式ウェブサイトでは、Rustの紹介を見ることができます。これは、パフォーマンスの高いシステムプログラミング言語であると同時に、コンパイル段階でメモリやマルチスレッドデータアクセスなどの問題を検出して、プログラムの安全性と堅牢性を確保するのに役立ちます。
言い換えれば、Rustを使用してプログラムを作成する場合、プログラムをコンパイルできれば、C ++での多くのメモリリーク、セグメントの障害、データ競合の問題について心配する必要はありませんが、これらはすべてコストがかかります。Rustを使い始めるのは簡単ではなく、難易度はC ++に匹敵します。Goの場合は、1週間の学習後にプロジェクトにコードを提供し始めることができますが、Rustに切り替えた場合でも、1か月間、コンパイラーに苦労している可能性があります。コードはコンパイルされません。
Rustにさらされた人の数がわからないため、Rustの構文の基本を理解するための例をいくつか示します。
1つ目は、最も一般的なHelloの世界です。
(画像をクリックすると、画像が全画面でズームされます)
fnはRustのキーワードで、関数を定義するために使用されます。関数名はmainで、println!はマクロです。rustスタイルでは、マクロは最後に「!」で表されます。
次に、いくつかの変数を定義しましょう。次に、rustと他の言語の違いを示します。
(画像をクリックすると、画像が全画面でズームされます)
上記では、タイプu32の変数を宣言しましたが、初期化されず、その値を出力します。Goなどの一部の言語では、0が出力されますが、Rustでは、これはコンパイルに失敗し、コンパイラは次のプロンプトを表示します。
(画像をクリックすると、画像が全画面でズームされます)
上記のエラーは、初期化されていない変数が使用されていることを示しています。最初に初期化して、次のようにしましょう。
(画像をクリックすると、画像が全画面でズームされます)
上記では、最初に初期値が0の変数を定義し、次にそれを10に変更して印刷、コンパイルしたところ、次のエラーが見つかりました。
(画像をクリックすると、画像が全画面でズームされます)
今回、コンパイラは不変の変数が変更されたことを教えてくれました。Rustでは、変数は不変と可変に分けられます。この変数を変更できるかどうかを明示的に定義する必要があります。次のように、mutキーワードを使用して、この変数が可変であることをRustに通知します。
(画像をクリックすると、画像が全画面でズームされます)
これは正常にコンパイルされます。
上記の簡単な例を通して、誰もがRustの予備的な印象を持っているはずです。より詳細な理解については、公式Webサイトを参照してください。
Rustの開発を容易にするための重要な技術的ポイント
Rustを選択する理由の前半で、Rustにはエラーが発生しにくい同時プログラムを作成できるいくつかの優れた機能があるため、前述しました。そして、この重要なポイントを理解して習得している限り、プログラム開発にRustを使用するのは非常に簡単だと思います。
型安全性
Rustは、型の安全性を厳密に要求する言語です。C/ C ++の世界では、次のように型変換を自由に実行できます。
(画像をクリックすると、画像が全画面でズームされます)
C / C ++で非常に一般的なこの種の処理は、Rustでは許可されていません。次に例を示します。
次のコンパイルエラーが発生します。
この種のメモリ変換を強制すると、安全でないものを使用できます。これにより、Rustに安全でないことを明示的に通知しますが、心配する必要はありません。
(画像をクリックすると、画像が全画面でズームされます)
通常の状況では、Rustは上記のコードの記述を許可しないため、プログラマーに安全でないものを選択する権利を与え、安全でないコードが安全でない場所からどこにあるかを明確に知ることができ、注意を払う必要があります。
メモリの安全性
Rustの最も厄介なキーポイントの1つに入りましょう。Rustはプログラムのメモリの安全性を保証できるので、どのように保証しますか?最初に理解する必要があるのは、Rustでの所有権と移動の概念です。
所有権+移動
Rustでは、どのリソースも1つの所有権しか持つことができません。たとえば、最も単純な例は次のとおりです。
ここでは、letを使用してベクトルを変数aにバインドし、aがこのベクトルの所有権であると考えることができます。次に、このベクトルのリソースとして、一度に1つの所有権のみが許可されます。次のコードを見てみましょう。
(画像をクリックすると、画像が全画面でズームされます)
上記の例では、aをbに割り当て、a [0]の値を出力し続けます。これは、ほとんどの言語で問題のない操作です。Rustでは、次のようにエラーが報告されます。
どうして?a [0]を出力する前に、let b = aという操作を実行しました。この操作はRustでの移動と呼ばれ、ベクトルに対するaの所有権がbに移され、aがベクトルの所有権を放棄することを意味します。aはこのベクトルの所有権を持っていないため、当然、関連データにアクセスできません。
所有権と移動の概念は、Rustを学習する最初の落とし穴と見なす必要があり、次のコードを簡単に記述できます。
(画像をクリックすると、画像が全画面でズームされます)
関数do_vecのため、このコードはまだコンパイルできません。aは、移動するベクトルの所有権をすでに与えています。
したがって、通常、let b = aのようなコードが表示される場合、移動が所有権を失ったことを意味しますが、例外が1つあります。実装のタイプがコピー特性を実装している場合、b = aは移動ではなく、コピーです。次のコードは正常にコンパイルできます。
上記のコードでは、b = aとします。aは移動しませんが、bが使用するために独自のデータをコピーします。通常、基本的なデータタイプはコピー特性を実装します。もちろん、カスタムタイプのコピーを実装することもできますが、コピーのパフォーマンスを比較検討する必要があります。
かりて
前にdo_vecの例を示しましたが、この関数を呼び出した後もこのベクトルを本当に使用し続ける必要がある場合は、どうすればよいでしょうか。
ここで私たちは2番目のピットと接触し始めました、借ります。上記のように、移動は私があなたに所有権を与えたことを意味し、借りることは私があなたにリソースを使用する権利を貸したことを意味します、そして私はそれを私に返します:
(画像をクリックすると、画像が全画面でズームされます)
上記の例では、パラメータで&を使用して借用を示しています。do_vec関数はaを借用し、関数の終了後にそれを返すため、後で使用し続けることができます。
借用なので、もちろん使用できるので、次のようなコードを書くことができます。
その後、Rustは再び豪華にエラーを報告し、次のように出力しました。
借用は不変の借用のみであり、データを変更できないためです。また、変数を変更する場合は、mutで明示的に宣言する必要があることも前述しました。借用についても同様です。借用したものを変更する場合は、次のように可変借用を使用する必要があります。
(画像をクリックすると、画像が全画面でズームされます)
Borrowにはスコープの概念もあります。次のようなコードを書くこともあります。
(画像をクリックすると、画像が全画面でズームされます)
コンパイラが再びエラーを報告したことがわかりました。出力:
yを使用してxの可変を借用しましたが、まだ返していないため、後で不変の借用は許可されていません。これにより、スコープを介して可変のライフサイクルを制御できます。
(画像をクリックすると、画像が全画面でズームされます)
このように、printIn!が不変の借用を実行すると、yの可変の借用が返されます。
変数は同時に複数の不変の借用を実行できますが、許可されるのは1つの可変の借用のみです。これは実際には読み取り/書き込みロックと非常によく似ています。複数の読み取りロックは同時に許可されますが、一度に許可されるのは1つの書き込みロックのみです。
一生
C ++では、誰もがワイルドポインターに非常に感銘を受けていると思います。削除されたオブジェクトを参照し、それを再び使用するとパニックになることがあります。Rustでは、この問題は生涯を通じて解決されますが、生涯の導入後、コードはさらに醜く見えます。簡単な例:
(画像をクリックすると、画像が全画面でズームされます)
構造を定義します。内部のフィールドbは外部のu32変数への参照であり、この参照の有効期間は 'aです。ライフタイムを使用すると、bによって参照されるu32データのライフタイムをAのライフタイムより長くする必要があります。
しかし、上記の例では、スコープ内にあり、bがcを参照するようにしますが、スコープが終了するとcは消えます。この時点で、bは無効な参照であるため、Rustはエラーをコンパイルします。
スレッドセーフ
先に述べたように、Rustは移動、借用、ライフタイムのメカニズムを使用してメモリの安全性を確保しています。これらの概念は理解しにくいものであり、コードを作成するときに誰もがコンパイラと格闘するのは簡単ですが、個人的にはこれらの概念を理解している限り、Rustを書くことは問題ではありません。
さて、メモリの安全性について話した後、すぐにスレッドの安全性に入ります。マルチスレッドプログラムを作成するのは難しいことは誰もが知っています。時々、少し不注意に、データの競合やその他の状況が発生し、データエラーが発生し、そのようなバグを検出するのが難しい場合があります。
たとえば、Goでは、次のことができます。
(画像をクリックすると、画像が全画面でズームされます)
上記の例は非常に極端です。ここにコードを記述しないでください。ただし、実際には、データ競合の問題に直面する可能性があります。Goは--raceを使用してデータレースチェックを開くことができますが、通常はテストにのみ使用され、オンラインでは使用されません。
一方、錆は、誰もがソースでデータレースコードを書くことを完全に防ぎます。まず、Rustの並行性に関する2つの特性、送信と同期について理解しましょう。
- 送信
型が送信を実装する場合、この型はあるスレッドから別のスレッドに安全に移動できると考えることができます。 - 同期
型が同期を実装する場合、この型は共有参照(つまり、Arc)を介して複数のスレッドで安全に使用できると考えることができます。
上記の概念はかなり紛らわしいようです。より単純なことは、型がSend + Syncを実装している場合、これはマルチスレッドで安全に使用できるということです。
簡単な例を見てみましょう。
(画像をクリックすると、画像が全画面でズームされます)
上記は、典型的なマルチスレッドデータレースの問題です。Rustはコンパイルできず、エラーが報告されます。
前述のように、Arcを使用して、タイプがマルチスレッドで安全に使用されるようにし、Arcを追加します。
(画像をクリックすると、画像が全画面でズームされます)
これで、複数のスレッドからベクターにアクセスできますが、それでもエラーが発生します。
(画像をクリックすると、画像が全画面でズームされます)
マルチスレッドの読み取りだけでなく、マルチスレッドの書き込みも必要なためです。当然、Rustはそれを許可しないため、次のようにロック保護を表示する必要があります。
(画像をクリックすると、画像が全画面でズームされます)
したがって、安全なマルチスレッドの方法でデータを使用する場合、最も一般的な方法は、カプセル化にArc <Mutex <T >>またはArc <RwLock <T >>を使用することです。
もちろん、Rustは、Arc + Lockに加えて、スレッド間のデータ通信を容易にするチャネルメカニズムも提供します。チャネルはGoチャネルに似ており、一方の端が送信され、もう一方の端が受信されますが、ここでは詳しく説明しません。
Rustでは、複数のスレッドで使用されるデータを明示的にロックする必要があるため、ここにポイントがあります。このプログラミングスタイルは、後でGoを作成したときに直接導入されました。Goでは、私は通常次のようにロックを記述していました。
(画像をクリックすると、画像が全画面でズームされます)
ミューテックス変数mを使用して、複数のスレッドでデータv1とv2を保護しますが、この書き込み方法では、実際には、ロックが保護するデータを忘れがちです。私はRustの影響を受けたので、次のように書くのが好きです。
(画像をクリックすると、画像が全画面でズームされます)
上記は、ロックと保護する必要のあるデータを構造に入れるためのものです。コードを見ると、ロックが保護するデータを知ることができます。
さび開発実習
Rustの基本的な機能のいくつかについて説明しましたが、プロジェクトでRustを使用した関連する経験から始めましょう。
貨物
Rustをプロジェクト開発に使用する場合、最初に知っておく必要があるのはCargoです。CargoはRustのビルドおよびパッケージ管理ツールであり、Rust開発プロジェクトの標準になるはずです。Cargoの使用はまだ非常に簡単で、公式Webサイト(https://crates.io/)を直接閲覧できます。
quick_error!
最初に、Cプログラムを作成していたときに、関数にエラーがあるかどうかを示すために、さまざまなint戻り値を定義しました。次に、C ++では、予期してエラーを処理できます。ただし、どの規格が採用されているかはまだ決定的ではありません。
Goに関しては、関数の最後の戻りパラメーターがエラーであることに直接同意します。Goのエラー処理を紹介する公式ブログもあります(http://blog.golang.org/error-handling-and-go)。
Rustでは、エラーには対応する処理仕様であるResultもあります。Resultは次のように定義された列挙型です。
(画像をクリックすると、画像が全画面でズームされます)
言い換えれば、私たちの関数はすべてResultを返し、それを外部で判断することができます。それがOKの場合は正しい処理であり、Errの場合はエラーです。
エラー処理の詳細な説明は次のとおりです(https://doc.rust-lang.org/book/error-handling.html)。
通常、誰もが上記の仕様に従ってエラーを処理します。つまり、独自のエラーを定義し、他のエラーを独自のエラーに変換するfrom関数を実装してから、try!を使用してコードのエラー処理を簡素化します。
しかし、すぐに非常に深刻な問題が見つかりました。独自のエラーを定義し、他のエラーを対応するエラーに変換することは、非常に冗長で複雑なことなので、quick_errorを使用します!(http:// dwz。 cn / 2XZpNo)を使用して、プロセス全体を簡素化します。
クリッピー
初めてGoコードを書いたとき、Goのfmtにとても感銘を受けました。コーディングスタイルの競合について心配する必要はありません。Rustにも関連するrust fmtがありますが、さらに驚いたのはRustのClippyツールでした。 。Clippyはもはやコーディングスタイルに巻き込まれていませんが、コードはこのように書かれるべきであると直接あなたに告げます、そしてそれは間違っています。
非常に簡単な例:
(画像をクリックすると、画像が全画面でズームされます)
このコードはコンパイルして渡すことができますが、Clippyサポートをオンにすると、次のプロンプトが直接表示されます。
(画像をクリックすると、画像が全画面でズームされます)
つまり、to_stringを使用せず、to_ownedを使用してください。
高性能ネットワークサービスを開発するための通常の選択は、イベントベースのネットワークモデルであるepollであることは誰もが知っています。Rustでは、この段階で成熟したライブラリはMIOです。MIOは、Linuxでのepoll、UNIXでのkqueue、WindowsでのIOCPなど、さまざまなオペレーティングシステムに統合された抽象サポートを提供する非同期IOライブラリです。ただし、プラットフォームの拡張を統合するために、MIOは一部の実装で妥協しました。
ぼくの
たとえば、Linuxでは、システムはイベントfdサポートを直接提供しますが、UNIXとの互換性を保つために、MIOはイベントfdの代わりに従来のパイプを使用してイベントループアウェイク処理を実行します。
これは、MIOが提供する個別のスレッド通信チャネルメカニズムです。スレッド通信にはRust独自のスレッドチャネルを使用できますが、MIOが導入された場合は、MIO独自のチャネルを使用することを好みます。主な理由は使用されるロックです。フリーキュー、パフォーマンスは向上しますが、キューサイズ制限の問題があります。送信頻度が高すぎても受信側が処理しない場合、送信に失敗します。
Rust言語の欠陥
私たちのチームはRustを数か月の開発期間使用してきましたが、もちろん、いくつかの不快な場所に遭遇しました。
1つ目は不完全なライブラリです。Goと比較すると、Rustのライブラリは本当に不完全です。Rustにはこの段階ではまだ大規模なアプリケーションがなく、不完全なlibが大きな理由であると思います。
TiKVはサーバープログラムであり、当然ネットワークプログラミングが含まれます。この段階での公式のネットモッドはブロックソケットでのみサポートされており、高性能ネットワークプログラムの開発には使用できません。幸い、MIOはありますが、MIOだけでは不十分です。GoではgRPCを使ってRPCを簡単に作成できますが、Rustではオープンソースがあるかどうかを確認するために長い間待たなければならないと思います。実現。
次に、Mac OS Xでは、パニックによって生成されたスタックが完全に読み取れなくなり、ファイルと行番号の情報がないため、バグを簡単にチェックすることができません。
もちろん、Rustは結局のところ比較的新しい言語であり、まだ改良と開発が続けられています。私たちは、Rustがどんどん良くなっていくと確信しています。
Q&A
-
GoのCgoとRustFFIの効率の違いは何ですか?
Tang Liu:SnappyのMaxCompressedLength関数をループで10,000回呼び出す簡単なテストを自分で作成しましたが、RustのFFIはGoのCgoよりも1桁速いことがわかりました。このテストはあまり正確ではありませんが、少なくともRustを証明しています。 FFIのパフォーマンスが向上します。 -
公式紹介では、Rustが推奨するプラットフォームはWindowsであるため、dllを生成できます。IDEは何ですか。
Tang Liu:私はWindowsを使ったことがないので、dllを生成する方法がわかりません。Rustの開発に使用されるIDEは、Vim、Emacs、Sublimeなどの一般的に使用されるIDEです。とにかく、すべてのRustプラグインがサポートされています。 -
RustがCライブラリを呼び出すのは便利ですか?
Tang Liu:RustがFFIを介してCを呼び出すのは非常に便利です。関連するドキュメント(https://doc.rust-lang.org/book/ffi.html)がありますが、結局のところ、これには言語間の関係があるため、コードを書くのは簡単ではありません。なんて格好良い。また、 FFIには安全でない保護が必要なため、通常、Rust関数のレイヤーを外側にラップします。 -
Rustのパフォーマンス指標とは何ですか?
Tang Liu:Cgo vs FFIテストなどの特定の環境でのみGoとの比較を行ったため、錆のパフォーマンスを測定するのはあまり良くありません。この点で、パフォーマンスはGoよりも優れています。さらに、RustはGCのない静的言語であるため、パフォーマンスに問題はないと思います。そうしないと、DropboxはRustでS3のようなアプリケーションを作成できません。 - Rustの周囲のエコロジーは何ですか?たとえば、DBSQL / MQなどの一般的に使用されるサードパーティシステムとのバインド?
Tang Liu:Rustのエコロジーは説明することしかできず、GoやJavaとは比較になりません。MySQLなどの一般的に使用されるバインディングがありますが、私はそれらを使用していません。主な理由は、公式ネットワークIOが同期しているため、これらの処理を実行するにはマルチスレッドを使用する必要があり、MIOの非同期モードでは、コードロジックが非常に厳しいことを誰もが知っています。そのため、通常、これらの複雑なビジネスシステムの開発にRustを使用することはありません。Goの方が適していると感じています。
関連記事
- Node.jsへのフルスタックエンジニアの旅(Sang Shilong)
- 並行性の痛み:スレッド、ゴルーチン、俳優(王元明)
- Codisの作者であるHuangDongxuが、分散型Redisアーキテクチャの設計について詳しく説明しています(Huang Dongxu)
この記事では、Liu Yun、ポスターTang Duanrong、編集者You Qian、Hao Yaqi、Yin Wenyu、Yin Xuegangの放送を計画しています。Rust言語の開発についてさらに話し合いたい場合は、公式アカウントに従ってグループに参加してください。高可用性フレームワーク「ArchNotes」WeChat公式アカウントからのものであることを示し、次のQRコードを含めてください。
利用性の高いアーキテクチャ
インターネットの構築方法を変える
QRコードを長押しして、「HighAvailabilityArchitecture」公式アカウントに登録します