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 agent
→ClusterDNS
への通信を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.10
がnode-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
は完全に使われていないので割愛します)
文章で示すと
-
raw hook
のOUTPUT Chain
においてNOTRACK
が設定される。(そのため、その後の通信は自ホスト内であるにも関わらずI/F間の通信がFORWARD Chain
ではなくINPUT Chain
に送られるようになっている。) - (上記の理由から)
raw hook
のPREROUTING Chain
に到着し、DNS機能宛の通信(53番ポート)はここでもNOTRACK
が設定される(これによりconntrackの無効化。nat hook
はnetfilterの定義より、conntrack上のステータスがNEWのものしか適用されないため、nat hook
のPREROUTING Chain
(Serviceで定義されるClusterIPをDNATする)、INPUT Chain
が適用外となる) -
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-proxy
やcalico
が作り出したルールを通過しますが、ルールを細かく追った結果を記載すると読みにくくなるのでこれも割愛。
動作内容まとめ
ここまでで、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用に設定されたCoreDNS
とNodeLocalDNS
が立ち上がり、元の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
を導入せよ」というだけでした。