학습배경
현재까지 학습을 진행한바에 따르면 쿠버네티스에서 파드는 배포 가능한 기본 단위입니다. 파드를 수동으로 생성, 감독, 관리하는 방법을 배웠지만 실제 환경에서는 배포한 애플리케이션이 자동으로 실행되고 수동적인 개입 없이도 안정적인 상태로 유지되길 원할겁니다.
대신 ReplicationController 나 Deployment 와 같은 유형의 리소스를 생성해서 실제 파드를 생성하고 관리하죠. [Kubernetes] 클러스터의 파드(Pod) 배치 아키텍처와 YAML 디스크립터로 파드 관리하기 에서 만든것과 같은 관리되지 않는 파드를 생성하면 파드를 실행할 클러스터 노드가 선택되고, 파드의 컨테이너가 해당 노드에서 실행됩니다.
이번에는 쿠버네티스가 해당 컨테이너를 모니터링하고 실패하면 자동으로 다시 시작하는 방법을 알아보겠습니다. 또 쿠버네티스가 어떻게 컨테이너가 살아있는지 체크하고, 프로세스가 kill 상태인 경우 어떻게 다시 시작하는지 알아보겠습니다.
왜 라이브니스 프로브?
쿠버네티스를 적용하다가, 파드의 컨테이너 중 하나가 죽으면 어떻게 될까요? 또는 파드 안의 모든 컨테이너가 죽으면 어떻게될까요?
Kubelet 에 의한 상태체크 및 자가치유
파드가 노드에 스캐줄링되는 즉시, 해당 노드의 Kubelet
은 파드의 컨테이너를 실행하고 파드가 존재하는 한 컨테이너가 계속 실행되도록 합니다. 컨테이너의 주 프로세스에 크래시(Crash)
가 발생하면 Kubelet 은 컨테이너를 다시 시작합니다.
만약 이렇게 애플리케이션에 장애가 발생시 K8S 는 자동으로 애플리케이션을 다시 시작하므로, 개발자는 특별한 작업없이도 자가 치유
할 수 있는 모습을 볼 수 있게됩니다.
외부에서 애플리케이션 상태를 체크해보자
그런데 때때로 애플리케이션은 프로세스의 크래시(Crash) 가 없이도 작동이 중단되는 상황이 발생하는 경우도 있습니다. 일례로 자바 애플리케이션이 메모리 누수가 있어서 OutofMemoryErrors 를 발생시키기 시작하더라도 JVM 프로세스는 계속 실행됩니다.
이런 상황이 발생하지 않도록, 애플리케이션이 더 이상 동작하지 않는다는 신호를 쿠버네티스에 보내서, 쿠버네티스가 애플리케이션을 다시 시작하도록 하는 방법이 있다면 좋을것입니다.
예를들어 애플리케이션이 무한 루프나 데드락(DeadLock)
상황에 빠져서 응답을 하지 않는 상황이라면 어떨까요? 이런경우 애플리케이션이 다시 시작되도록 하려면 애플리케이션 내부의 기능에 의존하지 말고 외부에서 애플리케이션의 상태를 체크해야 합니다.
라이브니스 프로브
쿠버네티스는 라이브니스 프로브(liveness probe) 를 통해 컨테이너가 살아있는지 확인할 수 있습니다. 파드의 명세(specification) 에 각 컨테이너의 라이브니스 프로브를 지정할 수 있죠. 쿠버네티스는 주기적으로 프로브라는것을 실행하고 프로브가 실패할경우 컨테이너를 다시 시작합니다.
쿠버네티스는 3가지 메커니즘으로 컨테이너에 프로브를 실행합니다.
HTTP GET 프로브
지정한 IP 주소, 포트, 경로에 HTTP GET 요청을 보냅니다. 프로브가 응답을 수신하고, Status Code 값이 정상이라고 판단되는 경우(2xx, 3xx 번대 코드값) 프로브가 성공됐다고 간주합니다. 반대로 비정상 코드값(4xx, 5xx 번대) 을 리턴하거나 전혀 응답하지 않으면(ex. DeadLock) 프로브가 실패한 것으로 간주돼어 컨테이너를 다시 시작합니다.
TCP 소켓 프로브
컨테이너를 지정된 포트에 TCP 연결을 시도하고, TCP 연결 성공여부에 따라 프로브 성공여부가 결정되는 방식입니다. 즉, TCP 연결에 성공하면 프로브가 성공한 것이고, 그렇지 않으면 컨테이너가 다시 시작됩니다.
Exec 프로브
컨테이너 내의 임의의 명령을 수행하고 명령의 종료상태 코드를 확인합니다. 상태 코드가 0이면 프로브가 성공한 것이고, 다른 모든 코드들은 실패로 간주합니다.
HTTP 기반 라이브니스 프로브 생성
이제부터 라이브니스 프로브에 기반하여 주기적으로 헬스 체킹(Health Checking)
을 시도하고, 스프링부트 애플리케이션에 라이브니스 프로브니스를 추가하는 방법을 살펴봅시다.
저희는 프로브를 테스트하기 위해, 스프링부트 애플리케이션 코드를 인위적으로 만들겁니다. 5번째 이후의 요청부터는 500 Status Code (Internal Server Error) 를 리턴하도록 만들겠습니다. 즉, 처음 5번째 요청까지는 적절히 처리하고 이후의 모든 요청은 모두 오류를 발생시키도록 500 상태코드 리턴하도록 하겠습니다. 이 상황에서 라이브니스 프로브를 활용하면 컨테이너가 다시 시작돼 클라이언트의 요청을 다시 적절히 처리하게 됩니다.
파드 YAML 디스크립터 작성
HTTP GET 라이브니스 프로브가 포함된 새 파드를 생성합니다. 아래는 파드 YAML 을 보여줍니다.
apiVersion: v1
kind: Pod
metadata:
name: kubia-liveness
spec:
containers:
- image: luksa/kubia-unhealthy // 5번이상 요청시 500 상태코드를 리턴하는 애플리케이션 이미지
name: kubia
livenessProbe: // HTTP GET 을 수행하는 라이브니스 프로브
httpGet:
path: / // HTTP 요청 경로
port: 8080 // 프로브가 연결해야하는 네트워크 포트
위 파드 디스크립터는 쿠버네티스가 주기적으로 "/" 경로와 8080포트에 HTTP GET 을 요청해서 컨테이너가 정상 작동하는지 확인하도록 httpGet 라이브니스 프로브를 정의합니다. 이런 요청은 컨테이너가 실행되는 즉시 시작됩니다.
5번의 요청 후에 애플리케이션은 HTTP 상태 코드 500을 리턴하기 시작하고, 쿠버네티스는 프로브를 실패한 것으로 간주하고 컨테이너를 재시작합니다. 그러고 아래와 같이 파드를 생성해봅시다.
$ docker pull luksa/kubia-unhealthy
$ kubectl create -f kubia-liveness-probe.yaml
파드를 조회해보면, 아래와 같이 RESTARTS 열이 0이 아니라 컨테이너가 무려 4번이나 재시작 되었음을 볼 수 있습니다. 약 90초의 주기로 다시 재시작을하죠.
SIGKILL 시그널 번호
그리고 kubectl describe 로 출력되는 내용을 조회해보면, 컨테이너가 재시작된 이유를 확인할 수 있습니다.
$ kubectl describe po kubia-liveness
로그를 출력시 컨테이너가 현재 실행중이지만 오류로 인해 이전에 종료된것을 알 수 있습니다. Containers - Last State - Exit Code 란을 보면 종료코드값 137
을 확인할 수 있는데, 이는 프로세스가 외부신호에 의해 종료되었음을 의미합니다. 숫자 137은 두 숫자를 합한것, 즉 128+0 인데 이때 9는 프로세스에 전송된 시그널 번호이며 이 시그널 번호에 의해 컨테이너가 종료되었음을 의미합니다.
즉, 9 는 SIGKILL 시그널 번호로, 프로세스가 강제로 종료되었음을 의미한다.
initialDelaySeconds
또 프로브를 정의할때 추가적인 매개변수로 초기 지연
에 대한 속성도 지정할 수 있습니다. 바로 initialDelaySeconds
속성을 라이브니스 프로브에 추가하면 됩니다.
만약에 초기 지연을 설정하지 않으면 컨테이너가 시작되자마자 프로브를 시작합니다. 이 경우 대부분의 애플리케이션이 요청을 아직 받을 준비가 돼 있지 않기때문에, 프로브가 실패합니다. 프로브의 실패 횟수가 임계값을 초과하면 요청을 올바르게 응답하게 전에 컨테이너가 다시 시작됩니다.
apiVersion: v1
kind: Pod
metadata:
name: kubia-liveness
spec:
containers:
- image: luksa/kubia-unhealthy
name: kubia
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 15 // K8S는 1번째 프로브 실행까지 15초를 대기한다.
애플리케이션 프로세스 시작 시간을 고려해 초기지연을 설정해주자!
만약 이 사실을 모른다면 개발자들은 왜 컨테이너가 재시작됐는지 혼란스러울 겁니다.
효과적인 라이브니스 프로브 생성
운영환경에서 실행중인 파드는 반드시 라이브니스 프로브를 정의해야 합니다. 프로브를 정의하지 않으면 쿠버네티스가 애플리케이션이 살아있는지를 알 수 있는 방법이 없습니다. 프로세스가 실행되는 한 쿠버네티스는 컨테이너가 정상적이라고 간주할것이죠.
라이브니스 프로브가 Health Check 해야할 목록들
앞서 정의한 라이브니스 프로스브는 간단히 정의해둔것이라서, 단순히 서버가 응답하는지를 검사합니다. 이 기능으로 컨테이너 내에서 실행중인 웹서버가 HTTP 요청에 응답하지 않으면 컨테이너가 다시 시작되는 엄청난 기능을 가지고 있습니다.
특정 URL로 헬스체킹 하도록 프로브를 정의하자
여기에 더 나아가서, 괜찮은 프로브를 구성하기 위해 특정 URL 경로(예를들어 "/health") 에 요청하도록 프로브를 구성하여 애플리케이션 내에서 실행중인 모든 주요 구성요소가 살아있는지, 또는 응답이 없는지 확인하도록 구성할 수 있습니다.
프로브를 가볍게 유지하자
라이브니스 프로브는 너무 많은 연산 리소스를 사용하지말고, 헬스체킹을 완료하는데도 너무 오래걸려서도 안됩니다. 기본적으로 프로브는 자주 실행되며 1초 내에 완료되는 것이 좋습니다. 프로브가 너무 많은 체킹을 수행하면 컨테이너의 속도를 저하시킬 수 있기 떄문이죠.
자바 애플리케이션은 HTTP GET 라이브니스 프로브를 사용하자
컨테이너에서 자바 애플리케이션을 실헹하는 경우, 라이브니스 정보를 얻기위해 Exec 프로브
로 전체 JVM 을 기동하는 대신에 HTTP GET
라이브니스 프로브를 사용합시다.
프로브에 재시도 루프를 구현하지 말자
앞서 언급했기를, 애플리케이션 코드에서 설정한 프로스의 실패 임계값를 초과했을때 컨테이너가 재시작된다고 했었습니다. 그런데 이때 실패 임계값을 고작 1로 설정하면 딱 1번만 요청을 보내고 컨테이너를 재시작할 것 같지만, 프로브를 여러번 재시도합니다.
따라서 프로브에 자체적인 재시도 루프를 구현하는것은 헛수고이죠.
정리
이렇게 컨테이너에 크래시(Crash) 가 발생하거나 라이브니스 프로브가 실패한 경우, 쿠버네티스가 컨테이너를 재시작해 컨테이너가 계속 실행되도록 한다는 점을 기억합시다. 이 작업은 파드를 호스팅하는 노드의 Kubelet
에서 시도합니다. 마스터 노드에서 실행중인 컨트롤 플래인(Control Plane) 은 이 프로세스에 관여하지 않습니다.