Deep dive into KubeDNS

KubeDNSを深く知るための解説記事

December 21, 2019
kuberenetes dns

こんにちは。今年4月に新卒でMerpayへ入社してSREとして働いている@_k_e_k_eです。
本記事はKubernetes Advent Calender 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サブドメインとして定義されます。つまりapp Namespaceから dst Namespaceへアクセスをしたい場合はdst.namespace.svc.cluster.localでアクセスをします。

Kubernetes Clusterは自動的に内部DNSを設定してサービスディスカバリのために最適なDNSを提供しています。これによってPodができたり、Serviceが追加されたりしてもアクセスしたいPodへアクセスすることができているのです。

KubernetesのDNSで一般的なのはCoreDNSとkube-dnsです。今回はKubeDNSについて解説をしていきます。CoreDNSについては、またの機会に紹介できれば嬉しいです。

それではDeep Dive in 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にデプロイしました。ServiceDeploymentリソースを確認してみます。

$ kubectl get svc,deploy
NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/nginx-1      ClusterIP   10.43.249.207   <none>        80/TCP    104s

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/nginx-1   1/1     1            1           104s

Cluster内のネットワークだと以下のようにアクセスすることが確認できます。ここでは透過的に名前解決をするためにTelepresenseを使っています。

$ telepresence --new-deployment tele-tmp-keke --run-shell
tele-tmp-keke$ curl nginx-1.default.svc.cluster.local
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Cluster Ingress Index</title>
    <link rel="stylesheet" href="main.css">
  </head>
  <body>
    <p>Hi I am nginx</p>
  <body>
</head>

コンテンツが返ってきました。Telepresenceによって生成されたPodからNginxのへアクセスしています。そして、ドメインのIPアドレスを取得しましょう。<SERVICE名>.<NAMESPACE名>.svc.cluster.localでアクセスをできることは説明しましたが、今回はそのIPアドレスを取得してみます。

tele-tmp-keke$ nslookup nginx-1.default.svc.cluster.local

Non-authoritative answer:
Name:	nginx-1.default.svc.cluster.local
Address: 10.43.249.207

nginx-1.default.svc.cluster.local10.43.249.207がPodのIPアドレスであることが分かります。IPアドレスで直接指定してアクセスしてみます。

tele-tmp-keke$ curl 10.43.249.207

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Cluster Ingress Index</title>
    <link rel="stylesheet" href="main.css">
  </head>
  <body>
    <p>Hi I am nginx</p>
  <body>
</head>

正しくレスポンスが返ってきました。

このリクエストがどのような流れになっているのかをみてみましょう。ここで適当にPodを作って中に入ってみましょう。ここでは、TelepresenceではtracerouteコマンドはサポートされていないのでPodの中に入っています。

$ kubectl exec nginx-1-xxxx -it /bin/sh
> traceroute nginx-1.default.svc.cluster.local
1  10.40.5.1 (10.40.5.1)  0.031 ms  0.011 ms  0.047 ms
...

1ホップで解決ができています。つまり以下のようになっていることがわかります。

クラスタ内は1ホップで解決をしている

また、NginxのPod内はHostsファイルとしては以下のように/etc/hostsになっています。

$ cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
fe00::0	ip6-mcastprefix
fe00::1	ip6-allnodes
fe00::2	ip6-allrouters
10.40.5.7	nginx-1-695dfff97b-8t9wf

ここではPodに割り当てられたIPとPod名があります。

1. kube-dnsのリゾルバ

先程はアプリケーションの問い合わせの一連の流れを簡単に見ました。

どのDNSへ問い合わせて解決をしているのでしょうか。

KubernetesのAPIのイベントを監視しServiceリソースかEndpointリソースが作成されるとDNSレコードをアップデートします。 そして各Podの/etc/resov.confにDNSのIPが設定されてあります。以下のようなものになっています。

nameserver 10.32.0.10
search namespace.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

