Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

【インフラ】xDS API の EDS に手を加えて動的に Envoy で weight 調整できるようにした

こんにちは、ミラティブのインフラを担当している清水です。

ミラティブではクラウドに Google Cloud を利用しているのですが、稀にインスタンスの性能劣化が発生し動かしているアプリケーションの応答時間が長くなることがあります。

非同期な処理の場合は一時的にタスクキューの時間あたりの処理数が減るものの、遅延を許容できる機能であれば時間経過で回復するのでシステムを継続稼働できます。

一方で、web サーバのように同期処理の場合はリクエストを時間内に返すことができずに timeout したりだとか、worker が埋まって新たにリクエストが返せなくなったりと、正常にリクエストを返すことができずにユーザのクライアントにエラーが返ってしまします。

そこで、今回はクラウドが不調になったときに web サーバへ流れるリクエストの流量を動的に制御し、一時的にリクエスト数を減らしてユーザのクライアントにエラーが返ることを抑制する方法をお伝えします。

クラウドのインフラ (IaaS) は突然不安定になり性能劣化することがある

クラウドのインフラ (IaaS) の利用者はサーバ (インスタンス) が仮想化されているので物理サーバで動かしていることを意識しませんが、 物理サーバーの突然の故障に巻き込まれて仮想サーバーが停止してしまうことはありえます。

ライブマイグレーションに対応しているクラウドベンダーは物理サーバの故障を検知すると、 ライブマイグレーションでインスタンスを別の物理サーバに移動させてくれますが、 ライブマイグレーション時のオーバーヘッドは小さくなるように設計されているものの一時的な性能劣化は避けられません。

ライブマイグレーション時のメトリクス

Google Cloudの場合はログエクスプローラにインスタンスのログが保存されているので、ライブマイグレーションしたことを確認することができます。 Google Cloud CLI の gcloud コマンドでも確認することができるので、私が確認するときは以下コマンドでホストエラーが発生していることを確認しています。

$ gcloud compute operations list --sort-by timestamp --filter "TYPE=('compute.instances.hostError')" --format "list(operationType,targetLink,status,startTime)"
 - compute.instances.hostError
   asia-northeast1-b/instances/${INSTANCE_1}
   DONE
   2023-09-21T22:55:43.380-07:00
 - compute.instances.hostError
   asia-northeast1-c/instances/${INSTANCE_2}
   DONE
   2023-09-21T17:17:54.766-07:00

hostError でライブマイグレーションしたことはインスタンス内部から以下curlコマンドを叩くことで確認することもできます。 通常時は NONE が返ってきますが、ライブマイグレーション時は MIGRATE_ON_HOST_MAINTENANCE が返ってくるようになります。

$ curl -s http://metadata.google.internal/computeMetadata/v1/instance/maintenance-event -H "Metadata-Flavor: Google"
NONE

弊社ではインスタンス内部でインスタンスの状態を定期取得するコンテナを立ち上げていて、以下コマンドでライブマイグレーション中であることを発見できるようにしています。

$ docker logs maintenance-event | grep -v NONE | tail
20220731 23:29:48       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:49       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:50       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:51       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:52       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:53       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:54       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:55       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:56       MIGRATE_ON_HOST_MAINTENANCE
20220731 23:29:57       MIGRATE_ON_HOST_MAINTENANCE

性能劣化したインスタンスはどうしていたのか?

インスタンスがライブマイグレーションしたときはすぐに回復することもあれば一定の時間を要することもあります。 マイグレーションが長引く場合はユーザ影響を最小にするためにも手動でサービスアウトして、 減ってしまった台数分のインスタンスを新たに再構築するという泥臭い運用をしていました。

日中の営業時間帯であればアラートを検知したときにすぐにでも対応できますが、外出中であったり就寝時の深夜帯にも検知することがあって、 なかなかに運用が大変だったので自動化することになりました。

手動でインスタンスを入れ替えていたとき

