Skip to content

Instantly share code, notes, and snippets.

@juner417
Created September 6, 2023 14:24
Show Gist options
  • Select an option

  • Save juner417/be1bbb3e5c03ed43d3a74965b2e88d5c to your computer and use it in GitHub Desktop.

Select an option

Save juner417/be1bbb3e5c03ed43d3a74965b2e88d5c to your computer and use it in GitHub Desktop.
k8s dns lookup timeout

dns delay issue

증상:

  • k8s pod에서 api 호출시 간헐적인 timeout 발생(5s)
  • 사용자 클러스터에서 확인을 해보니 dns resolving시 5초 정도의 딜레이가 생기고 다시 리졸빙한 이력이 있다(tcpdump 내용)
  • A(ipv4), AAAA(ipv6)로 동시에 요청을 보내는데 응답이 안오는 경우가 간헐적으로 있고, 5초 정도의 딜레이가 생기고 다시 요청한다.

추가 확인

  • dns resolving시 pod → kube-dns → local dns resolver 로 dns search를 하는 과정에서 no such domain의 딜레이가 발생함
    • no such domain(NXDOMAIN)의 경우는 k8s의 dns resolving시 클러스터 dns의 kube-dns를 조회하는 과정에서 생긴 결과이고,
    • kube-dns에서 search domain이 결과가 없는 경우, 호스트의 resolv.conf를 이용하여 외부 nameserver search(클러스터 도메인 조회 과정)
  • 하지만 A(ipv4), AAAA(ipv6) 리졸빙의 응답이 없는 경우가 발생함 - 이상한 부분...
# source pod
1. dns 질의를 요청하는 pod
2. 해당 pod에서 tcpdump를 보면 A, AAAA 두개의 요청 중 1개의 응답 미수신 (A의 응답은 있지만, AAAA의 응답이 없음 - 매번 같은 패턴 아님)
3. 5초 동안 기다리다 retry (dns query timeout이 5초)
4. 1의 요청 타임아웃

# dest pod(coredns)
1. A dns query 요청 수신
2. A dns query. 응답
-> AAAA dns query 없음

#AAAA(ipv6)가 응답이 없을 경우도 있고, A(ipv4)가 없을 경우도 발생함

확인 과정

물리 구간 확인

  • 우선 네트워크 팀에서 물리 네트워크 및 가상 네트워크 구간의 tcpdump 확인
    • 분석 결과의 내용을 확인해 보면 src에서는 a, aaaa로 dns 질의를 하는데(kube-dns 동작 방식)
    • 둘중 하나만 요청의 결과가 오고(no such domain) 하나는 응답이 없어서 딜레이가 발생함
      • 응답이 없는 경우 5초 되에 재시도 하고 응답 받음
    • dest인 core dns pod에서는 요청이 둘중 하나만 들어옴(core dns가 요청을 받기 이전에서 패킷이 유실됐을 가능성 확인)
      • 재시도 요청은 정상적으로
  • src pod node → dest pod node간 스위치(tor)에서는 패킷 유실 없음

클러스터 내부 확인

linux conntrack

  • iptable의 dnat는 conntrack 이라는 커널 모듈에 구현되어 있고 connection tracking 메카니즘으로 동작한다.
  • conntrack의 각각의 커넥션은 2가지 튜플을 나타내는데
    • 하나는 original request(IP_CT_DIR_ORIGINAL)
    • 다른 하나는 reply이다(IP_CT_DIR_REPLY)
  • udp의 경우 각 튜플은 src ip addr, src port, dest ip addr, dest port로 구성된다.
    • reply 튜플에는 src필드에 저장된 real ip가 포함된다.
    • 예를 들면 10.40.0.17 → 10.32.0.6
      • original request: src=10.40.0.17 dst=10.96.0.10(svc ip) sport=53378 dport=53
      • reply: src=10.32.0.6 dst=10.40.0.17 sport=53 dport=53378
    • 위와 같은 항목으로 dnat 규칙을 다시 통과할 필요가 없고, 관련 패킷의 대상 및 소스 주소를 그에 따라 수정할수 있다.
  • conntrack entry가 생성되면 그것은 처음에 confirm되지 않는다. 나중에 동일한 original 튜플이나 reply 튜플이 포함된 confirmed conntrack이 없는 경우 커널은 entry를 confirm한다.

A simplified flow of the conntrack creation and DNAT is shown below:

+---------------------------+      Create a conntrack for a given packet if
|                           |      it does not exist; IP_CT_DIR_REPLY is
|    1. nf_conntrack_in     |      an invert of IP_CT_DIR_ORIGINAL tuple, so
|                           |      src of the reply tuple is not changed yet.
+------------+--------------+
             |
             v
+---------------------------+
|                           |
|     2. ipt_do_table       |      Find a matching DNAT rule.
|                           |
+------------+--------------+
             |
             v