nameserverはkube-dnsのCluster IPになっています。

ここのnameserverkube-dns ServiceのClusterIPになっていて問い合わせています。

つまりkekeと問い合わせたときは

  • keke.namespace.svc.cluster.local
  • keke.svc.cluster.local

と検索してくれることになります。

またDNSにはAレコードと一緒にSRVレコードにProtocol,PortなどIPアドレス以外の情報が入っています。 SRVレコードはRFC2782で定義されているHostname、port、protocolを返すもので、_をプレフィックスでつけて定義します。

DNS問い合わせのパターン Pod’s DNS Policy

あとでDNSの構造を紹介しますが、はじめにどのようにDNSが設定されているのかを紹介します。

どのようにDNSがPodへ設定されるかは、PodのspecdnsPolicyを書くことで設定できます。

apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: default
spec:
  containers:
  - image: busybox:1.28
    command:
      - sleep
      - "3600"
    imagePullPolicy: IfNotPresent
    name: busybox
  restartPolicy: Always
  hostNetwork: true
  dnsPolicy: ClusterFirstWithHostNet  ← ここ追加する

以下の4種類があります。

1. Default

PodがスケジュールされたNodeの設定を継承する形になります。 何も書かなければこの設定になります。

デフォルトではNodeの設定を継承する

2. ClusterFirst

www.kubernetes.ioのようなクラスタードメインのサフィックスにマッチしないようなDNSクエリーは、Nodeから継承された上流のネームサーバーにフォワーディングされるものです。 このサーバーはClusterで設定することができます。

簡単にいうと、どんなDNSへの問い合わせもkube-dns podへ向けられ、必然的にdnsmasqskydns へアクセスをすることになります。

3. ClusterFirstWithHostnet

ホストのネットワークで動作するPodに対して明示的につける必要があります。以下のようにします。

apiVersion: v1
kind: Pod
...
spec:
  hostNetwork: true ← ここ追加
  containers:
  ...

少し説明をします。以下のようなクラスタがあって2つのPodが実行されている赤線で囲った枠のNodeへアクセスをします。

1つのNodeへsshする

Docker Bridgeの仕組みを知っている方はご存知かと思いますがDockeが走っているNodeにはdocker0は仮想ブリッジがあって、またcbr0というKubernetesが使うためのLinuxブリッジもあります。

$ ip a
...
4: cbr0: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1460 qdisc htb state UP group default qlen 1000
    link/ether 3a:6a:73:c4:62:84 brd ff:ff:ff:ff:ff:ff
    inet 10.40.0.1/24 scope global cbr0
       valid_lft forever preferred_lft forever
    inet6 fe80::386a:73ff:fec4:6284/64 scope link
       valid_lft forever preferred_lft forever
...

この仮想ブリッジは仮想イーサネット(veth)が構築されており、各Podへアクセスをすることができます。また2つのvethがありましたが簡略化のため2つまで載せています。

1つのHostのネットワークの概略図

ClusterFirstWithHostnetを指定すると以下のようになります。

4. None

Kubernetesから設定されるDNS設定をすべて破棄します。つまり、全部自分でdnsConfigで設定を渡す必要があります。例えばPodのspecに以下のように追加します。

dnsPolicy: "None"
  dnsConfig:
    nameservers:
      - 1.2.3.4
    searches:
      - ns1.svc.cluster-domain.example
      - my.dns.search.suffix
    options:
      - name: ndots
        value: "2"
      - name: edns0

DNSを構成しているコンポーネント Deep dive

DNSはいくつかのコンポーネントで構成されているため、一つずつ見ていって理解を深めようと思います。

kube-dnsを構成するServiceとDeployment

ここで注意点なのですがkube-dnsからは本来はdnsmasqが最初にリクエストを受け取っています

ServiceとDeploymentについて

KubernetesのDNSのServiceは以下のように構成されています。

