今年4月に新卒でMerpayへ入社してSREとして働いている@_k_e_k_eです。2019の22日目の記事になっています。KubeDNSについて解説していこうと思います。本記事の目的Kubernetesを扱う人・これから扱おうと思っている人がKubernetes内のDNS事情について詳しくなることです。読むのに以下の前提知識が必要です。DNSに関する基礎知識Kubernetesに関する基礎知識CoreDNSは本記事のスコープ対象外になっています。イントロ私たちがインターネットで検索をしている限りDNSへ問い合わせをしています。キャッシュされていない場合、DNS問い合わせはPCから日本のルートサーバーへ問い合わせて、そして他のDNSへ問い合わせて最終的にインターネットにおける住所であるIPアドレスを返しています。IPアドレスが分かるのでコンテンツへアクセスできています。Kubernetes内はどうやってPod間、またはKubernetesの外へアクセスをしているのでしょうか。Kubenetes外へアクセスする場合は私達がブラウザかアクセスするのと変わらないのです。PodがClusterIPをもつServiceへアクセスをするときはデフォルトではcluster.localドメインにアクセスをすることになります。Serviceの実態はiptablesであり、仮想IP(VIP)を使うことによってPodが変更されてもServiceを通してクライアントはアクセスできる仕組みなっています。iptablesはkube-proxyというコンポーネントが管理をしています。マルチテナントで運用している場合のネットワーク分割によく使われるように、論理的にネットワークはNamespaceによって分割されてそれぞれcluster.localサブドメインとして定義されます。Namespaceへアクセスをしたい場合はdst.namespace.svc.cluster.localでアクセスをします。Clusterは自動的に内部DNSを設定してサービスディスカバリのために最適なDNSを提供しています。これによってPodができたり、Serviceが追加されたりしてもアクセスしたいPodへアクセスすることができているのです。KubernetesのDNSで一般的なのはCoreDNSとkube-dnsです。今回はKubeDNSについて解説をしていきます。CoreDNSについては、またの機会に紹介できれば嬉しいです。KubeDNSしてみましょう。注意点DNSを使うのがあたかも当たり前のように書きますが、必ずしもサービスディスカバリのためにDNSへ問い合わせないといけないわけではありません。一時的なPodを作成して環境変数を出力してみます。set...NGINX_1_PORT='tcp://10.13.225.211:80'NGINX_1_PORT_80_TCP='tcp://10.13.225.211:80'NGINX_1_PORT_80_TCP_ADDR='10.43.255.211'NGINX_1_PORT_80_TCP_PORT='80'NGINX_1_PORT_80_TCP_PROTO='tcp'NGINX_1_SERVICE_HOST='10.43.255.211'NGINX_1_SERVICE_PORT='80'...このように環境変数を使ってもアクセスをすることができるのです。DNSを使うのが一般的なためDNSを使う前提で話をすすめます。Serviceの変更を検知してiptablesの設定を更新し、仮想IPとPodのIPをマッピングするためのkube-proxyは取り上げません。DNSにフォーカスした内容で、DNSが仮想IPを返すまでの話です。Pod間で通信をしてみるNginxアプリケーションをデプロイしてみる適当にindex.htmlを返すNginxのアプリケーションをdefaultNamespaceにデプロイしました。ServiceとDeploymentリソースを確認してみます。104sCluster内のネットワークだと以下のようにアクセスすることが確認できます。ここでは透過的に名前解決をするためにTelepresenseを使っています。<body></head>コンテンツが返ってきました。Telepresenceによって生成されたPodからNginxのへアクセスしています。ドメインのIPアドレスを取得しましょう。<SERVICE名>.<NAMESPACE名>.svc.cluster.localでアクセスをできることは説明しましたが、今回はそのIPアドレスを取得してみます。10.43.249.207nginx-1.default.svc.cluster.localは10.43.249.207がPodのIPアドレスであることが分かります。IPアドレスで直接指定してアクセスしてみます。<body></head>正しくレスポンスが返ってきました。このリクエストがどのような流れになっているのかをみてみましょう。ここで適当にPodを作って中に入ってみましょう。ここでは、TelepresenceではtracerouteコマンドはサポートされていないのでPodの中に入っています。ms...1ホップで解決ができています。以下のようになっていることがわかります。クラスタ内は1ホップで解決をしているまた、NginxのPod内はHostsファイルとしては以下のように/etc/hostsになっています。ip6-loopbackfe00::0ip6-localnetfe00::0ip6-mcastprefixfe00::1ip6-allnodesfe00::2ip6-allrouters10.40.5.7nginx-1-695dfff97b-8t9wfここではPodに割り当てられたIPとPod名があります。kube-dnsのリゾルバ先程はアプリケーションの問い合わせの一連の流れを簡単に見ました。どのDNSへ問い合わせて解決をしているのでしょうか。KubernetesのAPIのイベントを監視しServiceリソースかEndpointリソースが作成されるとDNSレコードをアップデートします。各Podの/etc/resov.confにDNSのIPが設定されてあります。以下のようなものになっています。ServiceのClusterIPになっていて問い合わせています。...と検索してくれることになります。DNSにはAレコードと一緒にSRVレコードにProtocol,PortなどIPアドレス以外の情報が入っています。SRVレコードはRFC2782で定義されているHostname、port、protocolを返すもので、_をプレフィックスでつけて定義します。PolicyあとでDNSの構造を紹介しますが、はじめにどのようにDNSが設定されているのかを紹介します。どのようにDNSがPodへ設定されるかは、PodのspecにdnsPolicyを書くことで設定できます。ここ追加する以下の4種類があります。DefaultPodがスケジュールされたNodeの設定を継承する形になります。何も書かなければこの設定になります。ClusterFirstwww.kubernetes.ioのようなクラスタードメインのサフィックスにマッチしないようなDNSクエリーは、Nodeから継承された上流のネームサーバーにフォワーディングされるものです。このサーバーはClusterで設定することができます。アクセスをすることになります。ClusterFirstWithHostnetホストのネットワークで動作するPodに対して明示的につける必要があります。...少し説明をします。以下のようなクラスタがあって2つのPodが実行されている赤線で囲った枠のNodeへアクセスをします。Bridgeの仕組みを知っている方はご存知かと思いますがDockeが走っているNodeにはdocker0は仮想ブリッジがあって、またcbr0というKubernetesが使うためのLinuxブリッジもあります。forever...この仮想ブリッジは仮想イーサネット(veth)が構築されており、各Podへアクセスをすることができます。2つのvethがありましたが簡略化のため2つまで載せています。1つのHostのネットワークの概略図ClusterFirstWithHostnetを指定すると以下のようになります。NoneKubernetesから設定されるDNS設定をすべて破棄します。全部自分でdnsConfigで設定を渡す必要があります。Podのspecに以下のように追加します。diveDNSはいくつかのコンポーネントで構成されているため、一つずつ見ていって理解を深めようと思います。kube-dnsを構成するServiceとDeploymentここで注意点なのですがkube-dnsからは本来はdnsmasqが最初にリクエストを受け取っていますServiceとDeploymentについてKubernetesのDNSのServiceは以下のように構成されています。8dここで注意点はPortはUDPとTCPともに53ポートで受けている点です。kube-dnsのServiceまたDNSに関わるDeploymentは以下の2つがあります。Deploymentは以下のコンテナで構成されており、私のクラスタだと2つPodのReplicaが出ていました。jsonpath="{.spec.containers[*].images}"k8s.gcr.io/k8s-dns-kube-dns-amd64:1.15.4k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.15.4k8s.gcr.io/k8s-dns-sidecar-amd64:1.15.4k8s.gcr.io/prometheus-to-sd:v0.4.2またkube-dns-autoscalerDeploymentは以下の1つのコンテナで構成されています。jsonpath="{.spec.containers[*].images}"k8s.gcr.io/cluster-proportional-autoscaler-amd64:1.3.0それではそれぞれのDeploymentについて見ていきましょう。Server本体です。以下のコンポーネントで構成されています。kube-dnsdnsmasq-nannysidecarprometheus-to-sd(GKEだけデフォルト)Containerの情報をまとめると以下のようになっています。-それぞれ順次おってみてみましょう。kube-dnskube-dns本体です。kubenetes/dnsリポジトリのdns/cmd/kube-dns以下にソースコードがあります。kube-dnsの元となっているのはSkyDNSです。TCPとUDPどちらでもクエリすることができます。実はkube-dnsとはSkyDNSとKubeDNS2つが合わさったコンポーネントになっています。そのため、kube-dns起動するとSkyDNSサーバーとKubeDNSの2つが別のGoルーチンで起動することになります。SkyDNSとKubeDNSSkyDNSSkyDNSサーバーはPort番号53でクエリを受け付けます。ヘルスチェック用のPortもあり8081になっています。DNSの設定ファイルは/kube-dns-configに格納されていますが、ConfigMapの設定を10秒ごとに同期して設定を変更させることができます。SkyDNSはメトリクスをPrometheusによってスクレイプさせることができ、そのためにPROMETHEUS_PORTで設定されるPortを公開します。TCPポートはゾーン転送機能によって使われます。これによって複数のDNSがあっても同期をすることができます。KubeDNSKubeDNSは2つのHandleと2つControllerで構成されています。(ここからDeploymentのkube-dnsとkube-dnsの構成要素であるKubeDNSの区別には注意が必要)kube-dnsのHandlerとControllerこれらのHandlerとControllerは何をしているのでしょうか。HandlerControllerControllerはいわゆるIPアドレスの変更をクラスタ単位で監視するものです。Serviceが変更されたときに呼ばれるControllerつまりすべてのNamespace以下でDeploymentを作る、Serviceを作るなどをした場合にこれらのControllerがリコンサイルされることになり、DNSの処理が行われることになります。Cacheでキャッシュをさせ、レコードを追加させます。常にInformerからのイベントを受け取っています。Controllerもそんなに変わりません。IPになっている点ぐらいです。これは単純にokとHTTPステータスコード200で返す。KubeDNSでキャッシュをしているEndpointの一覧を返すまたProfilingのためポート6060が割り当てられていますが、特になにかに使うことはないでしょう。どのようにDNSレコードが更新されるのか先程のControllerが大きな役目を果たします。先程もいいましたがKubeDNSには以下のように2つのコントローラーがあり、2つのgoroutineが実行されます。ControllerによってEndpointの追加処理をする流れは以下のようになっています。err)}}}ここのkd.addDNSUsignEndpointは以下のようになっています。svc)}これによってSkyDNSでレコードを更新するとともに、KubeDNSのTreeCacheにキャッシュされることになります。dnsmasqは軽量なDNSリゾルバでSkyDNSからのレスポンスをキャッシュしてくれます。Goで書かれていないため、バイナリをGoのプロセスで実行をするような形になっています。dnsmasqを詳しく知りたい方は公式ページへアクセスをしてください。Podは使われているため、Kubernetesを扱っている方はご存知だと思いますがSidecarとして機能を提供するものです。scalerの代表であるHeapsterだとNodeが増えるのにHeapster本体が増えないと収集する対象が多くなってしまいます。このdnsmasqがClusterIPで指定されるServiceからのトラフィックを最初に受け、キャッシュを返すためDNSサーバーへの負荷を軽減することができます。dnsmasqの設定dnsmasqを起動する際に-configDirで設定ファイルを指定することができます。GKEでKubernetesクラスタを作成したときはどのような設定になっているのでしょう。デフォルトで読み込まれるファイルは-configDir=/etc/k8s/dns/dnsmasq-nannyです。これはConfigMapkube-dns-configからマウントされています。--restartDnsmasqフラグを渡すことによって、もし設定ファイルが書き換わったときにdnsmasq自体をリスタートできるようになっています。フォワードするkube-dnsキャッシュがなければKubeDNSサーバーへアクセスをすることになります。sidecardnsmasqメトリクスをエキスポートしたり、ヘルスチェックを担っているものです。このsidecarがヘルスチェックできているとPodはヘルシー状態です。実際にトラフィックを流す部分はkube-dns本体がレスポンスを返せているかになります。メトリクスはdnsmasqからメトリクスを取得してメトリクスをExportして、Prometheusへ格納をしています。2つのEndpointも持っていますが、自分のアプリケーションから呼び出すことはしないでしょう。time.Now())})これはPrometheusのスクレイプ用とServiceからのヘルスチェックのためにあります。prometheus-to-sdPrometheusのメトリクスをStackdriverへアップロードするこのコンテナはGKEだけのデフォルトで入っているContainerなので他のクラウドプロバイダを使っている場合はない可能性があります。自前でインストールすることもできます。このContainerは何をしているか知らなかったのですが、どうやらGoogleCloudPlatform/k8s-stackdriverらしいです。Prometheusに格納されたメトリクスをStackdriverへPushしてくれます。StackdriverはAWSでも使うことができるのでGCPに限った話ではありません。このコンテナを入れることも可能です。メトリクスは以下の4つです。Loggingでもこれらのメトリクスは検索することができます。dnsmasqのキャッシュミスのログ(Legacy)もちろんこのようなログ・メトリクスがあがるためモニタリングなどにも使えます。kube-dns-autoscaler先程までで、KubernetesのDNS本体について解説しました。DNSに関連するDeploymentとしてkube-dns-autoscalerがあるので紹介します。これはkube-dnsをautoscaleするためのものです。Incubator段階ではあるのですがスケジュール可能なNodeとCore数を監視して対象とするReplica数を増減してくれるものです。このようなものがない場合、Nodeが増えるとDNSへ非常に高い負荷がかかります。HPAなどの特定のPodのリソースではなく、クラスタ全体のリソースをもとに定義します。APIを通してReplica数を調整します。この仕組はDNSだけでなくKubernetesのシステムに関わるもので他にも使われたりします。計算式の仕組みメトリクス(Node数・Core数)はPodからAPIサーバーへポーリングをして取得しています。「どのようにスケールさせるか」はConfigMapによって定義をされていえ、ポーリングの間隔のたびに更新されます。試しに見てみましょう。<none>これはいま時点の理想状態が記述されており、nodePerReplicaだけでみると1つのDNSのPodに対して16個のNodeまでが望ましいとされていることが分かります。LinearとLadderこのAutoscalerは2つのモードをサポートしています。Linearモードは以下のように計算されます。min)Ladderは階段みたいに、「このときはこんだけのDNSレプリカ数にする」などを定義することができます。そのように設定をした場合は、以下のようにConfigMapの出力はなります。}グラフにすると以下のようになっています。簡略化のためNode数に対してどのような設定にするのかを表示しています。単一障害点回避モードpreventSinglePointFailureをtrueにすると、いくらClusterが小さくても2つ以上のPodが存在することになり、単一障害点を避けることができます。パフォーマンスチューニングの話ここまでで仕組みは大方分かったのではないかと思います。次にどのようにご自身のKubernetesクラスタのDNSをパフォーマンス・チューニングすればいいのかを紹介します。Applicationの話アプリケーションレベルでDNSキャッシュを入れるのも一つの手です。インターネットからはHTTP/1.1でアクセスをされ、内部ではgRPCを使うケースが多いため以下のようにgRPCの例を示す。grpcHost,grpc.WithInsecure(),grpc.WithBalancer(balancer))naming.NewDNSResolverWithFreqでは定期的にPodのIPを取得しているが、その間は直接PodのIPを指定することによってアクセスを可能となる。time.Secondの箇所である「更新をチェックする頻度」をあげるとDNSへの負荷は高くなるものの変更が反映されやすくなります。トレードオフだがアプリケーション特性に合わせて考慮に入れるのもいいでしょう。Serviceであることが必要です。A(クライアント)があったとします。DNS問い合わせをするのですが/etc/resolv.confの中に以下のように設定値が書かれていることがありました。ndots:5以下の項目で負荷を減らすことが可能です。FQDNで問い合わせるFQDNを使うことによって名前解決のレイテンシを減らすことが可能になります。以下のように点線の時刻よりレイテンシの改善が見られます。6)このようにDNSへ問い合わせを行う回数を減らすことによってレイテンシを改善することができ、またDNSへの負荷の削減にも繋がります。最後に.をつけるだけでFQDNとして扱われます。Optionsをへらすこれらの値はdnsPolicyでPodのSpecで変更を加えることができますがoptionsなどを減らすとDNS問い合わせの回数が減ります。ホスト名によって問い合わせた場合はgetaddrinfo()などのシステムコールによってDNS解決が実行されます。その回数を減らすことができます。これはsearchドメインを保管してくれるもので、例えばデフォルト値である5にすると5以下の.を含む場合はsearchドメインによって補完をしてくれます。先程の場合でpingをすると以下のようになります。kekeすると以下ように問い合わせをしてくれます。keke.namespace.svc.cluster.localkeke.svc.cluster.localkeke.cluster.localです。どれかがレコードを返すとアクセスをできるようになります。ndotを減らし補完の回数(=問い合わせの回数)を減らすことができます。Serviceドメインを絞るndotsを減らすことのほかにも、serviceドメインを不用意に増やさないことも重要でしょう。namespace.svc.cluster.localだけでも十分かもしれず、問い合わせの回数を減らすことができます。Autoscalerを調整する先程も書きましたがAutoscaler(kube-dns-autoscaler)はデフォルトではlinearモードNode数・Core数でDNSのレプリカ数を調整するの要点があります。DNSの負荷が高いなどのことがあればkube-dns-autoscalerのConfigMapを変更してもいいかもしれません。同期頻度を下げるdnsmasqは設定が変更されると再起動をする仕組みになっていますが、そう簡単に変更されることはないと思います。APIから変更を(デフォルトでは)10秒ごとにポーリングしているため、変更の頻度を下げてもいいかもしれません。そのようなときはdnsmasqを起動する際にsyncIntervalフラグで渡してあげるといいです。TTLを調整する例えばSpinnakerではRed/Blackデプロイメント手法があり、ServiceによるL4のロードバランシングのためPodを名前解決することができなくなることはないです。そのため、TTLの値を上げても問題になることは少ないかなと思います。dnsmasq.confの中で以下のようにTTLを設定することができます。デフォルトでは5分になっています。min-cache-ttl=600ここまでは簡単に設定できるDNSのパフォーマンス・チューニングになっています。DNSを構成するコンポーネントやDNSレコードが更新される仕組みを解説することができました。大まかにDNSの挙動や、どのようなものであるかという実態についてはご理解をいただけたのではないかと思います。まとめ本記事は以下の要点で解説をしてきました。DNSの構成、それぞれのコンポーネントの役割KubernetesがどのようにDNSレコードを更新しているのかの仕組みKubernetesを使っている限りDNSに問題が発生する可能性もあります。負荷が高すぎて問い合わせのときのレイテンシがひどいときなどです。そのようなときにパフォーマンス・チューニングなどいろんなときに知っていると重宝します。kube-dns-autoscalerを見直してもいいかもしれません。dnsmasqのキャッシュの設定を見直してもいいかもしれません。Operatorを開発するときも必要になる知識で、DNSに限らずKubernetesのコンポーネントを理解するためにも必要になります。Kubernetesのこと以上に、基本的なコンピュータサイエンスの知識(Linux・ネットワークについて)が何より必要です。Kubernetesは何もわからないしがないSREですが、これからもっと頑張ろうと思いました。最後までありがとうございました。応用的なものまで、目を通したものを紹介します。どのようにすればKuberenetes上でのCoreDNSをスケールさせることができるか解説されてある。KeisukeYamashita。