커밋 사이의 기록

LitmusChaos로 카오스 실험하기

by rowing0328

Intro

이번 포스팅에서는 LitmusChaos를 사용해 카오스 실험을 만들어본 내용을 정리해보려고 한다.

 

이전에는 `kubectl delete pod` 명령어로 Pod를 직접 삭제하면서 쿠버네티스가 어떻게 다시 복구하는지 확인했다.

 

이번에는 같은 흐름을 사람이 직접 명령어로 실행하는 것이 아니라,

LitmusChaos의 실험으로 만들어 `pod-delete``pod-memory-hog` 장애를 주입해봤다.

 

단순히 Pod를 망가뜨려보는 것이 아니라,

장애가 발생했을 때 시스템이 기대한 상태를 유지하는지 검증하는 과정이 핵심이었다.

 

 

카오스 엔지니어링이란

카오스 엔지니어링은 시스템의 약점을 미리 찾기 위해 의도적으로 장애를 주입하는 실험 방법이다.

 

처음에는 일부러 장애를 만든다는 점이 조금 낯설게 느껴졌다.

 

하지만 목적은 시스템을 망가뜨리는 것이 아니라,

실제 장애가 발생하기 전에 통제된 환경에서 회복성을 확인하는 것이다.

 

예를 들어 운영 환경에서 갑자기 Pod가 죽거나,

특정 컨테이너의 메모리 사용량이 급격히 올라가는 상황은 충분히 발생할 수 있다.

 

이런 상황에서 서비스가 계속 응답하는지,

쿠버네티스가 정상적으로 복구하는지 미리 확인할 수 있다면 장애 대응 관점에서 훨씬 안정적이다.

 

카오스 실험에는 보통 아래 요소가 필요하다.

항목 설명
정상 상태 실험 전 시스템이 어떤 상태일 때 정상인지 정의한다.
가설 장애가 발생해도 유지되어야 하는 기대 상태를 적는다.
장애 주입 가설을 검증하기 위해 의도적으로 발생시키는 장애다.
관찰과 검증 장애 중 또는 장애 후 시스템 상태를 확인한다.
중단 조건 실험을 멈춰야 하는 상황을 정한다.

 

개인적으로 중요하다고 느낀 부분은 `장애 주입`보다 `가설과 검증`이었다.

 

그냥 Pod를 삭제하는 것은 장애 재현에 가깝지만,

“Pod가 삭제되어도 서비스는 정상 복구될 것이다”라는 가설을 세우고 결과를 확인하면 실험이 된다.

 

 

LitmusChaos의 기본 구조

LitmusChaos는 쿠버네티스 환경에서 카오스 실험을 만들고 실행할 수 있게 도와주는 도구다.

 

이번 실습에서는 ChaosCenter 웹 UI를 통해 실험을 만들고,

클러스터 안에 설치된 Chaos Infrastructure가 실제 장애를 주입하는 구조로 진행했다.

 

주요 구성 요소를 정리하면 아래와 같다.

구성 요소 역할
ChaosCenter 실험을 만들고 실행 결과를 확인하는 웹 UI
Chaos Infrastructure 클러스터 안에서 실제 장애를 실행하는 컴포넌트
ChaosHub `pod-delete`, `pod-memory-hog` 같은 Fault 템플릿 모음
Resilience Probe 장애 중 또는 장애 후 시스템 상태를 자동으로 검증하는 도구
ChaosResult 실험 결과와 verdict를 기록하는 쿠버네티스 리소스

 

여기서 ChaosCenter는 실험을 관리하는 화면이고,

Chaos Infrastructure는 실제 작업을 수행하는 실행 주체로 이해했다.

 

업무에서 요청서를 작성하는 사람과 실제 작업을 처리하는 담당자가 나뉘어 있는 것처럼,

LitmusChaos에서도 실험을 정의하는 부분과 장애를 실행하는 부분이 분리되어 있다.

 

 

실습 대상: podtato-head

이번 실습에서는 CNCF 샘플 애플리케이션인 `podtato-head`를 사용했다.

 