$ kubectl get services kube-dns
NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
kube-dns               ClusterIP   10.47.240.10    <none>        53/UDP,53/TCP   8d

ここで注意点はPortはUDPとTCPともに53ポートで受けている点です。

kube-dnsのService

またDNSに関わるDeploymentは以下の2つがあります。

  • kube-dns
  • kube-dns-autoscaler

kube-dns Deploymentは以下のコンテナで構成されており、私のクラスタだと2つPodのReplicaが出ていました。

$ kubectl get pods -o jsonpath="{.spec.containers[*].images}"
k8s.gcr.io/k8s-dns-kube-dns-amd64:1.15.4
k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.15.4
k8s.gcr.io/k8s-dns-sidecar-amd64:1.15.4
k8s.gcr.io/prometheus-to-sd:v0.4.2

またkube-dns-autoscalerDeploymentは以下の1つのコンテナで構成されています。

$ kubectl get pods -o jsonpath="{.spec.containers[*].images}"
k8s.gcr.io/cluster-proportional-autoscaler-amd64:1.3.0

それではそれぞれのDeploymentについて見ていきましょう。

kube-dns: DNS Server本体

kube-dnsは名前の通り、DNS Server本体です。

以下のコンポーネントで構成されています。

  1. kube-dns
  2. dnsmasq-nanny
  3. sidecar
  4. prometheus-to-sd(GKEだけデフォルト)

Containerの情報をまとめると以下のようになっています。

コンポーネント 役割 ポート/プロトコル 設定ファイルのディレクトリ(デフォルト)
kube-dns DNS本体 10053/UDP, 10053/TCP, 10055/TCP /kube-dns-config
dnsmasq-nanny DNSキャッシュサーバー 53/UDP, 53/TCP /etc/k8s/dns/dnsmasq-nanny
sidecar DNSのメトリクスをPrometheusにスクレイプさせるサイドカー 100054/TCP -
prometheus-to-sd PrometheusのメトリクスをStackdriverにアップロードしてくれるもの - -

それぞれ順次おってみてみましょう。

1. kube-dns

kube-dns本体です。kubenetes/dnsリポジトリのdns/cmd/kube-dns以下にソースコードがあります。 kube-dnsの元となっているのはSkyDNSです。TCPとUDPどちらでもクエリすることができます。

実はkube-dnsとはSkyDNSKubeDNS2つが合わさったコンポーネントになっています。そのため、kube-dns起動するとSkyDNSサーバーとKubeDNSの2つが別のGoルーチンで起動することになります。

SkyDNSとKubeDNS

SkyDNS

SkyDNSサーバーはPort番号53でクエリを受け付けます。またヘルスチェック用のPortもあり8081になっています。 DNSの設定ファイルは/kube-dns-configに格納されていますが、ConfigMapの設定を10秒ごとに同期して設定を変更させることができます。

SkyDNSはメトリクスをPrometheusによってスクレイプさせることができ、そのためにPROMETHEUS_PORTで設定されるPortを公開します。

TCPポートはゾーン転送機能によって使われます。これによって複数のDNSがあっても同期をすることができます。

KubeDNS

KubeDNSは2つのHandleと2つControllerで構成されています。(ここからDeploymentのkube-dnsとkube-dnsの構成要素であるKubeDNSの区別には注意が必要)

kube-dnsのHandlerとController

これらのHandlerとControllerは何をしているのでしょうか。

  • Endpoints Controller
  • Service Controller
  • Healthz Handler(これはreadinessに使われる)
  • Cache Handler

Controller

ControllerはいわゆるIPアドレスの変更をクラスタ単位で監視するものです。

  • Endpoints Controller: Endpointが変更されたときに呼ばれるController
  • Service Controller: Serviceが変更されたときに呼ばれるController

つまりすべてのNamespace以下でDeploymentを作る、Serviceを作るなどをした場合にこれらのControllerがリコンサイルされることになり、DNSの処理が行われることになります。