+---------------------------+
|                           |      Update the reply tuples src part according
|    3. get_unique_tuple    |      to the DNAT rule in a way that it is not used
|                           |      by any already confirmed conntrack.
+------------+--------------+
             |
             v
+---------------------------+
|                           |      Mangle the packet destination port and address
|     4. nf_nat_packet      |      according to the reply tuple.
|                           |
+------------+--------------+
             |
             v
+----------------------------+
|                            |     Confirm the conntrack if there is no confirmed
|  5. __nf_conntrack_confirm |     conntrack with either the same original or
|                            |     a reply tuple; increment insert_failed counter
+----------------------------+     and drop the packet if it exists.

conntrack race condition

  • 문제는 두개의 udp(connection-less protocol)패킷이 같은 소켓으로 다른 스레드에서 동시에 보내질때 발생한다.
  • 이 udp entry는 패킷이 전송될떄만 생성된다. 이는 아래의 경쟁을 일으킨다.
    • 어떤 패킷도 nf_conntrack_in 단계에서 확인된 conntrack을 찾지 못한다. 두 패킷 모두에 대해 동일한 튜플을 가진 두개의 conntrack 항목이 생성됨 - (1)
    • 하나의 패킷의 entry가 get_unique_tuple이 호출되기 전에 승인(먼저 승인된 패킷의 conntrack은 nat rule이 적용된 conntrack)된다. 그러면 또다른 패킷은 일반적으로 소스 포트가 변경되어 다른 reply 튜플을 얻는다. -(2)
    • 맨 첫번째 경우와 마찬가지지만, 사로다른 엔드포인트가 선택된 두개의 룰이 ipt_do_table 단계에서 (하나는 nat rule이 적용된 conntrack, 다른 하나는 nat rule이 적용 안된 conntrack) -(3)
  • 이 경쟁의 결과는 동일하다. - 하나의 패킷이 __nf_conntrack_confirm 단계에서 드롭된다.
    • dns case 에서 이 경우가 발생하는데 gnu c 라이브러리 그리고 musl libc 에서 A 그리고 AAAA dns lookup을 병렬로 진행한다. 하나의 udp 패킷은 이 경쟁중 커널에서 드롭된다. 그래서 클라이언트는 대부분 5초 의 타임아웃을 가지고 재시도 한다.
    • 이건 k8s만이 아닌 모든 udp 패킷을 병렬로 보내는 리눅스 시스템에서 발생할수 있다.
    • 심지어 nat룰이 없더라도 두번째 경쟁은 발생할수 있다. - get_unique_tuple의 호출을 활성화 하려면 nf_nat 커널 모듈을 로드 하는것만으로도 충분하다(nf_nat 커널 모둘이 로드 되면 get_unique_tuple이 호출된다. )
    • conntrack -S 를 이용하여 얻을수 있는 insert_failed 카운터는 문제가 발생했는지 여부를 확인하는 지표이다.

해결책...

insert_failed drop 확인

# src node - dns 질의시 응답 없었던 pod이 떠있던 노드1
cpu=0   	found=1476 invalid=2879 ignore=12322311 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=4797
cpu=1   	found=1618 invalid=2778 ignore=12441441 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=4840
cpu=2   	found=1623 invalid=2934 ignore=12419363 insert=0 insert_failed=2 drop=2 early_drop=0 error=0 search_restart=5298
cpu=3   	found=1505 invalid=2848 ignore=12377746 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=4952
cpu=4   	found=1540 invalid=2834 ignore=12388949 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=4847
cpu=5   	found=1617 invalid=2850 ignore=12399998 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=5386
cpu=6   	found=1602 invalid=2920 ignore=12420769 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=4977
cpu=7   	found=5275 invalid=51429 ignore=14295991 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=67525

# src node - dns 질의시 응답 없었던 pod이 떠있던 노드2
cpu=0   	found=82 invalid=2266 ignore=8317784 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=2729
cpu=1   	found=106 invalid=2163 ignore=8441126 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=2912
cpu=2   	found=101 invalid=2203 ignore=8437946 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=3354
cpu=3   	found=117 invalid=2305 ignore=8442352 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=2784
cpu=4   	found=93 invalid=2219 ignore=8443996 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=2898
cpu=5   	found=94 invalid=2174 ignore=8417693 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=2940
cpu=6   	found=100 invalid=2282 ignore=8438903 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=3440
cpu=7   	found=1667 invalid=7123 ignore=9790503 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=43224

# 두 노드 모드 sudo conntrack -S 로 확인 결과 insert_failed 발생하여 drop 발생

결론

  • aaaa, a 리졸빙이 패킷이 같은 소켓으로 다른 스레드에서 동시에 보내질때 발생하는 conntrack drop이 원인일 가능성이 높다(이슈가 발생한 노드의 insert_failed 증가 이력이 있음)해결책으로는
    • 1, ke에서는 커널패치를 준비해야 한다.
      1. 1번이 완벽한 해결책은 아니므로 다른 내용도 같이 확인 필요
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment