Azul Systems には、何千ものキャッシュ コヒーレント CPU をサポートするアプライアンスがあります。何千もの同時実行スレッドをスケジュールするために、オペレーティング システムにどのような変更を加える必要があるかについての洞察が欲しいです。
8 に答える
数千のスレッドをスケジュールすることは大したことではありませんが、数百の CPU でそれらをスケジュールすることは大したことです。何よりもまず必要なのは、非常にきめの細かいロック、さらにはロックフリーのデータ構造とアルゴリズムです。1 つの CPU がクリティカル セクションを実行している間、200 の CPU を待機させる余裕はありません。
あなたは OS の変更の可能性を求めているので、この取り組みの背後には重要なエンジニアリング チームがいると思います。
問題のパラメーターを定義するのに役立ついくつかの明確な情報もあります。
どのくらいの IPC (プロセス間通信) が必要ですか?
それらは本当にスレッドである必要がありますか、それともプロセスである可能性がありますか?
それらがプロセスである場合、共有メモリを使用するのではなく、ソケットを介して互いに通信する必要がありますか?
メモリ アーキテクチャとは何ですか? 1024 コアのストレート SMP ですか、それとも他の NUMA (Non-Uniform Memory Architecture) または MMP がここで進行中ですか? あなたのページテーブルはどのようなものですか?
Azul システムに関する最小限の情報しか知らないので、IPC はほとんどなく、単純な「コアごとに 1 つのカーネルを実行する」モデルが実際には問題なく機能する可能性があると推測できます。プロセスが互いに通信する必要がある場合、プロセスはソケットを作成し、その方法でデータを転送できます。お使いのハードウェアはこのモデルをサポートしていますか? (コアごとに 1 つの IP アドレスも必要になる可能性が高く、IP アドレスが 1024 の場合、これは面倒かもしれませんが、それらはすべて NAT される可能性があり、それほど大したことではないかもしれません)。もちろん、このモデルは余分なページ テーブルやかなりの RAM オーバーヘッドなどの非効率性につながり、ハードウェア システムでサポートされないことさえあります。
「コアごとに 1 カーネル」が機能しない場合でも、1024/8 カーネルを実行する可能性が高く、各カーネルが 8 つの物理 CPU を制御できるので問題ありません。
とはいえ、1024 個のコア (および物理 CPU が数個しかない) を備えた従来の SMP マシンでコアごとに 1 つのスレッドを実行したい場合は、昔ながらの O(1) スケジューラが必要であると思います。CPU[0] がカーネルでほぼ 100% になり、割り込み処理を行う可能性がありますが、ワークロードを処理するために複数のコアが必要でない限り、このユース ケースでは問題ありません。
Linux の規模を拡大することは、長期にわたって進行中のプロジェクトです。最初のマルチプロセッサ対応の Linux カーネルには、カーネル全体を保護する単一のロック (Big Kernel Lock、BKL) がありました。これはシンプルですが、スケーラビリティは制限されていました。
その後、ロックはよりきめ細かく行われました。つまり、多くの (数千?) ロックがあり、それぞれがデータの小さな部分のみをカバーしています。ただし、細粒度のロックは複雑になる傾向があり、特にほとんどのマルチ CPU Linux システムの CPU が比較的少ないことを考えると、ロックのオーバーヘッドがパフォーマンスの利点を食いつぶし始めるため、これをどこまで実行できるかには限界があります。
もう1つのことは、カーネルが可能な限りCPUごとのデータ構造を使用することです。これは、共有データのキャッシュ コヒーレンス パフォーマンスの問題を回避し、もちろんロックのオーバーヘッドがないため、非常に重要です。たとえば、すべての CPU は独自のプロセス スケジューラを実行し、時折のグローバル同期のみを必要とします。
また、一部のアルゴリズムはスケーラビリティを考慮して選択されています。たとえば、一部の読み取りがほとんどのデータは、従来のミューテックスの代わりに Read-Copy-Update (RCU) によって保護されます。これにより、リーダーは同時更新中に続行できます。
メモリに関しては、Linux はプロセスが実行されているのと同じ NUMA ノードからメモリを割り当てようとします。これにより、アプリケーションのメモリ帯域幅とレイテンシが向上します。
私の無知な推測では、プロセッサごとに実行キューがあり、プロセッサがアイドル状態のときにワークスティーリング アルゴリズムがあると思います。これは、CPU ごとに 1 つのプロセスがあり、作業項目として軽量プロセスが存在する M:N モデルで機能することがわかりました。これは、Java-7 の fork-join ライブラリにあるような、ワークスティーリング スレッドプールと同じように感じられます。
本当に知りたい場合は、Solaris Internals を入手するか、Solaris カーネル コードを掘り下げてください。私はまだ FreeBSD の Design & Impl を読んでおり、Solaris Internals は私のリストの次の記事です。
私たちが使用している SGI Altix (ccNUMA を実行する) は、キャッシュの一貫性のために特別なハードウェアを使用していると確信しています。
コヒーレントなコアごとに 4 MB のキャッシュを保持するために接続された巨大なオーバーヘッドがあります。ソフトウェアのみで発生する可能性は低いです。
256 CPU のアレイでは、キャッシュ無効化ビットを保持するためだけに 768 MB の RAM が必要になります。12MB キャッシュ / キャッシュ ラインあたり 128 バイト * 256² コア。
OS を変更することは 1 つのことですが、変更されていないアプリケーション コードを使用することは、ハードウェアの無駄遣いです。(ハードウェアによっては) 制限を超えると、一般的なコードを実行するためにコヒーレンシと同期を維持するための労力がかかりすぎます。あなたはそれを行うことができますが、それは非常に高価になります. OS 側からは、複雑なアフィニティ モデルが必要になります。つまり、CPU がビジーであるという理由だけで CPU をジャンプしないようにする必要があります。ハードウェア トポロジに基づいたスレッドのスケジューリング - ペナルティを最小限に抑えるために「近い」CPU 上のスレッドを連携させます。単純なワーク スチールは良い解決策ではありません。トポロジを考慮する必要があります。解決策の 1 つは、階層的なワーク スチールです。ワーク スチールを距離ごとに盗み、トポロジをセクターに分割し、最も近いものから盗もうとします。ロックの問題に少し触れます。スピンロックなどは引き続き使用しますが、まったく異なる実装を使用します。これはおそらく、最近の CS で最も特許を取得している分野です。しかし、繰り返しになりますが、そのような大規模な規模に合わせて特別にプログラムする必要があります。または、単にそれを十分に活用できなくなります。自動「パラレライザー」はそれを行いません。
これを行う最も簡単な方法は、各プロセス/スレッドをいくつかの CPU にバインドすることです。そうすれば、それらの CPU のみがそのスレッドのロックを求めて競合する必要があります。明らかに、負荷を均等にするためにスレッドを移動する何らかの方法が必要ですが、NUMA アーキテクチャでは、これを可能な限り最小限に抑える必要があります。
デュアルコアの Intel システムでさえ、Linux はすでにネイティブ posix スレッドで「数千」のスレッドを処理できると確信しています。
(ただし、これをサポートするには Glibc とカーネルの両方を構成する必要がありますが、最近のほとんどのシステムではデフォルトで構成されていると思います)。