例えば「Endpointが追加されたとき」のケースでTree Cacheでキャッシュをさせ、レコードを追加させます。常にInformerからのイベントを受け取っています。

Endpoints Controllerの一連の流れ

Service Controllerもそんなに変わりません。ただ対象がCluster IPになっている点ぐらいです。

Handler

  • Healthz Handler(これはreadinessに使われる): これは単純にokとHTTPステータスコード200で返す。
  • Cache Handler: KubeDNSでキャッシュをしているEndpointの一覧を返す

またProfilingのためポート6060が割り当てられていますが、特になにかに使うことはないでしょう。

どのようにDNSレコードが更新されるのか

先程のControllerが大きな役目を果たします。

先程もいいましたがKubeDNSには以下のように2つのコントローラーがあり、2つのgoroutineが実行されます。

kube-dnsのService

例えばEndpoints ControllerによってEndpointの追加処理をする流れは以下のようになっています。

func (kd *KubeDNS) handleEndpointAdd(obj interface{}) {
	if e, ok := obj.(*v1.Endpoints); ok {
		if err := kd.addDNSUsingEndpoints(e); err != nil {
			glog.Errorf("Error in addDNSUsingEndpoints(%v): %v", e.Name, err)
		}
	}
}

ここのkd.addDNSUsignEndpointは以下のようになっています。

func (kd *KubeDNS) addDNSUsingEndpoints(e *v1.Endpoints) error {
	svc, err := kd.getServiceFromEndpoints(e)
	if err != nil {
		return err
	}
	if svc == nil || v1.IsServiceIPSet(svc) || svc.Spec.Type == v1.ServiceTypeExternalName {
		// No headless service found corresponding to endpoints object.
		return nil
	}
	return kd.generateRecordsForHeadlessService(e, svc)
}

これによってSkyDNSでレコードを更新するとともに、KubeDNSのTreeCacheにキャッシュされることになります。

DNSレコードの更新の仕組み
2. dnsmasq-nanny(以下: dnsmasq)
dnsmasq

dnsmasqのPod Nannyです。 dnsmasqは軽量なDNSリゾルバでSkyDNSからのレスポンスをキャッシュしてくれます。Goで書かれていないため、バイナリをGoのプロセスで実行をするような形になっています。

dnsmasqを詳しく知りたい方は公式ページへアクセスをしてください。

多くのシステムでNannpy Podは使われているため、Kubernetesを扱っている方はご存知だと思いますがSidecarとして機能を提供するものです。 例えばMetrics scalerの代表であるHeapsterだとNodeが増えるのにHeapster本体が増えないと収集する対象が多くなってしまいます。

このdnsmasqがClusterIPで指定されるServiceからのトラフィックを最初に受け、キャッシュを返すためDNSサーバーへの負荷を軽減することができます。

dnsmasqの一連の流れ
2.1 dnsmasqの設定

dnsmasqを起動する際に-configDirで設定ファイルを指定することができます。 GKEでKubernetesクラスタを作成したときはどのような設定になっているのでしょう。デフォルトで読み込まれるファイルは-configDir=/etc/k8s/dns/dnsmasq-nannyです。これはConfigMapkube-dns-configからマウントされています。

また--restartDnsmasqフラグを渡すことによって、もし設定ファイルが書き換わったときにdnsmasq自体をリスタートできるようになっています。

2.2 フォワードするkube-dns

キャッシュがなければKubeDNSサーバーへアクセスをすることになります。

3. sidecar
dnsmasq

メトリクスをエキスポートしたり、ヘルスチェックを担っているものです。
このsidecarがヘルスチェックできているとPodはヘルシー状態です。

実際にトラフィックを流す部分はkube-dns本体がレスポンスを返せているかになります。

メトリクスはdnsmasqからメトリクスを取得してメトリクスをExportして、Prometheusへ格納をしています。

2つのEndpointも持っていますが、自分のアプリケーションから呼び出すことはしないでしょう。

