Enjoy Architecting

Twitter: @taisho6339

最短で理解して運用するGrafana Loki

本記事について

Lokiについてまったく知識のない状態の人にとって、1からキャッチアップしていくのは とても大変なことです。

特にLokiはマイクロサービスで構成されているため、何を知るべきなのかの全体像が見えにくいと思っています。

そのため、Lokiをまったく知らない状態から実際に運用検証を開始するために必要なインプットを体系的にまとめました。

具体的には下記の項目で整理します。

  1. Lokiの機能
  2. Lokiを構成するアーキテクチャ
  3. Lokiを構成するプロセス
  4. Lokiのモニタリング
  5. Lokiでのログのリテンション管理
  6. Lokiのデプロイ
  7. Lokiでのデータキャッシュ
  8. Lokiのベストプラクティス

※前提として、Prometheusについての基本的な知識があれば本記事についてもすぐに理解できるかと思います。

1. Lokiの機能

Grafana Lokiとは?

Lokiは3大監視項目である、メトリクス、ログ、トレースのうち、ログを担当するモニタリングツールです。

メトリクス収集のPrometheus、時系列データベースのCortexのアーキテクチャを参考に作られた分散システムの構成になっています。

Grafana Lokiでは何ができるのか?

Lokiの機能としては主には下記のようなことができます。

  • Grafanaと連携してダッシュボードでのログの可視化や検索
  • ログベースでのアラートルールの設定
  • ログデータのマルチテナント管理

注意点としてはマルチテナント前提に設計されてはいるものの、Lokiそのものにテナント認証機能はありません。 よってLokiに保存したり検索をかける前に、テナント認証のプロキシをはさみ、テナント識別用のHTTPリクエストヘッダーを埋め込む、といったことが必要になります。

Authentication

Lokiの主な特徴

Lokiは下記の様な特徴を持ったツールです。

  • Prometheusと同様1つ1つのログデータがラベルを持つ

  • Cortexと同様、書き込み、読み込み、アラーティング、データ圧縮など複数の役割を持った分散システム構成になっている

  • Cortexと同様自分ではデータストレージを持たず、AWSGCPのObject StorageやBigtable、DynamoDB、他のOSSソフトウェアなどを使ってデータを管理する

役割ごとにプロセスを分割しているため柔軟にスケールでき、可用性や信頼性もクラウドプロバイダーなどに移譲できるというのが大きな特徴です。

2. Lokiのアーキテクチャ

Lokiのアーキテクチャは図のようになっています。 データには、転置インデックスである「Index」と実データである「Chunk」の2種類が存在し、そのデータを書き込んだり、読み込んだり、定期的にチェックしてアラートを飛ばしたり、圧縮したりリテンションを管理するプロセスが存在しているという構成になります。 また、データにはキャッシュ機構も存在しています。

Lokiが扱うデータ

Lokiは受け取ったログデータから、Chunk(実ログデータ)とIndex(検索用転置インデックス)を生成し、 設定、連携されたログデータストレージに保存します。 ただ、ログデータストレージにはなんでも指定できるわけではなく、サポートされているものが決まっています。 また、ChunkとIndexで指定できるストレージも異なります。

Loki Storage

以前はIndexに関してはObject Storageへの保存がサポートされていませんでしたが、現在はBoltDBというローカルDBに一旦保存し、BoltDB Shipperという仕組みを使うことで、自動的にObject Storageへ同期してくれるようになりました。

3. Lokiを構成するプロセス

Lokiの主要なプロセスとして下記が挙げられます。

  • Distributor
  • Ingester
  • Querier
  • Querier Frontend
  • Ruler
  • Compactor
  • Table Manager

このうち、Lokiを最低限動かすのに必要なプロセスはDistributor、Ingester、Querierです。 他のプロセスは、機能的に必要な場合やパフォーマンス改善で必要な場合に足していく形になります。

では実際に各プロセスについて役割を追っていきます。

Distributor

書き込みリクエストを最初にハンドリングするコンポーネントで、受け取ったログを適切なIngesterへつなぎます。 IngesterへのルーティングはConsistent Hashアルゴリズムを用いてルーティングします。 ハッシュの計算にはlogのラベルとtenant IDを用い、計算されたハッシュ値より大きくて一番近い値を持つIngesterへルーティングされます。

また、バリデーション、データ加工、Rate Limitの役割も担っており、 特にRate Limitは全体のRate LimitをDistributorの台数で割った値を一台あたりのRate Limitに設定します。 よってロードバランサーを置いて、均等にトラフィックを分散するのが有効です。

レプリケーション

データ保護、及びIngesterの入れ替わりに対応するため、 通常複数のIngesterに複製してログを送ります。(デフォルトでは3台に送る) また、データの一貫性を保つため、書き込み完了の判定にはquorumを用いています。 quorumは、

floor(replication_factor / 2) + 1

で計算され、例えば送信先が3台あったら2台に書き込みが成功しないと書き込み失敗になります。 また複製として使用するIngesterは、最初にハッシュ値がヒットしたIngesterから、Consistent HashのRing上を時計回りに順番にレプリカ数分ピックアップします。

バリデーション

バリデーションプロセスでは以下のような内容をチェックしており、バリデーションに失敗すると書き込みエラーを返します。

  • ログの時系列はあっているか?
  • ログは大きすぎないか?
  • Prometheus形式のラベルになっているか?

特に時系列の概念は重要で、最後に受け取ったログのtimestampより前のtimestampのログは受け取ることができません。

詳しくこちらに記載されています。

Logs must be in increasing time order per stream

データ加工

ラベルを元にハッシュ値を作るので、同じ構成のラベルは同じハッシュ値になるようラベルの並び順をソートして正規化しています。

たとえば、これらのラベルが同じハッシュ値になるように並び順を揃えます。

{job="syslog",env="dev"}
{env="dev",job="syslog"}

Rate Limit

Rate Limitでは受け取る書き込みリクエストを制限します。

リクエストの制限は1つのテナントごとに設定され、 1つのDistributorが制限するRate Limitは、そのテナントに対するLimitをDistributorの数で割ったものになります。

これを実現するためには、Distributorが全体で何台いるのかを各メンバーが知る必要があり、 そのためにクラスタリングしています。

クラスタ情報の保管、管理には、Consul, etcd, memberlist, inmemoryのオプションを選択できます。

