東京進捗報告:100 万タスクのスケジュール化ではなく、注文ではない

2026/07/28 0:10

東京進捗報告:100 万タスクのスケジュール化ではなく、注文ではない

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

この記事は、「Rust 製サービスがメモリリークしているわけではない。むしろ、アロケータの問題かもしれない」という記事の前編として機能し、Kafka、Redis Streams、NATS からデータを読み取るイベント駆動型の Tokio サービスを分析する。アプリケーションは各イベントトークンごとにタスクを生成しており(1 つのイベントあたり最大 1000 タスク)、

JoinSet
を用いて応答を収集していたが、並列タスク数の上限を強制しなかった。約 1000 のイベント(合計約 1M のタスク)が集中して発生した際、固定サイズのワーカースレッドを持ち、256 キャパシティのローカルキューとグローバルオーバーフローキューを使用するマルチスレッドスケジューラーである Tokio がスケジューリング遅延を示した。スケジューラは、生成された順序とは異なるタイミングでポーリングを行うことが多く、これはランタイムがタスク総数が有界な場合にのみ公平性を保証するためである。しかし、このアプリケーションの設計ではそのようなバウンドが存在しなかった。メモリ使用量は、リークではなく、状態を保持する生きたタスクの純粋に多数によって急激に増加した。核心的な教訓は、タスクの作成が直ちにポーリングや完了を意味するわけではないこと、また明示的なバウンドがない限り順序が保たれないことである。エンジニアたちは、この問題を解決するために
Semaphore
を導入してイベント処理の並列性を制限し、イベントレベルの公平性を達成するとともにピークメモリ使用量を大幅に削減しつつも元のスループットを維持することに成功した。この問題はランタイムのバグではなく、Tokio のスケジューリング保証に対する誤解が原因であり、高スループットシステムにおけるファンアウトパターンを開発者が能動的に制限する必要性を強調している。

本文

Rust アプリケーションでのメモリーリークの原因:アロケーターとタスクの生命周期

前回の投稿では、特定のメモリ・アロケーターの振る舞いについて触れました。以前はアプリケーション側の最適化を試みましたが、根本的な原因はアロケーター(タスクの寿命管理)にありました。

当サービスはイベント駆動型アーキテクチャを採用しており、以下のフローで動作していました:

  • Kafka、Redis Streams、NATS などのメッセージキューからイベントを読み取る
  • 各イベントに対して
    tokio::spawn
    で新しいタスクを生成し、処理を実行する

ワークロードの課題:Fan-out と Fan-in

各イベントには最大 1000 ユーザー・トークン が含まれており、すべてのトークンに対して外部 API を呼び出して結果を集約する必要があるため、非同期処理が必須でした。

以下は簡略化したコード例です:

struct Event { 
    payload: Bytes,      // およそ 4KB
    user_tokens: Vec<String>, // 最大 1000 トークン
    // 他のフィールド
}

// メインループ内
loop {
    let event: Event = fetch_next_event().await; 

    tokio::spawn(async move {
        let data = event.payload.clone();
        
        let mut tasks = JoinSet::new();
        for token in &event.user_tokens {
            let token = token.clone();
            let data = data.clone();
            tasks.spawn(async move {
                // 外部 API を呼び出し
                process(token, data).await
            });
        }

        // 結果を集約
        let mut responses = Vec::with_capacity(event.user_tokens.len());
        while let Some(res) = tasks.join_next().await {
            responses.push(res);
        }

        generate_response_event(event, responses);
    });
}

当初の目標はシンプルでした:「すべての外部呼び出しを可能な限り速く行い、バースト(大量発生)時間を満たすこと」。スループットが最優先であり、タスク間の完了順序や遅れには配慮していなかったためです。

ログから見える異常な振る舞い

100 万個のタスクを生成したバースト時のログを見ると、以下のような現象が確認できました:

started: event 1, user 5
started: event 1, user 8
...
finished: event 779      <-- 早期イベントが完了
finished: event 976
...
started: event 900, user 42
started: event 900, user 261
started: event 1, user 974       <-- すでに完了しているイベントのタスクがここから始まる??
started: event 1, user 831
...
finished: event 5             <-- イベント番号が小さいのに遅く完了
finished: event 3

ログからは明確に以下の事実が読み取れます:

  • バーストは時間内に完了した。
  • 早期に開始されたイベントのタスクが、大幅に遅れて処理・完了していた
  • タスク間の厳密な順序性は意図していなかったものの、「提出された直後にポーリングされる」という仮定が崩壊していた

