AWS EKSのCoreDNSにおいて、名前解決が失敗する事例とその解決法について

はじめに

どうもrenjikariです。SREです。

AWS Elastic Kontainer Service(以下EKS)環境にて起きた事象の対策が結構たいへんだったので、対応とともに共有しておきます。 もっといいやり方あるぜ!って人はぜひ教えて下さい:bow:

今回の事象には原因が2つあり、それぞれに解決策を用意したため、記事でも原因その① => 解決策その① => 原因その② => 解決策その②という順番で書いていきます

TL;DR

  • EKSにおいてある程度以上のDNS通信がある中で、CoreDNSがshutdownする際に一部のpodで一定時間名前解決ができなくなる
  • 解決策① CoreDNSのhealthcheck pluginにlameduckを導入
  • 解決策② AWS AutoscalingGroupのLifecyleHookで安全に落とすためにNode-drainerを導入

名前解決が一部失敗する話

  • 数ヶ月前、EKS上で動かしているとあるサービスの名前解決が一部失敗していることに気づきました。
  • hpaなどによってpodが増減し、CoreDNSの乗っているNodeがshutdownされるときに名前解決が失敗していることに気づきました。
  • 補足するとCoreDNSはEKSを利用するときのデフォルトで起動しているDNS Cacheサーバ(という言い方であっているか…?)であり、今回の話でもこのCoreDNSを利用しているときに起きたものになります。

CoreDNSのhealth checkと終了時の挙動(一部推測が含まれる)

そもそものpodの終了時についてはこちらの記事がまじでわかりやすい。神。なので省略 https://qiita.com/superbrothers/items/3ac78daba3560ea406b2

CoreDNSはservice + deploymentによって宣言されているリソースで、podの終了時には以下の1及び2が順序制御されずに行われる。

  1. podにshutdown命令がだされる
  2. serviceのendpointの削除命令がだされる

つまり、"serviceのendpointの削除が行われる前"にpodにたどりついたdns要求には(podがshutdown中なので)CoreDNSから応答がないということになる。 このケースを図に書くとこうだ(図とは)

CoreDNS Podにshutdown命令が出される&serviceのendpoint削除命令が出される
↓
CoreDNS Podはshutdown命令により、新規のリクエストからは応答しないようになり、shutdownの準備に入る。
↓
タイミングの問題でServiceのendpointはまだ削除されず、(shutdown中の)CoreDNSに対して新規のリクエストが発行される
↓
CoreDNSは新規のリクエストを受け付けない状態なので応答しない(あるいはshutdownされていてrouting不可)

原因その① 調査編

再現検証

理屈では前述のとおり発生しうることですが、これは本当にどんなclusterでも発生しうるのか、今回起きたclusterでインフラorアプリ特有のなにか踏んでしまっているのかを切り分ける必要があったので新しく検証用clusterを作って、検証を行いました。 結構たくさん検証して紆余曲折したので、あとから振り返って意味のあった検証をピックアップしてこちらに残します。

検証環境について

  • AWS EKS v1.16を使用(ちなみにのちにv1.14でもやって発生した)
  • CoreDNSのversionは1.6.6
  • 以下のmanifestを使用
ore-dns-verification ❯ cat deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dig-deployment
spec:
  replicas: ${そのときに応じて変えた}
  selector:
    matchLabels:
      app: dig-pod
  template:
    metadata:
      labels:
        app: dig-pod

    spec:
      containers:
        - name: dig-pod
          image: giantswarm/tiny-tools
          args:
          - /bin/sh
          - -c
          - while true; do dig freee.co.jp; sleep 300; done;
          livenessProbe:
            exec:
              command:
              - /bin/sh
              - -c
              - dig +time=1 +tries=1
            initialDelaySeconds: 3
            periodSeconds: 1
            failureThreshold: 1
          resources:
            limits:
              cpu: "200m"
            requests:
              cpu: "150m"
  • livenessで名前解決をしているので、名前解決に失敗するとrestartしてわかるという仕組み
  • EKS上にたくさんの名前解決をするpodを並べて、CoreDNSやCoreDNS Podの乗っているNodeを落として、再現検証を行いました。
  • dig Podの数をふやしたり、1Podが行う名前解決数を増やしたりして挙動を確認しました。