弊社インフラは内製の監視がいくつかあるのですが、そのひとつとしてロードアベレージ(load5)が閾値を超えた場合に Slack に通知する仕組みがあり、 この仕組みを改良してサービスアウトを自動化する仕組みを作りました。 load5 を監視対象としているのは load1 だとキャンペーンなどでリクエスト数が一時的に増加しているときに瞬間的にロードアベレージが増加することがあって、 誤検知を回避するために load5 が増加しているインスタンスを対象にサービスアウトの自動化を行いました。 仕組みも単純で、load5 の値を見ている監視が閾値を超えたインスタンスを対象に弊社インフラの運用で利用しているサービスアウトのコマンドを実行します。 そして、ロードアベレージの増加していたインスタンスはサービスアウト状態となってリクエストが流れなくなり、クライアントもエラーが返らないようになる仕組みです。

自動でインスタンスを入れ替えていたとき

自動化することで運用は楽になりましたが、load5の値が閾値を微妙に下回っていい感じに検知してくれなかったり、 サービスアウトしてもインスタンスの再構築に一定の時間を要して復旧時間がかかっていました。

さらに仕組みを見直すため、次に着目したのはミラティブの web リクエストの振り分けを行っている Envoy でした。

これまでの仕組みはロードアベレージの増加しているインスタンスを入れ替えてユーザ影響を最小に抑える運用でしたが、 今回考えた仕組みではEnvoyを利用し、 web リクエストを振り分ける際にロードアベレージの増加しているインスタンスの weight を一時的に下げます。 そして、ロードアベレージが回復したら weight を戻して通常通りリクエストを流すので、インスタンスの再構築にかかる時間を待たずとも復旧することができます。

Envoyでロードアベレージの増加したインスタンスのweightを調整したとき

xDS API の EDS に手を加えて動的に Envoy で weight 調整できるようにした

Envoyにはさまざまな動的リソースを発見するディスカバリーサービスがあり、 これらディスカバリーサービスに対応するAPIの総称としてxDSと呼ばれるものがあります。

xDS API には、エンドポイントの設定を提供する EDS (Endpoint Discovery Service) や、クラスタの設定を提供する CDS (Cluster Discovery Service) 、リクエストとクラスタのマッピングを提供する RDS (Route Discovery Service)などがあり、Envoy は起動すると xDS API の提供するサーバに繋ぎにいって gRPC または REST API 経由で自身の設定変更を行ないます。

Envoy 側をデータプレーンと呼び、xDS API を提供するサーバをコントロールプレーンと呼ぶのですが、 ミラティブではこのコントロールプレーンを go-control-plane で実装していて xds-server という名称で呼んでいます。 詳細は過去記事の 【インフラ】 Envoy の導入と xDS API で Consul 連携やってみた話 で紹介されているのですが、 xds-server は Envoy の sidecar として動かし Envoy とともに冗長化していて、ミラティブのサービスディスカバリに利用している hashicorp 社製の Consul と連携しています。 xds-server は Consul と連携することで内部チェックの完了した active な状態のインスタンス一覧をEDSが返せるようにしていて、Envoyがリクエスト振り分け先のエンドポイントをEDSに問い合わせることができるようになっています。

今回は xds-server の EDS の実装に手を加えて、 ロードアベレージの増加しているインスタンスの weight を動的に変更できるようにしました。 具体的には endpoint v3 の LbEndpoint 構造体をエンドポイント (インスタンス) の数だけスライスで生成しているのですが、LoadBalancingWeight というパラメータも一律に設定していてこちらの値を書き換えています。

一部抜粋したコードのサンプルがこちらになります。

func (e *endpointDiscoveryService) lbEndpointsDefault(instances []EDSInstanceConfig) []*endpointv3.LbEndpoint {
    endpoints := make([]*endpointv3.LbEndpoint, len(instances))
    for idx, ins := range instances {
        weight := ins.Weight
        if weight < 1 {
            weight = 1
        }
        endpoints[idx] = &endpointv3.LbEndpoint{
            HostIdentifier: &endpointv3.LbEndpoint_Endpoint{
                Endpoint: e.instanceEndpoint(ins),
            },
            LoadBalancingWeight: &wrappers.UInt32Value{Value: weight},
        }
    }
    return endpoints
}