podtato-head는 감자 캐릭터를 화면에 그리는 데모 앱인데,

`frontend`, `hat`, `left-arm`, `right-arm`, `left-leg`, `right-leg`처럼 여러 서비스로 나뉘어 있다.

 

그중 이번 실험 대상은 `hat` 서비스였다.

kubectl apply -f https://github.com/podtato-head/podtato-head-app/releases/download/v0.3.3/manifest.yaml

 

배포 후에는 hat Deployment에 label을 붙여주었다.

kubectl label deployment podtato-head-hat app=podtato-head-hat -n podtato-kubectl

 

이 단계가 생각보다 중요했다.

 

LitmusChaos에서 `App Kind``deployment`로 지정하면,

Pod가 아니라 Deployment 객체의 label을 기준으로 대상을 찾는다.

 

podtato-head manifest에는 Pod label은 있지만 Deployment 자체에는 label이 없어서,

이 작업을 하지 않으면 실험 대상이 없다고 판단될 수 있다.

 

확인은 아래 명령어로 할 수 있다.

kubectl get deployment podtato-head-hat -n podtato-kubectl --show-labels

 

`LABELS``app=podtato-head-hat`이 보이면 정상이다.

 

 

pod-delete 실험

첫 번째 실험은 `pod-delete`였다.

 

이 실험은 이름 그대로 대상 Pod를 삭제하고,

쿠버네티스가 다시 원하는 상태로 복구하는지 확인한다.

 

이번 실험의 가설은 아래와 같이 잡을 수 있다.

podtato-head-hat Deployment는 Pod가 삭제되어도
Kubernetes가 새 Pod를 만들어 desired state로 복구하므로,
실험이 끝난 뒤 Running 상태인 hat Pod가 1개 이상 존재한다.

 

ChaosCenter에서 실험을 만들 때 Target Application은 아래처럼 지정했다.

항목
App Kind `deployment`
App Namespace `podtato-kubectl`
App Label `app=podtato-head-hat`

 

검증에는 CMD Probe를 사용했다.

kubectl get pods -n podtato-kubectl | grep podtato-head-hat | grep Running | wc -l

 

이 명령어는 Running 상태인 hat Pod의 개수를 센다.

 

결과가 `0`보다 크면 실험을 통과한 것으로 볼 수 있다.

 

Probe 모드는 `EOT`로 설정했다. EOT는 End of Test의 약자로,

실험이 끝난 시점에 검증한다는 의미다.

 

처음에는 장애가 발생하는 동안 계속 검사하는 것이 더 좋아 보일 수 있다.

 

하지만 `pod-delete`는 Pod가 삭제되고 새 Pod가 뜨는 짧은 공백이 정상적인 복구 과정에 포함된다.

 

이 순간까지 실패로 보면 실험의 의도와 맞지 않는다.

 

그래서 이 실험에서는 “삭제된 뒤 최종적으로 복구되었는가”를 보기 위해 종료 시점 검사가 더 자연스러웠다.

 

실험이 끝난 뒤 ChaosCenter에서 Resilience Score가 `100%`이고 Probe가 `PASSED`이면,

hat Pod가 삭제된 뒤에도 다시 복구되었다는 뜻이다.

 

명령어로는 `ChaosResult`를 확인할 수 있다.

kubectl get chaosresult -n litmus
kubectl describe chaosresult <ChaosResult-이름> -n litmus

 

`Verdict: Pass`가 보이면 실험이 통과한 것이다.

 

 

pod-memory-hog 실험

두 번째 실험은 `pod-memory-hog`였다.

 

`pod-delete`가 Pod가 사라졌다가 복구되는지를 확인했다면,

`pod-memory-hog`는 대상 컨테이너에 메모리 압박을 주고 서비스가 어떻게 버티는지 확인하는 실험이다.

 

실제 운영 환경에서도 메모리 누수나 순간적인 요청 증가로 특정 컨테이너의 메모리 사용량이 급격히 올라갈 수 있다.

 

이때 서비스가 계속 응답하는지,