memberlistはHashCorp製のクラスタ管理用ライブラリで、Consul等と同様にgossip-protocolを用いてクラスタメンバー間でクラスタ情報の更新を行っています。

memberlist

Ingester

ログを実際に保存する役割を担っています。

受け取ったログのラベルセット + テナントのIDを見て、対応するChunkにログを追加します。 また、もしChunkが存在しない場合は新規で作成します。

Chunkに仕分けられたログは一定時間メモリにバッファされ、一定のタイミングで永続化ストレージにflushされていきます。

この構成だと一定期間ログデータをメモリに置いた状態なので、この間にプロセスがダウンするとデータが揮発してしまいます。 よってWrite Ahead Logという仕組みを用いることで復旧できるようにしています。

WAL

Ingesterは書き込みリクエストを受け取ると、永続化領域にまずログを記録します。

flushしたものはflushしたことがわかるように更新されるため、もしプロセスダウンで復活したときでも、 どのログがflushされていないのかを判別することができ、復旧することができます。

Write Ahead Log

しかし注意点があります。

ログ書き込み用のディスクがfullになり、WALに書き込めない状態でもログの書き込みリクエストはエラーにならずに受信できてしまいます。

つまりWALに書き込まれずにログを処理する時間が発生する可能性があり、 この時間でプロセス停止などが意図せず起こればログデータを失う可能性があります。

ディスク使用量や、WALへの書き込み失敗数などをモニタリングし、検知できるようにしておくことが大切です。

クラスタリング

IngesterはDistributorからConsitent Hashによるルーティングがされるため、 Consistent HashのRingを保存しておく共通のデータストアなどが必要になります。

実際にクラスタリングに使えるのはDistributerで列挙したものと同様です。

実際の書き込みフロー

Distributorがリクエストを受け付けてから、Ingesterが処理するまでの流れはここに詳細に記載されています。

Write Path

Querier

LogQLの形式でクエリを受け取り、検索処理するコンポーネントになります。 具体的に、検索処理がどのようなフローで行われるのかは下記がわかりやすいです。

Read Path

Querier Frontend

Querierの前にProxyとして置くことができるOptionalなコンポーネントです。 主な役割として、パフォーマンス向上のために存在しています。

機能としては、検索結果のキャッシュ、クエリ処理のキューイング、大きなクエリの分割を提供します。

Querier Frontend

巨大なクエリを実行したときに困るのはOOMです。 よってQuerier Frontendのレイヤで小さなクエリに分割し、キューイングして別々のQuerierに分散させることで、一つのQuerierに大きな負荷がかかることを回避することができます。 Querierから返ってきた結果はこのレイヤで統合されて、最終的なレスポンスを返します。

Ruler

Querierに定期的にクエリ発行して、AlertManagerにアラートを飛ばすことのできるコンポーネントです。

クエリそのものはLogQLという形式ですが、Prometheusと同じようなフォーマットでルールを記載することができます。

Alerting

metricsの出力機能がなかったり、exporterが提供されていないようなプロダクトに対してもログベースで気軽にアラート設定を行うことができます。

Compactor

Compactorについてはあまり情報がありませんでしたが、一定周期でIndexデータを最適に圧縮してくれるコンポーネントのようです。

compactor_config

Table Manager

Lokiはストレージのバックエンドとして、DynamoDBやBigTableなどの、テーブルベースのDBをサポートしています。 Table ManagerはそういったテーブルベースのDBに対して、スキーマの変更、バージョン管理や、データのリテンション管理を行うことができます。

Table Manager

注意点として、S3などのObject Storageを使う場合は、Table Managerのスコープ外なので、S3側でLifecycle設定を通してリテンション管理を行うなどが別途必要になります。

Lokiのプロセス実行モードについて

Lokiはすべてのプロセスを一つのバイナリでまとめて実行するモード(モノリシックモード)と、マルチプロセスに構成するモード(マイクロサービスモード)があります。

モノリシックモードは、簡単にプロセスを立ち上げてすぐに機能を検証することが可能ですが、柔軟なスケールができません。

すべて一緒にスケールしないといけないので、例えば読み込み用のプロセスだけスケールするなどできず、リソース上の無駄が発生したりします。

そのため、モノリシックモードは検証やスモールスタート用に用いるのが推奨されており、ある程度の規模の本番環境ではマイクロサービスモードで運用するのがStandardのようです。

StatelessなプロセスとStatefulなプロセス

LokiにおいてStatefulなプロセスは、IngesterとQuerierです。 IngesterはWALやChunkを一定期間ローカルにバッファする性質上、Statefulなのは明白ですが、Querierに関してはboltdb-shipperを利用してIndexを保存している場合にStatefulになるようです。 ※この辺りの理由は調査中

Upgrade Guide

4. Lokiのモニタリング

Lokiの各プロセスはPrometheus形式のmetricsを出力します。 よって、汎用的なプロセスの死活監視などを行いつつ、Lokiのmetricsを見て詳細な機能に関するモニタリングを行うことになります。

前述したWALに関するメトリクスは、Observabilityに記載されてはいませんでしたが、実装には記述されていました。

5. Lokiでのログのリテンション管理

前述した通り、TableベースのDBをバックエンドにする場合は、TableManagerを使ってリテンション管理を設定するのが良いです。

そうでなければストレージそのものに備わっている機能を使って管理するか、自前で仕組みを作る必要があります。

6. Lokiのデプロイ

幸いHelmチャートが用意されているので、基本的にはこれを使うと良いです。

マイクロサービスモード

モノリシックモード

7. Lokiのデータキャッシュ

Lokiではアーキテクチャ図にもある通り、Ingester、Querier、Querier Frontend、Rulerのレイヤでそれぞれでキャッシュを保持します。 例えば、Ingesterは前述したとおり、データを一時的にメモリに置き、期限が切れたタイミングでWriteBackします。 また、Querier Frontendは検索結果のキャッシュを保持しています。 このキャッシュのバックエンドには、Redis、memcache、in-memoryを指定でき、 キャッシュの影響でパフォーマンスが大きく変わるため、チューニングの余地があります。

8. Lokiのベストプラクティス

ラベルのカーディナリティに配慮する

Lokiのログデータも、Prometheusと同様にラベルをつけることで検索に役立てることができますが、 同様にカーディナリティに配慮する必要があります。 カーディナリティとは「何種類の値を取りうるか」での数値で、これがあまりにも膨大、もしくは予測できない場合、チャンクのサイズも膨大になってしまい、ストレージ容量の消費とロードにかかる時間がボトルネックになってしまいます。 Lokiでは、経験則的に、1桁 ~ 10台の値に押さえておくように推奨されています。 これ以上にカーディナリティの高い、もしくは予測ができない無制限のパラメータはラベルではなく、文字列一致や、正規表現のパターンマッチなどを使うことが推奨されています。

まとめ

ここまでの内容を踏まえることで、Lokiは実際に自分たちの環境、要件にフィットするのかを検証することができるようになったはずです。

まだまだ情報不足であり、運用が難しいプロダクトだとは思いますが、本記事がお役に立てれば幸いです。

追記

こちらのkubenews 第20回でDemoを交えつつ、紹介させていただきました www.youtube.com

また、動画内のデモはこちらのmanifestを使うことで再現することができます github.com

Fluentdのバッファリングで抑えておくべき大事なポイント

概要

Fluentdで障害設計をする上でバッファリングの概念は切っても切り離せません。

本記事では、ドキュメントだけでは拾いきれないものも踏まえ、

Fluentdのバッファリングで抑えておくべき情報を体系的にまとめます。

バッファリングとは?

Fluentdではログをバッファリングしてまとめて送信するための仕組みが用意されています。

これは下記のような用途に用いることができます。

  • 送信先がダウンしていたときに一時的に保管しておく
  • 送信先のキャパシティに合わせて送信流量を制限する

Fluentdにはメモリ上、もしくは永続化ディスク上にバッファを保管しておく仕組みが用意されています。

バッファの構造

バッファの構造は下記のようになっています。

引用: https://docs.fluentd.org/buffer

Output Pluginごとに一つバッファ領域を持っており「stage」と「queue」という2つのフェーズを持ちます。

まずログが取り込まれるときは、「stage」へと書き込まれ、 これが成功することでInput/Filter Pluginの処理は完了とみなされます。

※例えば、forward pluginのrequire_ack_responseを用いる場合、Aggregator側で、受け取ったログがstageに書き込まれたタイミングでACKを返します。

そして特定のタイミングでqueueにenqueueされ、順番にPluginが扱う送信先に送信されていきます。

stageの構造

stageに書き込まれる時、ログはchunkという単位でグルーピングされてまとめられます。 このchunkは、chunk keyという指定されたkeyをもとに、同じ値を持つグループでまとめられます。 chunk keyには、時間、タグ、特定のレコードを指定することができます。

stageからenqueueする処理

Fluentdのプロセスは、1つ専用のスレッドを作り、 そのスレッドで、stageにいるchunkをすべて走査し、 条件にあてはまるchunkをenqueueしていくという処理をintervalごとにループして繰り返し実行しています。

この辺りの処理

flush modeについて

stageからqueueに追加されるときの動作には、4つのモードがあります。

これらはflush_modeと呼ばれ、bufferのconfigで指定可能です。

flushと呼ばれていますが、この文脈ではstageからqueueへの追加されるまでの処理を「flush」としています。

lazy mode

時刻と期間をベースにchunkを分けてflushしていくmodeです。

timekeyパラメータに指定されている期間と時刻をもとにchunkを分割します。

例えばtimekeyに1hを指定した場合、 12:00 ~ 12:59の間のログ, 13:00 ~ 13:59の間のログのようにグループ分けされ、1hごとにenqueueされます。

interval mode

flush intervalごとにenqueueしていくmodeです。

lazyと違って時刻の考慮はなく、シンプルに一定時間ごとにenqueueします。

flush_invervalという名前なのでわかりにくいですが、ログの送信先に送信されるまでのintervalではなく、あくまでもenqueueされるまでのintervalです。

immediate mode

chunkに書き込まれた瞬間にenqueueされるモードになります。

default mode

defaultでは、timekeyが指定されていればlazy、そうでなければintervalで実行されます。

chunkがenqueueされるタイミング

chunkがenqueueされるタイミングは2つあります。

  1. flush intervalもしくはtimekeyの時間ごとのタイミング
  2. chunk sizeが指定したサイズ以上になったタイミング

buffering parametersのように、chunkはsize limitやrecord数を指定でき、その制限を超えたタイミングでenqueueされます。

Buffer Overflowした時の挙動

buffering parametersに書かれているように、bufferの領域にはサイズ上限が設けられています。

これ以上のサイズのデータがたまるとBuffer Overflow Errorを起こします。

これが発生した時、どういう挙動になるかはInput Pluginのハンドリング次第になります。

よく使われるtail pluginの場合は、Buffer Overflowが収まるまで何度も同じ行の読み込みを繰り返し(1秒ごと、もしくはファイルの変更ごと)、成功するまではpos fileの更新を止めます。

この辺りの処理

設定次第ですが、ログがrotateされる前にバッファが復旧される必要があります。

あまりにもBuffer Overflowが発生する場合は、スループットが追いついていないのでスケールアップするなり、バッファサイズの見直しなりが必要になります。

ちなみにBuffer Overflow ErrorはRecoverableなErrorなため、secondaryなどをバックアップ先として指定していてもこのケースでは使用されません。

queueのサイズが制限に達した時の挙動

enqueueされたchunkの数が、queued_chunks_limit_sizeで指定された数に達している場合、chunkの数がはけるまでenqueueされません。

しかしデータが失われるわけではなく、前述したループ処理で、単にenqueueが見送られるだけになります。

この辺りの処理

ただflush_at_shutdownをtrueにしている場合、shutdown時のenqueue処理ではqueued_chunks_limit_sizeは無視されます。

ログの送信が失敗したときの挙動

ここに書かれている通り、成功するまで、もしくはリトライの制限に引っかかるまでリトライを行います。

リトライにはバックオフがあり、retry_max_intervalに達するまでリトライの間隔が延び続けます。

回数制限をつけたり、無限にリトライするように指定することができます。

また、この間にもstageにはどんどん書き込みが入るのでバッファの使用量には注意が必要です。

※prometheus形式のmetrics exportを有効にしている場合、使用可能な残りのバッファサイズを監視できます

まとめ

バッファを使うにあたって、知っておくべき概念と、細かい挙動についてまとめました。

コードを読まないと分からない点もふくめてまとめたので、KubernetesでFluentdの信頼性を担保するための3つの観点と合わせて、実際にFluentdをProduction運用にする際に役立てられれば幸いです。

KubernetesでFluentdの信頼性を担保するための3つの観点

概要

GKEなどを使えば自動的に標準出力のログが集計&集約され、Cloud Loggingなどを通して可視化されますが、 オンプレミス環境でKubernetesクラスタを構築する場合そうはいきません。 また単純なアプリケーションログの集計以外にも、 Kubernetesを使ってログ、データ集計をしている人はFluentdを運用しなくてはならない人は多いと思います。 本記事では、ログの集計、集約のデファクトスタンダードであるFluentdをKubernetes上に展開する上で、 信頼性を担保するための観点を整理します。

想定アーキテクチャ

想定アーキテクチャとしては現場でよく構築されている、図のような構成を用います。

f:id:taisho6339:20210422151430p:plain

アーキテクチャの特徴

クラスタに、FluentdがForwarderとAggregatorという2つのロールでそれぞれ存在しています。

  • Forwarder

    • DaemonSetでデプロイされる
    • 各コンテナの出力ログをあつめ、Aggregatorに送信することだけが唯一の責務
  • Aggregator

    • Deployment(もしくはStatefulSet)でデプロイされる
    • ForwarderからTCPでログを受け取る
    • filterを用いた加工処理や、最終的なデータストアへのログ送信を担う
  • ForwarderはAggregatorへServiceリソース経由でアクセスする

担保すべき信頼性

今回は「どこかに障害が発生したとしてもできる限りログを損失しないこと」を目標とし、 Forwarder, Aggregatorそれぞれで下記3つの観点をチェックしていきます。

  1. Podのクラッシュへの対応
  2. Podの退避への対応
  3. ログの宛先のダウンへの対応

※またログの損失を予防する代わりに、重複するログは多少許容することとします。(at-least-once)

1.Podのクラッシュへの対応

Forwarderの場合

f:id:taisho6339:20210422151446p:plain

Forwarderの役割は実際のログファイルから少しずつログを読み取りAggregatorへ送信することです。 また、Fluentdはパフォーマンス向上や、ログの送信先がダウンしていても問題ないようにバッファリング機構を持っています。 この前提から、FowarderのPodがクラッシュするときに想定したい注意点として以下の2つがあげられます。

(1) クラッシュからの復帰後、以前読んだところから読み取りを再開できるようにする

「ログの読み取り済みの位置を記録する」ことで対応します。 ログをファイルから読み取るとき、通常tail pluginを用いますが、 tail pluginにはpos fileという機能があります。

pos file

これを使うことで、読み取ってからバッファ済みになったログファイルの位置を記録しておくことができます。

(2) バッファされた未送信のログの損失を防ぐ

こちらは「未送信のバッファを永続化しておく」ことが必要です。 バッファリングにはメモリバッファとファイルバッファがありますが、 ファイルバッファを使っておくとクラッシュ時に損失を防ぐことができます。

file type buffer

Aggregatorの場合

f:id:taisho6339:20210422151532p:plain

Aggregatorの役割は、ForwarderからTCPで受け取ったログを加工、フィルタ処理を行い、最終的なデータストアへ送信することです。 受け取った時点で、加工処理が走り、バッファに書き込みがされてからForwarderへACKが返ります。 なので、Aggregatorとしては受け取ったログのbufferをPodがクラッシュしたとしても保持し続けることが大事です。

これは、Forwarderで用いたfile bufferを指定しておけばOKです。

2. Podの退避への対応

Podの退避、移動や削除は以下のような様々なタイミングで訪れます。

  • NodeのShutdown(Scale Inなど)、Replace、メンテナンス
  • DaemonSet, DeploymentのUpdate

つまり、Podの削除とノード間移動を前提に考えなくてはいけません。

Forwarderの場合

f:id:taisho6339:20210422170640p:plain

ForwarderはDaemonSetなので、 DaemonSetのUpdate発生時、Nodeが特にShutdownしないようなケースであれば、 hostPathに前述のpos fileとfile bufferをおいていればPodがいかに入れ替わろうと復旧可能です。 ※ただしemptyDirはPodが退避されると一緒に削除されるのでNG

しかし、NodeもShutdownするケースは、Nodeのディスクにデータを残しておけば済む話ではないため注意が必要です。 よって、以下の2点を考慮する必要があります。

(1) Nodeがshutdownしてしまうので、プロセス終了時にバッファをflushする

flush_at_shutdownというパラメータがbufferingの設定の中にあるので、これを有効にしておけばOKです。

flust_at_shutdown

また、ちゃんとflushするのに必要な時間を確保するため、terminationGracePeriodSecondsは十分に取っておきます。

(2) プロセス終了時のflushのタイミングでAggregatorがダウンしているケースを考える

このケースは完全にログの送信を担保することはできません。 プロセスshutdown時にflushが失敗した場合、 secondaryを設定していればsecondaryにバックアップしてくれるような実装になっていれば回避できますが、 現状コードを読む限りそうはなっていません。

この辺りの実装

つまりどうしても損失させられないようなログ(監査ログなど)は、 はじめからsecondaryではなくcopyプラグインを活用して、複数箇所に同時に保存しておく、 といった対応が必要になります。

Aggregatorの場合

f:id:taisho6339:20210422151544p:plain

DaemonSetと違うのは、すべてのNodeに1台だけ存在する構成にはなっていないので、 Node配置のAffinity設定や、一気にPodがダウンしないように気を使ってあげる必要があります。 またDaemonSetと違い、TCPでリクエストを受ける構成なので、 プロセスshutdown時にSIGTERMとbuffer flushのタイミングを考えなくてはいけません。 それを踏まえ、下記の2点を検討します。

(1) Pod削除時にbufferをflushする

Forwarderのときと同じく、flush_at_shutdownを行います。 AggregatorはForwarderと違って、TCPで通信を受け付けるので、 新規のリクエストを止めてからflushするようにする必要があります。 具体的にはPodのpreStopでsleepさせて、 しっかりServiceへのリクエストを止めてからSIGTERM => flush処理に移るようにするのが安全です。

また、flush時に宛先がdownしているとForwarderと同じようにデータが損失してしまうため、 Volumeをアタッチしておき、バッファの保存先をそこに指定おけば復旧可能になります。

(2) 一気に複数のPodが同時にUnavailableにならないようにする

特定NodeにPodが集中しないよう、Affinity設定を入れます。

Pod Affinity

また、PodDisruptionBudgetを活用し、UnavailableなPodの数を制限するようにします。

PDB