今回の改修で行ったことは、LoadBalancingWeight を一律100で設定している中でインスタンスのロードアベレージが閾値を超えた場合に、 LoadBalancingWeight を1に変更して微量なリクエストしか流れない状態としました。 加えて、インスタンスのロードアベレージが閾値を下回った場合は LoadBalancingWeight を再び100に戻して他のインスタンスと同じ比率でリクエストが流れるようにしました。

全体の構成図

今回の改修範囲の構成図

こちらが今回の改修をおこなった構成図です。 当初は xds-server の中でロードアベレージの収集から閾値の変更、そして EDS で返す weight の変更処理までする予定でした。 しかし、xds-server の数だけロードアベレージの収集処理が走って無駄がおおいので分離し、複数の xds-server がロードアベレージを参照できるように KV に格納しました。 weight を下げるためのロードアベレージの閾値の変更処理も1箇所に集約するために同じ KV を利用していています。 これらのツールはすべて Go で内製しています。

ロードアベレージの収集ツール

xds-server の EDS が各インスタンスのロードアベレージを判定できるようにするためにロードアベレージの収集ツールを作成しました。 ロードアベレージの収集ツールは Go で作った内製ツールとなっていて以下特徴を持ちます。

  • ロードアベレージの収集先は全てのインスタンスで動かしていてミラティブのリソース監視のために利用している node_exporter から取得
  • 収集したロードアベレージは TTL 付きで KV (内製の Redis 互換のミドルウェアを利用)に格納

KV に Redis 互換のミドルウェアを選定した理由は収集したロードアベレージを TTL 付きで KV に格納したかったからです。 TTL 付きであれば収集したデータが時間経過で消えてくれるので、データが増え続けて KV のリソースを圧迫する心配が無く、 ロードアベレージの収集ツールが何かしらの事故で長らく止まってしまったときも、 xds-server が古い値を参照しつづけて LoadBalancingWeight が下げられた状態のままになることも防げます。

xds-server がweightを下げるために参照するロードアベレージの閾値変更ツール

次に作成したのは xds-server が LoadBalancingWeight を下げるために、基準値となるロードアベレージの設定または変更できるツールです。 これはクラスタまたは個別のインスタンス単位で LoadBalancingWeight を下げる基準値を設定できるツールとなっていて、 ロードアベレージ収集ツールで収集したロードアベレージと共に閾値を KV へ格納しています。

ミラティブではミドルウェアやアプリケーションをクラスタ単位で管理していて、下記実行例の場合だと web-api というクラスタに閾値を設定しています。 クラスタごとに設定できるようにしている理由は系統ごとにロードアベレージの増加傾向が異なるためです。

# クラスタ単位の閾値変更例
$ infra-envoy-weight-config-manager put-load-average-threshold --usage web-api --threshold 5
 USAGE | OLD USAGE-THRESHOLD
---------+----------------------
 web-api | 8

put kv? (yes/no) [no]: yes
 USAGE | NEW USAGE-THRESHOLD
---------+----------------------
 web-api | 5

個別のインスタンス単位でも閾値を設定変更できるようになっていています。 xds-server は個別のインスタンスに設定した閾値を優先的に利用するようにしていますが、設定されてない場合はクラスタの閾値を利用するようにしました。

# インスタンス単位の閾値変更例
$ infra-envoy-weight-config-manager put-load-average-threshold --usage web-api --instance-name instance-1 --threshold 7
 USAGE | HOSTNAME | ADDRESS | OLD INSTANCE-THRESHOLD
---------+-----------+------------+-------------------------
 web-api | instance-1 | 192.168.0.11 | 

put kv? (yes/no) [no]: yes
 USAGE | HOSTNAME | ADDRESS | NEW INSTANCE-THRESHOLD
---------+-----------+-------------+-------------------------
 web-api | instance-1 | 192.168.0.11  | 7