OOMKill이 발생하는지,

발생한다면 어떤 프로세스가 죽는지 확인하는 것이 목적이었다.

 

이번 실험의 가설은 아래와 같이 잡았다.

podtato-head-hat 컨테이너에 메모리 부하가 들어오는 동안에도
hat 서비스는 HTTP 요청에 정상으로 계속 응답한다.

 

여기서는 CMD Probe 대신 HTTP Probe를 사용했다.

항목
URL `http://podtato-head-hat.podtato-kubectl.svc.cluster.local:8080/healthz`
Method `GET`
Response Code `200`

 

Probe 모드는 `Continuous`로 설정했다.

 

`pod-delete`와 달리 이 실험은 결과 상태보다 “부하를 받는 동안 계속 응답하는가”가 중요하다.

 

그래서 실험이 진행되는 동안 반복해서 HTTP 요청을 보내고,

계속 `200`을 반환하는지 확인했다.

 

 

컨테이너 런타임 설정에서 헷갈렸던 부분

`pod-memory-hog` 실험에서 특히 주의해야 했던 부분은 컨테이너 런타임 설정이었다.

 

`pod-memory-hog`는 대상 컨테이너 안에 stress 프로세스를 띄워 메모리를 잡는다.

 

이때 노드의 컨테이너 런타임 소켓을 사용하기 때문에,

LitmusChaos의 Fault 설정에 있는 `CONTAINER_RUNTIME``SOCKET_PATH`가 실제 클러스터 런타임과 맞아야 한다.

 

이번 실습에서는 minikube를 Docker driver로 사용했기 때문에 아래처럼 설정했다.

항목
`CONTAINER_RUNTIME` `docker`
`SOCKET_PATH` `/var/run/docker.sock`

 

만약 기본값인 `containerd`로 두면 helper pod가 대상 컨테이너를 찾지 못하고,

`CONTAINER_RUNTIME_ERROR`가 발생할 수 있다.

 

현재 클러스터의 런타임은 아래 명령어로 확인할 수 있다.

kubectl get nodes -o wide

 

`CONTAINER-RUNTIME` 칸에 `docker://...`, `containerd://...` 같은 값이 보인다.

 

이 부분은 단순 설정처럼 보이지만,

실제 장애 주입이 노드 내부의 런타임과 연결되어 있다는 점을 이해하게 해준 부분이었다.

 

 

메모리 부하 결과를 관찰하기

HTTP Probe가 통과했다고 해서 메모리 압박이 없었다는 뜻은 아니다.

 

Probe는 hat 서비스가 HTTP 요청에 응답했는지만 확인한다.

 

그래서 실험 중에는 별도 터미널에서 Pod 상태와 메모리 사용량을 함께 확인했다.

kubectl top pod -n podtato-kubectl -l app=podtato-head-hat

 

`kubectl top`은 metrics-server가 있어야 동작한다.

 

minikube에서는 아래 명령어로 켤 수 있다.

minikube addons enable metrics-server --profile=litmus-tutorial

 

Pod 상태는 watch 옵션으로 확인했다.

kubectl get pod -n podtato-kubectl -l app=podtato-head-hat -w

 

여기서 `RESTARTS` 값이 증가하면 컨테이너가 종료됐다가 다시 시작된 것이다.

 

메모리 부족 때문인지 확인하려면 `describe`를 보면 된다.

kubectl describe pod -n podtato-kubectl -l app=podtato-head-hat

 

`Last State``Reason``OOMKilled`가 있으면 컨테이너가 메모리 부족으로 종료된 것이다.

 

흥미로웠던 점은 메모리 부하가 들어와도 항상 컨테이너 전체가 재시작되는 것은 아니라는 점이었다.

 

컨테이너의 메모리 limit을 넘으면 cgroup 단위의 OOM은 발생할 수 있다.

 

하지만 그때 커널이 죽이는 프로세스가 컨테이너의 메인 프로세스가 아니라 stress 프로세스라면,

컨테이너 자체는 재시작되지 않는다.

 

