0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Kubernetes] Zone-Aware DNS (NodeLocalDNS) 概要を調べてみた

Posted at

About

Kubernetesにおけるクラスター内DNSのデファクトであるCoreDNSですが、クラスター内通信の最も基本的なコンポーネントであるにも関わらず単一のワークロードであり、リクエストはNode間を跨ぐ必要があるので通信的な負荷とリソース的な負荷によってボトルネックになりうる可能性が高いです。
そう言った問題に対応するためにNodeLocal DNSCacheというものが公式にリリースされています。

更に、クラウドプロバイダーの一つであるIBM Cloudにおいては、このNodeLocal DNSCacheの機能を利用・拡張し、Zone-Aware DNSという機能を提供しています。
各NodeにデプロイされたDNS Cacheエージェントからクラスター内のZone情報を持つCoreDNSに対する通信もAZに閉じるようにして、パフォーマンスと可用性向上を目的としております。

今回このNodeLocal DNSCacheとZone-Aware DNSについて、どのように機能を実現しているのが調べたのでその内容のメモ

環境

$ k version
Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.12", GitCommit:"b058e1760c79f46a834ba59bd7a3486ecf28237d", GitTreeState:"clean", BuildDate:"2022-07-13T14:59:18Z", GoVersion:"go1.16.15", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.12+IKS", GitCommit:"53eb562c6d6b891973e77f4f96ab3569b3630596", GitTreeState:"clean", BuildDate:"2022-09-21T20:06:42Z", GoVersion:"go1.17.13", Compiler:"gc", Platform:"linux/amd64"}

NodeLocal DNSCache

概要

公式ページでは、NodeLocal DNSCacheを用いることで以下の利点が得られるとしています。

  • Node間通信の発生に伴って発生するLatencyの軽減
  • iptablesでのDNATによる、conntrack tableへエントリが蓄積されることによるリソース逼迫を軽減
  • アプリNodeLocal DNSCacheのDNS caching agentへの通信はUDPとしたままで、DNS caching agentClusterDNSへの通信をTCPにupgradeし、conntrackのエントリのライフサイクルを効率的(セッション終了に同期して閉じる)に管理し無用に蓄積されないようにする
  • 上記の箇所でTCP通信を用いることにより、UDPパケットがドロップされた際のtail latencyを軽減
  • NodeレベルでDNSのメトリクスを取得可能にする
  • Negative Cacheを用いることによるさらなるClusterDNSへの負荷軽減

また、ClusterDNSと同じIPを用いることにより、万が一NodeLocal DNScache Podがダウンした場合も自動的にClusterDNSにfailbackできるという利点もあります。
ではNodeの中では実際にどのようにして、Cluster DNSへの通信をDNS Caching agentに転送し、上記のような機能を実現しているのかについてみていきたいと思います。

動作内容

まず言わずもがなですが、NodeLocal DNSCacheはクラスター上の各ノードで稼働するため、DaemonSetとしてデプロイします。

$ k get deploy,ds -o json | jq -r '.items[] | select(.metadata.name | contains("dns")) | {"Kind": .kind, "name":.metadata.name}'
{
  "Kind": "Deployment",
  "name": "coredns"
}
{
  "Kind": "Deployment",
  "name": "coredns-autoscaler"
}
{
  "Kind": "DaemonSet",
  "name": "node-local-dns"
}

実行しているプロセスは以下のようになります。こちらの引数で渡している-localipに指定したIPをbindします。そのため、クラスタ内や環境内のIPと衝突して想定外の挙動を引き起こさないように気を付ける必要があります。

下の例では169.254.20.10node-local-dnsのデフォルト値、172.21.0.10が検証した環境のClusterDNSのIPとなります。

