我對軟體開發充滿熱情,特別是按照人體工學創建軟體系統的難題,該系統可以解決最廣泛的問題,同時做出盡可能少的妥協。我也喜歡將自己視為系統開發人員,根據 Andrew Kelley 的定義,這意味著有興趣完全理解他們正在使用的系統的開發人員。在這篇部落格中,我與您分享我解決以下問題的想法:建立可靠且高效能的全端企業應用程式。這是一個很大的挑戰,不是嗎?在部落格中,我專注於「高效能網頁伺服器」部分 - 我覺得我可以在這方面提供一個新鮮的視角,因為其餘部分要么是眾所周知的,要么我沒有什麼可補充的。
一個重要的警告 - 將會有沒有程式碼範例,我還沒有實際測試過這一點。是的,這是一個重大缺陷,但實際上實現這一點需要花費很多時間,而我沒有時間,在發布有缺陷的部落格和根本不發布它之間,我堅持前者。我們已警告您。
我們將用哪些部分來組裝我們的應用程式?
確定了我們的工具,讓我們開始吧!
Zig 沒有對協程的語言等級支援:( 而協程是建構每個高效能 Web 伺服器的基礎。那麼,嘗試沒有意義嗎?
等一下,讓我們先戴上系統程式設計師的帽子。協程不是靈丹妙藥,沒有什麼是靈丹妙藥。涉及的實際好處和缺點是什麼?
眾所周知,協程(使用者空間執行緒)更加輕量級且速度更快。但具體是透過什麼方式呢? (這裡的答案大部分都是猜測,請持保留態度並親自測試一下)
例如,Go 運行時將 goroutine 重複使用到作業系統執行緒上。執行緒共享頁表以及其他行程擁有的資源。如果我們在混合中引入CPU 隔離和親和性- 線程將在各自的CPU 核心上持續運行,所有作業系統資料結構將保留在記憶體中,無需換出,用戶空間調度程序將CPU 時間分配給goroutine精度,因為它使用協作多任務模型。競爭還可能嗎?
效能的提升是透過將執行緒的作業系統級抽象放在一邊並用 goroutine 的抽象來代替來實現的。但翻譯中沒有遺失任何內容嗎?
我認為獨立執行單元的「真正」作業系統級抽象甚至不是線程 - 它實際上是作業系統進程。實際上,這裡的差異並不那麼明顯——區分執行緒和進程的只是不同的 PID 和 TID 值。至於檔案描述子、虛擬記憶體、訊號處理程序、追蹤資源 - 這些是否對於子進程是獨立的,在「複製」系統呼叫的參數中指定。因此,我將使用術語「進程」來表示擁有自己的系統資源(主要是 CPU 時間、記憶體、開啟檔案描述符)的執行線程。
これがなぜ重要なのでしょうか?各実行単位には、システム リソースに対する独自の要求があります。複雑なタスクはそれぞれ複数の単位に分割でき、それぞれが独自の予測可能なリソース (メモリと CPU 時間) の要求を行うことができます。そして、サブタスクのツリーをさらに上に行くほど、より一般的なタスクに向かって進みます。システム リソースのグラフは、長い尾を持つ釣鐘曲線を形成します。また、テールがシステム リソースの制限を超過しないようにするのはユーザーの責任です。しかし、それはどのように行われるのでしょうか?また、実際にその制限を超えた場合はどうなるのでしょうか?
独立したタスクに単一プロセスと多数のコルーチンのモデルを使用する場合、1 つのコルーチンがメモリ制限を超過すると、メモリ使用量がプロセス レベルで追跡されるため、プロセス全体が強制終了されます。これは最良のケースです。cgroup を使用する場合 (これは、ポッドごとに cgroup を持つ Kubernetes のポッドの場合に自動的に当てはまります)、cgroup 全体が強制終了されます。信頼性の高いシステムを作成するには、これを考慮する必要があります。 CPU 時間についてはどうでしょうか?サービスが同時に多くの計算負荷の高いリクエストに遭遇すると、サービスは応答しなくなります。その後、締め切り、キャンセル、再試行、再開が続きます。
ほとんどの主流のソフトウェア スタックでこれらのシナリオに対処する唯一の現実的な方法は、システム内に「脂肪」 (ベル曲線の末尾にある未使用のリソース) を残し、同時リクエストの数を制限することです。未使用のリソースに。それでも、外れ値と同じプロセスにたまたま存在する「無害な」リクエストを含め、OOM が強制終了されたり、応答しなくなったりすることがあります。この妥協は多くの人に受け入れられ、実際のソフトウェア システムに十分に役立ちます。しかし、もっと改善できるでしょうか?
リソースの使用量はプロセスごとに追跡されるため、理想的には、小さく予測可能な実行単位ごとに新しいプロセスを生成します。次に、CPU 時間とメモリの ulimit を設定します。これで準備完了です。 ulimit にはソフト制限とハード制限があり、ソフト制限に達するとプロセスが正常に終了しますが、それが発生しない場合は、おそらくバグが原因で、ハード制限に達すると強制的に終了します。残念ながら、Linux での新しいプロセスの生成は遅く、リクエストごとの新しいプロセスの生成は、多くの Web フレームワークや Temporal などの他のシステムではサポートされていません。さらに、プロセスの切り替えはより高価であり、これは CoW と CPU ピンニングによって軽減されますが、それでも理想的ではありません。残念ながら、プロセスの長時間実行は避けられない現実です。
存続期間の短いプロセスのきれいな抽象化から遠ざかるほど、より多くの OS レベルの作業を自分たちで行う必要があります。ただし、多くの実行スレッド間で IO をバッチ処理するために io_uring を使用するなど、得られる利点もあります。実際、大きなタスクがサブタスクで構成されている場合、個々のリソースの使用状況を本当に気にする必要があるでしょうか?プロファイリング専用。しかし、大規模なタスクに対してリソースのベル曲線の裾を管理 (切り取る) ことができれば、それで十分でしょう。そのため、同時に処理したいリクエストと同じ数のプロセスを生成し、それらのプロセスを長生きさせ、新しいリクエストごとに単純に ullimit を再調整することができます。そのため、リクエストがリソース制約を超過した場合、そのリクエストは OS シグナルを受け取り、他のリクエストに影響を与えることなく正常に終了できます。または、リソースの使用量が意図的に多い場合は、より高いリソース割り当ての料金を支払うようにクライアントに指示することもできます。なかなかいい感じですね。
ただし、リクエストごとのコルーチンのアプローチと比較すると、パフォーマンスは依然として低下します。まず、プロセス メモリ テーブルをコピーするとコストがかかります。テーブルにはメモリ ページへの参照が含まれているため、hugepage を利用して、コピーするデータのサイズを制限できます。これは、Zig などの低レベル言語でのみ直接可能です。さらに、OS レベルのマルチタスクはプリエンプティブであり、協調的ではないため、常に効率が低下します。それともそうですか?
システムコール sched_yield があり、これを使用すると、スレッドが作業部分を完了したときに CPU を解放できます。かなり協力的なようです。特定のサイズのタイム スライスをリクエストする方法もあるでしょうか?実際には、スケジュール ポリシー SCHED_DEADLINE が使用されます。これはリアルタイム ポリシーであり、要求された CPU タイム スライスの間、スレッドは中断されずに実行されることを意味します。ただし、スライスがオーバーランすると、プリエンプションが開始され、スレッドがスワップアウトされて優先順位が下がります。また、スライスがアンダーランの場合、スレッドは sched_yield を呼び出して早期終了を通知し、他のスレッドの実行を許可できます。これは、協調的かつ先制的なモデルという、両方の長所をとったもののように見えます。
制限は、SCHED_DEADLINE スレッドがフォークできないことです。これにより、並行性には 2 つのモデルが残されます。リクエストごとにプロセスが期限を設定し、効率的な IO のためにイベント ループを実行するか、最初からマイクロタスクごとにスレッドを生成するプロセスです。独自の期限を設定し、相互の通信にキューを利用します。前者はより単純ですが、ユーザー空間でのイベント ループが必要で、後者はカーネルをより多く利用します。
どちらの戦略もコルーチン モデルと同じ目的を達成します - カーネルと連携することで、最小限の中断でアプリケーション タスクを実行することが可能です。
これは、Zig が優れている、高性能、低遅延、低レベルの側面についてのすべてです。しかし、アプリケーションの実際のビジネスとなると、遅延よりも柔軟性の方がはるかに価値があります。プロセスに実際の人々が文書にサインオフすることが含まれる場合、コンピューターの遅延は無視できます。また、パフォーマンスに問題があるにもかかわらず、オブジェクト指向言語は開発者にビジネスのドメインをモデル化するためのより優れたプリミティブを提供します。そして、これの最も端にあるのが、Flowable や Camunda のようなシステムにより、管理スタッフや運用スタッフがより柔軟に、より低い参入障壁でビジネス ロジックをプログラムできるようになります。 Zig のような言語はこれには役に立たず、邪魔になるだけです。
一方、Python は、最も動的な言語の 1 つです。クラス、オブジェクト - これらはすべて内部で辞書であり、実行時に好きなように操作できます。これにはパフォーマンスが低下しますが、クラスとオブジェクト、および多くの巧妙なトリックを使用してビジネスをモデル化することが実用的になります。 Zig はその逆です。Zig には賢いトリックが意図的にほとんどなく、最大限のコントロールが可能です。相互運用させることで、それらの力を組み合わせることができますか?
両方とも C ABI をサポートしているため、確かにそれが可能です。 Python インタープリターを別のプロセスとしてではなく、Zig プロセス内から実行できるため、実行時のコストとグルー コードのオーバーヘッドが削減されます。これにより、Python 内で Zig のカスタム アロケーターを利用できるようになり、個々のリクエストを処理するためのアリーナを設定して、ガベージ コレクターのオーバーヘッドを排除しないまでも軽減し、メモリ キャップを設定することができます。主な制限は、CPython ランタイムがガベージ コレクションと IO 用のスレッドを生成することですが、それを示す証拠は見つかりませんでした。 AbstractMemoryLoop の「context」フィールドを利用することで、コルーチンごとのメモリ追跡により、Python を Zig のカスタム イベント ループにフックできます。可能性は無限大です。
同時実行性、並列処理、および OS カーネルとのさまざまな統合形式のメリットについて説明しました。この調査にはベンチマークとコードが不足していますが、提供されるアイデアの質がそれを補ってくれることを願っています。同様のことを試したことがありますか?どう思いますか?フィードバック歓迎:)
以上がZig と Python を使用した高性能で拡張可能な Web サーバーの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。