즉, `RESTARTS``0`이라고 해서 아무 일도 없었다고 단정할 수는 없다.

 

stress 프로세스만 종료되고 hat 서비스 본체는 살아남았을 수도 있다.

 

이 부분은 아래 명령어로 노드의 커널 로그를 확인하면 더 분명하게 볼 수 있다.

minikube ssh -p litmus-tutorial -- "sudo dmesg | grep -i 'memory cgroup out of memory'"

 

`stress-ng` 프로세스가 종료된 로그가 보이면,

메모리 압박은 실제로 발생했지만 서비스 본체는 살아남은 것으로 이해할 수 있다.

 

 

실습하면서 주의할 점

이번 실습을 하면서 몇 가지 주의할 점이 있었다.

 

첫 번째는 실험 대상 label이다.

 

앞에서 정리한 것처럼 `App Kind``deployment`로 두면 Deployment label을 기준으로 대상을 찾는다.

 

Pod label만 보고 괜찮다고 생각하면 `Target application not found` 같은 에러를 만날 수 있다.

 

두 번째는 Probe 경로다.

 

HTTP Probe에서 hat 서비스의 루트 경로(`/`)로 요청하면 `404`가 나올 수 있다.

 

이번 실습에서는 `/healthz` 경로에서 `200`을 반환하므로 URL을 정확히 적어야 한다.

http://podtato-head-hat.podtato-kubectl.svc.cluster.local:8080/healthz

 

세 번째는 메모리 부하 크기다.

 

`MEMORY_CONSUMPTION` 값을 너무 크게 잡으면 실험 대상뿐 아니라,

minikube 전체가 느려지거나 다른 Pod에 영향을 줄 수 있다.

 

로컬 실습 환경에서는 너무 공격적인 값보다 기본값이나 작은 값으로 먼저 확인하는 편이 안전하다.

 

 

마무리

이번 포스팅에서는 LitmusChaos로 `pod-delete``pod-memory-hog` 실험을 만들어본 내용을 정리했다.

 

처음에는 ChaosCenter에서 실험을 만드는 과정이 단순히 UI로 장애를 실행하는 것처럼 보였다.

 

하지만 실제로 해보니 중요한 것은 어떤 장애를 넣을지가 아니라, 어떤 가설을 세우고 어떤 Probe로 검증할지였다.

 

`pod-delete`에서는 실험이 끝난 뒤 Pod가 복구되었는지를 봤고,

`pod-memory-hog`에서는 메모리 부하가 들어오는 동안 서비스가 계속 응답하는지를 봤다.

 

같은 대상 앱이라도 장애의 성격에 따라 Probe 타입과 실행 모드가 달라져야 한다는 점이 인상적이었다.

 

또한 `pod-memory-hog`를 통해 컨테이너 런타임,

cgroup OOM, OOMKilled의 차이도 조금 더 구체적으로 이해할 수 있었다.

 

단순히 `RESTARTS` 숫자만 보는 것이 아니라,

어떤 프로세스가 죽었고 서비스 본체가 살아남았는지까지 확인해야 한다는 점을 배웠다.

 

아직 운영 환경에서 카오스 엔지니어링을 적용해본 것은 아니지만,

로컬 실습만으로도 장애를 실험으로 다루는 관점이 왜 필요한지 느낄 수 있었다.

 

다음에는 네트워크 지연이나 CPU 부하 같은 다른 Fault도 적용해보면서,

Probe를 어떻게 설계해야 하는지 더 살펴보고 싶다.

 

 

참고 자료:

LitmusChaos Official Docs - Podtato Head Tutorial

 

Injecting a pod-delete fault into a Pod | Litmus Docs

podtato-head

docs.litmuschaos.io

 

LitmusChaos-Korea-Mentorship/5week-lets-chaos

 

GitHub - LitmusChaos-Korea-Mentorship/5week-lets-chaos

Contribute to LitmusChaos-Korea-Mentorship/5week-lets-chaos development by creating an account on GitHub.

github.com

블로그의 정보

커밋 사이의 기록

rowing0328

활동하기