$ k get ds -o json | jq -r '.items[] | select(.metadata.name | startswith("node-local-dns")) |.spec.template.spec |  {"nodeSlector":.nodeSelector, "containers":(.containers[] | {"name":.name, "image":.image, "command":.command,"args":.args}), "volumes":.volumes} '
{
  "nodeSlector": {
    "ibm-cloud.kubernetes.io/node-local-dns-enabled": "true"
  },
  "containers": {
    "name": "node-cache",
    "image": "registry.au-syd.bluemix.net/armada-master/k8s-dns-node-cache:1.22.11",
    "command": null,
    "args": [
      "-localip",
      "169.254.20.10,172.21.0.10",
      "-conf",
      "/etc/Corefile",
      "-basecorefile",
      "/etc/coredns/Corefile",
      "-upstreamsvc",
      "node-local-dns"
    ]
  },
  "volumes": [
    {
      "hostPath": {
        "path": "/run/xtables.lock",
        "type": "FileOrCreate"
      },
      "name": "xtables-lock"
    },
    {
      "configMap": {
        "defaultMode": 420,
        "name": "node-local-dns"
      },
      "name": "base-config-volume"
    },
    {
      "configMap": {
        "defaultMode": 420,
        "name": "node-local-dns-config",
        "optional": true
      },
      "name": "kube-dns-config-volume"
    }
  ]
}

実際にNodeの中に入り該当のプロセスに関するソケット周りの情報を見ると、-localipで指定したIPでbindしていることが分かります。

/# ps -ef | grep node-cache | grep -v grep
root      14249  14053  0 Sep27 ?        00:40:08 /node-cache -localip 169.254.20.10,172.21.0.10 -conf /etc/Corefile -basecorefile /etc/coredns/Corefile -upstreamsvc coredns
/# ss -lun4p | grep 14249
UNCONN   0         0                172.21.0.10:53              0.0.0.0:*        users:(("node-cache",pid=14249,fd=12))
UNCONN   0         0              169.254.20.10:53              0.0.0.0:*        users:(("node-cache",pid=14249,fd=10))
/# lsof -n -i @172.21.0.10:53 -i @169.254.20.10:53
COMMAND     PID USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
node-cach 14249 root    9u  IPv4 354886644      0t0  TCP 169.254.20.10:domain (LISTEN)
node-cach 14249 root   10u  IPv4 354886645      0t0  UDP 169.254.20.10:domain
node-cach 14249 root   11u  IPv4 354886646      0t0  TCP 172.21.0.10:domain (LISTEN)
node-cach 14249 root   12u  IPv4 354886647      0t0  UDP 172.21.0.10:domain
/#

これらのIPに対応するI/Fを作成し、自分自身からこれらのIPに対して発生する通信はlocal table上で自分自身にルーティングするようにしております。

/# ip a show dev nodelocaldns
2720: nodelocaldns: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether 02:bd:e2:79:c8:5c brd ff:ff:ff:ff:ff:ff
    inet 169.254.20.10/32 scope global nodelocaldns
       valid_lft forever preferred_lft forever
    inet 172.21.0.10/32 scope global nodelocaldns
       valid_lft forever preferred_lft forever
/# ip r show table local dev nodelocaldns
local 169.254.20.10 proto kernel scope host src 169.254.20.10
local 172.21.0.10 proto kernel scope host src 172.21.0.10
/#

また、これらのパケットを受け取るNetfilterでは以下のようなINPUT/OUTPUT chainが構成され、nodelocaldns I/Fに対するトラフィックのconntrackの無効化(conntrackエントリの削減と、NATの無効化)、およびACCEPTが行われます。

netfilter設定内容コンセプト:
https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/1024-nodelocal-cache-dns/README.md#iptables-notrack
ルール本体:
https://github.com/kubernetes/dns/blob/master/cmd/node-cache/app/cache_app.go

# for t in $(cat /proc/net/ip_tables_names );do echo "TABLE: $t";iptables -n -t $t -L | awk 'BEGIN{p=0;s=""}{if($0~/^$/){if(p==1)print s;s="";p=0}s=s$0"\n";if($0~/udp.*169\.254\.20\.10/)p=1}';done
TABLE: security
TABLE: raw
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
cali-PREROUTING  all  --  0.0.0.0/0            0.0.0.0/0            /* cali:6gwbT8clXdHdC1b1 */
CT         udp  --  0.0.0.0/0            172.21.0.10          udp dpt:53 NOTRACK
CT         tcp  --  0.0.0.0/0            172.21.0.10          tcp dpt:53 NOTRACK
CT         udp  --  0.0.0.0/0            169.254.20.10        udp dpt:53 NOTRACK
CT         tcp  --  0.0.0.0/0            169.254.20.10        tcp dpt:53 NOTRACK


Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
cali-OUTPUT  all  --  0.0.0.0/0            0.0.0.0/0            /* cali:tVnHkvAo15HuiPy0 */
CT         tcp  --  172.21.0.10          0.0.0.0/0            tcp spt:8080 NOTRACK
CT         tcp  --  0.0.0.0/0            172.21.0.10          tcp dpt:8080 NOTRACK
CT         udp  --  0.0.0.0/0            172.21.0.10          udp dpt:53 NOTRACK
CT         tcp  --  0.0.0.0/0            172.21.0.10          tcp dpt:53 NOTRACK
CT         udp  --  172.21.0.10          0.0.0.0/0            udp spt:53 NOTRACK
CT         tcp  --  172.21.0.10          0.0.0.0/0            tcp spt:53 NOTRACK
CT         tcp  --  169.254.20.10        0.0.0.0/0            tcp spt:8080 NOTRACK
CT         tcp  --  0.0.0.0/0            169.254.20.10        tcp dpt:8080 NOTRACK
CT         udp  --  0.0.0.0/0            169.254.20.10        udp dpt:53 NOTRACK
CT         tcp  --  0.0.0.0/0            169.254.20.10        tcp dpt:53 NOTRACK
CT         udp  --  169.254.20.10        0.0.0.0/0            udp spt:53 NOTRACK
CT         tcp  --  169.254.20.10        0.0.0.0/0            tcp spt:53 NOTRACK

TABLE: nat
TABLE: mangle
TABLE: filter
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
cali-INPUT  all  --  0.0.0.0/0            0.0.0.0/0            /* cali:Cz_u1IQiXIMmKD4c */
ACCEPT     udp  --  0.0.0.0/0            172.21.0.10          udp dpt:53
ACCEPT     tcp  --  0.0.0.0/0            172.21.0.10          tcp dpt:53
ACCEPT     udp  --  0.0.0.0/0            169.254.20.10        udp dpt:53
ACCEPT     tcp  --  0.0.0.0/0            169.254.20.10        tcp dpt:53
KUBE-NODEPORTS  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes health check service ports */
KUBE-EXTERNAL-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            ctstate NEW /* kubernetes externally-visible service portals */
KUBE-FIREWALL  all  --  0.0.0.0/0            0.0.0.0/0
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0
DROP       all  --  127.0.0.0/8          0.0.0.0/0


Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
cali-OUTPUT  all  --  0.0.0.0/0            0.0.0.0/0            /* cali:tVnHkvAo15HuiPy0 */
ACCEPT     udp  --  172.21.0.10          0.0.0.0/0            udp spt:53
ACCEPT     tcp  --  172.21.0.10          0.0.0.0/0            tcp spt:53
ACCEPT     udp  --  169.254.20.10        0.0.0.0/0            udp spt:53
ACCEPT     tcp  --  169.254.20.10        0.0.0.0/0            tcp spt:53
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            ctstate NEW /* kubernetes service portals */
KUBE-FIREWALL  all  --  0.0.0.0/0            0.0.0.0/0
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0

上にリンクを張ったドキュメントで紹介されているようにnetfilterのrawおよびfilter hookにおいて関連するルール(53: DNS, 8080: healthcheck)が追加されております。
node-local-cacheではこれらのルールをcyclicにチェック・復元を行っております。
パケットの流れ(CNIにCalico、Service typeにiptablesを使った構成の場合)を図で示すと以下のようになります。

     ____________     ____________     ____________     ____________  
    | raw        |   | mangle     |   | nat        |   | filter     |  
    |  OUTPUT    |   |  OUTPUT    |   |  OUTPUT    |   |  OUTPUT    |  
Pod-|---NOTRACK--|->-|---ACCEPT---|->-|------------|->-|---ACCEPT---|->-
    |____________|   |____________|   |____________|   |____________|  
     ____________     ____________     ____________ 
    | raw        |   | mangle     |   | nat        |
    |  PREROUTING|   |  PREROUTING|   |  PREROUTING|
----|---NOTRACK--|->-|---ACCEPT---|->-|------------|->---------------
    |____________|   |____________|   |____________|  
                   ____________     ____________     ____________ 
                  | mangle     |   | nat        |   | filter     |
                  |  INPUT     |   |  INPUT     |   |  INPUT     |
------------------|---ACCEPT---|->-|------------|->-|---ACCEPT---|->-NodeLocalDNS
                  |____________|   |____________|   |____________|