3. ログの宛先のダウンへの対応

Forwarderの場合

f:id:taisho6339:20210422151501p:plain

宛先がダウンしている場合でもログが失われないようにするために、前述したfile bufferを用います。 この際、2つ注意点があります。

(1) Aggregatorがバッファに書き込み完了したことを保証する

AggregatorがTCPでログを受け取っても、バッファにちゃんと書き込まれているとは限りません。 よって、Aggregatorがデータを受け取った直後、バッファに書き込まれる前にプロセスが終了してしまった場合データが失われます。

これに対応するため、 forwardのpluginには、Aggregatorがbufferに書き込み完了しACKを返すまで、送信が完了したとみなさないようにするパラメータがあります。

require_ack_response

このパラメータを有効にすることで、Aggregator到達時点まではat-least-onceを保証することができます。

(2) Aggregatorへの送信失敗に備える

Aggregatorへの送信が失敗に備えるには、バッファの永続化、リトライ、secondaryを意識する必要があります。 そして、送信失敗したとしても復旧可能にしておく方法として2つ手段があります。

  1. リトライを無限にし、バッファをHostのディスク領域に永続化しておく

  2. リトライ回数に制限を設けておき、制限に達したらsecondaryの送信先に送信する

バッファサイズは、ディスク容量、オープンできる最大ファイルディスクリプタ数、時間辺りのログ流量 * 障害許容時間などを加味して決定します。 またsecondaryは、リトライが制限に達するか、回復不可能なエラーが発生した場合に送信先として使われることになります。

Aggregatorの場合

f:id:taisho6339:20210422151557p:plain

Aggregatorの場合は、Forwarderと違い、require_ack_responseなどのパラメータは使えません。 到達保証は宛先データストアと宛先へのoutput pluginの実装次第になります。

Datastoreへの送信失敗に備える

こちらもForwaderからAggregatorへの送信失敗ケースと同様の観点で考えます。

  1. リトライを無限にし、PersistentVolumeをAttachしてそこでバッファのファイルを永続化する

  2. リトライ回数に制限を設けておき、制限に達したらsecondaryの送信先に送信する

Aggregatorの場合は、DaemonSetではないので必ず同じノードで起動してくれる保証はありません。 そこでDeploymentではなくStatefulSetでデプロイしておき、VolumeをAttachすることでバッファを永続化しておくことができます。

まとめ

まとめるとこれまで、述べた3つの観点に対し、下記のように対応すればOKです。

  • Forwarder

    • input
      • tail ではpos fileを設定する
    • output
      • file bufferを使う
      • require_ack_responseを設定する
      • hostPathにfile bufferを永続化しておく
      • flush_at_shutdownを設定する
    • pos fileやbufferingにはemptyDirを使わない
    • どうしても失いたくないログはcopy pluginで二重に保存しておくと安心
  • Aggregator

    • affinityで分散しておく
    • PDBで一気にunavailableにならないよう気をつける
    • リクエストを止めてからflushするようSIGTERMのタイミングを調整する
    • outputでは、
      • file bufferを使う
      • VolumeをAttachしてそこにfile bufferを永続化しておく
      • flush_at_shutdownを設定する
    • bufferingにはemptyDirを使わない
    • どうしても失いたくないログはcopy pluginで二重に保存しておくと安心

2020年の振り返りと今後の展望

本記事について

今年ももう終わるので今年やったこと、考えたことを振り返り、来年につなげようと思います。 また今年で一旦フリーランス辞めて会社員に戻るつもりなので、何を考えてその決断をとったのかを整理しておきます。

今年注力した技術

今年やった仕事

  • 某求人サービスでKotlin, Scala + AWS ECSでのバックエンド開発
  • 某メディア会社のマイクロサービス基盤のリアーキテクト、運用整備、技術検証

アウトプット

登壇

ブログ

OSS

振り返り

良い点

今年は相当アウトプットを頑張った年になりました。 登壇、有名OSSへのContributeに加え、 zennで書いた本や、qiitaに上げたKubernetesの記事がうまいことバズリ、 多くの人に自分のアウトプットを見ていただくことができました。

また、仕事に関してもお世話になった某メディア会社では、 フリーランスの自分に、技術検証、選定、実装&リリース、運用と運用整備まで一貫して主導でやらせていただけて、 大きな成長を感じると共にとても楽しく働くことができました。

反省点

大規模サービスになってくると、扱う技術の制約はより重要になってきます。 管理できるノード数であったりクラスタ数、リクエスト数、コストなどの制約は、 技術選定のための検証時にしっかり限界まで見て測っておかないと、 後々の工程で大幅な手戻りをすることがあります。 リアーキテクトに携わる中でこれを身を持って痛感しました。

フリーランスをやめる理由

今後エンジニア市場により若い人たちがどんどん参入していく中で、 何十年後も一定以上の収入を維持しつづけることを目標にしたときに、 フリーランスだとそれに必要な経験を積んでいくことが難しいなという結論にいたりました。

というのも今後は、

  • 技術はどんどんコモディティ化し、単体の技術を扱って何かを作る障壁はどんどん下がってくる
  • エンジニアの需要増加、待遇向上につられてエンジニアの数自体はどんどん増えていく

ということが起こるのかなと考えています。 小学生でも優秀層はWebRTCなどを駆使してプロダクトを作れる時代の中、 すぐにキャッチアップできるスキルをいくら身につけてもすぐ追いつかれてしまうので、 経験を積むことで差別化できるような知見、スキルを得ていく必要があるなと感じてます。

そのために、

  • 幅広く技術やそのメリデメを熟知していて、要件に対して最適に選択でき、運用設計まで行える
  • 難易度の高い要件にもしっかり対応することができる
  • ある特定分野で業界をリードできるだけのスキルとブランディングを築いていく

このへんのポイントをキャリアに加えていくことを重点的にキャリア形成を考えています。 しかし、フリーランスではそもそも意思決定に関わる機会がどうしても減ってしまいます。 要件が難しくなる大規模環境では特にフリーランスへの情報開示、権限などがある程度制限されることが多いです。 そうなると、要件の落とし込みから導入、運用設計まで裁量を持ってやることのハードルが高くなってきてしまい、 良質な経験値を積めるかどうかは運次第になってきます。

よって、一旦会社員になろうと決意しました。

次はどこに行くのか?

LINE株式会社でプライベートクラウドプラットフォームを開発、運用する部署に行きます。 人生で一度はクラウドづくりしてみたかったのでとても楽しみです。