結果

  • CoreDNSのpod数やCluster全体のPod数よりもCluster全体のDNSリクエスト数に応じて、この現象が起こるようでした。
  • DNSリクエストが 1k req/secを超えたあたりからCoreDNSやCoreDNSの載っているNodeを落とすと名前解決が失敗するようです。
  • CoreDNSのPod数が2でも10でも発生はしていたので、今回の事象にPod数は関係はない気はしています。

原因その① 対策編

対策方針

  • 方針①
    • preStopでsleep(あるいは何らかの操作を)させる
  • 方針②
    • CoreDNSのhealth pluginに対してlameduckオプションを導入することで終了時にsleepさせる
  • 方針③
    • 各PodのSidecarにDNS Cacheを用意する

これらの対策を考えました。理想的には方針③でいつかはこういう方式にしたいと思ってはいるのですが、 非常にコストがかかるので、一旦諦めました。(というか、方針③を選んだとしても結局今回の対応は安全な終了のために行うべき)方針①と②は大変さも同じくらいなのでどっちでもいいんですが、 以下のようなissueにもlameduck使えばよいというような話も書いてあり、せっかくCoreDNS側で用意されている機能なので方針②を選びました。 https://github.com/coredns/coredns/issues/2984

corefileはこんな感じ。検証では10sでも名前解決が失敗することはありませんでしが(5sでは頻度は激減するもののたまに失敗する)、さらに念の為15sで運用することにしました

 .:53 {
        health {
            lameduck 10s
        }
    (関係ないので略)
    }

原因その②

今回の根本解決のためには解決策その①だけでは不十分でした。 ここでは解決策その①をしてなお失敗する事例を紹介します。

どんなときに名前解決が失敗するか

  • ClusterAutoscaler(以下CA. https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler)AWS AutoScalingGroup(以下ASG)の相性が良くないようで、CAがnodeをへらすときにASGがdesiredをフラッピングさせてしまうことがあるようです。
  • CAが正常に(CorednsPodの乗っている)Nodeをdrainしていれば解決策その①によって、CoreDNS Podは終了時にsleepし安全に落ちます。
  • しかし、上記のようにASGのdesiredのフラッピングによって、ASGがInstanceを落とす際にはdrainなどをせず突然落とします。そうするとlameduckを設定していても同様に名前解決が失敗する事象が発生します。

原因その② 対策編

  • ASGがInstanceを落とすときにlifecycle hookによって、安全に落としてあげれば良さそうです。
  • 今回はlifecycle hookの追加とそれをhookしてNodeをDrainするnode-drainerというものを作りました。

やり方

このnode-drainerは思想自体はもとともあったようで、実装もいくつか見かけましたが、そのままK8s上にデプロイできるものが見つからなかったのでマニフェスト等は自分で用意しつつ、 実装はkube-awsのやり方を参考に(というかほぼコピペして)使わせて頂いています。

参考実装の例4つと採用したやりかた

解説

  • ① ASGがInstanceをTerminateしようとする
  • ② Lifecycle Hookに以下の設定を入れておくことで、Status がTermination:waitになる
Name: nodedrainer
Lifecycle Transition: EC2_INSTANCE_TERMINATING
Default Result: CONTINUE
Heartbeat Timeout: 3600
  • K8s上にいるDaemonset node-drainerが定期的に自分のstatusをmetadata経由で監視していて、termination:waitになったら、自分自身に対してdrainをかける
  • ④ とても安全

まとめ

  • はじめにK8s上で名前解決失敗しだしたときは何事かと思いました。
  • いろんな原因とそれに対する複数の解決策を考えて検証してってやっててなんか楽しかったです。
  • 今回の話は全編に渡って、僕一人ではなく、チームで原因調査、検証、実装を行っています。