最初に頼るべきは「ログ」

大学卒業後に 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 の varargs 形式の文字列 API ではなく、単一の文字列メッセージを受け入れます。コードが DEBUG ステートメントでインストルメントされているが、本番環境が ERROR レベルで構成されている場合、無視される DEBUG メッセージ文字列のフォーマット作業を無駄にする可能性があります。一部のログ記録 API 操作は、ログエントリが書き出される場合にのみ toString() メソッドが呼び出される任意のオブジェクトの受け渡しをサポートします。
失敗したサービスコールからのリクエスト ID をログに記録する。 サービスが呼び出されてエラーが返された場合、サービスはリクエスト ID を返している可能性があります。ログにリクエスト ID を含めると便利です。そのため、サービス所有者のフォローアップが必要な場合、対応するサービスのログエントリを簡単に見つけることができます。サービスがリクエスト ID をまだ返していないか、クライアントのライブラリがそれを解析していない可能性があるため、タイムアウトエラーによりこの処理が難しくなります。それでも、サービスからリクエスト ID が返された場合は、ログに記録します。

高スループットサービスのベストプラクティス

Amazon のほとんどのサービスでは、すべてのリクエストにログオンしても、不合理的に過度な費用が発生することはありません。スループットの高いサービスはグレーの領域に入りますが、それでも多くの場合、すべてのリクエストでログオンしています。たとえば、ピーク時の Amazon 内部トラフィックのみで毎秒 2,000 万件を超えるリクエストを処理する DynamoDB は、あまりログを記録しませんが、実際にはトラブルシューティングのため、監査およびコンプライアンス上の理由ですべてのリクエストをログに記録します。ここに、ホストごとのスループットを高めてログ記録をより効率的にするために Amazon で使用する高度なヒントを示します。

サンプリングをログに記録する。 すべてのエントリを書き込む代わりに、N エントリごとに書き込むことを検討してください。各エントリには、スキップされたエントリの数も含まれているため、メトリック集約システムは、計算するメトリックの実際のログボリュームを推定できます。リザーバサンプリングなどの他のサンプリングアルゴリズムは、より代表的なサンプルを提供します。他のアルゴリズムは、成功した高速のリクエストよりもログ記録エラーまたは低速なリクエストを優先します。ただし、サンプリングでは、顧客を支援し、特定の障害をトラブルシューティングする機能が失われます。一部のコンプライアンス要件により、これは完全に禁止されています。
別のスレッドへのシリアル化とログフラッシュをオフロードする。 これは簡単な変更なので、一般的に使用されています。
ログローテーションを頻繁に行う。 ログファイルを 1 時間ごとにローテーションするのは便利に見えるので、処理するファイルが少なくなりますが、1 分ごとにローテーションすることで、いくつかの点が改善されます。たとえば、ログファイルの読み取りと圧縮を行うエージェントは、ディスクではなくページキャッシュからファイルを読み取り、ログの圧縮と送信からの CPU と IO は、終了時に常にトリガーされる代わりに、1 時間にわたって分散されます。
事前に圧縮されたログを書き込む。 ログを送信したエージェントがアーカイブサービスに送信する前にログを圧縮すると、システムの CPU とディスクが定期的に急増します。圧縮されたログをディスクにストリーミングすることにより、このコストを償却し、ディスク IO を半分に減らすことができます。ただし、いくつかのリスクが伴います。アプリケーションがクラッシュした場合に切り捨てられたファイルを処理できる圧縮アルゴリズムを使用すると便利です。
ramdisk/tmpfs へ書き込む。 ログをディスクに書き込む代わりに、サーバーから送信されるまで、ログをメモリに書き込む方が簡単な場合があります。私たちの経験では、これは 1 時間ごとにログをローテーションするよりも、1 分ごとにログをローテーションする場合に最適です。
インメモリで集計する。 1 台のマシンで 1 秒間に数十万件のトランザクションを処理する必要がある場合、リクエストごとに 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 などのログ記録システムが装備されています。

CloudWatch Logs Insights
• AWS X-Ray: https://thinkwithwp.com/xray/
• Amazon Athena: https://thinkwithwp.com/athena/

まとめ

サービス所有者およびソフトウェア開発者として、インストルメンテーションの出力 (ダッシュボード上のグラフ、個々のログファイル) を見て、CloudWatch Logs Insights などの分散ログ分析ツールを使用することに、膨大な時間を費やしています。これらのいくつかは、私がやりたいと思ったことです。困難なタスクをいくつか終えた後で休憩が必要なときは、バッテリーを充電し、ログダイビングで自分に報酬を与えます。「なぜこのメトリックがここで急上昇したのか?」または「この操作のレイテンシーを低くできますか?」といった質問から始めます。 私の質問が行き詰まると、コードで有用な測定値を見つけ出すことが多いので、インストルメンテーションを追加し、テストし、チームメートにコードレビューを送信します。

使用するマネージドサービスには多くのメトリックが付属しているという事実にもかかわらず、サービスを効果的に運用するために必要な可視性を確保するために、独自のサービスのインストルメントに多くの注意を払う必要があります。運用イベント中に、問題が発生した理由とその問題を軽減するためにできることを迅速に判断する必要があります。診断を迅速に行うためには、ダッシュボードに適切なメトリックを設定することが重要です。さらに、私たちは常にサービスを変更し、新しい機能を追加し、それらが依存関係と相互作用する方法を変更しているため、適切なインストルメンテーションを更新・追加する演習は永遠に続いています。

• 元 Amazonian 社員の John Rauser による「データ検索」: https://www.youtube.com/watch?v=coNDCIMH8bk (13:22 に、文字通りログを印刷して、よく観察してください)
• 元 Amazonian 社員の John Rauser による「異常の調査」: https://www.youtube.com/watch?v=-3dw09N5_Aw
• 元 Amazonian 社員の John Rauser による「人間によるデータの見方」: https://www.youtube.com/watch?v=fSgEeI2Xpdc
https://www.akamai.com/uk/en/about/news/press/2017-press/akamai-releases-spring-2017-state-of-online-retail-performance-report.jsp


著者について

David Yanacek は、AWS Lambda に取り組むシニアプリンシパルエンジニアです。2006 年から Amazon のソフトウェア開発者で、以前は Amazon DynamoDB と AWS IoT、内部のウェブサービスフレームワーク、フリート運用自動化システムにも取り組んでいました。David の職場でのお気に入りの活動の 1 つは、ログ分析を実行し、運用メトリクスをふるいにかけて、システムを徐々にスムーズに実行する方法を見つけることです。

負荷制限を使用して過負荷を回避する 乗り越えられないキューバックログの回避