쿠버네티스 운영 실무 · 노드에 문제가 생긴 뒤의 5분 · 이론
노드가 죽은 뒤의 5분은 누가 정했나
한 줄 요약
노드에 문제가 생기면 노드 컨트롤러가 NoExecute 테인트를 붙이고, 그때부터 파드가 언제 사라지는지는
파드의 tolerationSeconds 가 정한다. 대부분의 파드는 이 값을 적은 적이 없고, 어드미션이 넣어 준 300초로
살고 있다.
왜 이 값을 알아야 하나
드레인은 사람이 친다. 시각도 대상도 우리가 고른다. 그런데 새벽에 불려 나가는 사고는 대개 그 반대다 —
아무도 명령을 치지 않았는데 파드가 옮겨 다니기 시작하고, 어떤 워크로드는 너무 빨리 옮겨 가서 로컬
캐시를 다 잃고, 어떤 워크로드는 너무 오래 죽은 노드에 붙어 있어서 트래픽이 빈 곳으로 간다. 이 두 가지가
같은 클러스터에서 동시에 일어나는 이유는 단순하다. 두 워크로드가 같은 값을 쓰고 있기 때문이다.
그 값은 300초다. 쿠버네티스는 파드를 만들 때 node.kubernetes.io/not-ready 와node.kubernetes.io/unreachable 두 키에 대한 NoExecute 톨러레이션을 tolerationSeconds 300 으로
자동으로 붙인다. 여러분이나 컨트롤러가 그 톨러레이션을 명시하지 않았을 때만이다. 공식 문서의 표현대로
자동으로 추가된 이 톨러레이션 때문에 파드는 문제가 감지된 뒤 5분 동안 노드에 묶여 있게 된다.
어떻게 동작하나
먼저 노드 쪽이다. kubelet 은 주기적으로 자기 Lease 를 갱신해 살아 있음을 알린다. 이 갱신이 끊기면
노드 컨트롤러가 노드의 Ready 컨디션을 Unknown 으로 바꾸고, 그에 대응하는 테인트를 붙인다.
컨디션과 테인트는 이렇게 짝지어져 있다.
| 노드 컨디션 | 붙는 테인트 | 기본 효과 |
| --- | --- | --- |
| Ready = False | node.kubernetes.io/not-ready | NoExecute |
| Ready = Unknown | node.kubernetes.io/unreachable | NoExecute |
| MemoryPressure | node.kubernetes.io/memory-pressure | NoSchedule |
| DiskPressure | node.kubernetes.io/disk-pressure | NoSchedule |
| PIDPressure | node.kubernetes.io/pid-pressure | NoSchedule |
| NetworkUnavailable | node.kubernetes.io/network-unavailable | NoSchedule |
스케줄러가 노드 컨디션이 아니라 테인트를 본다는 점이 중요하다. 컨디션을 직접 보게 하면 배치 결정이
컨디션 종류만큼 갈라지지만, 테인트로 한 번 번역해 두면 배치도 축출도 같은 규칙 하나로 처리된다.
그다음이 효과의 차이다. NoSchedule 은 새로 앉히는 것만 막고 이미 떠 있는 파드는 건드리지 않는다.PreferNoSchedule 은 그 약한 판이다. NoExecute 만이 이미 떠 있는 파드를 내보낸다. 이때
톨러레이션이 세 갈래로 갈린다. 견디는 톨러레이션이 없으면 즉시 축출, 견디되 tolerationSeconds 가
없으면 영원히 남고, 초가 적혀 있으면 그 초만큼 버틴 뒤 축출된다. 테인트가 그 전에 걷히면 축출은
일어나지 않는다.
DaemonSet 은 예외다. DaemonSet 컨트롤러가 만드는 파드에는 not-ready 와 unreachable 두 키에 대한
NoExecute 톨러레이션이 tolerationSeconds 없이 붙는다. 노드가 흔들린다고 노드 에이전트까지 빠지면
그 노드를 관측하거나 복구할 수단이 함께 사라지기 때문이다.
한 가지 더. 축출이 한꺼번에 몰리는 것을 막기 위해 컨트롤 플레인은 노드에 새 테인트를 붙이는 속도를
제한한다. 대규모 네트워크 단절에서 클러스터 전체가 한순간에 재배치되는 일을 막는 장치다.
그리고 1.29 이후로는 이 축출 구현이 노드 컨트롤러에서 떨어져 나와 taint-eviction-controller 라는
별도 컨트롤러가 맡는다.
현장에서 만나는 모습
가장 흔한 사고는 짧은 네트워크 흔들림에 스테이트풀 워크로드가 통째로 옮겨 가는 것이다.
40초짜리 스위치 재시작인데 300초 뒤에는 파드가 이미 다른 노드에서 빈 로컬 디스크를 보며 처음부터
데이터를 받고 있다. 이런 워크로드에는 tolerationSeconds 를 길게 주는 편이 낫다 — 공식 문서도
로컬 상태가 많은 애플리케이션을 네트워크 단절 상황에서 오래 노드에 묶어 두고 싶을 수 있다고 적고
그 예로 6000초를 든다.
반대 방향의 사고도 같은 값에서 나온다. 노드가 진짜로 죽었는데 300초 동안 서비스 레플리카가 모자란
채로 돌아간다. 프런트엔드처럼 어디서 떠도 같은 워크로드라면 이 5분이 통째로 손해다. 다만 이런 곳은
값만 줄이기보다 레플리카를 늘리고 토폴로지를 흩는 편이 보통 낫다 — 값만 줄이면 흔들릴 때마다
재배치가 잦아져 오히려 불안정해진다.
세 번째는 테인트를 걷는 것을 잊는 것이다. 점검하려고 NoExecute 를 걸어 파드를 비웠는데 점검이
끝난 뒤 테인트를 지우지 않으면 그 노드는 계속 비어 있다. 클러스터 용량이 3분의 1 줄어든 채로 몇 주가
지나가는 일이 드물지 않다.
이 실습 환경의 한계
이 클러스터의 노드는 kwok 이 만든 가짜 노드라 진짜 kubelet 이 없다. 그래서 하트비트를 끊어
노드 컨트롤러가 스스로 테인트를 붙이게 만들 수는 없다. 게다가 여기서 한 가지를 직접 재 보았다 —
Ready 인 노드에 node.kubernetes.io/unreachable:NoExecute 를 손으로 붙여도 몇 초 안에 사라진다.
그 두 키의 테인트는 노드 컨트롤러가 노드 컨디션을 보고 직접 관리하는 것이라서, 컨디션이 정상인
노드에 붙은 것은 잘못된 상태로 보고 걷어 가기 때문이다(노드 status 를 직접 고쳐 Ready 를 False 로
바꾸는 것도 kwok 이 곧바로 되돌린다). 그래서 실습에서는 사용자 정의 키로 같은 NoExecute 테인트를
건다. 키만 다를 뿐 축출 규칙은 글자 그대로 같고, 그 뒤에 일어나는 축출 — 누가 언제 사라지는가 — 은
진짜 컨트롤러가 하는 진짜 동작이다.
또 kwok 파드는 컨테이너가 아니라서 컨테이너 로그도 exec 도 OOM 도 없다. 파드가 옮겨 가며 데이터를
다시 받는 장면은 이 환경에서 볼 수 없고, 우리가 볼 수 있는 것은 오브젝트가 사라지는 시각이다.
이 실습이 다루는 것이 정확히 그 시각이다.
다음 실습에서 할 것
건드리지 않을 비교군을 한 노드에 세우고, 아무도 적지 않은 기본 톨러레이션을 눈으로 확인한다.
그다음 견디는 시간만 다른 파드 둘을 다른 노드에 올려 NoSchedule 에서는 아무 일도 일어나지 않고
NoExecute 에서는 하나가 실제로 사라지는 것을 본다. 이어서 다른 노드에 링크 단절을 흉내 내는
NoExecute 테인트를 걸어 20초와 3600초의 차이를 재고, 네임스페이스 전체의 축출 시각표를 계산하는
스크립트를 만든다.
마지막에는 상태를 가진 워크로드에 줄 값을 정해 클러스터에 올리고 그 근거를 남긴다.
참고 문서:
- https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
- https://kubernetes.io/docs/concepts/architecture/nodes/