Tokio スケジューラの中身と課題

Tokio のマルチスレッド・ランタイムは、以下のようなアーキテクチャを持っています:

  • 固定数の ワーカースレッド
  • 各ワーカーのローカルキュー(最大 256 タスク)
  • すべてのワーカで共有するグローバルキュー

スケジューリングの流れ

                                    グローバルキュー
                      +------------------------------+
                      | * ローカルキューからのオーバーフロー |
                      | * 遠隔スケジューリングされたタスク   |
                      | * 古い/新しい作業が混在            |
                      +--------------+---------------+
                                     |
           +-------------------------+-------------------------+
           |                         |                         |
           v                         v                         v
        worker 0                  worker 1                  worker 2
     +-------------+           +-------------+           +-------------+
     | ローカルキュー|          | ローカルキュー |          | ローカルキュー |
     | 最大 256    |           | 最大 256    |           | 最大 256    |
     +------+------+           +------+------+           +------+------+
            |                         |                         |
            v                         v                         v
        タスクをポーリング         タスクをポーリング         タスクをポーリング

このアーキテクチャにおいて、以下のことが起こります:

  • イベントから Fan-out されたタスクは、他のイベントからのタスクや
    JoinSet
    で待機している親タスクと混ざり合う
  • Tokio は**「早期に提出したタスクを先にポーリングする」という保証を行わない**。
  • キューのオーバーフローによる移動や、ワークスティーリング(他ワーカーからタスクを奪う)により、タスクが取り出される順序が乱される。

重要な区別:3 つの状態

ここで最も重要なのは以下の事実認識です:

  1. タスクが生成されたこと
  2. タスクがポーリングされたこと(実行が始まったこと)
  3. タスクが完了すること

これらは互いに等しくありません。Tokio はすべてのタスクを公平に進行させますが、メモリー使用量は**「同時に生きている(完了していない)タスクの数」**によって決まります。

メモリー使用量の増大理由

  • 一部の早期イベントから生成されたタスクが、バースト終了まで生存し続けた。
  • これに伴い、親イベントタスクも生存し続け、イベントの状態データをメモリー上に保持し続けた
  • その結果、ピークメモリー使用量が異常に増大した。

解決策:タスク生成数の有界化(Semaphore の導入)

Tokio は「タスク数が有界」かつ「ブロッキングしない」という前提で公平なスケジューリングを保証します。しかし、初期コードではタスク生成には上限がありませんでした。

要件は**「イベントレベルの公平性」(同一イベント内のタスクを速く完了させる)でした。これを実現するため、 Semaphore を使用して同時処理可能なイベント数を制限**しました。

[解決策] 同時に処理中のイベント数を N に制限する (Semaphore)
    ├── 各イベントごとに 1 つのイベントタスクを生成(許可された場合のみ)
    └── 各イベントごとに最大 1000 のトークンタスクを生成

この対策により:

  • バーストは依然として想定内の時間内に完了した。
  • ピークメモリー使用量が大幅に削減できた。
  • スループットへの影響は無視できるほど小さかった。

結論と教訓

今回の問題は Tokio のバグではなく、アプリケーション設計上の限界でした:

  • 「早期に生成された」=「早期に最初にポーリングされる」とは限らない
  • 「早期に最初にポーリングされた」=「早期に完了する」とも限らない
  • ランタイムはアプリケーションの公平性ユニット(今回は「イベント」)を意識していません。

開発者に必要なアクション:

  1. 有界性の追加: アプリケーション側で、スケジューラブルなタスクの数や深さを制限する必要があります。
  2. メモリー影響の考慮: タスク生成前に、「同時に生きている最大タスク数」と「それがメモリーに与える負荷」をシミュレーションしてください。
  3. 関連リソースの確認: タスクが生存している間、ファイルディスクリプターやネットワークコネクションなどの他のリソースも同様に増加していないか確認してください。

同じ日のほかのニュース

一覧に戻る →

2026/07/28 7:03

オープンウェイトモデルに関する当社の立場

