【ITニュース解説】eliminating malloc contention in high-throughput rust services with thread-local aren
2026年10月23日に「Dev.to」が公開したITニュース「eliminating malloc contention in high-throughput rust services with thread-local aren」について初心者にもわかりやすく解説しています。
ITニュース概要
高負荷なRustサービスで、大量のメモリ確保がボトルネックとなり遅延が発生していた。各スレッドに専用のメモリ領域を割り当てる設定により、メモリ確保時の処理待ちを解消し、応答速度が向上する。導入は簡単だが、メモリ断片化などの考慮点もある。
ITニュース解説
システムエンジニアを目指す皆さんにとって、日々の生活で使われているWebサービスやアプリケーションがどのように動いているのか、その裏側を知ることはとても重要だ。特に、多くの人が同時に利用するような「高負荷サービス」では、目に見えないところで様々な技術的な工夫が凝らされている。今回紹介する内容は、そうした高負荷サービスにおいて、プログラムが一時的に使うメモリをどのように確保し、それが原因で発生する遅延をどのように解消するかという、非常に実践的な技術の話だ。
あるRust製のマイクロサービスで、1秒間に20万件を超えるリクエストを処理している状況を想像してみてほしい。これは非常に多くのユーザーが同時にサービスを利用している状態であり、サーバーは常に大量の処理を行っていることになる。このような状況で、サーバーの処理能力を最大限に引き出すためには、プログラムのあらゆる部分で無駄をなくし、効率的に動かす必要がある。このサービスでは、32コアのCPUを搭載した高性能なサーバーを使っているにもかかわらず、時折5〜10ミリ秒(ms)程度のわずかな遅延(レイテンシ)が発生し、これがユーザー体験を損なう原因となっていた。CPUの利用率が低い時でもこの遅延は観測されており、特定のボトルネックが原因であることが示唆されていた。
この遅延の主な原因は「メモリ確保(malloc)」の処理中に発生する「競合」だった。プログラムが動作する上で、データを一時的に保存したり、新しいオブジェクトを作成したりするためには、メモリ(記憶領域)が必要となる。このメモリをOSや専用の「アロケータ」と呼ばれるプログラムから借りてくるのがメモリ確保だ。Rustのサービスでは、デフォルトで「jemalloc」という高性能なアロケータが使われることが多い。しかし、このjemallocは、メモリの状態を正しく保つために「グローバルロック」という仕組みを採用している。グローバルロックとは、メモリの状態を変更する際に、複数のプログラム(スレッド)が同時にアクセスしてデータが壊れないように、一時的に他のスレッドのアクセスを停止させる「一時停止信号」のようなものだ。例えるなら、一本のレジしかないスーパーで、たくさんの買い物客が同時に精算しようとする状況に近い。安全のためには一人が精算を終えるまで次の客は待たなければならず、結果的に全体の処理が遅れてしまう。特に、高負荷なサービスでは、たくさんのスレッドが頻繁にメモリを確保しようとするため、このグローバルロックを巡って「競合」が発生し、処理が「直列化」されてしまう。つまり、同時に処理できるはずの複数のスレッドが、メモリ確保の時だけは順番待ちの状態になってしまうのだ。これが、CPUがアイドル状態(他にやることがない状態)でも、時折発生する遅延の原因となっていた。
この問題を解決するために導入されたのが、「スレッドローカルアリーナ」という考え方だ。これは、先ほどのスーパーの例で言えば、各買い物客が自分専用のレジ(アリーナ)を持つようなものだ。これにより、各スレッドは他のスレッドのメモリ確保処理を待つことなく、自分専用のアリーナからロックフリー(ロックなし)でメモリを確保できるようになる。アリーナは、特定のメモリ領域をスレッドが自由に使えるように割り当てられたプールのようなものだと考えるとわかりやすいだろう。各スレッドが自分専用のメモリプールを持つことで、メモリ確保の競合が根本的に解消されるのだ。さらに、スレッドローカルアリーナを使うことで、「キャッシュ局所性」も向上する。CPUには、よく使うデータを一時的に保存しておくための「キャッシュメモリ」という非常に高速な記憶領域がある。スレッドが自分専用のアリーナ内でメモリを確保すると、そのアリーナ内のメモリ領域がCPUキャッシュに読み込まれやすくなり、次にそのスレッドがメモリを必要とした際に、わざわざメインメモリまでデータを取りに行く必要が少なくなる。これにより、CPUがデータを高速に処理できるようになり、全体のパフォーマンスが向上する。
このスレッドローカルアリーナをRustサービスに導入するのは、実は非常に簡単だ。通常、Rustではデフォルトのアロケータとしてjemallocが使われるが、これを「tikv-jemallocator」というクレート(Rustのライブラリ)に置き換える。Cargo.tomlという設定ファイルに数行追記し、メインとなるプログラムファイル(src/main.rs)でアロケータを指定するだけでよい。さらに、プログラムが起動する前に、環境変数「JEMALLOC_TCACHE」を「true」に設定する。この設定によって、jemallocは各スレッドがメモリを確保する際に、それぞれ専用のアリーナを作成するようになる。例えば、Tokioのような非同期ランタイムを使ったサーバーでは、通常CPUのコア数に応じてワーカースレッドを起動する。もし32コアのCPUであれば、32個の独立したアリーナが作成され、それぞれが並行してメモリ確保を行えるようになるわけだ。
しかし、どんな優れた解決策にも「トレードオフ」、つまりメリットとデメリットの関係は存在する。スレッドローカルアリーナの導入にもいくつかの考慮すべき点がある。一つは「メモリ断片化」の問題だ。各スレッドが専用のアリーナを持つため、それぞれのアリーナ内でメモリが使われたり解放されたりする。しかし、あるアリーナで解放されたメモリは、他のアリーナからは直接使えない。長期間稼働するサービスでは、アリーナごとに不要なメモリ領域が増えていき、結果としてシステム全体で物理メモリの使用量(RSS: Resident Set Size)が高くなる可能性がある。これは、部屋の中のあちこちに小さなスペースが空いていても、大きな家具を置くスペースがない状態に似ている。この問題は、定期的に「malloc_trim」という関数を呼び出して不要なメモリをOSに返したり、独自のアリーナパージ機能(使われていないメモリを掃除する機能)を実装したりすることで軽減できる場合がある。
二つ目の考慮点は「コールドスタートレイテンシ」だ。新しいスレッドが初めてメモリを確保する際、そのスレッド専用のアリーナを初期化するためのわずかなオーバーヘッド(約10マイクロ秒)が発生する。もし必要に応じてスレッドを頻繁に起動・停止するようなアプリケーションであれば、この初期化コストが積み重なって全体のパフォーマンスに影響を与える可能性もある。この場合は、サービス起動時にあらかじめスレッドプールを「ウォームアップ」させておく、つまり事前にスレッドを起動してアリーナを初期化しておくことで、この遅延を回避できる。
三つ目は「プロファイリング互換性」だ。プログラムの性能を分析する「プロファイリングツール」の中には、一つのグローバルなアロケータを前提としているものがある。例えば、「perf heap profiling」のようなツールは、スレッドローカルアリーナを使用している場合、メモリ使用状況のレポートが断片化されてしまい、正確な情報を得られないことがある。これを避けるためには、jemallocの統計情報機能(JEMALLOC_STATSフラグ)を有効にするなどの工夫が必要になる。
実際にこの変更を行ったサービスでは、目覚ましい効果が得られた。32コアのXeonプロセッサを搭載したサーバーで性能を測定したところ、高負荷時のレイテンシ(p99という指標で、99%のリクエストがこの時間内に処理されることを示す)が7ミリ秒から2ミリ秒へと大幅に短縮された。これは、ほとんどのユーザーが感じる遅延が大きく改善されたことを意味する。さらに、全体のCPU使用率も約4%低下した。これは、メモリ確保のためのロック待ちに費やされていたCPUサイクルが減り、より本来の処理にCPU資源を割り当てられるようになったためだ。
もちろん、このスレッドローカルアリーナの戦略が常に最適なわけではない。例えば、ほとんどがデータの読み込み処理で、新しいメモリ確保がめったに発生しないようなサービスでは、各スレッドが専用のアリーナを持つことによるメモリのオーバーヘッドが、かえって無駄になる可能性がある。また、1メガバイト(MiB)以上の大きなバッファを多用するアプリケーションも、この恩恵を受けにくい場合がある。なぜなら、jemallocはそのような大きなメモリ領域は直接OSから確保するため、アリーナの仕組みがあまり関係しないからだ。
システムエンジニアとして、このような高負荷サービスを設計・開発する際には、様々なメモリ確保戦略の中から、自分のサービスの特性に最も適した方法を選択する必要がある。メモリの断片化による使用量増加と、レイテンシの低減という二つの側面をどのようにバランスさせるか、常に検討し続けることが求められる。今回紹介したスレッドローカルアリーナは、高負荷なRustサービスにおいて、メモリ確保のボトルネックを効果的に解消するための強力な手段の一つだと言えるだろう。