--- kind: CASE slug: two-ways-to-lose-a-node title: 노드를 잃는 두 가지 — 저장소가 같이 죽는 것과 들어갈 길이 없는 것 topic: losing-a-node-or-the-store topicName: PostgreSQL 을 내리고 노드 전원을 뽑았을 때 project: keycloak-session-store status: 게시 전 lastVerifiedOn: sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af source: - final/document.md#선택의-이유와-지킨-경계-a4 assets: - key: lab-topology file: ../../../final/assets/lab-topology/lab-topology.svg - key: a4-two-node-losses file: ../../../final/assets/a4-two-node-losses/a4-two-node-losses.svg evidence: - ../../../final/evidence/raw/a4-node-loss__02-worker-node-killed.txt - ../../../final/evidence/raw/a4-node-loss__03-state-during-loss.txt - ../../../final/evidence/raw/a4-node-loss__04-eviction-timing.txt --- # 노드를 잃는 두 가지 — 저장소가 같이 죽는 것과 들어갈 길이 없는 것 두 노드짜리 클러스터에서 노드를 하나씩 잃어 봤다. 워커를 끊었더니 외부 응답이 503 이 됐고, 컨트롤 플레인을 끊었더니 연결 자체가 되지 않아 000 이 됐다. 복구는 양쪽 다 가상머신을 다시 켜고 60 초였다. ## 관계 - **200 과 토큰을 받은 로그인 네 건이 데이터베이스에 없었다** 거기서는 데이터베이스 프로세스만 끊었고 여기서는 데이터베이스가 노드째 사라진다. 잃는 범위가 달라서 복구 절차도 갈린다. - **readiness 가 깨진 노드를 시야에서 먼저 치운다** 죽은 노드의 파드가 READY 로 남고 살아 있는 노드의 파드가 READY 에서 빠지는 역전이 이 실험에서 나왔다. - **장애 시간의 대부분은 알아채는 데 걸린다** 그 기준의 근거가 이 실험의 타이머 두 개다. 감지가 끝나기 전에는 복구 절차가 시작되지도 않는다. ## 문제 이 실험대는 노드가 둘이고 그중 kc-lab-2 한 곳에 PostgreSQL 과 Redis 가 몰려 있다. 진입 경로인 traefik 과 k3s API 서버는 반대쪽 kc-lab-1 에 있다. 워크로드를 두 노드에 나눠 띄웠으니 노드 하나를 잃어도 서비스가 이어져야 한다. 그런데 어느 노드를 잃느냐에 따라 남는 것이 다르고, 그것을 재기 전에는 두 경우가 같은 장애로 보인다. ## 결론 노드 하나를 잃으면 양쪽 다 전면 장애였고 원인은 달랐다. 워커(kc-lab-2) 상실 : 외부 응답 503 · kubectl 정상 · 원인은 데이터베이스가 같이 죽은 것 컨트롤 플레인(kc-lab-1) 상실 : 외부 응답 000 · kubectl 불통 · 원인은 들어갈 길이 없는 것 복구 : 양쪽 다 가상머신을 다시 켜고 60 초 컨트롤 플레인을 잃었을 때 keycloak-0 은 계속 돌고 있었다. 워크로드를 이중화해도 진입점이 한 노드에 있으면 그 노드가 서비스의 상한이 된다. 죽은 노드의 파드가 더 건강해 보였다. 전원이 끊긴 kc-lab-2 의 keycloak-0 : Running · READY true · up 0 살아 있는 kc-lab-1 의 keycloak-1 : Running · READY false · up 1 파드 상태는 그 노드의 kubelet 이 보고하는데 노드가 죽으면 보고할 주체가 없어져 마지막 값이 그대로 보인다. 축출까지 걸리는 시간은 설정 두 개의 합이다. node-monitor-grace-period : 40 초 NoExecute taint 의 tolerationSeconds : 300 초 합 : 5 분 40 초 그리고 축출이 시작돼도 StatefulSet 인 keycloak-0 은 대체 파드를 만들지 않았다. 이름이 같아야 하므로 Terminating 이 풀리기를 기다린다. ## 검증 환경 클러스터 : k3s · 노드 둘 kc-lab-1 : k3s server (컨트롤 플레인) · traefik · keycloak-1 kc-lab-2 : k3s agent · keycloak-0 · PostgreSQL · Redis 진입 경로 : 호스트 nginx 가 TLS 를 끝내고 traefik 으로 넘긴다 Keycloak 워크로드 종류 : StatefulSet 노드를 죽인 방법 : virsh destroy · 게스트에 종료 신호를 보내지 않는다 복구 방법 : virsh start 실험대 test-server : Arch Linux, 12GB, WiFi only 측정일 : 증거 원문에 시각만 있고 날짜가 없다 ## 재현 조건 1. 노드 둘로 클러스터를 띄우고 한쪽에 데이터베이스를, 다른 쪽에 진입 경로를 둔다. 어느 노드에 무엇이 있는지를 먼저 적어 두지 않으면 두 결과를 가를 수 없다. 2. 워커 노드의 전원을 끊는다. virsh destroy 로 끊는다. 게스트에 종료 신호가 가지 않아야 갑작스러운 상실이 된다. 3. 끊은 시각을 적고 15 초 간격으로 세 가지를 함께 기록한다. 노드 상태 : kubectl get nodes 파드 상태와 노드 이름 : kubectl get pods -o wide 외부 응답 코드 : 진입점 주소로 요청 4. 같은 시점에 Prometheus 의 up 지표도 읽는다. 파드 상태와 어긋나는지 보려는 것이므로 따로 읽어야 한다. 5. 파드의 tolerationSeconds 를 조회하고 축출이 실제로 일어나는 시점까지 30 초 간격으로 관찰한다. kubectl get pod -o jsonpath='{.spec.tolerations}' 6. 대체 파드가 만들어지는지 워크로드 종류별로 나눠 본다. StatefulSet 과 Deployment 의 동작이 여기서 갈린다. 7. virsh start 로 노드를 되살리고 외부 응답이 돌아오기까지 걸리는 시간을 잰다. 8. 컨트롤 플레인 노드로 2 번부터 다시 한다. 이때는 kubectl 이 통하지 않으므로 외부 응답과 호스트 쪽 기록만으로 관찰한다. ## 본문 ## 무엇이 어느 노드에 있었나 노드는 둘이다. kc-lab-1 이 k3s server 이자 컨트롤 플레인이고 traefik 과 `keycloak-1` 이 여기 있다. kc-lab-2 는 agent 이고 `keycloak-0` 과 PostgreSQL, Redis 가 함께 있다. 호스트의 nginx 가 TLS(Transport Layer Security) 를 끝내고 kc-lab-1 의 traefik 으로 넘긴다. ![호스트 nginx 가 kc-lab-1 의 traefik 으로 프록시하고, 그 아래 두 VM 에 Keycloak 과 데이터 저장소가 나뉘어 있는 구성](../../../final/assets/lab-topology/lab-topology.svg) Keycloak 은 두 노드에 하나씩 떠 있어 이중화돼 있다. 저장소는 그렇지 않다. PostgreSQL 과 Redis 가 kc-lab-2 에만 있으므로 그 노드를 잃으면 세션을 읽고 쓸 곳이 함께 사라진다. 이 배치가 아래 두 결과를 갈랐다. ## 워커를 끊었더니 살아남은 Keycloak 이 쓸모가 없었다 `virsh destroy` 로 kc-lab-2 의 전원을 끊었다. 이 명령은 게스트에 ACPI(Advanced Configuration and Power Interface, 전원 관리 규격) 신호를 보내지 않고 가상머신 프로세스를 끊으므로, 게스트 쪽에서는 예고가 없다. `virsh shutdown` 은 쓰지 않았다. 그쪽은 종료 신호를 보내서 kubelet 이 정상 종료하고 파드를 정리하므로, 쿠버네티스가 정상적인 노드 이탈로 처리해 이 실험의 발견 두 개가 통째로 안 나온다. ```text label="워커 노드를 끊은 직후" === 워커 노드(kc-lab-2) 전원 차단 — virsh destroy 는 종료 신호가 없다 === 차단 시각: 12:07:43 Domain 'kc-lab-2' destroyed +45초 node=NotReady | keycloak-0=Running | 외부 HTTP 503 ``` 기록을 처음부터 따라가면 응답 코드가 두 번 바뀐다. `+15초` 와 `+30초` 에는 노드가 아직 `Ready` 였고 외부 응답이 `000` 이었다. `+45초` 부터 노드가 `NotReady` 로 바뀌면서 응답이 `503` 이 됐고, 관찰을 멈춘 `+180초` 까지 `503` 이 이어졌다. 처음 40 초 동안 연결 자체가 안 된 이유를 증거 원문은 진입점 nginx 의 upstream 으로 적어 두었는데, 그 제목 아래에 출력이 없어서 여기서는 그 40 초의 원인까지는 말하지 못한다. 살아남은 쪽은 kc-lab-1 의 `keycloak-1` 이다. 프로세스는 돌고 있었지만 세션을 읽을 PostgreSQL 이 같은 순간에 사라졌으므로 로그인도 토큰 갱신도 처리할 수 없었다. 이 구성에서 워커 상실은 Keycloak 을 한 대 잃는 사건이 아니라 데이터베이스를 잃는 사건이다. ## 컨트롤 플레인을 끊었더니 돌고 있는데 닿을 수 없었다 kc-lab-1 을 같은 방법으로 끊으면 외부 응답이 `503` 도 아니고 `000` 이 된다. TLS 를 끝낸 nginx 가 넘길 traefik 이 없어서 연결이 맺어지지 않는다. 같은 노드에 있던 k3s API 서버도 사라지므로 `kubectl` 이 통하지 않고, 그래서 클러스터 안을 들여다보며 진단할 방법도 함께 없어진다. 이때 kc-lab-2 의 `keycloak-0` 은 계속 돌고 있었다. 같은 노드에 PostgreSQL 도 있으니 세션을 읽을 수도 있었다. 그런데 밖에서 들어갈 길이 없어서 장애였다. ![kc-lab-2 를 잃으면 데이터베이스가 함께 사라지고, kc-lab-1 을 잃으면 진입 경로가 사라지는 구성](../../../final/assets/a4-two-node-losses/a4-two-node-losses.svg) | 무엇을 잃었나 | 밖과 안에서 무엇이 보였나 | |---|---| | 워커 kc-lab-2 | 외부 `503` · `kubectl` 정상 · 데이터베이스가 같이 죽었다 | | 컨트롤 플레인 kc-lab-1 | 외부 `000` · `kubectl` 불통 · 들어갈 길이 없다 | 복구는 양쪽 다 `virsh start` 이후 60 초로 같았다. 복구 시간이 같아도 대비하는 방법은 갈린다. 워커 쪽은 저장소를 두 노드에 나누거나 밖으로 빼는 문제이고, 컨트롤 플레인 쪽은 진입점을 이중화하는 문제다. ## 죽은 파드가 더 건강해 보였다 여기까지가 노드를 잃는 두 경우를 가르려고 잰 것이고, 재는 동안 예상하지 못한 것이 셋 더 나왔다. 파드 상태가 뒤집혀 보이는 것과, 축출까지 5 분 40 초가 걸리는 것과, StatefulSet 이 대체 파드를 만들지 않는 것이다. 이 절과 다음 절이 그 셋을 적는다. 워커를 끊은 동안 `kubectl get pods -o wide` 와 Prometheus 를 같은 순간에 읽으면 두 값이 어긋난다. | 어느 노드의 파드인가 | kubectl 과 up 이 말한 것 | |---|---| | 전원이 끊긴 kc-lab-2 의 `keycloak-0` | Running · READY `true` · up `0` | | 살아 있는 kc-lab-1 의 `keycloak-1` | Running · READY `false` · up `1` | 파드 상태는 그 파드가 있는 노드의 kubelet 이 API 서버에 보고한다. 노드가 죽으면 보고하는 주체가 함께 사라지므로 아무도 그 상태를 갱신하지 못하고, 마지막으로 보고된 `Running` 과 `READY true` 가 화면에 계속 떠 있게 된다. 반대쪽 `keycloak-1` 은 살아 있으니 kubelet 이 계속 보고하는데, 데이터베이스를 잃어 readiness 프로브가 실패하므로 `READY false` 로 정직하게 내려간다. Prometheus 는 kc-lab-1 에 있어서 살아남았고 `keycloak-0` 을 긁지 못해 `up` 을 `0` 으로 적었다. 두 화면 가운데 `kubectl` 쪽이 뒤집혀 있었다. 이 실험에서 `kubectl get pods` 의 STATUS 를 그대로 믿었다면 살아 있는 노드를 장애로, 죽은 노드를 정상으로 판단했을 뻔했다. ## 6 분 가까이 아무 일도 일어나지 않는다 노드가 죽었다고 쿠버네티스가 곧바로 파드를 옮기지는 않는다. 두 단계를 거친다. 컨트롤러가 노드를 `NotReady` 로 판정하기까지 `node-monitor-grace-period` 40 초가 걸리고, 그렇게 붙은 `NoExecute` taint 를 파드가 견디는 시간이 `tolerationSeconds` 300 초다. 이 실험대에서 조회한 파드의 `tolerationSeconds` 는 `not-ready` 와 `unreachable` 양쪽 다 300 이었다. 40 초 쪽은 조회하지 않았다. 쿠버네티스 기본값을 그대로 적었고, 이 실험대가 직접 읽어 확인한 것은 `tolerationSeconds` 쪽이었다. 설정값을 더하면 5 분 40 초다. 관찰 기록에서는 축출이 이렇게 나타난다. ```text label="축출 관찰 — +270초 뒤로는 같은 줄이 반복돼 줄였다" +240초 a2-probe:Running keycloak-0:Running keycloak-1:Running postgres-7b474b88c8-2gf27:Running +270초 a2-probe:Terminating keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-2gf27:Terminating postgres-7b474b88c8-9cmsv:Pending +420초 a2-probe:Terminating keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-2gf27:Terminating postgres-7b474b88c8-9cmsv:Pending ``` `+240초` 까지 네 파드가 전부 `Running` 이었고 `+270초` 에 세 파드가 한꺼번에 `Terminating` 으로 바뀌었다. 다만 이 관찰 기록의 `+` 가 앞의 응답 코드 기록과 같은 시각에서 출발했는지는 원문이 말해 주지 않는다. 응답 코드를 적은 증거 파일은 머리말에 `차단 시각: 12:07:43` 을 적어 두었는데 축출을 적은 파일에는 그 줄이 없다. 그래서 관측된 `+270초` 와 설정값의 합 5 분 40 초를 빼서 쓰지는 않는다. 설정으로 정해진 값과 눈으로 본 전환이 각각 이 크기라는 데까지가 이 측정이 대는 것이다. 바뀐 뒤로도 화면은 움직이지 않았다. `keycloak-0` 은 `+420초` 까지 `Terminating` 이었고 대신 들어올 파드가 나타나지 않았다. StatefulSet 은 파드 이름이 안정적이어야 하므로 같은 이름을 두 개 띄울 수 없고, `Terminating` 인 파드가 완전히 지워지기 전에는 대체를 만들지 않는다. 노드가 죽어 지워지지 못하면 계속 기다린다. Deployment 인 PostgreSQL 쪽은 이름 제약이 없어 새 파드가 곧바로 만들어졌는데, 그 파드는 `+420초` 까지 `Pending` 에서 움직이지 않았다. 장애 시간을 나눠 보면 복구 절차가 차지하는 몫이 작다. 노드를 되살리고 나서 서비스가 돌아오기까지는 60 초이고, 그 앞에는 클러스터가 노드의 죽음을 인정하고 축출을 시작하기까지 기다리는 구간이 있다. 이 실험이 대는 것은 그 구간이 복구 구간보다 길었다는 방향까지이고, 그 길이가 5 분 40 초였다는 것까지는 아니다. ## 분단은 이 두 가지에 들어가지 않는다 노드 사이를 갈라 보는 실험(A-5)은 같은 종류의 전면 장애를 만들지 못했다. 한 방향만 막으면 열린 방향으로 다시 연결되므로 클러스터가 갈라지지 않고, 양방향을 다 막으면 갈라지기는 하는데 한쪽만 `DOWN` 이 되어 코디네이터 쪽이 살아남는다. 분단된 쪽은 스스로 로드밸런서에서 빠지므로 서비스는 이어진다. 그래서 이 실험대에서 전면 장애로 가는 길은 둘뿐이다. 저장소가 같이 죽거나, 들어갈 길이 사라지거나. ## 이번에 재지 않은 것 저장소를 두 노드에 나눠 배치한 구성에서는 재지 않았다. 워커 상실이 데이터베이스 상실과 같은 사건이 된 것은 PostgreSQL 과 Redis 가 kc-lab-2 한 곳에 있었기 때문이고, 그 결과는 이 배치에 걸려 있다. 저장소를 복제하거나 클러스터 밖에 두면 워커 상실의 결과가 달라진다. 컨트롤 플레인을 잃은 동안 `keycloak-0` 이 어디까지 처리할 수 있었는지도 재지 않았다. 밖에서 들어갈 길이 없어 요청을 보낼 방법이 없었고, 노드 안에서 직접 요청을 넣어 확인하지는 않았다. 워커 상실의 처음 40 초가 `000` 이었던 이유는 증거 원문에 제목만 있고 출력이 없다.