Optimizing M3: How Uber Halved Our Metrics Ingestion Latency by (Briefly) Forking the Go Compiler — Part1
M3の最適化:UberがGoがコンパイをフォークすることでメトリック収集のレイテンシーを半減する方法とは — 第一章
今回の記事は長いので四回に分けて投稿します。今回が第一回目です。Uberでの可観測性について書きます。
Uberのニューヨークエンジニアリングオフィスでは、可観測性チームはその問題が起きた時にできるだけ早くエンジニアに知らせたり、検索したり、問題を緩和したりする責任を担うしっかりとしていてケーパブルなメトリックとアラートパイプラインを維持しています。数千ものマイクロサービスのヘルスをモニタリングすることで、ライダーやドライバーパートナー、飲食店やレストランパートナーまで、世界中の数百万人のユーザーがプラットフォームをスムーズかつ効率的に実行できるようなりました。
数か月前、オープンソースメトリックと監視プラットフォームであるM3のコアサービスでのルーティーンデプロイメントにより、メトリックを収集してストレージに保持するための全体的なレイテンシーが2倍になり、メトリックのP99が約10秒から20秒を超えました 。この追加されたレイテンシーにより、内部システムに関わるメトリックのGrafanaダッシュボードの読み込みに時間がかかり、システム所有者への自動アラートの起動に時間がかかるようになりました。 しかし、その問題を解決するのは簡単でした。最後の正常なビルドに戻りましたが、それを修正するために根本的な原因を把握する必要がありました。
図1:左の矢印は、一般的なエンドツーエンドのレイテンシーを示しており、時折スパイクが発生して約10秒待機しています。 右の矢印は、パフォーマンスが低下した後のエンドツーエンドのレイテンシーを示しており、定期的なスパイクが20秒まで増加していることが分かります。
Goで書かれたソフトウェアのパフォーマンスを分析する方法について多くの記事がありますが、ほとんどの議論では、pprof を使って、CPUおよびヒーププロファイルを視覚化して問題を診断および解決する方法が提案されています。この場合、まずCPUプロファイルとpprofから始まりましたが、それらのツールが失敗するとすぐに軌道から外れ、git bisect、Plan 9アセンブリの読み取り、あともちろんGoコンパイラーのフォークなど、よりプリミティブなツールに頼らざるを得なくなりました 。
Observability at Uber
Uberの可観測性チームは、Uberのエンドツーエンドのメトリックプラットフォームの開発とメンテナンスを担当しています。 下の図2に示す取り込みプラットフォームのアーキテクチャー内で、ホスト上のアプリケーションは1秒間隔でそれらを集約するlocal daemon(「コレクター」)にメトリックを送信し、さらにティアーに集約します。またさらに10秒と1分間隔で収集します。 最後に、それらはストレージティアーであるM3DBに書き込む役割を担う M3DB ingesterに書き込まれます。
図2:M3のメトリック取り込みパイプラインのこの高レベルの外観では、メトリックはさまざまなコンテナーからUDPを介して、それぞれのホストで実行されるCollectorと呼ばれるlocal daemonに送信されます。 コレクターは、etcdから受信したシャード対応トポロジーを使用して、メトリックを集約ティアーに転送し、そこで1分10秒のタイルに集約されます。 最後に、集約ティアーはタイルをさまざまなバックエンドにフラッシュします。これには、M3DBへの書き込みをするM3DB ingesterが含まれます。
M3が取り込み時に集約を行う方法の特性により、取り込み側は、以下の図3に示すように、事前に集約されたタイルの形式で定期的に大きなメトリックのバッチを受け取ります。
図3:M3DBインジェスターが新しいメトリックを受信する率は一定ではありません。 集約ティアーがさまざまなサイズのタイルを作成してフラッシュしているため、インジェスターは定期的に多数の新しいメトリックを一度に受け取ります。
その結果、M3DBインジェスターはその場しのぎのキューとして機能します。そして、インジェスターがこれらのメトリックをM3DBに書き込む率は、エンドツーエンドのレイテンシーをコントロールします。 このサービスのエンドツーエンドのレイテンシーを低く抑えることは重要なことです。レイテンシーは、Uberの内部チームが最新のメトリックを表示できる速度、および自動アラートが障害を検出できる速度を制御するためです。
Bisecting production
M3DBインジェスターのルーティーンデプロイメントが、このサービスのエンドツーエンドのレイテンシーを2倍にしたとき、ベーシックから始めました。本番働環境で実行されているサービスのCPUプロファイルを取得し、pprofを使用してフレームグラフとして視覚化しました。 残念ながら、このフレームグラフには原因になるものが何も見つかりません。
CPUプロファイルに明らかなものは見当たらなかったため、次のステップはリグレッションを引き起こしたコミットを識別することであると判断し、特定のコード変更を確認することができました。 これは、予想よりも難しいことが分かりました。その理由は以下の通りです。
- M3DBインジェスターは数か月間デプロイされておらず、その間にかなりの数のコード変更が行われました。 どの変更が問題を引き起こしたかを正確に特定することは困難です。インジェスターサービス(および私たちのチームの他のすべてのサービス)のコードがmonorepoに格納されているため、コミット履歴が非常に分かりにくく、多くのコミットがサービスにまったく関係していないためです ただし、これらの無関係なコミットは、ディペンデンシーに影響を及ぼしたり、間接的に問題を引き起こす可能性があります。
- レグレッション(回帰)は、トラフィックが急増する傾向のある本番ワークロードでのみ発生し、負荷が大きくなります。 その結果、マイクロベンチマークやステージング/テスト環境でローカルに再現することができませんでした。
その結果、不正なコミットを識別する最良の方法は、本番環境でコミット履歴のバイナリー検索であるgit bisectを実行することであると判断しました。 最終的に不正なコミットを特定しましたが、git bisectでも予想よりはるかに困難であることが判明し、不正なコミットはディペンデンシーのディペンデンシーにあることが判明しました。3レベルのgit bisectを実行する必要があったということを意味します。言い換えると、オープンソースのディペンデンシー(M3DB)のバージョンを変更する内部monorepoのコミットに問題を絞り込み、そのディペンデンシーの1つのバージョン(M3X)を変更するリポジトリーのコミットに問題を絞り込みました。つまり、そのリポジトリーもgit bisectする必要があったのです。
図4:git bisectを実行すると、M3DBのバージョンが変更されるため、M3DB monorepoの別のgit bisectが必要になり、M3X monorepoを使用することになりました。
すべて完了したら、不正コミットを見つけてCloneメソッドに加えた小さな変更にパフォーマンスのレグレッションを絞り込むために、サービスを81回デプロイする必要がありました(図5を参照)。
図5:81回のデプロイの後、git bisectは最終的にCloneメソッドに小さな変更を加え、それがパフォーマンスのレグレッションを起こしたことを明らかにしました。
この一見無害な変更がエンドツーエンドのレイテンシーを2倍にすると信じがたかったのですが、証拠を無視することはできませんでした。 左側のコード(図5)でサービスをデプロイした場合、パフォーマンスレグレッションはなくなり、右側のコード(図5)でコードを展開した場合、戻りました。
Orangesys.ioでは、kuberneteの運用、DevOps、監視のお手伝いをさせていただいています。ぜひ私たちにおまかせください。