GitHubのプッシュ処理を改善した方法

Image of tomokota
Author


GitHub にプッシュすると何が起こるのでしょう?「リポジトリに私の変更が反映される」あるいは「リモートの参照元が更新される」という答えが正しいかもしれません。いくつか例を挙げてみましょう:

  • プルリクエストが同期されます。つまり、プルリクエストの差分とコミットが、新しくプッシュされた変更を反映します。
  • プッシュされたウェブフックがディスパッチされます。
  • ワークフローがトリガーされます。
  • アプリの設定ファイル(Dependabot や GitHub Actions など)をプッシュすると、アプリが自動的にリポジトリにインストールされます。
  • GitHub ページが公開されます。
  • Codespacesの設定が更新されます。
  • さらに色んなことがあります。

これらは重要ものの一部で、プッシュするたびに行われることのほんの一例にすぎません。実際、GitHubモノリスでは、プッシュに直接反応して実行される60以上のロジックが20の異なるサービスにあります。コードがGitHubにプッシュされると、面白いことがたくさん起きているはずです。ある意味、GitHubはコードのプッシュが大きな位置を占めていて、あなたがコードをプッシュすると1クールなことが起きる場所なのです。

問題点

あまりクールでないのは、つい最近まで、これらのことがすべて、単一の巨大なバックグラウンドジョブで行われていたことです。GitHub の Ruby on Rails モノリスは、プッシュの通知を受けるたびに RepositoryPushJobと呼ばれる巨大なジョブをキューに入れます。このジョブはすべてのプッシュ処理ロジックの本拠地であり、その大きさと複雑さが多くの問題を引き起こしていました。このジョブは、次のような長い一連のステップを次々とトリガーします:

A flow chart from left to right. The first step is

この図にはいくつか問題がありますのでハイライトしてみましょう:

  • このジョブは巨大で、再試行が困難です。 RepositoryPushJob のサイズでは、異なるプッシュ処理タスクを正しく再試行するのは非常に困難でした。再試行では、ジョブのすべてのロジックが最初から繰り返されるため、個々のタスクには必ずしも適切ではありません。例えば
    • データベースへPush のレコードの書き込みは、エラー時に自由に再試行することができ、プッシュ後いつまでも再試行でき、徐々に重複データを処理できます。
    • 一方、プッシュ・ウェブフックの送信は、より時間にシビアで、プッシュが発生してからあまり時間が経ってから再試行すべきではありません。また、同じウェブフックを複数ディスパッチすることも望ましくありません。
  • これらのステップのほとんどは、まったく再試行されませんでした。上記のようなリトライに関する相反する問題から、最終的にRepositoryPushJob の再試行はほとんどの場合回避されることになりました。しかし、1つのステップでジョブ全体が終了するのを防ぐため、プッシュ処理ロジックの多くは、あらゆるエラーをキャッチするコードに包まれていました。このリトライの欠如が、プッシュ処理の重要な部分が実行されない問題につながりました。
  • 多くの懸念が複雑に絡み合うことで、問題の影響は広範囲に及びました。このジョブに含まれる数十のタスクのほとんどはすべてのエラーをリカバリーしましたが、歴史的な経緯から、ジョブの最初の方にあるいくつかの作業はリカバリーしませんでした。これは、後のすべてのステップが、ジョブの初期部分に暗黙の依存関係を持つことを意味しました。同じジョブ内でより多くの懸念事項が組み合わされるにつれて、エラーがジョブ全体に影響を及ぼす可能性が高まります。
    • たとえば、Pushes MySQLクラスタへのデータ書き込みは、RepositoryPushJob の最初のほうで発生しました。これは、それ以降に発生するすべての処理が、このクラスタに暗黙の依存関係を持つことを意味します。この構造により、プルリクエストにはこのクラスタに接続する明示的な必要性がないにもかかわらず、このデータベースクラスタからのエラーによってユーザーのプルリクエストが同期されないというインシデントが発生しました。
  • 超長時間のシーケンシャル処理はレイテンシーに悪影響があります。最初の数ステップについては問題なくとも、最後に起こることについてはどうでしょうか?他のロジックが実行されるのを待つ必要があります。この構造によって、プルリクエストの同期を含むユーザー向けのプッシュタスクに1秒以上の不要な待ち時間が発生することもありました。

私たちのとった対応