マルチクラスタKubernetes 3つのパターンと実運用事例

この記事はKubernetes Advent Calendarの7日目の記事です。 今回は、Kubernetesのマルチクラスタ化についての考察記事を書きます。

マルチクラスタの定義

マルチクラスタと一重にいっても色々とありますが、本記事では、「複数のKubernetesクラスタを並列に並べ、トラフィックを特定の条件でそれぞれにルーティングする」構成のことを指すとします。

また常時マルチクラスタではなく、普段はシングルでも、いつでもクラスタを並列に並べることができる構成もマルチクラスタ構成とします。

f:id:taisho6339:20201206170325p:plain

マルチクラスタが必要になるケース

運用する側としては、クラスタの数は少なければ少ないほど嬉しいはずです。

では、どのようなケースでマルチクラスタ構成を取る必要が出てくるのでしょうか?

Multi-cluster use casesにも記載されていますが、コアなものに絞って要約すると、下記のようになると解釈しています。

  • 可用性の向上

  • 地理分散への考慮

    • リクエスト元のロケーションを考慮したルーティング
    • Locationごとの固有サービスのデプロイ
  • セキュリティポリシーへの対応

    • 特別セキュリティが厳しいワークロードとそうでないものを区別して、別々のクラスタで運用
  • パフォーマンス改善

    • master nodeを分離することでクラスタ機能そのものの負荷を分散

私の現場のケースで採用に至った理由

私のケースでは「可用性の向上」が主たる理由になります。

  • Surge Upgrade + Graceful Shutdownを有効にしているものの、Master Upgradeしただけでも一瞬ダウンタイムが挟まってしまっている(原因究明中)

  • アップグレード起因で何か問題があったとしてもFail Over、ないしロールバックはできるようにしておきたい

  • BCP観点でリージョン障害に耐えうる構成を取る必要性

  • マイクロサービスで、各サービスチームがAWSGCPを選択可能な世界観にしておきたかった

これらがマルチクラスタで実現したかった要件になります。

マルチクラスタの実現方法

では、この要件を満たすためにどうすればマルチクラスタ化を実現できるでしょうか?

主に一般的なのはこの3つではないでしょうか。

  1. DNSパターン
  2. HA Proxyパターン
  3. Global Load Balancerパターン

1. DNSパターン

f:id:taisho6339:20201206170311p:plain

これは一番シンプルで運用が楽なパターンではないでしょうか? これはクラスタを指すドメインに対して、複数のクラスタのIPを登録しておく構成になります。 クラスタのIPはNodePortもしくは、L4ないし、L7のLBを払い出すことになります。

  • メリット

    • シンプルで管理運用コストが低い
    • Route53などを使えばGeographic Routingも可能
  • デメリット

    • DNSにはTTLがあるので即時FailOverができない
    • 常にTTLを意識して慎重にオペレーションする必要がある

私のケースでは特にこのTTLの部分で、即時FailOverできない点、ロールバックなどが気軽に行えい点を踏まえてこのパターンは見送りました。

2. HA Proxy パターン

f:id:taisho6339:20201206170342p:plain

このパターンは、前段にHAProxyのクラスタを設置し、そのバックエンドとしてKubernetesクラスタを置くパターンです。 HA Proxy自体を冗長化しておくために、何台かを並列で並べて管理する必要があります。

HAProxy 入門

可用性の文脈ではなく、各コンポーネントごとに配置するクラスタを分散して、そのためのトラフィック制御を行う文脈ですが、コロプラさんがこの構成をとっています。

コロプラさんの事例

  • メリット

    • かなり細かいルーティング制御が可能
    • クラスタごとのカナリアリリースやFail Overなども簡単に実現することができる
  • デメリット

    • Managedなものを使わない限り、HA ProxyがSPOFにならないよう管理運用コストがかかる

このパターンもメリットは大きいものの、HA Proxy自体の可用性、耐障害性への管理運用コストを鑑みて見送りました。

3. Global Load Balancerパターン

f:id:taisho6339:20201206170359p:plain

これは、クラスタの前段にグローバルなLBを配置し、その下にクラスタを置くパターンです。 私のケースでは、前述2パターンのデメリットが許容できず、消去法でこのパターンになっています。

LBそのものの機能性に左右されそうですが、 このパターン自体のメリデメは下記のようになっています。

  • メリット

    • LBからのヘルスチェック機構により、即時でFail Overできる
    • TTLなどに支配されない
    • 管理運用コストが比較的低め
  • デメリット

    • HAProxyのような細かいトラフィックルーティングができない ※1
      • 各ゾーンごとに均等に分散されるだけ

※1 AWSでEKSを使えばできる模様

ALB Weighted Target Groups による EKS Cluster の Canary Switching

Global Load Balancerパターンでの実運用

前述した通り、最終的にはLoadBalancerパターンを用いて対応しました。 このパターンを実現するために、GCPIngress For Anthosの機能を使って実現しており、構成の詳細について、Kubernetes MeetUp Tokyoにて私がLTした資料に記載してあります。

Ingress For Anthosを活用した安全なk8sクラスタ運用

Ingress For Anthosを使うことで、LBの設定は完全に自動化されています。 これにより、

といったことが担保できています。

この構成での惜しいところ

消去法で選んでいるので、当然完璧ではありません。 少なからず辛いところは存在しています。 例えば下記のようなポイントです。

  • Config ClusterというLBの設定同期用のクラスタを用意し、そこにLBの設定をデプロイしなくてはならない

  • 細かいトラフィックルーティングができないので、カナリアリリースのようなことはできない

  • マルチクラウドが実現できることにはできるが、EKSをそのまま突っ込んだりできるわけではなく、それなりに複雑な構成を取る必要がある

実際のクラスタアップグレードについて

実際のアップグレードとしては、

  1. 対象クラスタをサービスアウト
  2. 対象クラスタをアップグレード
  3. 対象クラスタをサービスイン

といったようにローリングアップデートの形をとっています。

ただ、カナリアリリースのようなことはできないので、 いきなりクラスタをサービスアウトし、アップグレードするようなことをやると、一気にトラフィックが片側に流れることになってしまいます。

現在は、ある程度minimumのPod数をある程度積んでおくことで対処していますが、これはリソース効率が悪く、本意ではありません。

