HN 日本語サマリー

← 一覧へ戻る
プログラミング

PyPI Blog: ファイルホスティングエラーのインシデント

PyPI Blog: Incident File Hosting Errors (blog.pypi.org)

4 pointsby miketheman0 コメント

要約

2026年8月に発生したPyPIのファイルホスティングエラーについて、インシデントレポートが公開されました。約2週間にわたり、一部のユーザーがファイルダウンロード時に502および503エラーに遭遇し、インストールの失敗を引き起こしました。原因は、Fastlyネットワーク内の設定ミスと、PyPI側のFastly設定におけるオリジンフォールバックおよびレンジリクエストのバグの組み合わせでした。これらの問題は修正され、8月28日以降、ダウンロードは完全に復旧しています。

全文翻訳

インシデントレポート: ファイルホスティングエラー

infrastructure transparency

エグゼクティブサマリー

2026年8月の約2週間にわたり、一部のPyPIユーザーは files.pythonhosted.org からファイルをダウンロードする際に断続的な502および503エラーに遭遇し、PyPIからのインストールに失敗しました。ユーザーがサポートトラッカーに報告を提出してくれたおかげで、特に一つの報告が問題を単一のキャッシュノードに絞り込むことができました。

二つの別々の問題が明らかになりました。Fastlyのネットワーク内でのカナリアデプロイメントが、一つのキャッシュノードで設定ミスを引き起こし、Fastlyのルーティングレイヤーが影響を受けたキャッシュノードに到達するトラフィックに対して502レスポンスを返す原因となりました。別途、オリジンフォールバックとレンジリクエストの動作に関する当社のFastly設定でいくつかのバグを発見し、修正しました。これらは以前から存在していましたが、これらの報告を調査する中で表面化しました。

両方の断続的な問題は現在修正されており、8月28日以降、ダウンロードは完全に機能しています。

背景: PyPIのファイルホスティングキャッシュの仕組み 1

ほとんどの人は pip install (または使用するインストーラー) を実行し、それが機能することを期待します。裏側では、files.pythonhosted.org は3つのオリジン(または「バックエンド」)の前に配置されたFastly CDNサービスです。

ファイルがアップロードされると、PyPIはまずAmazon S3に書き込み、これを主要な永続コピーとします。バックグラウンドジョブがそれをBackblaze B2に同期します。これは、FastlyとBackblazeの間にはゼロコストのEgress契約があるためです。B2からFastly経由でファイルを配信する際のコストは、ストレージ料金以外はかかりません。

読み取り(インストーラー)パスでは、FastlyはまずB2を試します。B2が応答しない場合、または予期しない応答を返した場合、FastlyはS3にフォールバックします。PyPIはAmazon S3 Glacier Instant Retrievalストレージクラスを使用して、ストレージと取得のコストのバランスを取っています。FastlyがフォールバックとしてS3を呼び出す場合、これはB2からよりもコストがかかりますが、一時的な対策としてコンシューマーにとっては引き続き機能します。

一度キャッシュされると、FastlyはB2またはS3を確認する必要がなくなります。ファイルは決して変更されないはずであり、Cache-Controlヘッダーは max-age=365000000, immutable, public を設定します。これは約11.5年に相当します。

Conveyorという名前の3つ目のバックエンドは、予測可能なURLのようなパッケージファイルリクエスト以外のすべて、および少数のレガシーリダイレクトを処理します。

    Client([Client request]) --> Edge{Fastly edge}
    Edge -->|package file| B2[(B2: egress-free cache)]
    Edge -->|everything else| Conveyor[Conveyor]
    B2 -->|200 or 206| Response([Response to client])
    B2 -.->|404, timeout, or 5xx| Archive[(S3: origin, fallback)]
    Archive --> Response
    Conveyor --> Response

このフォールバックパスは、エッジがB2の失敗を認識した場合にのみ機能します。私が修正したバグの一つは、それが常に正しく認識していなかったことで、これが「通常の」エラーノイズを増加させていました。

タイムライン

8月15日

Fastlyのレポートによると、問題はこの土曜日にシアトルエリアの単一のキャッシュノードで発生しました。これは#11876の報告とも一致しています。

8月17日

files.pythonhosted.org からの持続的な502エラーに関する最初の2つの報告が開かれました。#11895/#11897。

8月18日

#11908は詳細な再現手順を追加し、6時間で88件の502エラーが記録されたことを示しています。これらは32の無関係なパッケージにまたがり、インストーラーがPEP 658メタデータを読み取るために使用する小さな.whl.metadataレンジリクエストも含まれていました。3つのネットワーク報告が開かれていたため、Datadog Logsで対応するPyPI Filesのエラーを調査しましたが、重要なものは見つかりませんでした。infra#237がマージされ、B2がエラーで応答するのではなく、全く応答しない場合のB2からアーカイブへのフェイルオーバーが修正されました。

8月19日

#11925は問題を1つのFastlyキャッシュノード cache-pae2080020 に特定しました。このノードにルーティングされたすべてのリクエストに対して19時間以上にわたり502エラーが発生し、失敗したレスポンスの x-served-by ヘッダーで確認されました。Fastlyのネットワークオペレーションは、影響を受けたポイントオブプレゼンスにトラフィックの一部をルーティングしていたルーティングオーバーライドを削除しました。infra#238がマージされ、ファイルホスティングサービスのロギング設定が改善されました。infra#239がマージされ、ファイルホストにアクセスする必要のないHTTPメソッドが拒否されるようになりました。

8月20日

Fastlyは影響を受けたポイントオブプレゼンスでの回復を確認し、その後エラー率の上昇が停止したことを確認しました。

8月21日

infra#241がマージされ、午前中のスパイクが論理的に不正なレンジを送信した単一クライアントに起因することが判明した後、セグメント化されたキャッシュからサフィックスおよびマルチレンジリクエストが除外されました。

8月24日

infra#243がマージされました。これは、2つの壊れた並列ダウンローダーが1日で同じクラスのエラーを41,315件生成した後に行われました。

8月28日

Fastlyは、基盤となるカナリア設定バグを自社側でパッチし、PyPIを含むすべてのPSFトラフィックをカナリアコホートから除外しました。infra#245がマージされ、URL正規化の順序付けバグが修正されました。これにより、セグメント化されたキャッシュの不正なレスポンスがキャッシュされ、同じファイルに対する後続のすべてのリクエストに配信される可能性がありました。

寄与した要因

POPがカナリアにやられる 2

Fastlyは、フルロールアウトに先立ってキャッシュおよびルーティングソフトウェアを実行するサブセットであるカナリアコホートを実行しています。PyPIのトラフィックは長年そのコホートの一部であり、Fastlyのエンジニアリングが変更を検証するのに役立っていました。

カナリアデプロイメント中の部分的なロールバックにより、あるシアトルのポイントオブプレゼンス(POP)のキャッシュ設定がロールバックされたまま、その前のルーティング設定はロールバックされませんでした。この不一致により、そのルーティングレイヤーが502を返す原因となりました。

Fastlyはその後、PyPIを含むすべてのPSFトラフィックをカナリアプログラムから除外しました。より明確な制御と通知がこのトラフィックに関して存在するようになれば、最終的には参加に戻りたいと考えています。これは、インフラストラクチャの変更をすべての人に影響する前に検証するのに役立つ合理的な方法であり、これまでそれほどコストはかかっていませんでした。

自社内のバグ

それが進行している間、私は当社自身の設定で、同じ症状(502エラーの増加、および一部のケースでは外部から見ると502のように見える501エラー)を引き起こす無関係なバグを発見しました。

上記の図のアーカイブフォールバックは、B2がエラー状態を返した場合にのみ実行されました。B2が全く応答しなかった場合(タイムアウト、接続拒否、TLS障害)、Fastlyは独自の503を生成し、アーカイブを試みるコードをスキップしました。パッケージファイルは不変であるため、そのケースではアーカイブを試みるパスは存在しませんでした。infra#237がそれを追加するまで。

別途、セグメント化されたキャッシュを使用して、1ギガバイトサイズのホイール全体をキャッシュしてから部分コンテンツのRangeリクエストを配信するという事態を避けています。この機能は、HTTP全般よりもレンジ構文のサポートが狭くなっています。サフィックスレンジ(bytes=-1024、最後のNバイトを読み取る)や、レンジの開始が終了を超えているリクエストには対応できず、どちらの場合も合成された501を返します。一部のインストーラーは、ファイル全体をダウンロードせずにホイールメタデータを読み取るために、まさにこの種のサフィックスレンジを使用しているため、それらの読み取りは私がそれらを除外するまで完全に失敗していました。

    subgraph Before["Before infra#241 and infra#243"]
        direction TB
        C1([Client: suffix or inverted range]) --> E1{Fastly edge}
        E1 --> S1[Segmented caching]
        S1 --> R1([501 Not Implemented])
    end
    subgraph After["After"]
        direction TB
        C2([Client: suffix or inverted range]) --> E2{Fastly edge}
        E2 -->|range shape not supported| N2[Segmented caching skipped]
        N2 --> R2([Normal range handling: 206 or 416])
    end
    Before ~~~ After

