Intro

NodeLocalDNS is a DNS caching agent that runs as a DaemonSet on cluster nodes to improve DNS performance and reduce DNS query load on CoreDNS. It provides several key benefits:

NodeLocalDNS는 coredns를 보조해주는 도구이다. coredns는 deployment로 동작해서 일반적으로 2개의 Pod가 생성된다. large cluster의 경우 모든 DNS질의가 coredns를 경유하기 때문에 coredns에서 부하가 발생할 수 있으며 다음과 같은 문제가 발생할 수 있다.

  1. conntrack table 고갈
  2. VPC에 dns 질의 limit 초과
  3. coredns pod 부하로 인한 지연

그래서 kubernetes 공식적으로 nodelocaldns를 제공하고 있다. [+] https://kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/

kubernetes에서 공식적으로 어떻게 해당 기능을 구현했고, 실제 잘 동작하는지 확인해보자.

Deep dive

Installation

설치 방법은 매우쉽고, 다음 순서로 설치하면 된다. 이때 kube-proxy 설정을 잊지말자.

Step 1: nodelocaldns 구성 파일을 다운 받기

# Download the official NodeLocalDNS manifest
curl -o nodelocaldns.yaml https://raw.githubusercontent.com/kubernetes/kubernetes/refs/heads/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml

Step 2: 구성 파일 상세 설정하기

kube-proxy 모드를 체크한다.

kubectl get cm -n kube-system kube-proxy-config -oyaml | grep mode 
kube-proxy가 iptables로 동작하는 경우
# set environment variables for node-local-dns
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
domain=cluster.local
localdns=169.254.20.10
 
# Update the manifest with your cluster's specific values:
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/__PILLAR__DNS__SERVER__/$kubedns/g" nodelocaldns.yaml
kube-proxy가 IPVS로 동작하는 경우
# set environment variables for node-local-dns
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
domain=cluster.local
localdns=169.254.20.10
 
# Update the manifest with your cluster's specific values:
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/,__PILLAR__DNS__SERVER__//g; s/__PILLAR__CLUSTER__DNS__/$kubedns/g" nodelocaldns.yaml

IPVS를 사용하는 경우 pod가 nodelocaldns를 찌를 수 있게 매뉴얼하게 DNS 설정을 바꿔야한다. dnsConfig나 kubelet 설정을 조정할 수 있다.

option 1. add dnsConfig in your pod

apiVersion: v1
kind: Pod
metadata:
  name: dns-ipvs
  labels:
    app.kubernetes.io/name: dns-ipvs
spec:
  containers:
  - name: dns-ipvs
    image: registry.k8s.io/e2e-test-images/agnhost:2.39
    resources:
      limits:
        memory: "128Mi"
        cpu: "500m"
  dnsPolicy: "None"
  dnsConfig:
    nameservers:
      - 169.254.20.10
    searches:
      - default.svc.cluster.local 
      - svc.cluster.local 
      - cluster.local
      - ap-northeast-2.compute.internal
    options:
      - name: ndots
        value: "5"

options 2. change kubelet clusterDNS to localdns

MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="BOUNDARY"
 
--BOUNDARY
Content-Type: application/node.eks.aws
 
---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
  kubelet:
    config:
      clusterDNS: 169.254.20.10
 
--BOUNDARY--

Step 3: nodelocaldns 설치하기

apply your manifest

# Apply the NodeLocalDNS DaemonSet
kubectl apply -f nodelocaldns.yaml

if node-local-dns is installed on your cluster successfully, you can check that node-local-dns daemonset and pod is running.

# Verify the deployment
kubectl get daemonset node-local-dns -n kube-system
kubectl get pods -n kube-system -l k8s-app=node-local-dns

Step 5: 설치 확인

# Check if NodeLocalDNS pods are running on all nodes
kubectl get pods -n kube-system -l k8s-app=node-local-dns -o wide
 