http.Handle(options.PrometheusPath, promhttp.Handler())
http.HandleFunc("/healthz", func(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "ok (%v)\n", time.Now())
})

これはPrometheusのスクレイプ用とServiceからのヘルスチェックのためにあります。

4. prometheus-to-sd

PrometheusのメトリクスをStackdriverへアップロードする

このコンテナはGKEだけのデフォルトで入っているContainerなので他のクラウドプロバイダを使っている場合はない可能性があります。もちろん自前でインストールすることもできます。

このContainerは何をしているか知らなかったのですが、どうやらGoogleCloudPlatform/k8s-stackdriverらしいです。

Prometheusに格納されたメトリクスをStackdriverへPushしてくれます。 もちろんStackdriverはAWSでも使うことができるのでGCPに限った話ではありません。このコンテナを入れることも可能です。

メトリクスは以下の4つです。

  • probe_kubedns_latency_ms
  • probe_kubedns_errors
  • dnsmasq_misses
  • dnsmasq_hits

このprometheus-to-sdのおかげでStackdriver Loggingでもこれらのメトリクスは検索することができます。

dnsmasqのキャッシュミスのログ(Legacy)

もちろんこのようなログ・メトリクスがあがるためモニタリングなどにも使えます。

kube-dns-autoscaler

先程までで、KubernetesのDNS本体について解説しました。DNSに関連するDeploymentとしてkube-dns-autoscalerがあるので紹介します。

これはkube-dnsをautoscaleするためのものです。 まだIncubator段階ではあるのですがスケジュール可能なNodeとCore数を監視して対象とするReplica数を増減してくれるものです。

このようなものがない場合、Nodeが増えるとDNSへ非常に高い負荷がかかります。

HPAなどの特定のPodのリソースではなく、クラスタ全体のリソースをもとに定義します。 Kubernetes APIを通してReplica数を調整します。

この仕組はDNSだけでなくKubernetesのシステムに関わるもので他にも使われたりします。

計算式の仕組み

メトリクス(Node数・Core数)はPodからAPIサーバーへポーリングをして取得しています。

そして「どのようにスケールさせるか」はConfigMapによって定義をされていえ、ポーリングの間隔のたびに更新されます。試しに見てみましょう。

$ kubectl get configMap kube-dns-autoscaler --namespace kube-system

Name:         kube-dns-autoscaler
Namespace:    kube-system
Labels:       <none>
Annotations:  <none>

Data
====
linear:
----
{"coresPerReplica":256,"nodesPerReplica":16,"preventSinglePointFailure":true}
Events:  <none>

これはいま時点の理想状態が記述されており、nodePerReplicaだけでみると1つのDNSのPodに対して16個のNodeまでが望ましいとされていることが分かります。

Nodeが16個を超えるとスケールする

2つのモード LinearとLadder

このAutoscalerは2つのモードをサポートしています。

Linearモードは以下のように計算されます。

replicas = max( ceil( cores * 1/レプリカ辺りのCore数 ) , ceil( nodes * 1/ノードあたりのレプリカ数 ) )
replicas = min(replicas, max)
replicas = max(replicas, min)

Ladderは階段みたいに、「このときはこんだけのDNSレプリカ数にする」などを定義することができます。そのように設定をした場合は、以下のようにConfigMapの出力はなります。

data:
  ladder: |-
    {
      "coresToReplicas":
      [
        [ 1, 1 ],
        [ 64, 3 ],
        [ 512, 5 ],
        [ 1024, 7 ],
        [ 2048, 10 ],
        [ 4096, 15 ]
      ],
      "nodesToReplicas":
      [
        [ 1, 5 ],
        [ 3, 3 ],
        [ 5, 8 ],
        [ 7, 6 ]
      ]
    }

グラフにすると以下のようになっています。簡略化のためNode数に対してどのような設定にするのかを表示しています。

LadderモードのNode数とReplica数の関係の例

