이번 글에서는 Kubernetes 워커노드에 OOM이 발생하여 모든 워커노드가 연쇄적으로 장애를 일으키는 바람에 쿠버네티스 클러스터가 통째로 먹통이된 사건에 대해서 다뤄볼까합니다.
0. 상황
현재 저는 엘리스파크 라는 토이프로젝트를 진행중에 있습니다. 그리고 엘리스파크의 백엔드를 쿠버네티스 환경에서 운영중이며, 동시에 엘리스파크의 백엔드를 빌드시키는 젠킨스 서버를 모두 쿠버네티스 위에서 운영중에 있습니다.
제가 운영중이던 쿠버네티스 환경은 아래와 같습니다.
- Amazon EKS (Kubernetes v1.22)
- 각 워커노드는 t3.medium 인스턴스로 운영중에 있었음 (vcpu 2 + 4Gi Memory)
- 단일 노드그룹에 워커노드는 min size = 2, desired size = 2, max size = 6 로 설정이 되어있는 상태임
그리고 엘리스파크의 백엔드 레포지토리 두개를 동시에 빌드를 돌리는 순간, 갑자기 쿠버네티스 클러스터 상에서 운영하던 모든 서비스들이 503 에러를 내뿜으며 뻗어버리는 현상이 발생하였습니다.
1. 원인
사실 이번 일이 한번만 있었던 것은 아니었습니다. 이전에도 똑같은 현상이 있었기에, 그 당시에는 클러스터 자체를 내린 다음에 새로운 클러스터를 만드는 방식으로 해결했었지만, 해당 현상이 앞으로도 일어나지 않으리라는 보장이 없기 때문에 정확한 해결 방법을 찾기로 결심하였습니다.
우선, 현재 운영중인 파드의 로그를 전부 체크해보기로 결심했었습니다. 첫번째로 확인해보았던 것은 Jenkins 였는데요, Jenkins Pod의 상태를 관찰해보았더니, 매우 당황스러운 상황을 마주하게 되었습니다.
NAME READY STATUS RESTARTS AGE
pod/docker-build-m0q5w-t9034 3/3 Terminating 0 8m11s
pod/jenkins-5ff87ffcf7-prdxk 1/1 Terminating 0 3d4h
pod/jenkins-5ff87ddqwv-qwerf 0/1 ContainerCreating 0 3d4h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/jenkins-service NodePort 10.100.xxx.xxx <none> 8080:32000/TCP,50000:32001/TCP 6d12h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jenkins 0/1 0 0 3d4h
NAME DESIRED CURRENT READY AGE
replicaset.apps/jenkins-5ff87ffcf7 1 0 0 3d4h
다름아닌, 빌드를 위한 동적 파드와 젠킨스 파드가 모두 Terminating에 놓여있었고, jenkins pod가 죽음에 의해서 새로 생겨나는 파드 또한 ContainerCreating 상태에서 벗어나지 못하며 계속 멈춰있는 상태였습니다.
프로메테우스 또한 kubectl 명령어를 통해서 확인을 해보았으나, alertManager를 비롯한 prometheus, grafana 모두 Terminating 상태에 놓여있었습니다.
다음으로 Kubernetes의 워커노드들의 상태를 확인해보았습니다.
ubuntu@ip-xxx-xx-xx-xx:~$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-xxx-xx-xx-xxx.ap-northeast-2.compute.internal NotReady <none> 3d5h v1.22.17-eks-a59e1f0
ip-xxx-xx-xx-xxx.ap-northeast-2.compute.internal NotReady <none> 3d5h v1.22.17-eks-a59e1f0
쿠버네티스 워커플레인을 구성하는 모든 노드들이 NotReady에 빠져있었으며, describe 명령어를 이용해 노드에서 발생한 이벤트를 조회한 결과, 최초로 docker 동적 파드가 생성된 노드에서 동적 파드가 3~4Gi 가량의 메모리를 잡아먹으며 해당 동적 파드에서 크래시가 발생하였으며, 그로 인해서 해당 노드가 OOM (Out Of Memory) 상태에 빠지게 되었습니다. 그리고 다른 워커노드 또한 동시에 OOM이 발생하게되면서 쿠버네티스 클러스터를 구성하는 모든 워커노드에 장애가 발생하게 된것이었습니다.
2. 어떻게 문제를 해결할것인가
해당 문제의 해결 방법은 아주 간단합니다. 워커노드를 구성하는 모든 인스턴스를 scale-up 해주면 됩니다. 하지만 아래의 사항이 고려되어야합니다.
기존의 워커노드에 올라간 모든 파드들이 정상적으로 다시 동작해야한다.
그렇기 때문에 아래의 절차에 따라서 문제를 해결하기로 결심하였습니다.
- 우선 스케일업된 워커노드를 묶은 노드그룹을 쿠버네티스 클러스터에 배포한다
- 기존의 워커노드들을 모두 재시작한다
- 기존의 워커노드에 있던 모든 파드들을 새로운 노드그룹에 마이그레이션한다
- 기존의 워커노드를 쿠버네티스 클러스터에서 제거한다
3. 해결 과정
저는 EKS를 Terraform 코드로 관리하고 있었기 때문에, Terraform 코드를 수정하여 새로운 워커노드를 추가하였습니다.
eks_managed_node_groups = {
default_node_group = {
desired_size = 2
min_size = 2
max_size = 6
instance_types = ["t3.medium"]
}
// 새로운 노드그룹
new_node_group = {
desired_size = 2
min_size = 2
max_size = 6
instance_types = ["t3.large"]
}
}
수정된 테라폼 코드를 배포하여 EKS 클러스터 상에 새로운 워커노드를 등록하였습니다.
그리고 기존의 워커노드들을 EC2 대시보드에서 재시작 해준 다음, 기존의 워커노드들에 대해서 kubectl drain <node_name> 명령어를 통해 쿠버네티스 스케쥴링에서 기존 노드를 제외함과 동시에 파드들을 모두 스케일업 된 노드로 마이그레이션 하는 작업을 하였습니다.
그리고 기존의 파드들이 정상적으로 동작함을 확인하였다면, 테라폼 코드 상에서 default_node_group을 제거하여 재배포를 합니다. 그렇게 되면 기존의 t3.medium 타입의 워커노드들이 모두 제거되면서 자연스럽게 모든 워커노드를 스케일업 할 수 있게됩니다.
4. 이상한 점
그러나, 뭔가 이상한 점이 있습니다. 분명히 모든 워커노드가 OOM에 빠져있고, EKS를 생성하면 워커노드는 AutoScaling 그룹에 소속되어있기 때문에 새로운 워커노드가 생성되어야하지 않나? 라는 생각을 했었지만, 실제로는 워커노드가 스케일링 되지 않았습니다.
그에 대한 이유는 [EKS] 환경 구축하기3 - AutoScaling 설정 by 피클s 에서 찾을 수 있었는데요, EKS는 워커노드를 생성 시에 오토스케일링 그룹 단위로 워커노드를 관리한다는 것은 사실이지만, 쿠버네티스 클러스터가 스케일링 명령을 해당 오토스케일링 그룹에 내릴 수 있어야 워커노드의 개수가 늘었다 줄었다 할 수 있다는 것입니다.
해당 기능을 지원하는 플러그인은 AutoScaling - Amazon EKS (공식문서) 에서 찾을 수 있었습니다. Cluster AutoScaler를 배포하는 방법과, Karpenter를 배포하는 방법이었습니다.
저는 아무래도 쿠버네티스 클러스터를 아주 헤비하게, 엔터프라이즈 급으로 운영할 생각은 없기 때문에 Cluster AutoScaler를 배포하는 방식을 채택하여 쿠버네티스 클러스터의 HA (High Availability)를 한층 강화할 수 있게 되었습니다.
5. 이걸로 충분한가요?
사실 충분치는 않습니다. 이 사건의 근본 원인은 OOM 발생은 맞지만, OOM이 발생하기 이전에 이를 예측하거나, 혹은 사전에 막을 수 없었을까? 에 초점을 둬야하기 때문입니다.
저는 워커노드의 OOM으로 인한 연쇄 장애 현상을 겪으면서도 그라파나 대시보드를 통해서 쿠버네티스 클러스터 전체의 CPU와 메모리 사용량을 모니터링 하고 있었습니다. 하지만, 쿠버네티스 전체 메모리 사용량은 4.7Gi 가 맥스였고, t3.medium 두 개의 메모리 총량은 8Gi 였기 때문에 설마 OOM이 터지겠어? 라고 방심을 했기 때문입니다.
이번 사건으로 인해 깨달은 점은 크게 2가지입니다.
- 클러스터 단위 말고도 노드 단위로도 모니터링을 해줘야한다.
- 인스턴스 타입을 산정할 때는 배포할 파드의 최대 메모리 사용량도 고려를 해봐야한다. 예를 들어서, 1개 파드가 최대 3Gi를 사용할 잠재적인 가능성이 존재한다면, 인스턴스 타입은 적어도 해당 파드 메모리를 고려해서 결정해줘야한다.
이상으로 글을 마치도록 하겠습니다.
6. 참고할만한 영상 (저도 아직 안 봤습니다!)
[데브옵스] 오픈소스 Karpenter를 활용한 Amazon EKS 확장 운영 전략 - 신재현, 무신사
'devOps' 카테고리의 다른 글
서비스 장애 잘 이해하고 대비하기 (0) | 2024.07.06 |
---|---|
Redis OOM 장애 (0) | 2024.06.25 |
Docker File System (Overlay2) (0) | 2023.08.06 |
Jenkins on K8S를 설정하며 겪은 일들 (0) | 2023.03.04 |
내가 쿠버네티스 설정하며 겪은 삽질들 (alb-controller, jenkins, monitoring) (0) | 2023.02.27 |