- Amazon Builders' Library›
- 運用の可視性を高めるために分散システムを装備する
運用の可視性を高めるために分散システムを装備する
ソフトウェアの配信と運用 | レベル 400
はじめに
最初に頼るべきは「ログ」
大学卒業後に Amazon に入社したときの最初のオンボーディング演習の 1 つは、開発者デスクトップで amazon.com ウェブサーバーを起動して実行することでした。最初の試みではうまくいかず、何を間違えたのかすらわかりませんでした。親切な同僚が私に、ログを見て何が間違っているのかを確認するように提案しました。彼は、そのために「ログファイルを猫にする」べきだと言いました。 私は、彼らが何らかのいたずらをしているか、私には理解できない猫に関する冗談を言っているのだと確信しました。私は大学で Linux のみで、コンパイル、ソース管理、およびテキストエディタを使用しました。そのため、「cat」が実際に端末にファイルを出力するコマンドであり、別のプログラムにフィードしてパターンを探せるものだとは知りませんでした。
同僚に、cat、grep、sed、awk などのツールを教えてもらいました。この新しいツールセットを装備して、開発者デスクトップの amazon.com ウェブサーバーログにアクセスしました。ウェブサーバーアプリケーションは、あらゆる種類の有用な情報をログに出力するように既にインストルメントされていました。これにより、ウェブサーバーの起動を妨げる構成、クラッシュする可能性がある場所、またはダウンストリームサービスとの通信に失敗した場所を示す、欠けた構成を確認することができました。ウェブサイトは多くの有働的な作品で構成されており、最初は基本的にブラックボックスになっています。しかし、システムに真っ先に取り組んだ後、サーバーがどのように機能し、その依存関係とどのようにやり取りするのかを、インストルメンテーションの出力を調べるだけで理解する方法を学びました。
インストルメンテーションする理由
Amazon での長年の勤務においてチームからチームへと移動する中、私はインストルメンテーションが、私と Amazon の他の職員がシステムの仕組みを学ぶために使う非常に貴重なレンズであることに気付きました。ただし、インストルメンテーションは、システムについて学習するだけではありません。これが Amazon の運用文化のコアです。優れたインストルメンテーションでは、顧客に提供しているエクスペリエンスを確認できます。
この運用パフォーマンスへの焦点は会社全体にまたがるものです。amazon.com に関連付けられたサービス内では、レイテンシーが長くなるとショッピングエクスペリエンスが低下し、コンバージョン率が下がります。AWS を使用するお客様は、AWS サービスの高可用性と低レイテンシーを頼りにしています。
Amazon では、平均レイテンシーのみを考慮しているわけではありません。99.9 パーセンタイルや 99.99 パーセンタイルなど、レイテンシーの異常値にさらに焦点を当てています。これは、1,000 件または 10,000 件のリクエストのうち 1 件が遅い場合でも、好ましくない体験になってしまうためです。システム内の高パーセンタイルレイテンシーを削減すると、レイテンシーの中央値が減少するという副作用があることがわかりました。対照的に、レイテンシーの中央値を減らすと、高いパーセンタイルのレイテンシーが少なくなることがわかります。
高いパーセンタイル値のレイテンシーに焦点を当てるもう 1 つの理由は、1 つのサービスでの高いレイテンシーが他のサービス全体で乗数効果を持つ可能性があるためです。Amazon は、サービス指向アーキテクチャに基づいて構築されています。多くのサービスは、amazon.com でのウェブページのレンダリングなど、何らかの作業を完了するために相互連携します。その結果、コールチェーンの深部にあるサービスのレイテンシーが増加すると、たとえ増加分が高パーセンタイル内であっても、エンドユーザーが経験するレイテンシーに大きな波及効果を及ぼします。
Amazon の大規模システムは、多くの協力サービスによって構成されています。各サービスは、単一のチームによって開発および運用されます (大規模の「サービス」は、舞台裏で複数のサービスまたはコンポーネントで構成されます)。サービスを所有するチームは、サービス所有者として知られています。そのチームのメンバーは全員、そのメンバーがソフトウェア開発者、ネットワークエンジニア、マネージャー、またはその他の役割であるかどうかにかかわらず、サービスの所有者および運営者のように思えます。所有者として、チームは関連するすべてのサービスの運用パフォーマンスに目標を設定します。また、サービス運用の可視性を確保して、これらの目標を確実に達成し、発生した問題に対処し、翌年にはさらに高い目標を持つようにします。目標を設定して可視性を得るには、チームがシステムをインストルメントする必要があります。また、インストルメンテーションにより、運用イベントを戦術的に検出して対応することができます。
インストルメンテーションは、運用ダッシュボードにデータをフィードするため、オペレーターはリアルタイムのメトリックを表示できます。また、データをアラームにフィードし、システムが予期しない方法で動作しているときにオペレーターがトリガーして作動するようにします。オペレーターは、インストルメンテーションの詳細出力を使用して、問題が発生した理由を素早く診断します。そこから問題を軽減し、後で問題が再発しないようにすることができます。コード全体で適切なインストルメンテーションを行うことができないと、貴重な時間を問題の診断に費やすことになります。
測定対象
可用性とレイテンシーに関する当社の高い基準に従ってサービスを運用するには、サービス所有者としてシステムがどのように動作するのかを測定する必要があります。
必要なテレメトリを取得するために、サービス所有者は複数の場所から運用パフォーマンスを測定し、物事がエンドツーエンドでどのように動作するかについて複数の視点を得ます。これは単純なアーキテクチャでも複雑になります。顧客がロードバランサーを通じて呼び出すサービスについて考えてみましょう。サービスはリモートキャッシュとリモートデータベースと通信します。各コンポーネントがその動作に関するメトリックを出力するようにします。また、各コンポーネントが他のコンポーネントの動作をどのように認識するかに関するメトリックも必要です。これらすべての観点からのメトリックが集められると、サービス所有者は問題の原因をすばやく追跡し、原因を掘り下げることができます。
多くの AWS サービスは、リソースに関する運用上のインサイトを自動的に提供します。たとえば、Amazon DynamoDB は、サービスで測定された成功率、エラー率、レイテンシーに関する Amazon CloudWatch メトリックを提供します。ただし、これらのサービスを使用するシステムを構築するときは、システムの動作をより詳細に把握する必要があります。インストルメンテーションには、タスクの所要時間、特定のコードパスの実行頻度、タスクの作業内容に関するメタデータ、タスクの成功または失敗部分を記録する明示的なコードが必要です。チームが明示的なインストルメンテーションを追加しない場合、ブラックボックスとして独自のサービスを運用する必要があります。
たとえば、製品 ID で製品情報を取得するサービス API 操作を実装した場合、コードは次の例のようになります。このコードは、ローカルキャッシュで製品情報を検索してからリモートキャッシュで検索し、次にデータベースで検索します。
public GetProductInfoResponse getProductInfo(GetProductInfoRequest request) {
// check our local cache
ProductInfo info = localCache.get(request.getProductId());
// check the remote cache if we didn't find it in the local cache
if (info == null) {
info = remoteCache.get(request.getProductId());
localCache.put(info);
}
// finally check the database if we didn't have it in either cache
if (info == null) {
info = db.query(request.getProductId());
localCache.put(info);
remoteCache.put(info);
}
return info;
}
このサービスを運用しているなら、本番環境での動作を理解できるように、このコードに多くのインストルメンテーションが必要になります。失敗したリクエストや遅いリクエストのトラブルシューティングを行い、さまざまな依存関係が過小されているか誤動作している傾向や兆候を監視する機能が必要です。以下は同一のコードですが、本番システム全体について、または特定のリクエストについて回答できる必要のある質問で注釈が付けられています。
public GetProductInfoResponse getProductInfo(GetProductInfoRequest request) {
// Which product are we looking up?
// Who called the API? What product category is this in?
// Did we find the item in the local cache?
ProductInfo info = localCache.get(request.getProductId());
if (info == null) {
// Was the item in the remote cache?
// How long did it take to read from the remote cache?
// How long did it take to deserialize the object from the cache?
info = remoteCache.get(request.getProductId());
// How full is the local cache?
localCache.put(info);
}
// finally check the database if we didn't have it in either cache
if (info == null) {
// How long did the database query take?
// Did the query succeed?
// If it failed, is it because it timed out? Or was it an invalid query? Did we lose our database connection?
// If it timed out, was our connection pool full? Did we fail to connect to the database? Or was it just slow to respond?
info = db.query(request.getProductId());
// How long did populating the caches take?
// Were they full and did they evict other items?
localCache.put(info);
remoteCache.put(info);
}
// How big was this product info object?
return info;
}
これらすべての質問 (またはこれ以上の質問) に回答するためのコードは、実際のビジネスロジックよりもかなり長くなります。一部のライブラリはインストルメンテーションコードの量を減らすことができますが、開発者はライブラリが必要とする可視性について質問する必要があります。また、開発者はインストルメンテーションでの配線を意図的に行う必要があります。
分散システムを通過するリクエストのトラブルシューティングを行う場合、1 つの対話に基づいてそのリクエストのみを見ると、何が起こったのかを理解するのが難しい場合があります。パズルをつなぎ合わせるには、これらすべてのシステムに関するすべての測定値を 1 か所にまとめると便利です。それを行う前に、各サービスのインストルメンテーションを実行して、各タスクのトレース ID を記録し、そのタスクで共同作業する他の各サービスにそのトレース ID を伝達する必要があります。所定のトレース ID に関するシステム全体でのインストルメンテーションの収集は、必要に応じて事後に、または AWS X-Ray のようなサービスを使用してほぼリアルタイムで行うことができます。
ドリルダウン
インストルメンテーションにより、アラームをトリガーするには微細すぎる異常があるかどうかを調べるためのメトリックの確認から、それらの異常の原因を調べる調査の実施まで、複数のレベルでのトラブルシューティングが可能になります。
最高レベルでは、インストルメンテーションは、アラームをトリガーしてダッシュボードに表示できるメトリックに集約されます。これらの集約メトリックにより、オペレーターは全体的なリクエスト率、サービス呼び出しのレイテンシー、およびエラー率をモニターできます。これらのアラームとメトリックにより、調査する必要がある異常や変更を認識できます。
異常を見つけたら、その異常が発生している理由を把握する必要があります。その質問に答えるために、当社は、さらに多くのインストルメンテーションによって可能になったメトリックに依存します。リクエストを処理するさまざまな部分を実行するのにかかる時間をインストルメントすることにより、処理のどの部分が通常より遅いか、またはより頻繁にエラーをトリガーしているかを確認できます。
集計されたタイマーとメトリックは、原因を除外したり、調査分野を強調したりする場合は役立ちますが、必ずしも完全な説明を提供するとは限りません。たとえば、特定の API オペレーションからエラーが発生していることをメトリックを通して把握することはできるかもしれませんが、メトリックではそのオペレーションが失敗する理由についての詳細を十分に説明できないかもしれません。この時点で、その時間枠のサービスによって出力された詳細な未加工ログデータを確認します。未加工のログは、問題の原因 (発生している特定のエラー、または一部エッジケースをトリガーしているリクエストの特定の側面) を示します。
インストルメント方法
インストルメンテーションにはコーディングが必要です。つまり、新しい機能を実装するときに、何が起こったのか、成功したのか失敗したのか、どのくらい時間がかかったのかを示すために、時間をかけて余分なコードを追加する必要があります。インストルメンテーションは非常に一般的なコーディングタスクであるため、一般的なインストルメンテーションライブラリの標準化と、構造化されたログベースのメトリックレポートの標準化という、一般的なパターンに対処するために、Amazon では長年にわたってプラクティスが登場しました。
メトリックインストルメンテーションのライブラリを標準化すると、ライブラリ作成者がライブラリの消費者に対してライブラリの動作方法を可視化できるようになります。たとえば、一般的に使用される HTTP クライアントはこれらの共通ライブラリと統合されるため、サービスチームが別のサービスへのリモート呼び出しを実装すると、それらの呼び出しに関するインストルメンテーションが自動的に取得されます。
インストルメントされたアプリケーションが実行され、作業が実行されると、結果のテレメトリデータが構造化ログファイルに書き込まれます。一般的には、HTTP サービスへのリクエストであろうとキューからプルされたメッセージであろうと、「作業単位」ごとに 1 つのログエントリとして出力されます。
Amazon では、アプリケーションの測定値は集約されず、メトリック集約システムに時々フラッシュされます。すべての作業のタイマーとカウンターはすべてログファイルに書き込まれます。そこから、ログが処理され、他のシステムによって、後から集約メトリックが計算されます。このようにして、高レベルの合計運用メトリックからリクエストレベルの詳細なトラブルシューティングデータに至るまで、すべてコードのインストルメントへの単一のアプローチを完了します。Amazon では、最初にログを記録し、後から集約メトリックを作成します。
ログ記録によるインストルメンテーション
最も一般的な方法は、リクエストデータとデバッグデータの 2 種類のログデータを出力するようにサービスをインストルメントすることです。リクエストのログデータは通常、作業単位ごとに 1 つの構造化されたログエントリとして表れます。このデータには、リクエストに関するプロパティ、リクエストの実行者、リクエストの目的、発生頻度のカウンター、および所要時間のタイマーが含まれます。リクエストログは、監査ログおよびサービスで発生したすべてのトレースとして機能します。デバッグデータには、アプリケーションが出力するデバッグ行の非構造化データまたは大まかに構造化されたデータが含まれます。通常、これらは Log4j エラーまたは警告ログ行のような非構造化ログエントリです。Amazon では、これらの 2 種類のデータが別々のログファイルに出力されるのが一般的です。履歴上の理由がその一因ですが、同種のログエントリフォーマットでログ分析の行うのが便利だということもあります。
CloudWatch Logs Agent などのエージェントは、両方のタイプのログデータをリアルタイムで処理し、CloudWatch Logs にログを送信します。そうすると、CloudWatch Logs がサービスに関する集約メトリックをほぼリアルタイムで生成します。Amazon CloudWatch Alarms はこれらの集約メトリックを読み取り、アラームをトリガーします。
すべてのリクエストの詳細をログに記録するのは費用がかかる可能性がありますが、Amazon で記録を行うことは非常に重要です。結局のところ、可用性のブリップ、レイテンシースパイク、および顧客から報告された問題を調査する必要があります。詳細なログがなければ、お客様に回答することができず、サービスを改善することもできません。
詳細を調べる
監視と警告に関するトピックは広大です。この記事では、アラームしきい値の設定と調整、運用ダッシュボードの整理、サーバー側とクライアント側の両方からのパフォーマンスの測定、継続的に実行される「Canary」アプリケーション、およびメトリックの集計とログの分析に使用する適切なシステムの選択などのトピックは扱いません。
この記事では、適切な未加工の測定データを生成するためにアプリケーションをインストルメントする必要性に焦点を当てています。Amazon のチームがアプリケーションのインストルメンテーションを行うときに含める (または回避する) ように努めている事柄について説明します。
リクエストログのベストプラクティス
このセクションでは、私が Amazon で長年にわたって学んできた、構造化された「作業単位ごと」のデータをログに記録するという良い習慣について説明します。これらの基準を満たすログには、発生頻度を表すカウンター、処理にかかった時間を含むタイマー、各作業単位に関するメタデータを含むプロパティがあります。
ログを記録する方法
-
作業単位ごとに 1 つのリクエストのログエントリを作成する。 作業単位は通常、サービスが受け取ったリクエスト、またはキューからプルするメッセージです。サービスが受け取るリクエストごとに 1 つのサービスログエントリを書き込みます。複数の作業単位を組み合わせることはありません。これにより、失敗したリクエストのトラブルシューティングを行うときに、1 つのログエントリを確認できます。このエントリには、リクエストが何をしようとしていたかを確認するためのリクエストについての関連入力パラメータ、発信者に関する情報、すべてのタイミングとカウンターの情報が 1 か所にまとめられています。
-
特定のリクエストに対して 1 つ以上のリクエストのログエントリを作成しない。 ノンブロッグサービスの実装では、処理パイプラインのステージごとに個別のログエントリを出力することが便利に思われるかもしれません。代わりに、パイプラインのステージ間で単一の「メトリックオブジェクト」へのハンドルを組み込み、すべてのステージが完了した後にメトリックを 1 つの単位としてシリアル化することにより、これらのシステムのトラブルシューティングに成功しています。作業単位ごとに複数のログエントリがあると、ログ分析がより困難になり、乗数によって既に高価なログに記録するオーバーヘッドが増加します。新しいノンブロッグサービスを作成している場合は、メトリックのログ記録ライフサイクルを事前に計画しようと務めています。後でリファクタリングおよび修正を行うことは非常に困難になるためです。
-
長時間実行タスクを複数のログエントリに分割する。 前の推奨事項とは対照的に、長時間実行されるタスク、数分または数時間のワークフローのようなタスクがある場合は、個別のログエントリを定期的に出力して、順調に前進しているかどうか、または減速している箇所はどこかを判断できるようにします。
-
検証などを行う前に、リクエストに関する詳細を記録する。 トラブルシューティングと監査ログ記録では、何を達成しようとしていたのかを理解できるように、リクエストに関する十分な情報をログに記録することが重要です。また、検証、認証、調整ロジックによってリクエストが拒否される可能性が発生する前に、できるだけ早くこの情報をログに記録することが重要であることがわかりました。着信リクエストからの情報をログに記録する場合は、ログに記録する前に入力を確実にサニタイズ (エンコード、エスケープ、切り詰め) してください。たとえば、発信者が 1 MB の文字列を渡した場合、サービスログエントリに 1 MB の長い文字列を含めるのを望みません。これを行うと、ディスクが一杯になり、ログストレージで予想以上の費用が発生します。サニタイズのもう 1 つの例は、ログ形式に関連する ASCII 制御文字またはエスケープシーケンスを除外することです。発信者が独自のサービスログエントリを渡し、それをログに組み込めるとしたら、混乱が生じかねません。 https://xkcd.com/327/ も参照してください。
-
詳細度を上げてログを記録する方法を計画する。 何らかの問題のトラブルシューティングを行う場合、ログには問題のあるリクエストが失敗した理由を調べるために十分な詳細情報が含まれていません。その情報はサービスで利用できる場合がありますが、情報の量が多すぎて常にログを記録することを正当化できない場合があります。問題を調査している間、一時的にログの詳細度を高めるためにダイヤルできる設定ノブがあると便利です。個々のホスト、個々のクライアント、またはフリート全体のサンプリングレートでノブを回すことができます。完了したら、ノブを再度回して下げることを忘れないでください。
-
メトリック名を短くする (ただし、短すぎてはいけない)。 Amazon は 15 年以上にわたって同一のサービスログシリアル化を使用してきました。このシリアル化では、各カウンターとタイマーの名前がすべてのサービスログエントリでのプレーンテキストで繰り返されます。ログ記録のオーバーヘッドを最小限に抑えるために、タイマーには短いながらも説明的な名前を使用しています。Amazon は、Amazon Ion として知られるバイナリシリアル化プロトコルに基づく新しいシリアル化形式を採用し始めています。最終的には、ログ分析ツールが理解でき、シリアル化、逆シリアル化、保存も可能な限り効率的に行える形式を選択することが重要です。
-
最大スループットでログ記録を処理するのに十分な大きさのログボリュームを確保する。 最大持続負荷 (または過負荷) 状態でのサービスの負荷テストを何時間も行います。サービスが過剰なトラフィックを処理している場合、新しいログエントリを生成する速度でログをすぐに使える状態にするためのリソースがサービスに残っていることを確認する必要があります。そうしないと、ディスクがいっぱいになります。また、ログ記録がルートパーティションとは異なるファイルシステムパーティションで発生するように構成することもできます。これにより、過剰なログ記録が発生してもシステムが故障することはありません。スループットに比例する動的サンプリングの使用などのその他の緩和策については後ほど説明しますが、戦略に関わらず、テストすることが極めて重要です。
-
ディスクがいっぱいになったときのシステムの動作を検討する。サーバーのディスクがいっぱいになると、ディスクにログを記録できません。その場合、サービスはリクエストの受け入れを停止する必要がありますか、それともログを削除して監視せずに操作を続行する必要がありますか? ログ記録なしでの操作はリスクが高いため、システムをテストして、ディスクがほぼいっぱいのサーバーが検出されることを確認します。
-
クロックを同期する。 分散システムにおける「時間」の概念は、複雑であることで知られています。分散アルゴリズムではクロック同期に依存していませんが、これはログを理解するために必要です。クロック同期のために Chrony や ntpd などのデーモンを実行し、サーバーでクロックドリフトを監視します。これを容易にするには、Amazon Time Sync Service をご覧ください。
-
可用性メトリックにゼロカウントを出力する。エラーカウントも有用ですが、エラーの割合も同じく有用です。「可用性の割合」メトリックをインストルメントするために、リクエストが成功した場合に 1 を、リクエストが失敗した場合に 0 を出力するのが有用であることがわかりました。その場合、結果メトリックの「平均」統計は可用性率です。意図的に 0 データポイントを出力することは、他の状況でも役立ちます。たとえば、アプリケーションがリーダー選挙を実行する場合、1 つのプロセスがリーダーである場合は定期的に 1 を出力し、プロセスがリーダーでない場合は 0 を出力することで、フォロワーの状態を監視できます。このように、プロセスが 0 の出力を停止すると、その中の何かが壊れていることを簡単に知ることができますが、リーダーに何かが起こった場合に引き継ぐことはできません。
ログ記録の内容
-
すべての依存関係の可用性とレイテンシーを記録する。 これは、「なぜリクエストが遅くなったのか?」や「なぜリクエストが失敗したのか?」という質問への回答に特に役立ちます。 このログがなければ、依存関係のグラフとサービスのグラフのみを比較し、依存しているサービスのレイテンシーの急上昇が調査中のリクエストの失敗につながったかどうかを推測できます。多くのサービスフレームワークとクライアントフレームワークはメトリックを自動的に出力しますが、他のフレームワーク (AWS SDK など) では手動インストルメンテーションが必要です。
-
コールごと、リソースごと、ステータスコードごとなどの依存関係メトリックを分割する。 同じ作業単位で同じ依存関係と複数回やり取りする場合、各呼び出しに関するメトリックを個別に含めて、各リクエストがやり取りしているリソースを明確にします。たとえば、Amazon DynamoDB を呼び出すとき、一部のチームは、エラーコードごとに、さらには再試行の回数ごとに、テーブルごとにタイミングとレイテンシーのメトリックを含めると役に立つことがわかりました。これにより、条件付きチェックの失敗によりサービスの再試行が遅くなった場合のトラブルシューティングが容易になります。また、これらのメトリックは、クライアントが認識するレイテンシーの増加がパケット損失やネットワークレイテンシーによるものではなく、再試行のスロットリングまたは結果セットのページネーションによるものであったケースも明らかにしました。
-
アクセスするときにメモリキューの深さを記録する。 リクエストがキューとやり取りしており、オブジェクトをそこからプルしたり、何かを投入したりする場合、現在のキューの深度も同時にメトリックオブジェクトに記録します。インメモリキューの場合、この情報は非常に安価で入手できます。分散キューの場合、このメタデータは API 呼び出しへの応答で無料で利用できる場合があります。このログ記録は、将来のバックログとレイテンシーの原因を見つけるのに役立ちます。さらに、キューから物を取り出すとき、物がキューに入っていた時間を測定します。これは、そもそもキューに入れる前に、独自の「エンキュー時間」メトリックをメッセージに追加する必要があることを意味します。
-
エラーの理由ごとにカウンターをさらに追加する。失敗したリクエストごとに特定のエラー理由をカウントするコードを追加することを検討してください。アプリケーションログには、障害の原因となった情報と詳細な例外メッセージが含まれます。ただし、アプリケーションログの情報を発掘する必要なく、時間と共にメトリックのエラー原因の傾向を確認することも役に立つことがわかりました。失敗の例外クラスごとに個別のメトリックから始めると便利です。
-
原因のカテゴリ別にエラーを整理する。 すべてのエラーが同じメトリックにまとめられると、そのメトリックはノイズが多くなり、役に立たなくなります。少なくとも、「クライアントの障害」であるエラーと「サーバーの障害」であるエラーを区別することが重要であることはわかりました。 さらに、さらなる内訳が役立つ場合があります。例えば、DynamoDB では、クライアントが、変更中のアイテムがリクエストの前提条件と一致しない場合にエラーを返す条件付きの書き込みリクエストを行うことができます。これらのエラーは意図的なものであり、時折発生することが予想されます。一方、クライアントからの「無効なリクエスト」エラーは、おそらく修正が必要なバグです。
-
作業単位に関する重要なメタデータを記録する。 構造化されたメトリックログには、後ほどリクエストの送信者とリクエストの実行内容を判断できるように、リクエストに関する十分なメタデータも含まれています。これには、顧客が問題に手を差し伸べたときにログに記録されると、顧客が期待するメタデータが含まれます。たとえば、DynamoDB は、リクエストが対話するテーブルの名前と、読み取り操作が一貫した読み取りであったかどうかなどのメタデータをログに記録します。ただし、データベースに保存されたデータやデータベースから取得されたデータはログに記録されません。
-
アクセス制御と暗号化でログを保護する。ログにはある程度の機密情報が含まれているため、そのデータを保護して安全に保つための対策を講じています。これらの手段には、ログの暗号化、問題のトラブルシューティングを行うオペレーターのアクセス制限、そのアクセスのベースラインを定期的に設定することが含まれます。
-
ログに極秘情報を過度に入力しない。 ログには、有用性を高めるためにも機密情報が含まれている必要があります。Amazon では、特定のリクエストの送信元を知るのに十分な情報をログに含めることが重要ですが、ルーティングやリクエスト処理の動作に影響を与えないリクエストのパラメータなど、過度に機密性の高い情報は除外しています。たとえば、コードが顧客のメッセージを解析し、その解析が失敗した場合、後でトラブルシューティングが困難になる場合があっても、顧客のプライバシーを保護するためにペイロードをログに記録しないことが重要です。オプトアウト方式ではなくオプトイン方式でログに記録できるものを決定し、後ほど追加される新しい機密パラメータのログ記録を防ぐためのツールを使用します。Amazon API Gateway などのサービスでは、アクセスログに含めるデータを構成できます。これは、優れたオプトインメカニズムとして機能します。
-
トレース ID をログに記録し、バックエンドコールで伝播する。所定のカスタマーリクエストについて、連携する多くのサービスが関与する可能性があります。これは、多くの AWS リクエストに対する 2~3 のサービスから、amazon.com リクエストに対するはるかに多くのサービスまでさまざまです。分散システムのトラブルシューティングを行ったときに何が起こったのかを理解するため、これらのシステム間で同じトレース ID を伝達し、さまざまなシステムのログを列挙して障害が発生した場所を確認できるようにします。トレース ID は、作業単位の開始点である「フロントドア」サービスによって分散作業単位にスタンプされる一種のメタリクエスト ID です。AWS X-Ray は、この伝播の一部を提供できるようにするサービスの 1 つです。トレースを依存関係に渡すことが重要であることがわかりました。マルチスレッド環境では、フレームワークが私たちに代わってこの伝播を行うことが非常に難しく、エラーが発生しやすいため、メソッドシグネチャでトレース ID や他のリクエストコンテンツ (メトリックオブジェクトなど) を渡す習慣を身につけました。また、メソッドシグネチャでコンテキストオブジェクトを渡すと便利であるため、将来、同様のパターンを見つけたときにリファクタリングする必要はありません。AWS チームにとっては、システムのトラブルシューティングだけでなく、お客様のトラブルシューティングも重要です。顧客は、顧客に代わって相互作用する場合、AWS サービス間で渡される AWS X-Ray トレースに依存しています。完全なトレースデータを取得できるように、サービス間で顧客の AWS X-Ray トレース ID を伝達する必要があります。
-
ステータスコードとサイズに応じて異なるレイテンシーメトリックをログに記録する。アクセス拒否、スロットリング、検証エラー応答など、エラーは急速に発生することがほとんどです。クライアントが高速で調整され始めると、レイテンシーが一見良く見えるかもしれません。このメトリック汚染を回避するために、成功した応答に向けて別のタイマーをログに記録し、一般的な時間メトリックを使用する代わりにダッシュボードとアラームでそのメトリックに焦点を合わせます。同様に、入力サイズまたは応答サイズに応じて遅くなる可能性のある操作がある場合、SmallRequestLatency や LargeRequestLatency のように分類されたレイテンシーメトリックの作成を検討します。さらに、複雑なブラウンアウトと障害モードを回避するために、リクエストと応答が適切に制限されていることを確認します。慎重に設計されたサービスであっても、このメトリックバケット技術は、顧客動作を隔離し、ダッシュボードから邪魔なノイズを排除することができます。
アプリケーションログのベストプラクティス
このセクションでは、非構造化デバッグログデータのロギングについて Amazon で学んだ良い習慣について説明します。
-
アプリケーションログにスパムが届かないようにする。 テスト環境での開発とデバッグに役立てるための INFO および DEBUG レベルのログステートメントがリクエストパスに存在する場合がありますが、本番環境ではこれらのログレベルを無効にすることを検討します。アプリケーションログでリクエストのトレース情報を取得する代わりに、サービスログをトレース情報の場所と見なします。トレース情報は、メトリックを簡単に生成し、時間の経過に伴う全体的な傾向を確認できます。ただし、ここには白黒のルールはありません。私たちのアプローチは、ログを継続的に確認してノイズが多すぎる (またはノイズが少ない) かどうかを確認し、時間の経過とともにログレベルを調整することです。たとえば、ログダイビングをしていると、ノイズが多すぎるログステートメントや、求めていたメトリックが見つかることがよくあります。幸い、これらの改善は簡単に行えることが多いため、ログをクリーンに保つために、迅速なフォローアップのバックログアイテムを提出する習慣が身に付きました。
-
対応するリクエスト ID を含める。 アプリケーションログのエラーをトラブルシューティングするときは、エラーをトリガーしたリクエストまたは発信者に関する詳細を確認したいと考えることがほとんどです。両方のログに同じリクエスト ID が含まれている場合、一方のログから他方のログに簡単にジャンプできます。アプリケーションログ記録ライブラリは、適切に構成されている場合に対応するリクエスト ID を書き出し、リクエスト ID は ThreadLocal として設定されます。アプリケーションがマルチスレッドの場合、スレッドが新しいリクエストの処理を開始するときは、正しいリクエスト ID を設定するよう細心の注意を払ってください。
-
アプリケーションログのエラースパムのレートを制限する。通常、サービスからアプリケーションログへの出力はそれほど多くありませんが、大量のエラーが突然発生すると、スタックトレースを含む非常に大量のログエントリを突然書き込み始める可能性があります。これを防ぐ方法の 1 つは、特定のロガーがログを記録する頻度を制限することです。
-
String#format または文字列結合よりもフォーマット文字列を優先する。古いアプリケーションのログ API 操作は、log4j2 の可変引数のフォーマット文字列 API ではなく、単一の文字列メッセージを受け入れます。コードが DEBUG ステートメントでインストルメントされているが、本番環境が ERROR レベルで構成されている場合、無視される DEBUG メッセージ文字列のフォーマット作業を無駄にする可能性があります。ロギング API 操作には、ログエントリが書き出される場合にのみ toString() メソッドが呼び出される任意のオブジェクトの受け渡しをサポートするものもあります。
-
失敗したサービスコールからのリクエスト ID をログに記録する。 サービスが呼び出されてエラーを返す場合、サービスがリクエスト ID を返した可能性が高くなります。ログにリクエスト ID を含めると便利です。そのため、サービス所有者のフォローアップが必要な場合、対応するサービスのログエントリを簡単に見つけることができます。サービスがリクエスト ID をまだ返していないか、クライアントのライブラリがそれを解析していない可能性があるため、タイムアウトエラーによりこの処理が難しくなります。それでも、サービスからリクエスト ID が返された場合は、ID をログに記録します。
高スループットサービスのベストプラクティス
Amazon のほとんどのサービスでは、すべてのリクエストをログに記録しても法外なコストオーバーヘッドは発生しません。スループットの高いサービスはグレーの領域に入りますが、それでも多くの場合、すべてのリクエストでログオンしています。たとえば、ピーク時の Amazon 内部トラフィックのみで毎秒 2,000 万件を超えるリクエストを処理する DynamoDB は、あまりログを記録しませんが、実際にはトラブルシューティングのため、監査およびコンプライアンス上の理由ですべてのリクエストをログに記録します。以下は、ホストごとのスループットが高いときに Amazon で使用してログ記録をより効率的にするための高レベルのヒントです。
-
サンプリングをログに記録する。 すべてのエントリを書き込む代わりに、すべての N エントリを書き込むことを検討してください。各エントリには、エントリが計算するメトリックの真のログボリュームをメトリック集約システムが推定できるように、スキップされたエントリの数も含まれています。リザーバサンプリングなどの他のサンプリングアルゴリズムは、より代表的なサンプルを提供します。他のアルゴリズムは、成功した高速のリクエストよりもログ記録エラーまたは低速なリクエストを優先します。ただし、サンプリングでは、顧客を支援し、特定の障害をトラブルシューティングする機能が失われます。一部のコンプライアンス要件では、これが完全に禁止されています。
-
別のスレッドへのシリアル化とログフラッシュをオフロードする。 これは簡単に行える変更で、一般的に使用されています。
-
ログローテーションを頻繁に行う。 処理するファイルが少なくなるようにログファイルを 1 時間ごとにローテーションすることは便利な方法に思えますが、1 分ごとにローテーションすることでいくつかの点を改善できます。例えば、ログファイルの読み取りと圧縮を行うエージェントは、ディスクではなくページキャッシュからファイルを読み取り、ログの圧縮と送信からの CPU と IO は、1 時間の終了時に常にトリガーされるのではなく、1 時間全体に分散されます。
-
事前に圧縮されたログを書き込む。 ログを送信するエージェントがアーカイブサービスへの送信前にログを圧縮する場合、システムの CPU とディスクが定期的に急増します。圧縮されたログをディスクにストリーミングすることにより、このコストを償却し、ディスク IO を半分に減らすことができます。ただし、いくつかのリスクが伴います。アプリケーションがクラッシュした場合に切り捨てられたファイルを処理できる圧縮アルゴリズムを使用すると役に立ちます。
-
ramdisk/tmpfs へ書き込む。 サービスにとって、ログをディスクに書き込むのではなく、ログがサーバーから送信されるまでメモリに書き込む方が簡単な場合があります。私たちの経験によると、これが最も良好に機能するのは、ログを 1 時間ごとにローテーションする場合ではなく、1 分ごとにローテーションする場合です。
-
インメモリで集計する。 1 台のマシンで 1 秒間に何 10 万件ものトランザクションを処理する必要がある場合、リクエストごとに 1 つのログエントリを書き込むには費用がかかりすぎる可能性があります。ただし、これを省略するとオブザーバビリティが大きく低下するため、時期尚早に最適化しないことが有益だとわかりました。
-
リソース使用率を監視する。 スケーリングの限界にどの程度近づいているかを注視しています。サーバーごとの IO と CPU を測定し、それらのリソースのうちログエージェントが消費している量を測定します。負荷テストを実行するときは、ログ送信エージェントがスループットに対応できることを証明できるように、十分に長い時間をかけて実行します。
適切なログ分析ツールを用意する
Amazon では、作成したサービスを独自に運営しているため、全員がそれらのトラブルシューティング専門家になる必要があります。これには、ログ分析を簡単に実行できることが含まれます。比較的少数のログを調べるためのローカルログ分析から、膨大な量のログ結果を選別して集約するための分散ログ分析まで、さまざまなツールが用意されています。
ログ分析のためにチームのツールとランブックに投資することが重要であることがわかりました。現在はログが小さいですが、時間の経過とともにサービスが大きくなると予想される場合、分散ログ分析ソリューションの採用に投資できるように、現在のツールがスケーリングを停止するタイミングに注意を払っています。
ローカルログ分析
ログ分析のプロセスには、さまざまな Linux コマンドラインユーティリティの経験が必要な場合があります。例えば、一般的な「ログ内のトップトーカー IP アドレスを見つける」作業は、次のようにシンプルに行えます。
cat log | grep -P "^RemoteIp=" | cut -d= -f2 | sort | uniq -c | sort -nr | head -n20
しかし、より複雑な質問の回答を得るために役立つツールが他にもたくさんあります。これには以下が含まれます。
• jq: https://stedolan.github.io/jq/
• RecordStream: https://github.com/benbernard/RecordStream
分散ログ分析
ビッグデータ分析サービスを使用して、分散ログ分析 (Amazon EMR、Amazon Athena、Amazon Aurora、Amazon Redshift など) を実行できます。しかし、サービスには Amazon CloudWatch Logs などのログ記録システムが装備されているものもあります。
-
AWS X-Ray: https://aws.amazon.com/xray/
-
Amazon Athena: https://aws.amazon.com/athena/
まとめ
サービス所有者およびソフトウェア開発者として、私はインストルメンテーションの出力 (ダッシュボード上のグラフ、個々のログファイル) の考察と、CloudWatch Logs Insights などの分散ログ分析ツールの使用に膨大な時間を費やしています。これらのいくつかは、私がやりたいと思ったことです。困難なタスクをいくつか終えた後で休憩が必要なときは、バッテリーを充電し、ログダイビングで自分に報酬を与えます。「なぜこのメトリックがここで急上昇したのか?」または「この操作のレイテンシーを低くできますか?」といった質問から始めます。 私の質問が行き詰まると、コードで有用な測定値を見つけ出すことが多いので、インストルメンテーションを追加し、テストし、チームメートにコードレビューを送信します。
使用するマネージドサービスには多くのメトリックが付属しているという事実にもかかわらず、サービスを効果的に運用するために必要な可視性を確保するために、独自のサービスのインストルメントに多くの注意を払う必要があります。運用イベント中に、問題が発生した理由とその問題を軽減するためにできることを迅速に判断する必要があります。診断を迅速に行うためには、ダッシュボードに適切なメトリックを設定することが重要です。さらに、私たちは常にサービスを変更し、新しい機能を追加して、それらが依存関係と相互作用する方法を変更しているため、適切なインストルメンテーションの更新と追加の実施も永遠に終わりません。
リンク集
-
元 Amazonian 社員の John Rauser による「Look at your data」: https://www.youtube.com/watch?v=coNDCIMH8bk (13:22 に、John が実際にログを印刷して、それらをより詳しく検証する箇所を含む)
-
元 Amazonian 社員の John Rauser による「nvestigating anomalies」: https://www.youtube.com/watch?v=-3dw09N5_Aw
-
元 Amazonian 社員の John Rauser による「How humans see data」: https://www.youtube.com/watch?v=fSgEeI2Xpdc
著者について
David Yanacek は、AWS Lambda に取り組むシニアプリンシパルエンジニアです。2006 年から Amazon のソフトウェア開発者で、以前は Amazon DynamoDB と AWS IoT、内部のウェブサービスフレームワーク、フリート運用自動化システムにも取り組んでいました。David の職場でのお気に入りの活動の 1 つは、ログ分析を実行し、運用メトリクスをふるいにかけて、システムを徐々にスムーズに実行する方法を見つけることです。