適用される各hookとdefault Chainのみ記載しています(security hookは完全に使われていないので割愛します)

文章で示すと

  1. raw hookOUTPUT Chain においてNOTRACKが設定される。(そのため、その後の通信は自ホスト内であるにも関わらずI/F間の通信が FORWARD Chainではなく INPUT Chainに送られるようになっている。)
  2. (上記の理由から)raw hookPREROUTING Chainに到着し、DNS機能宛の通信(53番ポート)はここでもNOTRACKが設定される(これによりconntrackの無効化。nat hookはnetfilterの定義より、conntrack上のステータスがNEWのものしか適用されないため、nat hookPREROUTING Chain(Serviceで定義されるClusterIPをDNATする)、INPUT Chainが適用外となる)
  3. filter hookにおいて、INPUT ChainでACCEPTされる(*1)

(*1) Cluster Net I/F (そのWorker Node上で稼働するPod用に割り当てられたI/F)の場合はデフォルトで存在するcali-INPUT Chain、hostNetの場合はnode-local-dnsによって追加されたルールによってACCEPTされます。

本当は各Chainの中で細かくkube-proxycalicoが作り出したルールを通過しますが、ルールを細かく追った結果を記載すると読みにくくなるのでこれも割愛。

動作内容まとめ

ここまでで、NodeLocalDNSとは

  • DaemonSetで各Worker Node上で稼働する
  • 上記のプロセスがlocalhost用のダミーデバイスとルーティングテーブルを作成する。
  • 同様にnetfilterルールを作成し、ローカルプロセスからの受信とリソースの無駄な消費を避ける。
  • Worker Node上でDNS Caching agent(例えばCoreDNS)プロセスを稼働させ、上で作成したデバイスが持つアドレスでbindさせる。

のように稼働し、bindさせるIPをCluster DNSとすることで、Podやクラスターの設定に手を加えることなく(ここが肝心)NodeLocalDNSを通常系で用いることができ、かつNodeLocalDNSのプロセスが落ちてしまったときは終了処理(SIGTERMで呼ばれるNodeLocalDNS内のTeardownNetworking()関数)によってルーティングルールが削除され、元々のCluster DNSにfallbackされるという設計になっております。

Zone-Aware DNS

概要

IBM Cloud Kubernetes Service(IKS)の環境では、上記で紹介したNodeLocalDNSの機能を少し拡張し、DNSへのリクエストが一つのAZで完結するZone-Aware DNSが提供されております。
この仕組みにより、

  • Zoneを跨いだ通信を発生させないことによるパフォーマンスの向上
  • Zone間通信障害の影響を最小限にする構成をとれる
  • 元から存在するCoreDNSに追加して稼働するため、冗長性が増す

といった利点が考えられます。
クラウドのPrivate DNS系のサービスを用いている場合、可用性やパフォーマンスに関わる部分でもあるのでAZを意識したロードバランシング(クライアントのAZに応じて返却するレコードを変える)といったニーズがあるかもしれません。
そのような場合、

  • NodeLocalDNSへの特定のZoneへのクエリを直接外部のDNSに転送する
  • NodeLocalDNSがデフォルトで転送するCoreDNSを各AZに配置し、全てのクエリを同じAZ内のCoreDNSに転送する

といったいずれかのアプローチが考えられますが、Zone-Aware DNSは結果的に後者の形式で、DNSクライアントの問い合わせ元AZを限定することになります。

実装

実装というほどでもありませんが、IBM Cloud Kubernetes Service(IKS)においてZone-Aware DNSを有効化させると、以下の通り各Zone用に設定されたCoreDNSNodeLocalDNSが立ち上がり、元のNodeLocalDNSは0にスケールされます。

