NodeLocalDNSCache를 사용하여 쿠버네티스 클러스터의 DNS 성능과 안정성을 개선하는 방법을 소개합니다. 배포, 설정 및 원리 분석을 포함하며, 부하 테스트 결과를 통해 50% 성능 향상을 확인했습니다.
- 배경
NodeLocalDNS란 무엇인가
NodeLocal DNSCache는 DNS 로컬 캐시 솔루션입니다. NodeLocal DNSCache는 클러스터 노드에서 DaemonSet를 실행하여 DNS 성능과 안정성을 향상시킵니다.
NodeLocalDNS가 필요한 이유
ClusterFirst DNS 모드의 Pod는 kube-dns의 serviceIP에 연결하여 DNS 쿼리를 수행할 수 있습니다. kube-proxy 구성 요소가 추가한 iptables 규칙을 통해 CoreDNS 엔드포인트로 변환되어 최종적으로 CoreDNS Pod에 요청됩니다.
각 클러스터 노드에서 DNS 캐시를 실행함으로써, NodeLocal DNSCache는 DNS 조회 지연 시간을 단축하고, DNS 조회 시간을 더 일관되게 만들며, kube-dns로 전송되는 DNS 쿼리 횟수를 줄일 수 있습니다.
클러스터에서 NodeLocal DNSCache를 실행하면 다음과 같은 이점이 있습니다:
- 로컬에 CoreDNS 인스턴스가 없는 경우, 가장 높은 DNS QPS를 가진 Pod는 다른 노드로 이동하여 해석해야 할 수 있습니다. NodeLocal DNSCache를 사용하면 로컬 캐시를 보유하여 지연 시간을 개선할 수 있습니다
- iptables DNAT 및 연결 추적을 건너뛰면 conntrack 경쟁을 줄이고 conntrack 테이블이 가득 차는 것을 방지할 수 있습니다 (위에서 언급한 5초 타임아웃 문제가 이로 인해 발생합니다)
- 로컬 캐시에서 kube-dns 서비스로의 연결은 TCP로 업그레이드될 수 있으며, TCP conntrack 항목은 연결이 종료될 때 삭제되지만 UDP 항목은 타임아웃되어야 합니다 (기본 nfconntrackudp_timeout은 30초)
- DNS 쿼리를 UDP에서 TCP로 업그레이드하면 손실된 UDP 데이터包 및 DNS 타임아웃으로 인한 테일 와잇 시간이 줄어듭니다. 일반적으로 30초까지 걸릴 수 있습니다 (3회 재시도 + 10초 타임아웃)
- NodeLocalDNS 사용 방법
NodeLocalDNS 배포
NodeLocal DNSCache를 설치하는 것은 매우 간단합니다. 공식 리소스 매니페스트를 직접 가져오면 됩니다:
wget -c https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
기본적으로 사용되는 이미지는 registry.k8s.io/dns/k8s-dns-node-cache이며, 이미지를 가져올 수 없는 경우 국내 docker.io/dyrnq/k8s-dns-node-cache로 교체할 수 있습니다.
cp nodelocaldns.yaml nodelocaldns.yaml.bak
sed -i 's#registry\.k8s\.io/dns/k8s-dns-node-cache#docker\.io/dyrnq/k8s-dns-node-cache#g' nodelocaldns.yaml
이 리소스 매니페스트 파일에는 몇 가지 변수가 포함되어 있으며, 각각의 의미는 다음과 같습니다:
__PILLAR__DNS__DOMAIN__: 클러스터 도메인을 나타내며, 기본값은cluster.local입니다. 쿠버네티스 클러스터 내부 서비스를 해석하는 도메인 접미사로 사용됩니다.__PILLAR__LOCAL__DNS__: DNSCache의 로컬 IP를 나타내며, NodeLocalDNS가 사용할 IP입니다. 기본값은 169.254.20.10입니다__PILLAR__DNS__SERVER__: kube-dns 이 Service의 ClusterIP를 나타내며, 일반적으로 기본값은 10.96.0.10입니다.kubectl get svc -n kube-system -l k8s-app=kube-dns -o jsonpath='{$.items[*].spec.clusterIP}'명령을 통해 가져올 수 있습니다
아래 두 변수는 신경 쓸 필요가 없습니다. NodeLocalNDS Pod가 자동으로 구성하며, kube-dns의 ConfigMap과 사용자 정의 Upstream Server 구성에서 파생된 값입니다. 다음과 같은 명령을 직접 실행하여 설치할 수 있습니다:
__PILLAR__CLUSTER__DNS__: 클러스터 내 쿼리를 위한 업스트림 DNS 서버를 나타내며, 일반적으로 kube-dns의 service IP를 가리키며, 기본값은 10.96.0.10입니다.__PILLAR__UPSTREAM__SERVERS__: 외부 쿼리를 위한 업스트림 서버를 나타내며, 사용자 정의 DNS 서비스가 없는 경우 kube-dns의 service ip를 입력할 수도 있습니다.
다음으로 해당 변수를 실제 값으로 바꾸면 다음과 같습니다:
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
domain=cluster.local
localdns=169.254.20.10
echo kubedns=$kubedns, domain=$domain, localdns=$localdns
주의할 점: kube-proxy 실행 모드에 따라 대체할 매개변수가 다릅니다. 다음 명령을 사용하여 kube-proxy가 있는 모드를 확인합니다
kubectl -n kube-system get cm kube-proxy -oyaml|grep mode
kube-proxy가 iptables 모드에서 실행 중인 경우 다음 명령을 실행하여 생성합니다
cp nodelocaldns.yaml nodelocaldns-iptables.yaml
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g;
s/__PILLAR__DNS__DOMAIN__/$domain/g;
s/__PILLAR__DNS__SERVER__/$kubedns/g" nodelocaldns-iptables.yaml
node-local-dns Pod는
PILLAR__CLUSTER__DNS와PILLAR__UPSTREAM__SERVERS를 설정합니다.
kube-proxy가 ipvs 모드에서 실행 중인 경우 다음 명령을 실행하여 생성합니다
cp nodelocaldns.yaml nodelocaldns-ipvs.yaml
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-ipvs.yaml
node-local-dns Pod는
PILLAR__UPSTREAM__SERVERS를 설정합니다.
그런 다음 대체된 yaml을 클러스터에 적용합니다:
#kubectl apply -f nodelocaldns-iptables.yaml
kubectl apply -f nodelocaldns-ipvs.yaml
다음 객체가 생성됩니다
serviceaccount/node-local-dns created
service/kube-dns-upstream created
configmap/node-local-dns created
daemonset.apps/node-local-dns created
service/node-local-dns created
생성이 완료되면 각 노드에 Pod가 실행됩니다. 여기에는 하나의 노드만 있으므로 하나만 실행됩니다
[root@caas ~]# kubectl -n kube-system get po
NAME READY STATUS RESTARTS AGE
node-local-dns-m8ktq 1/1 Running 0 8s
주의할 점은 여기서 hostNetwork=true를 사용하여 node-local-dns를 DaemonSet로 배포하면 호스트의
8080포트를 사용하므로 해당 포트가 사용되지 않도록 해야 합니다.
NodeLocalDNS 구성
위 단계에서 NodeLocal DNSCache를 배포했지만 매우 중요한 단계인 구성이 남아 있습니다. Pod가 NodeLocal DNSCache를 우선 DNS 서버로 사용하도록 구성해야 합니다.
다음과 같은 방법이 있습니다:
- 방법 1: kubelet의 dns nameserver 매개변수를 수정하고 노드 kubelet을 다시 시작합니다. 비즈니스 중단 위험이 있으므로 이 방법을 사용하지 않는 것이 좋습니다.
- 테스트 시에는 이 방법을 사용할 수 있으며 비교적 간단합니다
- 방법 2: Pod를 생성할 때 수동으로 DNSConfig를 지정합니다. 비교적 번거롭고 권장하지 않습니다.
- 방법 3: DNSConfig 동적 주입 컨트롤러를 사용하여 Pod 생성 시 DNSConfig 자동 주입을 구성합니다. 이 방법을 권장합니다.
- 웹훅을 직접 구현해야 하며, 방법 2를 자동화하는 것과 같습니다.
방법 1: kubelet 매개변수 수정
kubelet은 --cluster-dns와 --cluster-domain 두 매개변수를 통해 Pod DNSConfig를 전역적으로 제어합니다.
- cluster-dns: Pod를 배포할 때 기본적으로 사용되는 DNS 서버 주소이며, 기본적으로
kube-dns의 ServiceIP만 참조합니다. NodeLocalDNS의 169.254.20.10을 추가해야 합니다. - cluster-domain: Pod를 배포할 때 기본적으로 사용되는 DNS 검색 도메인이며, 기존 검색 도메인을 유지하면 됩니다. 일반적으로
cluster.local입니다.
/etc/systemd/system/kubelet.service.d/10-kubeadm.conf 구성 파일에서 NodeLocalDNS의 169.254.20.10 값을 설정하는 --cluster-dns 매개변수를 추가해야 합니다.
기존 앞에 --cluster-dns를 추가하는 것이며, 기존 것을 변경하는 것이 아닙니다.
이렇게 하면 Pod에 두 개의 dns nameserver가 있게 되며, 새로 추가된 것이 실패하더라도 이전 것을 사용할 수 있습니다.
vi /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
# --cluster-dns 추가
--cluster-dns=169.254.20.10 --cluster-dns=<kube-dns ip> --cluster-domain=<search domain>
그런 다음 kubelet을 다시 시작하여 적용합니다
sudo systemctl daemon-reload
sudo systemctl restart kubelet
방법 2: 사용자 정의 Pod dnsConfig
dnsConfig 필드를 통해 Pod의 dns 구성을 사용자 정의합니다. nameservers에는 NodeLocalDNS 외에도 KubeDNS를 지정하여 NodeLocalDNS에 문제가 발생해도 Pod의 DNS 해석에 영향을 주지 않습니다.
apiVersion: v1
kind: Pod
metadata:
name: alpine
namespace: default
spec:
containers:
- image: alpine
command:
- sleep
- "10000"
imagePullPolicy: Always
name: alpine
dnsPolicy: None
dnsConfig:
nameservers: ["169.254.20.10","10.96.0.10"]
searches:
- default.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "3"
- name: attempts
value: "2"
- name: timeout
value: "1"
- dnsPolicy:
None이어야 합니다. - nameservers: 169.254.20.10과 kube-dns의 ServiceIP 주소로 구성됩니다.
- searches: 검색 도메인을 설정하여 클러스터 내 도메인이 올바르게 해석되도록 합니다.
- ndots: 기본값은 5이며, 해석 효율성을 높이기 위해 적절히 낮출 수 있습니다.
방법 3: Webhook 자동 주입 dnsConfig
DNSConfig 동적 주입 컨트롤러는 새로 생성된 Pod에 DNSConfig를 자동으로 주입하여 수동으로 Pod YAML을 구성할 필요를 피할 수 있습니다. 이 애플리케이션은 기본적으로 node-local-dns-injection=enabled 레이블이 포함된 네임스페이스에서 새로 생성된 Pod 요청을 모니터링하며, 다음 명령을 사용하여 네임스페이스에 레이블을 지정할 수 있습니다.
배포 후에는 네임스페이스에 node-local-dns-injection=enabled 레이블을 지정하기만 하면 Webhook 검사가 해당 네임스페이스의 모든 Pod에 DNSConfig를 자동으로 구성합니다.
다음 편에서 간단한 구현을 만들어 보겠습니다.
- 부하 테스트
다음으로 부하 테스트를 진행하여 성능 향상을 확인해 보겠습니다.
여기서는 Pod가 모두 NodeLocalDNS를 사용하도록 kubelet 매개변수를 수정하는 방식을 임시로 사용하여 테스트를 용이하게 합니다.
테스트 환경:
1개 마스터 1개 워커의 k8s 클러스터, 노드 규칙은 통일 4C8G, 유휴 상태, 다른 부하 실행 안 함.
Kubernetes 튜토리얼(11)---KubeClipper를 사용하여 한 줄 명령으로 k8s 클러스터를 빠르게 생성하는 방법을 참조하여 클러스터를 빠르게 생성할 수 있습니다.
부하 테스트 스크립트
다음 파일을 사용하여 성능 테스트를 수행합니다
// dns_test.go
package main
import (
"context"
"flag"
"fmt"
"net"
"sync/atomic"
"time"
)
var targetHost string
var concurrentConnections int
var testDuration int64
var timeoutLimit int64
var timeoutCounter int64
func main() {
flag.StringVar(&targetHost, "target", "", "DNS resolve target")
flag.IntVar(&concurrentConnections, "conns", 100, "Number of concurrent connections")
flag.Int64Var(&testDuration, "duration", 0, "Test duration in seconds")
flag.Int64Var(&timeoutLimit, "timeout", 0, "Timeout limit in milliseconds")
flag.Parse()
var requestCount int64 = 0
var errorCount int64 = 0
connectionPool := make(chan interface{}, concurrentConnections)
testExit := make(chan bool)
var (
minLatency int64 = 0
maxLatency int64 = 0
totalLatency int64 = 0
)
go func() {
time.Sleep(time.Second * time.Duration(testDuration))
testExit <- true
}()
testLoop:
for {
select {
case connectionPool <- nil:
go func() {
defer func() {
<-connectionPool
}()
dnsResolver := &net.Resolver{}
startTime := time.Now()
_, err := dnsResolver.LookupIPAddr(context.Background(), targetHost)
responseTime := time.Since(startTime).Nanoseconds() / int64(time.Millisecond)
if minLatency == 0 || responseTime < minLatency {
minLatency = responseTime
}
if responseTime > maxLatency {
maxLatency = responseTime
}
totalLatency += responseTime
if timeoutLimit > 0 && responseTime >= timeoutLimit {
timeoutCounter++
}
atomic.AddInt64(&requestCount, 1)
if err != nil {
fmt.Println(err.Error())
atomic.AddInt64(&errorCount, 1)
}
}()
case <-testExit:
break testLoop
}
}
fmt.Printf("Total requests: %d\nError count: %d\n", requestCount, errorCount)
fmt.Printf("Response times: min(%dms) max(%dms) avg(%dms) timeouts(%d)\n", minLatency, maxLatency, totalLatency/requestCount, timeoutCounter)
}
먼저 golang 환경을 구성하고 위의 테스트 애플리케이션을 직접 빌드합니다:
go build -o dns_test dns_test.go
빌드가 완료되면 dns_test 이진 파일이 생성됩니다
노드 간 DNS 성능 테스트
먼저 노드 간 DNS 성능 테스트를 수행합니다. 클러스터 규모가 확장됨에 따라 CoreDNS 복제본 수와 노드 수가 명확히 1:1 비율을 유지할 수 없으므로 대부분의 DNS 요청은 노드 간에 발생하며, 이 성능은 정상적인 상황에서의 DNS 성능을 더 잘 반영합니다.
일반적으로 1:8 비율, 즉 8개 노드에 1개 CoreDNS Pod가 권장됩니다
먼저 CoreDNS 복제본 수를 1로 조정하여 테스트를 용이하게 합니다.
kubectl -n kube-system scale deploy coredns --replicas=1
이렇게 하면 두 노드에 하나의 CoreDNS Pod가 있으므로 노드 간 DNS 해석 성능을 테스트할 수 있습니다.
[root@dns-1 go]# kubectl get node
NAME STATUS ROLES AGE VERSION
dns-1 Ready control-plane 48m v1.27.4
dns-2 Ready <none> 48m v1.27.4
[root@dns-1 go]# kubectl -n kube-system get po -owide -l k8s-app=kube-dns
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-5d78c9869d-l7vgv 1/1 Running 0 12m 172.25.173.4 dns-1 <none> <none>
현재 CoreDNS가 dns-1 노드에 있으므로 테스트 Pod를 dns-2 노드에 지정하여 예약합니다.
overrides를 통해 nodeName을 직접 지정하여 Pod와 CoreDNS가 다른 노드에 분산되도록 합니다.
kubectl run dns-test-pod --image=busybox:latest --restart=Never --overrides='{ "spec": { "nodeName": "dns-2" } }' -- sleep 10000
그런 다음 이 이진 파일을 Pod에 복사하여 테스트합니다:
kubectl cp dns_test dns-test-pod:/
복사가 완료되면 이 테스트 Pod에 들어갑니다:
kubectl exec -it dns-test-pod -- /bin/sh
그런 다음 200개의 동시 연결, 30초 동안 부하 테스트를 수행하기 위해 dns_test 프로그램을 실행합니다:
# kube-dns.kube-system 주소를 해석합니다
/ # ./dns_test -target kube-dns.kube-system -conns 200 -duration 30 -timeout 5000
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
lookup kube-dns.kube-system on 10.96.0.10:53: no such host
Total requests: 131063
Error count: 23
Response times: min(1ms) max(15050ms) avg(39ms) timeouts(624)
평균 지연 시간이 약 39ms인 것을 확인할 수 있으며, 이 성능은 상당히 저조하며 일부 해석 실패 항목도 있습니다.
동일 노드 DNS 성능 테스트
busybox pod를 다시 생성하고 CoreDNS와 동일한 노드에 예약하여 동일 노드 DNS 해석 성능을 테스트합니다.
이론적으로 동일 노드 성능은 노드 간 성능보다 훨씬 향상될 것입니다
그런 다음 테스트를 위해 Busybox Pod를 생성하고, overrides를 통해 nodeName을 직접 지정하여 Pod와 CoreDNS가 다른 노드에 분산되도록 합니다.
kubectl delete pod dns-test-pod
kubectl run dns-test-pod --image=busybox:latest --restart=Never --overrides='{ "spec": { "nodeName": "dns-1" } }' -- sleep 10000
그런 다음 이 이진 파일을 Pod에 복사하여 테스트합니다:
kubectl cp dns_test dns-test-pod:/
복사가 완료되면 이 테스트 Pod에 들어갑니다:
kubectl exec -it dns-test-pod -- /bin/sh
그런 다음 200개의 동시 연결, 30초 동안 부하 테스트를 수행하기 위해 dns_test 프로그램을 실행합니다:
# kube-dns.kube-system 주소를 해석합니다
/ # ./dns_test -target kube-dns.kube-system -conns 200 -duration 30 -timeout 5000
Total requests: 217030
Error count: 0
Response times: min(1ms) max(5062ms) avg(26ms) timeouts(311)
대부분의 평균 지연 시간이 약 26ms인 것을 확인할 수 있으며, 이전의 40ms에 비해 약 50% 향상되었으며, 타임아웃 또는 실패 사례도 발생하지 않았습니다.
NodeLocalDNS 테스트
Pod를 직접 시작합니다
kubectl delete pod dns-test-pod
kubectl run dns-test-pod --image=busybox:latest --restart=Never -- sleep 10000
그런 다음 이 이진 파일을 Pod에 복사하여 테스트합니다:
kubectl cp dns_test dns-test-pod:/
복사가 완료되면 이 테스트 Pod에 들어갑니다:
kubectl exec -it dns-test-pod -- /bin/sh
그런 다음 200개의 동시 연결, 30초 동안 부하 테스트를 수행하기 위해 dns_test 프로그램을 실행합니다:
Pod의 DNS Nameserver를 169.254.20.10(NodeLocalDNS 주소)로 지정한 후 다시 테스트합니다
vi /etc/resolv.conf
다음 내용을 추가합니다
nameserver 169.254.20.10
그런 다음 다시 테스트합니다
/ # ./dns_test -target kube-dns.kube-system -conns 200 -duration 30 -timeout 5000
Total requests: 224103
Error count: 0
Response times: min(1ms) max(5057ms) avg(24ms) timeouts(333)
평균 지연 시간이 24ms인 것을 확인할 수 있으며, 노드 간의 39ms에 비해 50% 향상되었으며, 동일 노드의 26ms에 가까워 노드 간 DNS 해석에 상당한 성능 손실이 있음을 보여줍니다.
NodeLocalDNS와 동일 노드 비교 시 여전히 약간의 성능 향상이 있습니다. 이유는 다음과 같습니다:
- CoreDNS에 액세스하는 데는 service의 clusterIP 10.96.0.10이 사용되며, 최종적으로 iptables/ipvs 등 규칙을 통해 백엔드 CoreDNS Pod로 전달됩니다
- NodeLocalDNS에 액세스하는 데는 link-local ip 169.254.20.10이 사용되며, iptables/ipvs 규칙을 건너뛰고 바로 NodeLocalDNS Pod로 들어가기 때문입니다.
따라서 약간의 성능 향상이 있습니다.
- NodeLocal DNSCache 작동 원리
이 부분에서는 NodeLocal DNSCache 작동 원리를 분석합니다.
작동 원리 분석
NodeLocalDNS는 실제로 각 노드에 캐시를 추가한 것과 유사합니다. CDN처럼 중앙 CoreDNS를 원본 사이트로 볼 때, node-local-dns는 서로 다른 영역에서 실행되는 캐시입니다.
Pod는 로컬 NodeLocalDNS에서 DNS 해석을 우선 수행하고, 데이터가 있으면 직접 반환하며, 그렇지 않으면 NodeLocalDNS가 KubeDNS를 찾아 해석한 후 로컬에 데이터를 캐시합니다.
구체적인 흐름은 Alibaba Cloud 문서의 그림과 같습니다:
먼저 제어면, Pod를 생성할 때 Admission Webhook은 자동으로 DNSConfig를 주입하며, 이미 DNSConfig가 주입된 Pod와 주입되지 않은 Pod는 다른 상황을 가집니다.
다음과 같이 구체적으로 설명됩니다:
1) DNS 로컬 캐시가 주입된 Pod는 기본적으로 NodeLocal DNSCache가 노드에서 수신 대기하는 IP(169.254.20.10)를 통해 도메인을 해석합니다.
Pod 내의 DNS 구성은 다음과 같습니다:
/ # cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 169.254.20.10
nameserver 10.96.0.10
options ndots:5
169.254.20.10이 첫 번째 nameserver이므로 우선 사용됩니다.
2) NodeLocal DNSCache 로컬에 캐시된 응답이 없으면 CoreDNS를 통해 해석을 요청합니다.
NodeLocalDNS의 Corefile에서 관련 구성은 다음과 같습니다:
.:53 {
errors
cache 30
reload
loop
bind 169.254.20.10 __PILLAR__DNS__SERVER__
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
해석할 수 없는 경우 업스트림 서비스, 즉 kube-dns로 전달됩니다.
3) DNS 로컬 캐시가 주입된 Pod는 NodeLocal DNSCache에 연결할 수 없는 경우, kube-dns 서비스를 통해 CoreDNS에 연결하여 해석을 계속 수행합니다. 이 연결은 대체 연결입니다.
Pod의 DNS 구성:
/ # cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 169.254.20.10
nameserver 10.96.0.10
options ndots:5
Kube-dns에 해당하는 IP 10.96.0.10도 두 번째 nameserver로 사용되므로 NodeLocal DNS에 문제가 발생하면 Pod도 정상적으로 DNS 해석을 수행할 수 있습니다.
4) DNS 로컬 캐시가 주입되지 않은 Pod는 표준 kube-dns 서비스 연결을 통해 CoreDNS에 연결하여 해석합니다.
DNSConfig가 주입되지 않은 Pod의 기본 DNS 구성은 다음과 같습니다:
/ # cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
자연스럽게 kube-dns를 직접 요청합니다.
5) CoreDNS는 클러스터 내 도메인이 아닌 경우 현재 노드의 /etc/resolv.conf를 통해 외부 DNS 서버로 전달합니다.
Kube-dns의 Corefile에서 관련 구성은 다음과 같습니다:
.:53 {
errors
health {
lameduck 5s
}
// 생략...
forward . /etc/resolv.conf {
max_concurrent 1000
}
}
다른 관련 구성은 생략되었습니다. forward . /etc/resolv.conf는 해석할 수 없는 요청을 만나면 /etc/resolv.conf 파일의 구성에 따라 전달됨을 의미합니다.
반면 CoreDNS Pod의 /etc/resolv.conf 파일은 Pod가 시작될 때 현재 노드에서 복사되므로 실제로 어디로 전달되는지는 Pod가 시작될 때 노드의 /etc/resolv.conf 구성과 관련이 있습니다.
왜 169.254.20.10인가?
왜 169.254.20.10 이라는 IP에 액세스하면 NodeLocalDNS에 액세스할 수 있는가?
NodeLocalDNS는 DaemonSet 방식으로 실행되므로 클러스터의 각 노드에 Pod가 시작됩니다. 이 Pod는 현재 노드에 네트워크 인터페이스를 추가하고 IP를 169.254.20.10으로 지정합니다.
다음과 같습니다:
47: nodelocaldns: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 56:9b:08:18:a6:75 brd ff:ff:ff:ff:ff:ff
inet 169.254.20.10/32 scope global nodelocaldns
valid_lft forever preferred_lft forever
NodelocalDNS는 hostNetwork 네트워크 모드로 시작되며, 앞에 추가된 네트워크 인터페이스에 해당하는 IP (169.254.20.20)에서 서비스를 시작합니다.
우리가 앞서 구성한 내용(kubelet 수정 또는 Pod의 dnsConfig)에 따라 Pod 내의 가장 높은 우선순위 DNS 서버는 169.254.20.20이므로 Pod가 DNS 해석이 필요할 때 169.254.20.10에 우선 액세스하며, 최종적으로 동일 노드의 NodelocalDNS Pod에서 요청이 처리됩니다.
이 네트워크 인터페이스를 추가하는 구체적인 역할은 다음과 같습니다:
- 로컬 DNS 서비스:
nodelocaldns는 각 노드에서 실행되며, 169.254.20.10 주소를 수신 대기하여 로컬 DNS 서비스를 제공합니다. 이 주소는 link-local 주소이며 로컬 노드에서만 사용 가능합니다. Pod 내의 DNS 쿼리는 이 주소로 리디렉션되어 노드 내에서 서비스 도메인을 해석할 수 있습니다. - DNS 쿼리가 노드를 벗어나지 않도록 방지:
nodelocaldns가 노드 내에서 DNS 해석 서비스를 제공하므로 이 네트워크 인터페이스는 DNS 쿼리가 노드를 벗어나지 않도록 보장합니다. 이는 클러스터 내 DNS 쿼리에 매우 효율적이며, 노드를 벗어나지 않고도 서비스 도메인을 해석할 수 있습니다. - DNS 조회 지연 시간 감소:
nodelocaldns가 각 노드에서 실행되므로 노드 내 DNS 쿼리는 더 빠르게 완료될 수 있으며, 클러스터 네트워크를 거칠 필요가 없습니다.
간단한 실험을 해보겠습니다
# 새 네트워크 인터페이스 mynic 생성
sudo ip link add mynic type dummy
# eth1에 IP 주소 할당
sudo ip addr add 1.1.1.1/24 dev mynic
# 프로그램을 시작하여 지정된 IP 주소에서 수신 대기
# 예: Python 기반 간단한 HTTP 서버:
python3 -m http.server 9090 --bind 1.1.1.1
동일 노드에서 새 터미널을 열어 액세스할 수 있는지 테스트합니다
curl 1.1.1.1:9090
직접 액세스할 수 있으며, NodeLocalDNS가 네트워크 인터페이스를 추가하는 것이 이 역할입니다.
왜 169.254.20.10 이라는 IP인가?
그 이유는 169.254.0.0/16 주소 범위가 link-local 통신에 전용으로 사용되기 때문입니다. 이는 이 주소가 동일한 서브넷 내에서만 사용 가능하며 라우터를 통해 통신할 필요가 없음을 의미합니다.
이 네트워크에서 169.254.20.10을 .1, .2와 같은 것 대신 사용하는 것은 충돌을 피하기 위해 몇 개의 위치를 남겨두기 위한 것입니다.
- 요약
CoreDNS 자체 성능이 저하되는 이유는 노드 간 액세스로 인한 상당한 성능 손실과 커널 DNAT 버그로 인한 타임아웃 문제 때문입니다.
NodeLocal DNSCache는 다음과 같은 이점이 있습니다:
- 평균 DNS 조회 시간 감소
- Pod에서 로컬 캐시로의 연결은 conntrack 테이블 항목을 생성하지 않습니다. 이를 통해 conntrack 테이블이 가득 차고 경쟁 조건으로 인한 연결 중단 및 연결 거부를 방지할 수 있습니다.
NodeLocalDNS를 사용한 후 성능 향상이 약 40%에 달하며, DNS 해석 지연이 39ms에서 24ms로 감소하고 오류 횟수도 크게 감소했습니다.
NodeLocalDNS는 DaemonSet 방식으로 시작되어 각 노드에 Pod를 시작하고, hostnetwork + link-local 주소를 사용하여 Pod의 DNS 요청이 로컬 NodeLocalDNS Pod에만 요청되도록 보장하여 노드 간 문제를 피하고 성능을 크게 향상시킵니다.
마지막으로 NodeLocalDNS는 link-local 주소를 사용하여 기본적으로 service의 clusterIP를 사용할 때 필요한 iptables/ipvs 등 규칙을 건너뛰는 문제를 피하고, 동일 노드 기반에서도 약간의 성능 향상을 구현합니다.
따라서 대규모 클러스터에서 고빈도 DNS 요청이 있는 경우 NodeLocal DNSCache를 사용하는 것이 권장됩니다.
- 참고 자료
쿠버네티스 클러스터에서 NodeLocal DNSCache 사용
NodeLocal DNSCache 사용
DNS 타임아웃 문제 분석
DNS 부하 테스트
lixd/nodelocaldns-admission-webhook