こんにちは!SREグループ コンテナ化推進チームの楠本です。
EKSへのコンテナ移行では、これまで紹介した記事以外にも様々なトラブルがありました。
- EKSコンテナ移行のトラブル事例:ALBの設定とPodのライフサイクル管理 - MonotaRO Tech Blog
- EKSコンテナ移行のトラブル事例:推測するな計測せよ -CoreDNS暴走編- - MonotaRO Tech Blog
今回のトラブルでは、コンテナ移行に伴ってSLOが未達状態になりエラーバジェットを急激に消費してしまいました。 その対策としてマルチAZ間の通信遅延の解消をEKS on Fargateで実施したお話をご紹介します。 先に断っておくと私自身がアプリケーション開発者だったため、 インフラの話は都度インフラの方からサポートを受けながら対応しました。そのためズレている点などあればご了承ください。
VMからEKS on Fargateへシステム変更
まず前提として、もともとVM使用時には通信制御を行っていたのですが、運用負荷軽減のため、 今回行ったVMからEKS on Fargateへのコンテナ移行では、それらの通信制御を外す方向で進めていました。 AWS Blogでも述べられていますが、AZ間をまたぐ通信の場合、1リクエストに対して1桁ミリ秒の遅延が発生します。
1桁のミリ秒であれば、プロダクト次第ですが許容できる範囲であると思います。 ただ、今回私達が取り組んだAPIのコンテナ化はこれまでのシステム運用を通して巨大なモノリシックAPIとなっていたため、多くのプロダクトから多くのAPIへリクエストされるものでした。 そのため、APIによっては許容できるものもありましたが、特定のAPIや呼び出し元のAPIコール方法によっては100ms以上の許容できない範囲まで遅延してしまうケースが発生しました。
そのため、当初はシステムの複雑性を解消する・運用負荷を軽減する観点から、 コンテナ化に伴って実装を見合わせていたローカルDNSを、EKS on Fargateの環境下でも 実装する必要性がでてきたというわけです。
実現手段としてはすでにVMでの実績がありました。 コンテンツが入ったAPサーバのEC2に対してローカルのDNSキャッシュサーバ(以下:Unbound)を持つことで、要求するBackendのIPアドレスをDNSサーバgdnsdまたはRoute53どちらに問い合わせをするかを選択できるようにしています。 これによって、あるAZのEC2からのBackendのIP要求をgdnsdからIPを返却することで同一AZのBackendサービスへ通信させることができます。
VM時の構成
AZ間の通信遅延によるSLO違反
こちらはあるページのSLOのダッシュボードの状況です。 これまでp50, p95でSLIが問題なく推移していたラインがコンテナ移行のリリースを行ったタイミングで急降下し、またエラーバジェットも一気に消費へ傾いてしまいました。
巨大なAPIでもあるので、多くのコンテンツへ波及したためSlackでは ワイワイ(おいおいー!?)と賑わっていました。
まずはVMへ切り戻しを行いSLOを再度正常値に戻すようにし、コンテナ環境の調査を行いました。 API毎にどの程度遅延してしまうかを調査を行ったところ、大きく2つのパターンのAPIで影響を受けていました。
- APIの呼び出し元プロダクトで複数回呼び出している
- API内でBackendに対して複数回呼び出しを行うAPI
前者は1-2ミリ秒などわずかな遅延のAPIでも呼び出し元の実装方法によって影響がでていることがわかりました。 例えば、1ミリ秒でも100回呼び出されると100ミリ秒の遅延となります。 それについては呼び出し元コンテンツへ状況の説明と調整を行い、呼び出し回数の見直しの改修をしていただき解消することができました。
後者は今回多くのAPIで影響を受けていたもので、APIの1リクエストあたりのレスポンスタイムがBackendへの呼び出しが少ないもので2ms程度から、多いもので50ms程度の悪化が出ていました。さらにAPIの呼び出し元の実装方法によっては大きく影響を受け、SLOの未達に繋がったわけです。
EKS on Fargateでの通信制御
実装の要点は以下3点です。
- Unboundをサイドカーコンテナとして実装する
- PodのDNS設定を変更する
- gdnsdのgdnsd-plugin-geoipを利用する(こちらの詳細は本記事では説明しません)
まずEKS on Fargateの仕様として、1node:1podとなっており、Fargateプロファイルで指定したAZのいずれかにノードが配置され、Podが起動します。 ノードについてFargateはマネージドサービスなので、EKS on EC2と違ってどのAZにノードを配置するか操作することはできません。 事前にどのAZにノードが配置されるかということがわからないわけです。
つまり
- FargateはどのAZにノードが配置されたかわからない
ただし
- 同じAZに対してのBackendのIPがほしい
という要求がある状態です。 そこで利用したのが、Unboundとgdnsdとなります。
Unboundは、オープンソースソフトウェアのDNSキャッシュサーバです。 EKS on Fargateは1node:1podという制約から、アプリケーションと同一のノードにUnboundを設置するため、サイドカーコンテナという方式が必要となります。 Unboundは公式からDockerイメージを提供していません。そのため、コンテナイメージを作成するためのDockerfileが必要となります。 参考とさせていただいたのはこちらです。 公式でもLinux環境のビルド手順が公開されています。
gdnsdも同じくオープンソースソフトウェアのDNSサーバです。 権威DNS・キャッシュDNSどちらの役割も果たすことができます。 特定のドメインの権威DNSとして機能することができ、gdnsd-plugin-geoipのアドオンを利用することでDNSへ問い合わせをしたクライアントの地理的な位置に基づいて返すべきIPを選択することができます。 いわゆる「地理的ロードバランシング」を実現するツールとなります。
Unboundは以下のようにDNS起動のconfig設定を行います。
server: verbosity: 1 do-ip6: no msg-buffer-size: 4096 rrset-roundrobin: yes minimal-responses: yes module-config: "iterator" private-address: ::/0 stub-zone: name: "hoge.monotaro.com." stub-addr: xxx.xxx.xxx.xxx # gdnsdへ問い合わせするIP forward-zone: name: "." forward-addr: yyy.yyy.yyy.yyy # クラスタのCoreDNSへのIP
問い合わせの流れはこのようなイメージです
Unbound containerをサイドカーコンテナとして実装しただけでは機能しません。
他のコンテナたちのDNS問い合わせはデフォルトではCoreDNSへ問い合わせされます。(参考:PodのDNSポリシ)
Podに通常 dnsPolicy
が指定されていない場合は、ClusterFirstがデフォルトとなります。
つまりクラスタのDNSであるCoreDNSへ問い合わせされることになります。
そのため、dnsPolicyにはNoneを設定し個別にどこのDNSへ問い合わせするのかを dnsConfig
を使って指定します。
Pod(またはDeployment->containers)のspecに記載するのは以下のような内容です
dnsPolicy: "None" dnsConfig: nameservers: - 127.0.0.1 searches: - hoge.monotaro.com
アプリケーションコンテナに入って/etc/resolv.confを確認してみます
今回はPod内に複数のコンテナがあるため、コンテナ間通信を利用します。
application container -> Unbound containerへの問い合わせには、ローカルループバックアドレスである127.0.0.1を使います。 この場合、ログや監視用アプリケーションなど、他のサイドカーコンテナも同様にUnboundへ問い合わせがされることに注意です。
Unboundのコンテナは以下のように設定します
- name: local-unbound-dns ports: - containerPort: 53 protocol: UDP - containerPort: 53 protocol: TCP volumeMounts: - name: unbound-config mountPath: /opt/unbound/etc/unbound/unbound.conf subPath: unbound.conf livenessProbe: tcpSocket: host: 127.0.0.1 port: 53 readinessProbe: tcpSocket: host: 127.0.0.1 port: 53 〜〜〜 volumes: - name: unbound-config configMap: name: unbound-configmap 〜〜〜
また、アプリケーションコンテナ側でもコンテナ起動順序によってDNSエラーになってしまうため、startupProbeでコンテナ起動時に確認をしておきます。
startupProbe: tcpSocket: host: 127.0.0.1 port: 53
以上で設定は完了です。 補足となりますが、片方のAZのBackendへ正しく通信できない状況になった場合には gdnsdがDNSのフェイルオーバーを行い、AZまたぎにはなるがもう一方のBackendに対して通信するようにもできています。
APIのレイテンシの改善
対応の結果、影響が出ていたAPIについて改善することができました。 APIの3ヶ月時系列とp50(青色), p95(紫色), p99(黄色)のグラフがこちらです。
A,Bのように改善が大幅に見えるものもあれば、Cのように変わらないものもありました。 これはAPI - Cは、Backendへ問い合わせがないものだったため変わらないことが期待されており、 サイドカー導入によって影響がないことも確認ができました。
APIのレイテンシが改善したことによって、APIを呼び出しているプロダクトのSLOも再び達成することができました。
コンテナ基盤の課題と展望
AZ間の通信遅延は、物理的に離れている関係上避けられないものです。 とはいえ1回の通信にかかる遅延はそれほど大きなものではなく、プロダクトとして許容できる範囲なのであれば特に制御を行わないほうが良いと思います。 それだけシステムの複雑性を上げることにもなり、管理運用が大変になるためです。
また注意点として、Fargateプロファイルで、利用するAZを指定しますが、 1つのFargateプロファイルで利用するすべてのAZを記載している場合、Fargateの場合はどのAZにノードが配置されるかを操作することはできません。 つまり、AZによってノードの偏りが生まれます。
例えばaz-aには5台あるが、az-cには3台といったような均等に配置されるわけではないということです。 そのため、readerは各AZに配置しているがwriterが1つといった場合には、 writerと同じAZに配置されていないPodはAZ間の通信遅延が発生します。
アプリケーションとして書き込み回数が少ないものであれば十分許容できるのではないかと思います。
現在、コンテナ化推進チームは社内のコンテナ環境を
- どうすれば楽に管理ができるのか
- どうすれば利用するアプリケーションが使いやすくできているか
を考え、改善にも注力しています。 今回の対応はVMからコンテナへの移行として対応を行いましたが、アプリケーションのPodに対して、インフラの事情で依存関係を作ってしまいました。 他の目的もありますが、1つの目的としてこの依存関係を分離するため、EKS on FargateからEKS on EC2への移行を検討しています。
今回のトラブルを通して、所属チーム・グループを超えて様々な方から協力をいただきスムーズに対応することができました。 本当に感謝しています。
モノタロウの企業理念にある「他者への敬意」「傾聴」のとおり、状況説明において他グループの方々も親身に聞いてくださり、 また組織全体として必要な対応ということで、自部門のタスクを調整して対応していただきました。
私はバックエンド開発、フロントエンド開発とアプリケーション開発でこれまでやってきたので インフラに対しての知見が足りないと実感しています。ただチームメンバーのバックグラウンドは様々で、 アプリもいればインフラもいる混合チームなのでお互いの観点で意見交換をしながらシステムを作り上げています。
当記事を通じてMonotaROに興味を持っていただいた方は、カジュアル面談もやっていますのでぜひご連絡ください。 チームの採用募集も合わせてご覧ください。