単一障害点回避モード

preventSinglePointFailuretrueにすると、いくらClusterが小さくても2つ以上のPodが存在することになり、単一障害点を避けることができます。

パフォーマンスチューニングの話

ここまでで仕組みは大方分かったのではないかと思います。次にどのようにご自身のKubernetesクラスタのDNSをパフォーマンス・チューニングすればいいのかを紹介します。

0. Client Applicationの話

アプリケーションレベルでDNSキャッシュを入れるのも一つの手です。インターネットからはHTTP/1.1でアクセスをされ、内部ではgRPCを使うケースが多いため以下のようにgRPCの例を示す。

resolver, _ := naming.NewDNSResolverWithFreq(1 * time.Second)
balancer := grpc.RoundRobin(resolver)
conn, _ := grpc.DialContext(context.Background(), grpcHost,
	grpc.WithInsecure(),
	grpc.WithBalancer(balancer))

naming.NewDNSResolverWithFreqでは定期的にPodのIPを取得しているが、その間は直接PodのIPを指定することによってアクセスを可能となる。もちろん1 * time.Secondの箇所である「更新をチェックする頻度」をあげるとDNSへの負荷は高くなるものの変更が反映されやすくなります。

トレードオフだがアプリケーション特性に合わせて考慮に入れるのもいいでしょう。この場合はHeadless Serviceであることが必要です。

1. スタブ・リゾルバ

とあるPod BにアクセスしたいPob A(クライアント)があったとします。DNS問い合わせをするのですが/etc/resolv.confの中に以下のように設定値が書かれていることがありました。

nameserver 10.32.0.10
search namespace.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

以下の項目で負荷を減らすことが可能です。

1.1 FQDNで問い合わせる

FQDNを使うことによって名前解決のレイテンシを減らすことが可能になります。以下のように点線の時刻よりレイテンシの改善が見られます。

FQDNによるレイテンシの改善(引用: 6)

このようにDNSへ問い合わせを行う回数を減らすことによってレイテンシを改善することができ、またDNSへの負荷の削減にも繋がります。最後に.をつけるだけでFQDNとして扱われます。(もちろんFQDNである必要はあります)

1.2 Optionsをへらす

これらの値はdnsPolicyでPodのSpecで変更を加えることができますがoptionsなどを減らすとDNS問い合わせの回数が減ります。

ホスト名によって問い合わせた場合はgetaddrinfo()などのシステムコールによってDNS解決が実行されます。その回数を減らすことができます。

これはsearchドメインを保管してくれるもので、例えばデフォルト値である5にすると5以下の.を含む場合はsearchドメインによって補完をしてくれます。

先程の場合でpingをすると以下のようになります。

$ ping keke

すると以下ように問い合わせをしてくれます。

  • keke.namespace.svc.cluster.local
  • keke.svc.cluster.local
  • keke.cluster.local

です。どれかがレコードを返すとアクセスをできるようになります。ndotを減らし補完の回数(=問い合わせの回数)を減らすことができます。

1.3 Serviceドメインを絞る

ndotsを減らすことのほかにも、serviceドメインを不用意に増やさないことも重要でしょう。

service namespace.svc.cluster.local

だけでも十分かもしれず、問い合わせの回数を減らすことができます。

2. DNSの負荷を下げる

2.1 Autoscalerを調整する

先程も書きましたがAutoscaler(kube-dns-autoscaler)は

  • デフォルトではlinearモード
  • Node数・Core数でDNSのレプリカ数を調整する

の要点があります。もしDNSの負荷が高いなどのことがあればkube-dns-autoscalerのConfigMapを変更してもいいかもしれません。

2.2 dnsmasqをチューリングする

2.2.1 同期頻度を下げる

dnsmasqは設定が変更されると再起動をする仕組みになっていますが、そう簡単に変更されることはないと思います。

