こんにちは、モノタロウの SRE グループ・コンテナ化推進チームの田中です。
現在、私たちはシステムモダナイゼーションのプロジェクトの一環として、200以上のエンドポイントを持つモノリスのバックエンド API を EC2 上から Kubernetes マネージドサービスの EKS(Elastic Kubernetes Service)に移行しています。ノードは Fargate を使用し、監視には Datadog と Sentry を導入しています。
今回、EC2 に流れているリクエストを全て EKS に振り分けを行おうとしておりました。その際に外部(DB、 サービス)への疎通ができないといった内容の Sentry のエラーが大量に発生し、切り戻しをせざるを得ない状況に陥ったのです。エラー内容を詳しくみたところ名前解決に関するものであり、今回私たちは CoreDNS の設定を行うことで解決しました。
このブログではその根本的な解決策を2点ご紹介するとともに、「推測するな計測せよ」の重要性についても述べていきます。
背景
今回の移行対象のインフラ周りの概略図を図1に示します。
EC2 から EKS への移行では、ALB を使用してエンドポイントごとにリクエストの比率を変更しながら進めていました。そのため、一部のエンドポイントのみを切り替えた後、残りのエンドポイントを全て切り替えようとした際に、多数のエラーが発生してしまいました。
起きた問題
残りのエンドポイントのリクエスト割合をEKS版に1%から25%に増やした際に、DNS の名前解決に関するエラーが大量に発生したことを Sentry で確認しました。エラー内容は以下の通りです。(API は Python で書かれています)
Proxy 接続エラー
NewConnectionError('<urllib3.connection.HTTPSConnection object at 12345678>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')
DB 接続エラー
OperationalError:(2005, "Unknown MySQL server host 'db.example.com' (-3)")
名前解決は Kubernetes 環境において CoreDNS リソースによって行われていることがわかっていたため、原因は CoreDNS にあるのではないかと真っ先に疑いました。一旦エラー終息のためリクエストの割合を VM版に戻した後に、CoreDNS の CPU とメモリ使用量を確認しました。その結果、CoreDNS の Pod 設定で CPU の requests は 100mになっているにもかかわらず、実際にはその制限を超えた負荷がかかっていたことが分かり、CoreDNS の高負荷が原因であることがすぐに判明しました。
更に調べてみると、CoreDNS の Pod リソース設定は以下になっており、CPU 指定量が少ないために Fargate で立ち上げると 0.25vCPUのマシンにプロビジョニングされていました。
apiVersion: v1 kind: Pod ... spec: containers: - name: coredns resources: limits: memory: 170Mi requests: cpu: 100m memory: 70Mi
また、Deployment 設定で replicas: 2 と設定されており、Pod は合計 2台しか稼働しない状態でした。
切り替えの時間帯は、アクセスが少ない時に行い、リクエスト量はピーク時でも 2000〜3000RPS、切り替え時には約1000RPS程度でした。
原因調査
今回、原因調査を行う前に、私たちは暫定対処として HPA (Horizontal Pod Autoscaler) を用いて CoreDNS の Pod をオートスケールさせました。この方法の良いところは、既存の CoreDNS の設定を変更せずに HPA リソースを作成するのみで対応可能という点です。この設定を入れることで、VM版から EKS版への移行は無事に完了できましたが、2点課題がありました。
CoreDNS の高負荷の根本原因を突き止めないとコストが増加する
CoreDNS のスケールイン時に、時折名前解決のエラーが発生する
私が所属する SRE グループは、数年ほど前から可用性の測定方法が不十分であったため、障害発生時の迅速な対応が困難であったという課題がありました。その課題を解決するために、 SRE を導入することで SLO を定義し可用性の定量的な測定を可能にし、「推測するな計測せよ」を掲げて活動してきました。そういった経緯もあり、私たちはこの課題を解決するために、まずはデータの収集から行いました。
SRE 導入の詳細に関しては以下の記事にて紹介しております。
CoreDNS の高負荷の根本原因を突き止める
この課題を解決しない限り、オートスケールによって高いランニングコストがかかり続けてしまいます。まずは本番の CoreDNS のログを確認しました。以下は実際のログです。(一部マスクしてあります)
[INFO] 10.XX.XX.XX:55805 - 47442 "A IN example.com.default.svc.cluster.local. udp 67 false 512" NXDOMAIN qr,aa,rd 160 0.00006798s [INFO] 10.XX.XX.XX:55805 - 57431 "AAAA IN example.com.default.svc.cluster.local. udp 67 false 512" NXDOMAIN qr,aa,rd 160 0.000043203s [INFO] 10.XX.XX.XX:53282 - 42558 "AAAA IN example.com.svc.cluster.local. udp 62 false 512" NXDOMAIN qr,aa,rd 155 0.000056169s [INFO] 10.XX.XX.XX:53282 - 28219 "A IN example.com.svc.cluster.local. udp 62 false 512" NXDOMAIN qr,aa,rd 155 0.000070776s [INFO] 10.XX.XX.XX:51519 - 10742 "AAAA IN example.com.cluster.local. udp 58 false 512" NXDOMAIN qr,aa,rd 151 0.000143229s [INFO] 10.XX.XX.XX:51519 - 44792 "A IN example.com.cluster.local. udp 58 false 512" NXDOMAIN qr,aa,rd 151 0.00019187s [INFO] 10.XX.XX.XX:49881 - 4361 "AAAA IN example.com. udp 44 false 512" NOERROR qr,aa,rd,ra 387 0.000064401s [INFO] 10.XX.XX.XX:49881 - 29966 "A IN example.com. udp 44 false 512" NOERROR qr,aa,rd,ra 340 0.000034223s
ネットワークにはあまり詳しくなかったので、調べながらこのログを解析していきました。その結果、NXDOMAIN となっているものはその名前解決先のドメインがない場合に返されるものだと分かりました。また、NXDOMAIN が何回か出たのちに NOERROR で名前解決できていることから、名前解決するために余計なコストをかけていることにも気付きました。
名前解決方法の設定は Pod 内の /etc/resolv.conf に書かれているとのことで、その設定を見にいきました。以下は実際の /etc/resolv.conf です。(一部変更しております)
search default.svc.cluster.local svc.cluster.local cluster.local nameserver 172.20.0.10 options ndots:5
ここで注目していただきたいのが、options ndots: 5
という部分です。この ndots は与えられたドメインが FQDN (Fully Qualified Domain Name: 完全に指定されたドメイン名) でない(末尾にドット【.】がない)場合に、ドットの数が 5未満であれば search に指定されているドメインを末尾に追加して名前解決を行うというものです。
分かりにくいので、いくつか例を出してどのように名前解決が行われるのか説明します。
(例1)example.com.
を名前解決する場合
- 末尾にドット【.】が付いているため、
example.com.
を FQDN としてみなす example.com.
を CoreDNS に問い合わせる
(例2)hoge.fuga.piyo.hogera.example.com
を名前解決する場合
- 末尾にドット【.】がないため FQDN ではない
- ドットの数が 5 であり ndots で指定された 5 未満ではないため、
hoge.fuga.piyo.hogera.example.com
で問い合わせを行う
(例3)example.com
を名前解決する場合
- 末尾にドット【.】がないため FQDN ではない
- ドットの数が 1 であり ndots で指定された 5 未満であるため、search ドメインを末尾に追加して DNS 問い合わせを行う
example.com.default.svc.cluster.local
で DNS に問い合わせ ⇒ 見つからないので次へexample.com.svc.cluster.local
で DNS に問い合わせ ⇒ 見つからないので次へexample.com.cluster.local
で DNS に問い合わせ ⇒ 見つからないので次へ
- search ドメイン全て見つからない場合は最後に
example.com
で問い合わせを行う
例3 を見ていただいたら分かる通り、まさしく今回起きていたことと同じです。ndots の設定が 5 になっているために、本来問い合わせを行ってほしいドメインではないもので何回も名前解決をしてしまうのです。
これで、1つ目の課題である CoreDNS の高負荷の原因が分かりました。
余談ですが、ndots のデフォルト設定が 5 になっているのは、SRV レコードを使っても意図した名前解決を行えるようにしているためです。例えば、_$port._$proto.$service.$namespace.svc
という SRV レコードを指定した際に、本来は _$port._$proto.$service.$namespace.svc.cluster.local
で名前解決して欲しいはずです。ところが、ndots: 4 だと最後に cluster.local
を付けずに名前解決して NXDOMAIN になり名前解決に失敗します。詳しくはこちらの Github issue をご覧ください。
また、ログ調査と並行して CoreDNS の設定を確認したところ、キャッシュ TTL が短いことにも気が付きました。明示的に設定はしていなかったのでデフォルトの 5 秒になっており、CoreDNS の先にある DNS のキャッシュ TTL より短かったので過剰な設定となっておりました。
CoreDNS のスケールイン時に、時折名前解決のエラーが発生する
2つ目の課題である、CoreDNS のスケールイン時に時折名前解決のエラーが発生する件に関しては、Kubernetes でよくある Pod 終了時の問題だと気付きました。Kubernetes の場合アプリケーションが graceful shundown をする設定であっても、Pod 終了時に 処理を打ち切られてしまうことがあるので要注意です。対策としては、Pod 終了時に遅延処理を入れる方法があります。詳しい設定方法は以下のブログに記載がありますので参考にしてください。
恒久対応
CoreDNSの負荷軽減
CoreDNS の負荷軽減に関しては、ndots とキャッシュに対してそれぞれ対応を行いました。
まず ndots の件ですが、アプリケーションの deployment リソースに以下の設定を追記しました。
apiVersion: apps/v1 kind: Deployment ~~~~~ spec: template: spec: dnsConfig: options: - name: ndots value: "1"
この設定を追加することで、Pod の /etc/resolv.conf の ndots の設定が 1 になり、常に FQDN として名前解決を行うようになります。
ndots: 1 に変更した際の注意点としては、クラスター内へのドメイン名も全て FQDN として指定する必要があり、移植性が低くなります。例えば、アプリの設定で他の namespace の service へのドメインを FQDN で書いている場合、その service の namespace が変わるたびにアプリの設定も変更する必要が出てきます。
次にキャッシュに関しては、 CoreDNS の公式サイトで調べたところ、設定について書いている部分がありましたのでその部分を参考にしました。また、Kubernetes 上での CoreDNS の設定は configMap リソースで提供されており、その configMap を以下のように変更しました。
# kubectl get cm/coredns -nkube-system -oyaml apiVersion: v1 kind: ConfigMap ~~~~~ data: Corefile: | .:53 { ... cache 600 ... }
kubectl edit で内容を変更した後ですが、CoreDNS Pod の再起動は必要ありません。アプリケーション内部に Corefile のハッシュ値を持っており、変更が加わると以下のように自動的にリロードするようになっていました。
[INFO] plugin/reload: Running configuration SHA512 = f8b77546... # ------- configMap を変更 ------- [INFO] Reloading [INFO] plugin/health: Going into lameduck mode for 30s [INFO] plugin/reload: Running configuration SHA512 = f0584eee... [INFO] Reloading complete
※ configMap によりマウントされた Corefile のハッシュが f8b77546…
から f0584eee…
に変わったことを検知してサービスがリロードされている
CoreDNS のスケールイン時の遅延処理
Pod 終了時の遅延設定に関しては、preStop で sleep の処理を入れることが一般的です。しかしながら、CoreDNS の Pod は Google が提供している distroless イメージを使用しているため、shell を含んでおらず sleep コマンドが使えません。
sleep コマンドが使えないので他の方法を探したところ、CoreDNS にはヘルスチェックのエンドポイントが提供されており、その中に遅延処理が設定できる lameduck
というオプションがありました。この lameduck
を用いると、その指定された時間の間はヘルスチェックが 200 のステータスコードを返答するようになります。そうすることで、指定した時間は Pod が終了されずに、残った処理を続けることができるようになります。
以下が実際に lameduck
を使用した遅延処理設定になります。
# kubectl get cm/coredns -nkube-system -oyaml apiVersion: v1 kind: ConfigMap ~~~~~ data: Corefile: | .:53 { ... health { lameduck 30s } ... }
まとめ
今回は、Kubernetes 環境への大量のリクエストを流す際に CoreDNS 起因でエラーになりえることと、その解決策をご紹介しました。
また、そもそも今回のトラブルを事前に防ぐためには私たちは何をすべきだったのかという視点に立った際に、以下2点の課題が見つかりました。
- 本番環境とステージング環境の環境差異が大きい
- ネットワーク構成が違うので ndots が環境によって異なる
- ステージング環境では DB のスペックとして高負荷の試験が行えない
- 自身で管理していない部分のリソースに関する知見習得が足りていなかった
1に関しては、アプリケーションリリースの際にも稀に環境差異によって開発・ステージング環境のテストをすり抜けて本番エラーになることもあるので、コストとのバランスを見つつ要検討です。
2に関しては、Kubernetes 環境へ移行した他社事例を調べることで、ある程度どういったことが起きるか予測が立てられたと思います。事前のリサーチ不足でした。
最後に、マネージドサービスであるので問題は起きないだろうという勝手な「推測」をしていたために、事前の事例調査や負荷試験・監視などの「計測」することを怠っており今回のトラブルが起きました。私たちは改めて「推測するな計測せよ」の大切さに気付くことができました。
このように SRE グループでは API のコンテナ化をはじめ、SRE の導入・実践や SLO ベースの運用を行っておりますので、興味を持った方はぜひカジュアルMTGにご応募ください。お待ちしてます!