エリクサーの方法で錆を書く

14929 ワード

私が大ファンなのは秘密じゃないElixir , だから私が錆の開発を始めたとき、私はエリクサーから世界のいくつかのアイデアを持ってきましたRust . このポストは、私が錆にエリキシルの力をもたらすために構築しているツールのいくつかを説明します.

何がエリクサーはとても素晴らしいですか?
それらのいくつかを選ぶのは難しいです、しかし、私はエリクサーの最も大きな利点が使用から来ると思っていますErlang 基本的な仮想マシンとして、これらの2つのプロパティから特に以下のようにします.
  • 巨大な並行性
  • 耐故障性

  • 巨大な並行性
    あなたがそれを経験するまで、これは説明するのが難しい何かです.私はキャリアの早い段階で、リクエストを処理しながらスレッドを作成しないことを学びました.スレッドは重く、高価であり、それらのあまりにも多くのマシン全体をダウンさせることができます.ほとんどの場合、それはAを使用するのに十分ですthread pool , しかし、一度並列タスクの数がプール内のスレッド数を超えてしまうと、このアプローチは失敗します.
    例を見てみましょう:ちょうど100 msごとに目を覚ますとすぐにスリープ状態に行く2000スレッドを作成する錆アプリケーションを想像してみましょう.
    use std::thread;
    use std::time::Duration;
    
    fn main() {
        for _ in 0..2_000 {
            thread::spawn(|| loop {
                thread::sleep(Duration::from_millis(100));
            });
        }
        thread::sleep(Duration::from_secs(1_000));
    }
    
    スレッドは何もしないが、ちょうど私のMacBook上でこれを実行すると、数秒後に再起動することを強制します.これにより、スレッドに対して大量の並行処理を行うことができなくなります.この問題には多くの解決策がある.Elixirによって選択されたものは、何かと呼ばれる抽象的な並行タスクであるProcesses . 彼らは非常に軽量なので、実行しても2 million of them 挑戦を提示しません.

    さびの大きい同時性
    あなたは達成することができますamazing concurrency and performance async - rustを使用していますが、async - rustで動作するのは、通常の錆コードを書くのと同じくらい簡単ではありませんし、Elixirプロセスと同じ機能を提供していません.
    私は長い間、私はどのようにさびのエリクサープロセスを再構築する何かを作ることができた後、私は中間段階を導入するアイデアを思いついた.WebAssembly . webassemblyは、Rustがターゲットにすることができる低レベルバイトコードの仕様です.このアイデアは単純だったが、X 86 - 64用の錆をコンパイルするのではなく、WASMターゲットにコンパイルする.そこから、ライブラリのセットと、Ruustプロセスの概念を公開するWebssemblyランタイムを構築します.オペレーティングシステムのプロセスやスレッドに反して、彼らは、小さなメモリのフットプリント、高速に作成し、終了し、軽量化され、スケジューリングのオーバーヘッドは低いです.他の言語ではgreen threads and goroutines , しかし、私は彼らにElixirの命名規則の近くにとどまるプロセスと呼びます.
    それが最初の一歩だったLunatic .
    同じ錆の例を見てみましょう.同時に並行処理の数を20 kにクランクアップする.
    use lunatic::{Channel, Process};
    
    fn main() {
        let channel: Channel<()> = Channel::new(0);
    
        for _ in 0..20_000 {
            Process::spawn((), process).unwrap();
        }
    
        channel.receive();
    }
    
    fn process(_: ()) {
        loop {
            Process::sleep(100);
        }
    }
    
    これを実行するには、このrustコードを.wasm ファイル名:
    ○ →  cargo build --release --target=wasm32-wasi
    
    それから以下を実行します.
    ○ →  lunaticvm example.wasm
    
    以前の例とは異なり、これは私の後半の13のMacBookでしゃっくりなしで実行され、CPUの利用は、我々は10倍の同時タスクを使用している場合でも、最小限です.ここで何が起きているかを調べましょう.
    Lunaticによって生成されたプロセスは、Async Rustによって提供される電力を実際に利用しています.彼らはAの上に予定されているwork stealing async executor , 同じasync-std . 呼び出しProcess::sleep(100) 実際に呼び出すsmol's at function .
    ちょっと待って!どうやってこの仕事をするの.await キーワード、あなた自身を尋ねることがあります.LunaticはGo、Erlang、緑のスレッドに基づいて錆の以前の実装と同じアプローチを取る.これは、プロセスを実行するための小さなスタックを作成し、アプリケーションが必要なときに成長します.これはコンパイル時に正確なスタックサイズを計算するよりも効率的ではありません.
    今、あなたは通常のブロッキングコードを書くことができますが、実行者はあなたが待っているなら、実行スレッドからあなたのプロセスを動かす気がします.
    以前に見たように、スケジューリングスレッドはオペレーティングシステムにとって大変な課題です.実行中のスレッドを別のスレッドに置き換えるには、たくさんの作業を行う必要があります(すべてのレジスタとスレッドの状態を保存するなど).しかし、狂ったプロセスの間の切り替えは、可能な最小限の仕事だけをします.というアイデアをlibfringe library 使用するasm! macro マジックは、狂気のコンパイラは、コンテキストスイッチ中に保存されるレジスタの最小数を把握することができます.これにより、スケジューリング・ラウナティック処理がゼロコストになる.私のマシンでは通常1 ns、関数コールと等価です.
    スレッドを使用する代わりにユーザ空間でプロセスをスケジューリングする別の利点は、アプリケーションが正常に動作していても、他のアプリケーションが通常マシン上で実行を続けることです.
    Lunaticが大規模な同時実行でアプリケーションを作成できるようになったので、フォールトトレランスを見てみましょう.

    耐故障性
    たぶん、最も知られているeralng/エリクサー哲学は"let it crash" . 複雑なシステムを構築するなら、すべての失敗シナリオを予測することは不可能です.何かがあなたのアプリケーションで失敗するだろうが、この失敗は、全体をダウンさせる必要はありません.
    Elixirプロセスは完全に孤立しており、互いにメッセージを通して通信することができます.これは、失敗が1つのプロセスの中に含まれていて、それらの残りに影響を及ぼす方法であなたのアプリケーションを設計するのを許します.
    LunaticはここでErlangより強い保証を提供します.
    それぞれの狂気のプロセスは、独自のヒープ、スタックとsyscallsを取得します.
    Lunaticの単純なTCPエコーサーバの例を見てみましょう.
    use lunatic::{Process, net}; // Once WASI gets networking support you will be able to use Rust's `std::net::TcpStream` instead.
    use std::io::{BufRead, Write, BufReader};
    
    fn main() {
        let listener = net::TcpListener::bind("127.0.0.1:1337").unwrap();
        while let Ok(tcp_stream) = listener.accept() {
            Process::spawn(tcp_stream, handle).unwrap();
        }
    }
    
    fn handle(mut tcp_stream: net::TcpStream) {
        let mut buf_reader = BufReader::new(tcp_stream.clone());
        loop {
            let mut buffer = String::new();
            buf_reader.read_line(&mut buffer).unwrap();
            tcp_stream.write(buffer.as_bytes()).unwrap();
        }
    }
    
    このアプリケーションを聴くlocalhost:1337 TCP接続のために、各々の入って来る接続を扱うためにプロセスを生み出して、ちょうど入って来る線を反響してください.
    あなたはそれを使用してテストすることができますtelnet :
    ○ → telnet 127.0.0.1 1337
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    Hello world
    Hello world
    
    あなたが気づく最初のことは、私たちがasync or .await キーワードは、このアプリケーションは完全にフードの下で錆のasync IOを利用します.
    また、TCP接続はプロセスに完全にカプセル化されます.
    クラッシュしない安全なCコードを呼び出しても
    fn handle(mut tcp_stream: net::TcpStream) {
        ...
        unsafe { crashing_c_function() };
        ...
    }
    
    クラッシュはこの場合の1つの接続にのみ含まれます.C関数のクラッシュを呼び出すと、仮想マシン全体が必要になるので、Elixirではこのような実装はできません.
    Lunatic専用の別の機能は、プロセスのsyscallアクセスを制限する可能性です.我々が前に取ったならばspawn 呼び出し:
    // Process::spawn_without_fs is not implemented yet.
    Process::spawn_without_fs(tcp_stream, handle).unwrap();
    
    内部から呼ばれるコードhandle 関数はファイルシステムへのアクセスのためにsyscallsを使うことを禁止する.実行がこのような低いレベルで起こっているので、これはC依存関係のためにも働きます.それはあなたがプロセスのサンドボックス要件を表現し、恐れなしで任意の依存関係を使用することができます.私は、あなたがこれをする他のランタイムに気づいていません.

    未来
    これは、単に能力の難題ですLunatic を提供します.が来るより多くの機能があります.一旦この基礎があるならば、可能性の新しい世界は開きます.いくつかの私は興奮している機能:
  • つのマシンから別のプロセスに透過的に移動する機能.プログラミングモデルはメッセージを通して通信するプロセスに依存します、そして、これらのメッセージがローカルで、または、異なるコンピュータの間でネットワークに送られるなら、それは本当に重要でありません.
  • ホットリロード今、私たちにはWASMバイトコードがあるとして、ステップ間で新しいJITマシンコードを生成して、システム全体がまだ動いている間、それを置き換えることが可能になります.
  • プロセスとしてWASMにコンパイルされた完全なアプリケーションの実行つの例は、私たちがsyscallsの完全な充電にあるように、アプリケーションからファイルストリーム/書き込みをTCPストリームにリダイレクトするでしょう.ここでの利点は、コードで実行環境をモデリングしている点です.
  • 狂人はまだ初期の日であるので、するために残っている多くの開発があります.あなたがそれについて興奮しているか、あなたがLunaticを利用したいと思う若干の考えがあるならば、電子メールの上に私に手を差し伸べてください[email protected] またはTwitterで.
    私はまた、この機会を使用して、錆に取り組んでいるチームに感謝します.Wasmer , Wasmtime , Lucet and waSCC . このプロジェクトにすべてのハードワークを置くことなく、狂気を構築することは不可能だろう.
    あなたがアーランとエリキシルの魔法についてもっと学びたいならば、これはそれについての私の大好きな会談のうちの1つです.まじめに、行って見て!