## Japanese Translation: Anthropic の CEO ダリオ・アモデイは、オープンウェイトの AI モデルに対する全面的な禁止に反対し、同社がそのような制限を支持したことはないと主張している。彼は、危険な能力を持たないオープンウェイトモデルを不可欠な公共財として位置づけ、主な国家安全保障上の懸念として、共産主義中国(CCP)などの権威主義体制が、恒久的な軍事優位や抑圧のために優れた AI を構築しようとするリスクを挙げており、このリスクは副大統領ヴァンスの最近の警告や米国当局による世界的競争力に関する情報評価によって強調されている。彼の二次的な懸念には、サイバー攻撃、生物学的脅威、またはアライメント(調整)失敗への悪用が含まれる。アモデイは、オープンウェイトモデルはガードレールの実行が難しく、リリースされた重み(weights)を回収できないため、閉鎖型モデルよりも高いリスクをもたらすと指摘している。これらの脅威を緩和しつつ有益なイノベーションを維持するため、彼は以下の目標志向戦略を提唱している:中国への高度な半導体および装備の輸出制限、モデルの密輸入や産業規模での蒸留(distillation)操作への取り締まり、そして公開前に十分に能力のあるすべてのモデルに対して安全性テストの実施を義務付ける。彼は、全面的な禁止は効果的でもなく、Anthropic が求める解決策でもないとし、代わりに回収不能なシステムにおけるリスク管理を進めつつ、安全性にコミットしている者にとってのアクセスを維持することを主張している。

2026/07/25 16:55

Go の新しいガベージコレクションがヒープを走査していく様子

## Japanese Translation: Go の新しい Green Tea ゴー・コレクター(v1.26 以降のデフォルト)と C# を比較した評価から得られる主な示唆は、CPU キャッシュ効率とメモリアン断片化の間にある明確なトレードオフである。Green Tea は連続したスパンへの割り当てを最適化することでキャッシュミスが大幅に減少するが、オブジェクトを解放する際に移動やコンパクト化を行うことはできない。その結果、C# の移動式コレクターが積極的に関与してヒープをコンパクト化し OS に散在ページを返すのと異なり、Go は大量の解放後(例:90% のオブジェクトを解放)も未解放オブジェクトがばら撒かれたままとなる。 裸の金属 x86 システムでのテストにより、この断片化は介入なしに持続することが確認された。しかし、開発者は `unsafe` ポインタを使用して残存するオブジェクトを手動でパッケージ化することでこの制限を回避し、実質的にコンパクト化を模倣してシステムリソースを取り戻すことができる。結論として、Go は真の CPU パフォーマンス向上を提供するものの、複雑な回避策なしにヒープをコンパクト化できないという Go の不具合は依然として大きな制約であり、長期的動作を必要とし効率的なメモリ回収を要求するアプリケーションにおいては、C# が優れたアーキテクチャ上の振る舞いを提供することが組織にとって認識すべき点である。

2026/07/28 4:58

HN ランチ:Rise(YC S26)——廃棄物ガスを貴重な化学物質に変える

## Japanese Translation: Rise Reforming は、シカゴ郊外の下水処理施設でパイロットプラントの建設を正式に開始し、2026 年 7 月の稼働を目指しています。2025 年 12 月には 65 万ドルの前種投資を受け、初雇用の Nina Kritikos を擁し、同社は 2026 年 4 月に 1,800 時間以上の連続運転を通じて安定した概念実証結果を達成しました。また、プロジェクトはバイオガス生産者との拘束力のある供給契約および複数の覚書(MOU)を確保しています。 同社の固有技術では、下水処理廠などの発生源から排出される国内の滞留バイオガス(廃棄ガス)をメタノール、ジメチルエーテル(DME)、ジメチルカーボネート(DMC)といったグリーン化学物質に変換します。このアプローチは、化石燃料の採掘・精製・輸送に伴う環境コストを回避する低価格な代替策を提供することで、従来の石油化学製品と直接的に競合しています。主要な革新点は、許可手続きの長期遅延なく特定の施設の要件に合わせて迅速に導入および容易にスケールアップ可能なモジュール式でコンテナサイズの設計にあります。 Rise Reforming は 2026 年 7 月、成長とスケールアップのさらなる加速のために Y Combinator の S26 バッチに参加しました。廃棄物を価値ある化学物質に変えることで、即座の財政的リターンを提供しつつクリーンなエネルギー未来を支援することを目的としています。過去のマイルストーンには、2025 年 5 月に European Aerosols Federation 2025 Start-Up Award を受賞し、George Rose が 776 Foundation から Climate Fellow に選出されたことが含まれます。

東京進捗報告:100 万タスクのスケジュール化ではなく、注文ではない | そっか~ニュース