$ k get deploy,ds -o json | jq -r '.items[] | select(.metadata.name | contains("dns")) | {"Kind": .kind, "name":.metadata.name,"replicas(deploy)":.status.replicas, "replicas(ds)":.status.numberReady}'
{
  "Kind": "Deployment",
  "name": "coredns",
  "replicas(deploy)": 3,
  "replicas(ds)": null
}
{
  "Kind": "Deployment",
  "name": "coredns-autoscaler",
  "replicas(deploy)": 1,
  "replicas(ds)": null
}
{
  "Kind": "Deployment",
  "name": "coredns-autoscaler-jp-tok-1",
  "replicas(deploy)": 1,
  "replicas(ds)": null
}
{
  "Kind": "Deployment",
  "name": "coredns-autoscaler-jp-tok-2",
  "replicas(deploy)": 1,
  "replicas(ds)": null
}
{
  "Kind": "Deployment",
  "name": "coredns-autoscaler-jp-tok-3",
  "replicas(deploy)": 1,
  "replicas(ds)": null
}
{
  "Kind": "Deployment",
  "name": "coredns-jp-tok-1",
  "replicas(deploy)": 3,
  "replicas(ds)": null
}
{
  "Kind": "Deployment",
  "name": "coredns-jp-tok-2",
  "replicas(deploy)": 3,
  "replicas(ds)": null
}
{
  "Kind": "Deployment",
  "name": "coredns-jp-tok-3",
  "replicas(deploy)": 3,
  "replicas(ds)": null
}
{
  "Kind": "DaemonSet",
  "name": "node-local-dns",
  "replicas(deploy)": null,
  "replicas(ds)": 0
}
{
  "Kind": "DaemonSet",
  "name": "node-local-dns-jp-tok-1",
  "replicas(deploy)": null,
  "replicas(ds)": 2
}
{
  "Kind": "DaemonSet",
  "name": "node-local-dns-jp-tok-2",
  "replicas(deploy)": null,
  "replicas(ds)": 2
}
{
  "Kind": "DaemonSet",
  "name": "node-local-dns-jp-tok-3",
  "replicas(deploy)": null,
  "replicas(ds)": 2
}
$

そして、それぞれのAZにデプロイされたNodeLocalDNSは、それぞれのAZにいるCoreDNSにクエリを転送するようになっております。

$ k get ds -o json | jq -r '.items[] | select(.metadata.name | startswith("node-local-dns-")) |.spec.template.spec |  {"nodeSlector":.nodeSelector, "containers":(.containers[] | {"name":.name, "command":.command,"args":.args})} '
{
  "nodeSlector": {
    "ibm-cloud.kubernetes.io/zone-aware-dns-enabled": "true",
    "topology.kubernetes.io/zone": "jp-tok-1"
  },
  "containers": {
    "name": "node-cache",
    "command": null,
    "args": [
      "-localip",
      "169.254.20.10,172.21.0.10",
      "-conf",
      "/etc/Corefile",
      "-basecorefile",
      "/etc/coredns/Corefile",
      "-upstreamsvc",
      "coredns-jp-tok-1"
    ]
  }
}
{
  "nodeSlector": {
    "ibm-cloud.kubernetes.io/zone-aware-dns-enabled": "true",
    "topology.kubernetes.io/zone": "jp-tok-2"
  },
  "containers": {
    "name": "node-cache",
    "command": null,
    "args": [
      "-localip",
      "169.254.20.10,172.21.0.10",
      "-conf",
      "/etc/Corefile",
      "-basecorefile",
      "/etc/coredns/Corefile",
      "-upstreamsvc",
      "coredns-jp-tok-2"
    ]
  }
}
{
  "nodeSlector": {
    "ibm-cloud.kubernetes.io/zone-aware-dns-enabled": "true",
    "topology.kubernetes.io/zone": "jp-tok-3"
  },
  "containers": {
    "name": "node-cache",
    "command": null,
    "args": [
      "-localip",
      "169.254.20.10,172.21.0.10",
      "-conf",
      "/etc/Corefile",
      "-basecorefile",
      "/etc/coredns/Corefile",
      "-upstreamsvc",
      "coredns-jp-tok-3"
    ]
  }
}

上記において-upstreamsvcで指定したService名から対応する環境変数(各Podの .spec.enableServiceLinks: <bool>によってPod内に自動的に定義される)よりClusterIPを取得し、baseCoreFileを書き換えて使用するようにしています。

これにより、各AZ特有のCoreDNSおよびNodeLocalDNSが稼働し、クエリが通常系では同一Zone内で完結するようになっております。

まとめ

少しだけソースの方に触れつつ、主にNodeLocalDNSが稼働するとHost Nodeにどのような影響を及ぼすのか、どういう目的で使われるのかという観点でまとめてみました。
色々工夫されているなぁと思いつつ、結局結論としては「クラスター全体のパフォーマンス・可用性・冗長性を向上させたいならNodeLocalDNSを導入せよ」というだけでした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?