高レベルでは、この非常に長い逐次プロセスを、多くの分離された並列プロセスに切り離しました。その際には次のようなアプローチをとりました:

  • 新しいKafkaトピックを追加し、プッシュごとにイベントを発行するようにしました。
  • 多くのプッシュ処理タスクのそれぞれを調べ、所有するサービスや論理的な関係(例えば、順序依存関係や再試行可能性)によってグループ化しました。
  • 各タスクのまとまりについて、明確なオーナーと適切なリトライ設定を持つ新しいバックグラウンドジョブに配置しました。
  • 最後に、新しい Kafka イベントが発行されるたびに、これらのジョブがエンキューされるように設定しました。
    • これを行うために、独立したコンシューマー経由でKafkaイベントに応答してバックグラウンドジョブをエンキューするGitHubの内部システムを利用しました。

このアーキテクチャをサポートするために、私たちは以下のようないくつかの分野で投資を行う必要がありました:

  • Kafka イベント用に信頼できるパブリッシャーを作成します。
  • このレベルのファンアウトに必要な新しいジョブキューを処理するために、ジョブワーカーの専用プールを設定します。
  • このパイプライン全体のプッシュイベントの流れを注意深く監視し、ボトルネックや問題を検出できるように、オブザーバビリティを向上させました。
  • 新旧のパイプライン間でデータの損失やイベントの二重処理のリスクを負うことなく、新システムを徐々にロールアウト(必要であればロールバック)できるようにするため、イベントごとに一貫した機能フラグを立てるシステムを考案しました。

現在では以下のようになっています:

A flow chart from left to right. The first step is

プッシュがKafkaイベントをトリガーし、独立したコンシューマーを経由して、他のコンシューマーを気にすることなくイベントを処理できる多くの独立したジョブに送られます。

結果

  • 問題発生の半径が小さくなりました。
    • これは図を見れば一目瞭然です。以前は、非常に長いプッシュ処理プロセスの1つのステップで問題が発生すると、下流のすべてに影響が及ぶ可能性がありました。現在では、プッシュ・ハンドリングのロジックの一部分に問題が発生しても、それ以外の部分に影響を及ぼすことはありません。
    • 構造的に、これは依存関係のリスクを減少させます。例えば、新しいパイプラインで1日に実行されるプッシュ処理オペレーションは約3億件あり、以前はPushes MySQLクラスタに暗黙的に依存していましたが、現在は分離されたプロセスに移動したため、そのような依存関係はありません。
    • デカップリングによって所有権における効果もありました。これらのジョブを分割することで、プッシュ処理コードの所有権を1つのチームから15人以上のより適切なサービス所有者に分散させました。モノリスの新しいプッシュ機能は、他のチームに意図しない影響を与えることなく、所有チームによって追加され、反復されます。
  • プッシュは低レイテンシーで処理されます。
    • これらのジョブを並列に実行することで、どのプッシュ処理タスクも他のタスクの完了を待つ必要がありません。これは、プッシュで発生するあらゆることのレイテンシーが改善されることを意味します。
    • 例えば、プルリクエストの同期時間が顕著に減少していることがわかります:

    A line chart depicting the p50 pull request sync time since head ref update over several previous months. The line hovers around 3 seconds from September 2023 through November 2023. In December 2023, it drops to around 2 seconds.

  • オブザーバビリティの向上。

    • 小さなジョブに分割することで、各ジョブで何が起こっているかをより明確に把握することができます。これによって、以前よりもはるかに細かいスコープで観測と監視を設定することができ、プッシュに関するあらゆる問題を素早く突き止めることができます。
  • プッシュはより確実に処理されます。
    • プッシュを処理するジョブのサイズと複雑さを減らすことで、以前のシステムよりも多くのことをリトライできるようになりました。各ジョブは、リトライ時に他の無関係なロジックの再実行を心配することなく、それぞれに適したリトライ設定を持つことができます。
    • 失敗することなく意図されたオペレーションがすべて完了したプッシュ・イベントを”完全に処理された”プッシュだと定義すると、旧システム(RepositoryPushJob )はプッシュの約99.897%を完全に処理しました。
    • 新しいパイプラインでは最悪の場合の推定でも99.999%のプッシュを完全に処理します。

結論

GitHub へのコードのプッシュは、開発者が毎日 GitHub とやりとりする最も基本的なもののひとつです。この数ヶ月の間に、私たちはユーザーからのプッシュを正確かつ完全に処理するモノリスの能力を大幅に向上させました。このようなプラットフォームレベルの投資を通じて、私たちはGitHubを将来にわたってすべての開発者(と彼らの多くのプッシュ!)のためのホームにするよう努めています。

備考


  1. ご想像のとおり、GitHub にはたくさんのプッシュが寄せられています。この30日間で、850万人のユーザーから約5億のプッシュがありました。↩️

The post How we improved push processing on GitHub appeared first on The GitHub Blog.