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에서 부하가 발생할 수 있으며 다음과 같은 문제가 발생할 수 있다.
- conntrack table 고갈
- VPC에 dns 질의 limit 초과
- 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.yamlStep 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.yamlkube-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.yamlIPVS를 사용하는 경우 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.yamlif 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-dnsStep 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.localHow 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:
- autonodelocaldns는 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-cache8080은 테스트는 잘쓰이는 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: 5DNS 질의시 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 timeoutnodelocaldns는 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