よって将来的には、

  • ALB + EKSの構成に寄せる
  • GCPがGCLBにトラフィック制御の機能を出してくれるまで待つ
  • 別のパターンの構成に変える(HA Proxy)

といった対応をする必要が出てくる可能性があります。

(現在はコスト面でもトラフィック面でもゆとりがあるので予定はありません)

マルチクラスタとGitOps

これは余談ですがマルチクラスタ化するにあたり、GitOpsの考え方を取り入れて、ArgoCDによってデプロイパイプラインを組んでいたことにより、ここに関してはほぼノーコストで移行できました。 また、移行後も特に問題なく稼働できています。

ただ、現状各クラスタごとにArgoCDを独立してデプロイしており、各クラスタの状態を見るためには各クラスタ用のダッシュボードをみる必要があります。

一応ArgoCD側でもMultiClusterのための提案がなされていて、一つのApplicationに対して複数クラスタへのデプロイができるような、ApplicationSetというCRDおよびOperatorの開発が進んでいるようです。

まとめ

本記事ではマルチクラスタ化する理由、実運用してみた所感について紹介してきました。 Kubernetesを安全に運用するためのエコシステムはどんどん様々なソリューションが生まれてきており、今後も益々発展していくと思いますが、本記事が何かのお役に立てれば幸いです。

k8sを運用するなら絶対抑えておきたい、可用性とScalabilityを担保するための大事な観点

概要

先日、Kubernetes Novice Tokyoというイベントで「k8sのAvailabilityとScalabilityを担保するための大事な観点」というタイトルで登壇させていただきました。 運用する上で気にするべき、可用性とスケーラビリティに関する基本的な内容を、 今後のプロジェクトで振り返って参照できるよう体系的にまとめて資料にしています。

内容

スライドはこちら。

speakerdeck.com

まとめ

今後もk8sを本番で運用する上で得られた知見を積極的に発信していく予定です。

Kubernetesの負荷試験で絶対に担保したい13のチェックリスト

概要

ここ最近、Kubernetesクラスタを本番運用するにあたって負荷試験を行ってきました。

Kubernetesクラスタに乗せるアプリケーションの負荷試験は、通常の負荷試験でよく用いられる観点に加えて、クラスタ特有の観点も確認していく必要があります。

適切にクラスタやPodが設定されていない場合、意図しないダウンタイムが発生したり、想定する性能を出すことができません。

そこで私が設計した観点を、汎用的に様々なPJでも応用できるよう整理しました。 一定の負荷、スパイク的な負荷をかけつつ、主に下記の観点を重点的に記載します。

  • Podの性能
  • Podのスケーラビリティ
  • クラスタのスケーラビリティ
  • システムとしての可用性

本記事ではこれらの観点のチェックリスト的に使えるものとしてまとめてみます。

kubernetes

確認観点

  • 攻撃ツール

  • Podレベル

    • 2: 想定レイテンシでレスポンスを返せること
    • 3: 想定スループットを満たせること
    • 4: 突然のスパイクに対応できること
    • 5: ノードレベルの障害、ダウンを想定した設定になっていること
    • 6: 配置が想定どおりに行われていること
    • 7: 新バージョンリリースがダウンタイム無しで可能なこと
    • 8: 長時間運転で問題が起こり得ないこと
  • クラスタレベル

    • 9: Podの集約度が適切であること
    • 10: 配置するPodの特性に合わせたノードになっていること
    • 11: 突然のスパイクに対応できること
    • 12: クラスタの自動アップグレードの設定が適切であること
    • 13: Preemptibleノードの運用が可能であるか

攻撃ツールの観点

1: 攻撃ツールがボトルネックになりえないこと

攻撃ツール(locust, JMeter, Gatlingなど)を使って、担保したいRPSの負荷を攻撃対象に対してかけることができるか

解説

意外と失念しがちですが、攻撃ツール自体が想定する負荷をかけられるとは限りません。

たとえば一つのマシンで大きな負荷をかけようとすればファイルディスクリプタやポート、CPUのコアなどが容易に枯渇します。

大きなRPSを扱う場合は、JMeterやlocustなど柔軟にスケールして、分散して負荷をかけることのできる環境を用意しましょう。

検証方法

私のPJではlocustのk8sクラスタを立て、同じクラスタ内にNginxのPodを立て、静的ページに対してリクエストさせてどのくらいのスループットが出るかを検証しました。

要件的には6000RPSほど担保すれば良いシステムだったので、workerの数やユーザの数を調整して余裕をもって8000RPSくらいまでは出せることを確認しました。

Podの観点

2: 想定レイテンシでレスポンスを返せること

Pod単体で想定するレイテンシでレスポンスを返すことができるか

解説

HPAを無効にしてPod単体の性能が要件を満たすかをまず確認します。

想定レイテンシを超過してしまう場合、Podのresource requestとlimitを積んでいきましょう。

また、もしアプリケーションに問題があってレイテンシが超過する場合はアプリケーションのチューニングが必要です。

3: 想定スループットを満たせること

Podレベルで見たときに想定するスループットを出すことができるか

解説

HPAを無効にしてPodを手動でスケールアウトしていき、どこまでスループットを伸ばせるかを確認します。

スケールアウトしてもスループットが伸びない場合、どこかにボトルネックが出ている可能性が高いです。

その場合まず私は下記を確認します。

  • 各PodのCPU使用率

    • 特定のPodだけCPU使用率が偏ってる場合はルーティングポリシーの再確認
  • 各Podのレイテンシ

    • 一つ前の「想定レイテンシでレスポンスを返せること」に戻って確認
  • 攻撃側のCPU使用率

    • 攻撃ツールのスケールアップ、スケールアウト

4: 突然のスパイクに対応できること

突然急激に負荷が高まったときに対応することができるか

解説

HPAを設定しておけばオートスケーリングしてくれますが、ポリシーを適切に設定する必要があります。

HPAは一定期間でPodの数をアルゴリズムに応じて算出し、定期的に調整することで実現されています。

HPA Image

HPAのスケールアルゴリズム

よって、スケールする条件がギリギリに設定されていたりすると突然負荷が高まっても急にスケールできずに最悪ダウンタイムを挟んでしまったりします。

また、k8sの1.18からはスパイクで急激にPodが増えたり減ったりしすぎないように、behaviorという設定項目も追加されています。

私の場合はlocustで急激な負荷を再現し、下記の観点をチェックしました。

  • 監視するメトリクスは適切か?
  • 監視するメトリクスのしきい値は適切か?
  • Podの増減制御が必要そうか?