dnsmasqはKubernetes APIから変更を(デフォルトでは)10秒ごとにポーリングしているため、変更の頻度を下げてもいいかもしれません。

そのようなときはdnsmasqを起動する際にsyncIntervalフラグで渡してあげるといいです。

2.2.2 TTLを調整する

例えばSpinnakerではRed/Blackデプロイメント手法があり、ServiceによるL4のロードバランシングのためPodを名前解決することができなくなることはないです。そのため、TTLの値を上げても問題になることは少ないかなと思います。

dnsmasq.confの中で以下のようにTTLを設定することができます。デフォルトでは5分になっています。

min-cache-ttl=600

ここまでは簡単に設定できるDNSのパフォーマンス・チューニングになっています。


DNSを構成するコンポーネントやDNSレコードが更新される仕組みを解説することができました。大まかにDNSの挙動や、どのようなものであるかという実態についてはご理解をいただけたのではないかと思います。


まとめ

本記事は以下の要点で解説をしてきました。

  • Kubernetes DNSの構成、それぞれのコンポーネントの役割
  • KubernetesがどのようにDNSレコードを更新しているのかの仕組み

Kubernetesを使っている限りDNSに問題が発生する可能性もあります。例えば負荷が高すぎて問い合わせのときのレイテンシがひどいときなどです。 そのようなときにパフォーマンス・チューニングなどいろんなときに知っていると重宝します。例えばkube-dns-autoscalerを見直してもいいかもしれません。dnsmasqのキャッシュの設定を見直してもいいかもしれません。

またInformerやControllerなど、Kubernetes Operatorを開発するときも必要になる知識で、DNSに限らずKubernetesのコンポーネントを理解するためにも必要になります。

Kubernetesのこと以上に、基本的なコンピュータサイエンスの知識(Linux・ネットワークについて)が何より必要です。

まだまだKubernetesは何もわからないしがないSREですが、これからもっと頑張ろうと思いました。 最後までありがとうございました。CoreDNSの勉強も途中なので再開しようと思います。


参考文献

入門記事から応用的なものまで、目を通したものを紹介します。

(1)A introduction to the Kubernetes DNS Service, Digital Ocean

  • CoreDNSとkube-dnsの双方に関する基本的なことが書かれてある
  • 特にCoreDNS寄りに書かれているが、これからのデフォルトはCoreDNSなので気にしない

(2)Debugging DNS Resolution, Kubernetes

  • DNSのデバックをする方法が書かれた公式ドキュメント
  • Podを作ってデバックしたり、DNSに限らずPod間での通信ができないときなどにやる方法と一緒なのでDNSだから特別ということではない
  • 私はこういうときはTelepresenceを使うのでそっちも選択肢に入れているといいかもしれない(このケースではTelepresenseがとりわけいいわけではなく勧めたいというわけではない)

(3)権威DNSサーバーってどこにあるの?, IIJ
* 内部DNSだけでなく、DNSを詳しく知りたいために参照した * IIJという日本のレジストラのDNS話が書いてある * ディザスタリカバリの切り口で日本のDNS事情を解説しており、面白かった

(4)Scaling CoreDNS, coredns github repository * どのようにすればKuberenetes上でのCoreDNSをスケールさせることができるか解説されてある。 * Pod、Service比など実証実験をしているので瞠目に値する * 一部のメトリクスでkube-dnsとの比較もあり、なぜKubernetesがCoreDNSを採用するのか直感的に分かる

(5)Service discovery(kube-dns), IBM Knowledge Center

  • IBM CLoud Privateで使用されているサービスディスカバリの話である
  • (kube-dns)とタイトルについている割にはCoreDNSのことしか書いてない

(6)Kubernetes pods /etc/resolv.conf ndots:5, Marco Pracucci

  • Podの中にある/etc/resolv.confoptionフィールドのパフォーマンス・チューニングについて

(7)DNS for Services and Pods

  • Pod DNS Policyについて書かれてある公式ドキュメント