# Test DNS resolution from a test pod
kubectl run dnsutils --image=registry.k8s.io/e2e-test-images/agnhost:2.39
kubectl exec -it dnsutils -- nslookup kubernetes.default.svc.cluster.local

How it works?

기본 개념은, “coredns로 트래픽을 전달하기 전에, 모든 노드에 cache agent를 설치하자” 이다. 이걸 따로 kube-proxy에서는 따로 kubelet등 설정 변경없이 사용할 수 있도록 하였는데, ipvs인 경우 이후 설명할 제약으로 설정 변경이 불가피하다.

case 1. kube-proxy mode

kube-proxy로 동작하는 경우, 다음과 같이 그림을 그려봤다. 그림1. iptables 모드에서 nodelocaldns 연결 흐름

Pod는 항상 coredns svc를 찌를텐데, 어떻게 nodelocaldns 가 응답할 수 있을까? 요건 위 이미지에서 dummy interface를 보면된다.

아래와 같이 nodelocaldns dummy interface가 만들어지고, 172.20.0.10과 169.254.20.10 IP를 listen하고 있다.

[root@ip-10-0-162-110 ~]# ip a show dev nodelocaldns
23: nodelocaldns: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 56:79:38:84:b7:79 brd ff:ff:ff:ff:ff:ff
inet 169.254.20.10/32 scope global nodelocaldns
valid_lft forever preferred_lft forever
inet 172.20.0.10/32 scope global nodelocaldns
valid_lft forever preferred_lft forever

실제로 route를 떄려보면 local로 넘어간다 (ip rule우선, 원래는 node eni로 넘어가야 정상이다.).

[root@ip-10-0-162-110 ~]# ip route get 169.254.20.10
local 169.254.20.10 dev lo table local src 169.254.20.10 uid 0
cache <local>
 
[root@ip-10-0-162-110 ~]# ip route get 172.20.0.10
local 172.20.0.10 dev lo table local src 172.20.0.10 uid 0
cache <local>

또한 nodelocaldns는 설치시, localdns, coredns IP에 대하여 NOTRACK rule을 추가한다. 따라서 conntrack entry를 차지하지 않고, kube-proxy에 의한 DNAT도 발생하지 않는다. (iptables nat 테이블로 전달이 안되기 떄문에, SVC IP Pod IP로 변환이 안된다)

따라서 NOTRACK으로 인해 처음에 말한 conntrack table 꽉차는것을 방지할 수 있다.

case 2. ipvs mode

ipvs 모드에서는 어떨까? ipvs 모드에서는 dummy interface가 169.254.20.10 만 listen 하고 있다.

[root@ip-10-0-162-35 ~]# ip a show dev nodelocaldns
20: nodelocaldns: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 5e:73:18:a0:bf:53 brd ff:ff:ff:ff:ff:ff
inet 169.254.20.10/32 scope global nodelocaldns
valid_lft forever preferred_lft forever

요 이유는 172.20.0.10가 어디 묶여있는지 보면 되는데, ipvs 동작에따라 ipvs interface는 service ip에 listeng하고 트래픽을 전달해준다.

[root@ip-10-0-142-222 ~]# ip addr show dev kube-ipvs0
5: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether c6:f1:27:17:be:80 brd ff:ff:ff:ff:ff:ff
inet 172.20.0.10/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
...

그래서 ipvs에서는 172.20.0.10을 찔러도 nodelocaldns에 연결하지 못한다. 그러므로 Pod가 lodecaldns에 연결할 수 있게 dns 설정해주어야한다.

nodelocaldns는 9153 포트에 매트릭을 노출해서, 실제 coredns 부하가 줄어드는지 확인해봤다.

그림 2. nodelocaldns가 없을 때, coredns의 req/s

그림 3. nodelocaldns가 있을 때, coredns의 req/s

아래 명령어로 0.1초마다 5분간 curl을 날렸다.