不正なクライアントリクエストに対する501は、間違ったステータスクラスです。セグメント化されたキャッシュのレンジ処理についてFastlyにサポートチケットを開きました。修正されれば、私の処理コードはおそらく元に戻せるでしょう。

別のバグ:既存のセグメント化されたキャッシュ除外チェックは、リクエストURLが正規化される前に実行されていました。そのため、クエリ文字列が付いた .metadata リクエストは一致せず、セグメント化されたキャッシュが有効なままになり、アーカイブバックエンドから本来は通常のフェッチであるべきものが501で返されていました。

    subgraph Before["Before infra#245"]
        direction TB
        C3([Client: `GET name.whl.metadata<b>?token=x</b>`]) --> E3{Fastly edge}
        E3 -->|exemption checked before URL is normalized| S3[Segmented caching stays on]
        S3 --> B23[(B2)]
        B23 -->|501 for the 1MiB segment, cached at the edge| R3([Every later request: 501])
    end
    subgraph After2["After"]
        direction TB
        C4([Client: `GET name.whl.metadata<b>?token=x</b>`]) --> E4{Fastly edge}
        E4 -->|URL normalized first| X4[Exemption matches: segmented caching off]
        X4 --> B24[(B2)]
        B24 --> R4([Normal response, cached correctly])
    end
    Before ~~~ After2

infra#245は、URL正規化後に除外チェックを実行するように変更し、その穴を塞ぎました。

これらのバグはどれも新しいものではありませんでした。すでに設定に存在しており、実際のトラフィックが大きなファイルとレンジリクエストで発生したことで、調査と解決につながりました。

5xxのボリューム推移

ファイルホスティングサービスに関するFastly自身のリアルタイム分析によると、8月15日以降、B2エラー(青)が増加し、S3アーカイブバックエンド(ピンク)はゼロで横ばいでした。これは、そこへルーティングされるはずだったフォールバックがまだ機能していなかったためです。

[画像: 5xx responses by backend, Aug 13-31]

当社のDatadogメトリックは、インシデント前後のより完全なストーリーを示しています。

[画像: 5xx count for PyPI file hosting, Aug 13 - Sep 1]

ログスケールに注意してください。インシデント開始前にはすでにノイズが多く、数千から数万の5xxレスポンスがありましたが、8月15日に始まった増加は簡単に見逃せます。8月21日のスパイクはそうではありませんでした。論理的に不正なレンジを送信した単一クライアントが、カウントを100万近くまで押し上げました。

その直後の急落はinfra#241によるものです。それ以降のトラフィックは、インシデント前のベースラインよりも2〜3桁下、数千ではなく数十から数百のエラーにとどまっています。私たちがバックグラウンドノイズとして扱っていたもののいくつかは、このバグがずっと実行されていたことによるものでした。

今後について

Python Software Foundationは、4名のエンジニアリングスタッフに加わるインフラストラクチャエンジニアを募集しています。その職務の一部はPyPIを担当し、このような状況をより早期に発見し、反応するのではなく予防できるようになるはずです。コミュニティからの継続的な財政的支援(個人および企業の両方)がそれを可能にしています。

files.pythonhosted.org にヒットするトラフィックの多くは、前回の実行と同じ依存関係をインストールしているCIジョブであり、そのかなりの部分はGitHub Actionsの実行に起因しています。これらのダウンロードをまだキャッシュしていない場合は、有効にすることを検討してください。setup-pythonのpip, pipenv, poetryのキャッシュは、cache入力でオプトイン可能で、デフォルトではオフになっています。setup-uvのキャッシュは、ほとんどのGitHubホストランナーイベントでデフォルトでオンになっていますが、確認する価値があります。変更されていない依存関係がキャッシュされていれば、次回の実行で私たちに全くヒットしないため、私たちが配信する必要のあるリクエストが減り、私たちやFastlyが困難な日を迎えた場合にビルドが壊れる可能性のあるものが一つ減ります。

感謝

問題を回避するだけでなく、時間を割いて問題を報告し、詳細を記録してくれたすべての人に感謝します。

将来、ファイルホスティングの問題に遭遇した場合は、pypi/supportが引き続き適切な場所です。詳細(特に x-served-by ヘッダーとタイムスタンプ)を含めれば含めるほど、迅速に対応できます。

この作業は、寛大な寄付なしには不可能でした。この種のインフラストラクチャを維持するために、PSFを支援することをご検討ください。私の役割をスポンサーしてくれているAlpha-Omegaに感謝します。


  1. または、どのように機能するはずか ↩

  2. 正確ではありませんが、我慢できませんでした。↩