5: ノードレベルの障害、ダウンを想定した設定になっていること

ノードが柔軟にダウンしてもServiceに紐づくPodレベルで正常にレスポンスを返し続けることができるか

解説

k8sは、クラスタのオートアップグレードなどでノードが柔軟にダウンしたり、 クラスタのオートスケールでノードがスケールインするので、かなりの頻度でPodが削除されて再作成されることを考慮する必要があります。

そこで注意することとして2つの観点があります。

1つ目はPodのライフサイクルです。

Podが削除されるとき、まずServiceからルーティングされないようにすることと、コンテナへのSIGTERMが同時に発生するため、ルーティングが止まる前にコンテナが終了しないようにする必要があります。

具体的にはlifecycle hookを使い、preStopでsleepしてルーティングが止まるまで待ちましょう。 Kubernetes: 詳解 Pods の終了

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 30"]

また、NEGなどを活用してContainer Nativeなロードバランシングを行っている場合、下記のような配慮も必要です。

【Kubernetes】GKEのContainer Native LoadbalancingのPodのTerminationの注意点

2つ目はPod Distruption Budgetです。

これを適切に設定しておくことで、ノードのアップデートなどでPodが排出される際に一気に排出されないよう制御することができます。 後述のノードのSurge Updateと合わせて確認するといいでしょう。

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: sample-service
spec:
  maxUnavailable: "25%"
  selector:
    matchLabels:
      app: sample-service
      namespace: default
      project: default

クラスタのサージアップグレード

6: Podの配置が想定どおりに行われていること

Podが想定するノードに配置されているか、想定どおりに分散されているか

解説

Podの配置は何も考えないと空いているリソースからkube-schedulerが任意に選択してしまうため、下記のような観点の考慮が不可欠です。

  • 特定のゾーン、ノードに集中して置かれてしまっている場合、いくらPodを冗長化したとしても、ノードのダウンや、ゾーン障害で一斉にサービスが止まる

  • Preemptibleノードなどを使っている場合は一斉に複数ノードが落ちることがある

    • (通常ノードとPreemptibleノードは併用するのが定石になっています)
  • Container Native Loadbalancingを使用する場合、LBはPodに平等にルーティングするわけではなく、NEGに対して均等にルーティングしようとするため、NEG(つまりはゾーン)でPod数の偏りがあると安定性やスケーラビリティ、スループットに悪影響を与える

  • ディスクIO処理が多いPodなどはディスクタイプがssdのノードの配置するなど、Podの特性に応じたノード選択が必要かどうか

Podは、下記を活用することで配置制御を行うことができるので、これらを駆使して配置制御を行いましょう。

  • TaintとToleration
  • Pod Affinity
  • Node Selector
  • Node Affinity
  • Topology Spread Constraints

Affinity.png

7: 新バージョンリリースがダウンタイム無しで可能なこと

PodのDeploy戦略が正しく機能しているか

解説

これはチームのデプロイ運用方針でも変わりますが、一定の負荷をかけつつ、ダウンタイム無しでPodのバージョンを切り替えられるか検証しておくと良いと思います。

8: 長時間運転で問題が起こり得ないこと

長時間運用することによって顕在化する問題を含んでいないか

解説

アプリケーションの実装がイケてない場合、メモリリークや、ファイルディスクリプタの枯渇などがよく発生します。

1日以上負荷をかけ続けたときに消費が増加し続けるようなリソースがないか、ノードのスケールイン、スケールアウト、GKEならメンテナンスウィンドウの時間でも問題なく稼働し続けられているかは検証しておくと良いでしょう。

クラスタ観点

9: Podの集約度が適切であること

Podが効率的かつ安全にノードのリソースを活用できているか

解説

Podは、PodのRequestされたリソースと、ノード内の割当可能なリソースを加味してスケジューリングされます。

つまりRequestが適切に設定されていないとリソースが全然余っているのにどんどんノードが増えてしまったり、逆にスケールしてほしいのに全然スケールしてくれない、といったことが起こりえます。

GCPなどのダッシュボードや、kubectl topコマンドを用いてノードのリソースを有効に活用できているかをチェックしておきましょう。

10: 配置するPodの特性に合わせたノードになっていること

IOアクセスが頻繁なPodなど、特性に応じたノードが選択されているか

解説

特定のPodはSSDを搭載したNodeに配置されてほしいなど、Podに応じた要件がある場合、 Node Selector、Node Affinityを利用して適切に配置されているかを確認しましょう。

11: クラスタの自動アップグレードの設定が適切であること

クラスタの自動アップグレード設定が意図した通りになっていること

解説

例えばGKEを利用している場合、メンテナンスウィンドウとしてメンテナンス可能な時間を設定してあげることで、アクセスが少ない時間にアップグレードを行うなどの制御が可能です。 また、一気にノードが再起動しないようサージアップグレードを積極的に活用していきましょう。

クラスタのサージアップグレード

12: 突然のスパイクに対応できること

クラスタがPodのスケールに追従し、スパイクに対応することができるか

解説

これは前述の集約度の話と共通していますが、PodのRequestによってどのようにしてノードがスケールするか、という点が決まります。

基本的に、GKEではスケジュールするためのノードが足りなくなって初めてスケールアウトします。 つまり、突然スパイクしてPodがスケールアウトしようとしたものの、配置できるノードが足りないため、まずノードがスケールアウトしてからPodのスケジューリングがされるケースが発生します。 このような場合にも突然のスパイクに耐えうるか、というのは検証しておく必要があります。

運用したいシステムの要件次第ですが、柔軟にスケールしたい場合はPodのHPAの設定をゆるくしたりなど工夫が必要になります。

クラスタ オートスケーラー

13: Preemptibleノードの運用が適切であるか

Preemptibleノードへの過度なPodの集中など、意図しないリソースの使われ方をしていないか

解説

基本的に本番クラスタでPreemptibleノードOnlyで運用するのは危険です。 Preemptibleノードを運用する場合は、通常のノードと一緒に運用し、 かつTaintとTolerationを適切に設定して、Preemptibleノードによりすぎないようにしましょう。

まとめ

今回は私が負荷試験によって担保した、

  • スケーラビリティ
  • 可用性、安定性
  • レイテンシとスループット
  • リソース利用効率

の観点を整理しました。 もしご意見、感想あればぜひコメントなどいただけると嬉しいです!