end=$(($(date +%s)+300)); while [ $(date +%s) -lt $end ]; do curl -s -o /dev/null naver.com; sleep 0.1; done

확실히 nodelocaldns 설치후 coredns로의 요청수가 확연히 줄어들었다.

Troubleshooting

Nodelocaldns가 CrashLoopBackOff 상태인 경우

보통 로그를 보면 되는데 아래와 같이 nodelocaldns가 사용중인 포트가 점유하고 있을 수 있다.

Listen: listen tcp 172.20.0.10:53: bind: address already in use

Note: EKS auto 모드에서는 이미 nodelocaldns가 설치된다. 아래와 같이 affinity를 줘서 auto 모드가 아닌 노드에만 설치하자.

      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: eks.amazonaws.com/compute-type
                operator: NotIn
                values:
                - auto

nodelocaldns는 hostNetwork로 올라가고, 53, 8080 포트를 점유해서 해당 포트가 사용중인지 확인하면 된다. netstat으로 어떤 포트가 점유중인지 확인하자.

netstat -tulpn | grep node-cache
tcp        0      0 169.254.20.10:53        0.0.0.0:*               LISTEN      34249/node-cache
tcp        0      0 169.254.20.10:8080      0.0.0.0:*               LISTEN      34249/node-cache
tcp6       0      0 :::9253                 :::*                    LISTEN      34249/node-cache
tcp6       0      0 :::9353                 :::*                    LISTEN      34249/node-cache
udp        0      0 169.254.20.10:53        0.0.0.0:*                           34249/node-cache

8080은 테스트는 잘쓰이는 port라서 아래와 같이 health check 포트를 바꿀수도 있겠다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
data:
  Corefile: |
    __PILLAR__DNS__DOMAIN__:53 {
        errors
        cache {
                success 9984 30
                denial 9984 5
        }
        reload
        loop
        bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
        forward . __PILLAR__CLUSTER__DNS__ {
                force_tcp
        }
        prometheus :9253
        health __PILLAR__LOCAL__DNS__:8082     ## Change from port 8080 to 8082
        }
...
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    k8s-app: node-local-dns
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
spec:
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 10%
  selector:
    matchLabels:
      k8s-app: node-local-dns
...
        livenessProbe:
          httpGet:
            host: __PILLAR__LOCAL__DNS__
            path: /health
            port: 8082          ## Update livenessProbe port to 8082 as well
          initialDelaySeconds: 60
          timeoutSeconds: 5

DNS 질의시 timeout 발생하는 경우

아래와 같이 dns 질의시 timeout이 날 수 있는데 IP를 잘 확인해봐야겠지만 대부분 coredns를 못찌르는 거다.

[ERROR] plugin/errors: 2 amazon.com.default.svc.cluster.local. A: dial tcp 172.20.195.98:53: i/o timeout

nodelocaldns는 TCP로 요청하기 때문에 UDP뿐만아니라 TCP도 53포트로 열렸는지 방화벽등을 점검해볼 수 있다.

DNS 질의시connection refused 발생하는 경우

[ERROR] plugin/errors: 2 nginx.default.svc.cluster.local. A: dial tcp 172.20.195.98:53: connect: connection refused

이건 coredns가 없는 경우가 매우크다. coredns가 잘 떠있는지 확인해보자.

kubectl get pod -n kube-system -l k8s-app=kube-dns

그 외로는 cni가 만드는 route rule과 충돌하는게 없는지 자세히 검증해야한다.. 이게 쫌 어려웠는데, amazon-vpc-cni의 경우 secondary ENI와 primary ENI에 대한 Pod의 ip rule이 달라서 잘 체크해야한다.

참고자료

  • kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/
  • docs.aws.amazon.com/eks/latest/best-practices/scale-cluster-services.html
  • kubernetes.io/docs/tasks/administer-cluster/dns-debugging-resolution/
  • github.com/coredns/deployment/blob/master/kubernetes/Scaling_CoreDNS.md