現在の設定状況を俯瞰的にみるために設定一覧を表示するツールも作成しました。 収集したインスタンスのロードアベレージも見れるようになっているので状況を見つつ閾値を調整できるようにしています。

# 設定した閾値と収集したロードアベレージ一覧
$ infra-envoy-weight-config-manager load-average-list
 USAGE | HOSTNAME | ADDRESS | LOAD1 | LOAD5 | LOAD15 | USAGE-THRESHOLD | INSTANCE-THRESHOLD
---------+----------+-------------+-------+-------+--------+-----------------+---------------------
  web-api | instance-1 | 192.168.0.11 | 0.53  | 0.54  | 0.54 | 5 | 7
  web-api | instance-2 | 192.168.0.12 | 0.25  | 0.24  | 0.24 | 5 |
  web-api | instance-3 | 192.168.0.13 | 0.61  | 0.47  | 0.48 | 5 |
  web-api | instance-4 | 192.168.0.14 | 0.55  | 0.47  | 0.46 | 5 | 
  web-api | instance-5 | 192.168.0.15 | 0.58  | 0.50  | 0.47 | 5 |
  web-api | instance-6 | 192.168.0.16 | 1.16  | 0.68  | 0.62 | 5 |
その他実装にあたって気を配ったところ

クラウドでインフラを運用していると zone 単位でネットワークのレイテンシが増加して通信しづらくなったりして zone 障害が心配になります。 zone 障害が発生すると web サーバは別 zone の DB やミドルウェアに接続できるまでの時間が増加するので、cpu の待ち時間が増えて zone 全体の web サーバのロードアベレージが増加します。 この場合、立て続けに LoadBalancingWeight が下げられてしまって zone 内で通信できる web サーバがいなくなってしまう恐れがあったので、安全弁として LoadBalancingWeight を下げられる台数を1台に固定しました。 複数台のインスタンスが閾値を上回ったときは zone 障害のおそれがあると判断して LoadBalancingWeight を下げないようにしています。

実際に運用してみての感想とこれからやってみたいこと

いかがだったでしょうか、今回はEnvoy で動的にエンドポイントの weight を下げる方法を紹介しました。 実際に本改修を運用で利用するようになってからは1台の web サーバが性能劣化してアラートが通知することがなくなり、運用がぐっと楽になりました。 これからも Envoy を活用して運用の負荷を減らせていければと思っています。

今後やってみたいことはロールバックに時間のかかっているミラティブのdeployに活用してより早く切り戻せないかなと思っています。 Envoy を活用すればバージョニングルーティングも可能になるので、バージョンごとのクラスタをCDSで返すようにしてパスとクラスタのマッピングをRDSで返すようにすれば特定バージョンのクラスタにしかリクエストを流せないようにできますし、リクエストヘッダにバージョン情報を持たせて特定のクラスタに転送するようRDSで返すようにすることもできます。

また、ミラティブでは外部からアプリケーションに入ってくるトラフィック(Ingress)しかコントロールできていないので今後は egress も活用していきたいと思っています。 egress を活用すればアプリケーションから外へ出ていくトラフィックをコントロールできるようになるので、アプリケーションが外部サービスに繋ぎに行くところもバージョン管理してしまって特定バージョンでしか繋ぎにいくことができないといったことも可能になります。

まだアイディアを出している段階なので、いつか形になって運用で活用するようになったら記事にしてみたいと思っています。

今回の改修を行うにあたり、弊社のはたが公開している example-envoy-xds で勉強できました。 xDS API を go-control-plane で実装してあって、機能ごとにシンプルに記述されているのでわかりやすくて入門におすすめです。

We are hiring!

ミラティブのインフラでは日々成長しているミラティブを観測して、サービスの品質をより良くしていくためにインフラを設計してミドルウェアを選定したり、または運用ツールや監視を内製したりしています。 サービスの手触りを感じつつ日々成長するサービスを支えるための技術や知識を学んでみたいという方おまちしております!

mirrativ.co.jp

mirrativ.notion.site

speakerdeck.com