Kubernetes in Action : Chapter10. Statefulset

    들어가기 전

    이 글은 쿠버네티스 인 액션 10장을 공부하며 작성한 글입니다. 


    10.1 Stateful 파드 복제하기

    ReplicaSet은 하나의 Pod Template을 이용해 여러 개의 파드를 생성한다. 이 때 ReplicaSet이 PVC를 사용하도록 작성하면 각 파드는 어떤 차이가 있을까? 생성된 파드는 IP가 다른 것을 제외하고는 모두 동일한 상태를 가진다.

    각 파드가 아래처럼 각각의 PVC를 참조하는 상황을 고려해보자. 이것은 각 파드가 독립적인 '상태'를 가지고 있음을 의미한다. 이렇게 구성하려면 어떻게 해야할까? 우선 우리가 알고 있는 ReplicaSet으로는 구현할 수 없다. 


    10.1.1 개별 스토리지를 갖는 ReplicaSet 여러 개 실행하기

    아래그림처럼 각 파드가 독립적인 PVC를 참조해서, 상태를 가지도록 하고 싶다. 이 절에서는 어떤 방법들로 아래 그림을 구현할 수 있는지를 살펴보고자 한다. 

     


    수동으로 파드 생성하기

    • 수동으로 하나씩 파드를 생성하고, PVC를 연결하는 방법
    • 쿠버네티스 컨트롤러의 관리 대상이 아니기 때문에 파드가 사라지면 개발자가 직접 복구해야함. 

    파드 인스턴스별로 하나의 ReplicaSet 사용하기

    아래 그림처럼 여러 ReplicaSet을 하나씩 생성해서, 쿠버네티스 컨트롤러의 관리 아래에 두면서 각각을 PVC를 연결하는 방법이 있다. 하지만 이 방법은 다음 단점이 존재하기 때문에 좋은 방법이 아니다. 

    • Scale 하는 것이 어려움. Scale 하기 위해 ReplicaSet을 생성하거나 삭제해야 함. 
    • 업데이트 하기 어려움. 모든 ReplicaSet에 각각 업데이트 사항을 반영해야함. 


    동일 볼륨을 여러 개 디렉토리로 사용하기

    모든 파드가 동일한 PV를 참조하도록 하고, PV 내에서 디렉토리를 나눠서 데이터를 저장하도록 할 수 있다. 하지만 이 방법 역시 단점이 존재한다.

    1. 공유 스토리지 볼륨에 병목 현상이 발생할 수 있음.
    2. 각 파드 인스턴스가 PV 내의 어떤 디렉토리를 사용해야 할지 결정하는 것이 어려움. 

     


    10.1.2 각 파드에 안정적인 Identity 제공하기 (네트워크 관점)

    위 방법을 이용해 ReplicaSet에 의해서 Pod가 새로 만들어졌을 때, 이전 Pod가 참조하던 PV를 참조해서 데이터 관점에서 동일한 '상태'를 가져갈 수는 있다. 하지만, ReplicaSet은 파드를 새로 생성할 때 전혀 다른 이름(해시값)과 IP로 생성한다. 즉, 네트워크 관점에서의 동일성은 가져갈 수 없다. 

    # 파드 재기동 전
    hello-dfskwer
    
    # 파드 재기동 후
    heelo-abcde123

    이런 문제는 ReplicaSet이나 Service 만으로는 해결할 수 없다. 다행히 쿠버네티스에는 이런 부분을 손쉽게 해결하기 위한 Object를 제공해준다. 


    10.2 StatefulSet 이해하기

    앞서 ReplicaSet으로는 다음 관점에서 '상태'를 유지하는 것이 힘든 것을 알 수 있었다. 

    • 네트워크의 동일성 (파드 이름 / 파드 IP)
    • 스토리지의 동일성 (PV)

    쿠버네티스는 위 두 가지를 간단히 제공하기 위해 ReplicaSet이라는 오브젝트를 재공한다. 


    10.2.1 StatefulSet과 ReplicaSet 비교하기

    StatefulSet과 ReplicaSet의 차이는 각 파드가 Stateful한지, Stateless한지에 따라 갈린다. 여기서 Stateful / Stateless는 주로 애완동물과 가축의 개념으로 비견된다. 아래에서 정리한 내용을 살펴보자. 

    • Stateless (가축)
      • 농부가 병든 가축을 교체함. → Identity가 같지 않아도 됨. 
      • ReplicaSet
        • Stateless한 파드 복제를 제공한다. 
        • Replicaset이 생성한 각 파드는 완벽히 동일한 파드다.
    • Stateful (애완동물)
      • 애완동물은 이름을 붙이고 관리함. 애완동물이 죽는다면, 대체할 애완동물을 가지기 위해 동일한 생김새 / 목소리 / 행동을 가진 녀석을 찾아야 함. → Identity가 같아야 함. 
      • StatefulSet
        • Stateful한 파드 복제를 제공함. 
          • 동일한 이름
          • 동일한 네트워크 Identity
          • 동일한 State(데이터 관점) 
        • StatefulSet이 생성한 파드는 파드끼리 서로 다를 수 있음. 

    거버닝 서비스 소개

    StatefulSet으로 만들어진 파드는 동일하지 않은 파드다. 예를 들어 Kafka Broker를 StatefulSet으로 만들었다면, 각각 다른 데이터가 저장되어 있기 때문에 이 녀석들은 서로 다른 파드다. 이런 분산환경 아래에서는 특정한 파드끼리 통신해야 할 필요가 있다. 하지만 다음 문제가 존재한다

    • Pod IP로 접근 : 특정 파드로 접근할 수 있지만, 재기동 할 때 마다 IP가 바뀐다.
    • 서비스로 접근 : 특정 파드로 접근할 수 없음. 임의의 파드에 접근함. 

    이런 문제가 있기 때문에 특정 Pod끼리 통신을 할 수 없다. StatefulSet은 Headless 서비스를 사용할 때, 다음과 같은 FQDN을 제공하기 때문에 특정 Pod끼리 통신할 수 있게 된다. 

    # 형식
    <Pod 이름>.<서비스 이름>.<네임 스페이스>.svc.cluster.local
    
    # 실제
    a-0.foo.default.svc.cluster.local

    예를 들어 a-0 / a-1 파드가 존재한다고 했을 때, a-1 파드는 위의 FQDN을 이용해 특정해서 통신 요청할 수 있게 된다. 


    잃어버린 애완동물 교체하기

    StatefulSet은 파드가 죽으면, 동일한 이름을 가지는 파드를 다시 생성한다. ReplicaSet과의 차이점은 다음과 같다.

    # ReplicaSet Before
    hello-dbxsewr
    
    # ReplicaSet After(재기동)
    hello-abxcdf
    
    # StatefulSet Before
    hello-0
    
    # StatefulSet After(재기동)
    hello-0

    StatefulSet의 스케일링

    StatefulSet의 스케일링은 다음과 같은 형태로 이루어진다. 

    • 한 번에 하나의 파드만 생성한다. 이 파드가 '정상'인 것이 보장될 때만 다음 파드를 생성함. 
    • 생성하는 순서는 0 → 1 → 2... 로 생성함. 제거할 때는 2 → 1 → 0으로 제거함. 

    왜 StatefulSet은 특정 파드가 정상적으로 완전히 종료되었을 때, 다음 파드를 종료시킬까? 그것은 분산 시스템의 상황을 고려하기 때문이다. 카프카를 예로 들어보자. 다음 상황이다.

    • 브로커 파드 0 : 토픽 A 보관
    • 브로커 파드 1 : 토픽 B 보관
    • 브로커 파드 2 : 토픽 C보관 

    이 때, 브로커 파드2를 제거하면 토픽 C가 다른 브로커 파드로 복사되는 과정이 필요하다. 이 때 파드 1로 복사하는 결정을 내렸다고 해보자. 만약 StatefulSet이 한번에 여러 개의 파드를 제거한다면 파드1,2가 동시에 제거될 수 있다. 이 경우, 토픽 C의 데이터는 유실될 수도 있기 때문이다.

    이런 상황 때문에 StatefulSet은 분산 시스템 환경에서의 State를 안정적으로 유지하기 위해서 특정 파드가 완벽히 종료되거나 생성되었을 때, 다음 파드를 생성하도록 한다. 


    10.2.3 각 StatefulSet 인스턴스에 안정적인 전용 스토리지 제공하기

    StatefulSet으로 생성되는 파드는 다음을 반드시 만족해야 한다.

    • 재생성되는 파드는 이전 파드가 참조했던 PVC와 반드시 연결되어야 함. 

    StatefulSet은 어떻게 이것을 달성할까?


    VolumeClaim Template + Pod Template 함께 사용하기

    StatefulSet을 구성할 때는 다음 두 가지를 선언할 수 있다.

    • Pod Template : 파드를 생성할 때 사용함
    • VolumeClaimTemplate : 파드에 필요한 PVC를 생성할 때, Template으로 사용함. 

    StatefulSet은 VolumeClaimTemplate을 설정하면서, 각 파드가 생성될 때 마다 필요한 PVC를 찍어낼 수 있다.


    StatefulSet에서 PVC의 생성과 삭제 이해

    StatefulSet에서 스케일링 업 / 다운을 할 때의 쿠버네티스 오브젝트 변화는 다음과 같다. 

    • Up : 파드 1개 + PVC 1개 생성
    • Down : 파드 1개 삭제. PVC는 유지

    Scale Down을 했을 때 PVC가 삭제되면 어떻게 될까? PVC는 이미 삭제되었기 때문에 나중에 다시 Scale UP을 하면, 새로운 PVC가 생성된다. 즉, 예전에 있던 Pod1과 새로 생성된 Pod1가 가지고 있는 '상태'는 전혀 다른 상태가 된다. 따라서 PVC를 유지해서 '상태'를 보존할 필요가 있다.

    이런 상황들 때문에 StatefulSet은 스케일 다운할 때, 기존의 PVC를 보존한다. 


    동일 파드의 새 인스턴스에 PVC 다시 붙이기

    StatefulSet을 Scale Down하면, PVC는 남겨둔다고 한다. 이 말은 Scale Up을 했을 때, 기존의 PVC가 존재한다면 새로 생성된 Pod는 기존의 PVC로 연결된다는 것을 의미한다. 따라서 새롭게 생성된 파드는 동일한 이름 / 상태를 유지한다. 


    10.2.4 StatefulSet 보장 이해하기 

    StatefulSet은 '최대 하나'를 보장한다. 이것의 자세한 내용은 다음과 같다. 

    • 두 개의 StatefulSet 파드가 절대 동일한 Identity(이름)으로 생성되지 않음.
    • 두 개의 StatefulSet 파드가 절대 동일한 PVC로 바인딩 되지 않음. 

    이것을 만족하기 위해서 StatefulSet은 다음 형태로 동작한다. 

    반드시 이전 파드가 더 이상 실행 중이지 않을 때만, 교체 파드를 생성한다. 이것은 이전 파드가 Terminating 상태일 때는 다음 파드가 생성되지 않는 것을 의미한다. 

    이것은 이런 상황도 발생시킬 수 있다. 예를 들어서 워커 노드에 파드는 정상적으로 돌아가고 있지만, 워커 노드 - 마스터 노드가 통신하지 못하는 경우 파드는 Unknown 상태가 될 수 있다. Unknown 상태는 '파드가 실행되고 있는지 / 아닌지'를 확신할 수 없는 상태이다. 따라서 StatefulSet은 '파드가 실행되지 않는다고 확신할 수 없기' 때문에 다음 Pod를 생성하지 않는다. 


    10.3 StatefulSet 사용하기 

    이 절에서는 실제 StatefulSet을 사용하는 방법을 공부한다. 


    10.3.1 어플리케이션과 컨테이너 이미지 생성하기

    이번 절에서는 아래 이미지를 사용해서 실습한다. 아래 이미지에 있는 어플리케이션은 다음과 같이 동작한다.

    • GET : /var/data/kubia.txt 파일에 기록된 값을 반환한다. 
    • POST : Body로 전달받은 데이터를 / var/data/kubia.txt 파일에 저장한다. 
    luksa/kubia-pet

    10.3.2 StatefulSet을 통한 어플리케이션 배포하기

    StatefulSet을 이용해 클러스터링을 해보려면 다음 자원을 생성해야 한다. 

    • StatefulSet을 위한 거버닝 서비스
    • StatefulSet 

    순차적으로 각 오브젝트를 생성해보자. 


    거버닝 서비스 생성

    아래와 같이 거버닝 서비스를 생성한다. StatefulSet은 각 파드가 서로 다른 State를 가진 것으로 판단되기 때문에 Headless 서비스를 생성해 특정 파드에 접속하도록 한다. 

    apiVersion: v1
    kind: Service
    metadata:
      name: c-10-4-kubia
    spec:
      clusterIP: None
      selector:
        app: c-10-4-kubia
      ports:
        - port: 80
          name: http

    StatefulSet 매니페스트 생성하기

    StatefulSet의 매니페스트는 다음과 같다.

    • PodTemplate : 파드를 생성할 때 사용하는 템플릿이다. 
    • VolumeClaimTemplate : 각 파드에서 사용할 PVC를 생성할 때 사용하는 템플릿이다. StorageClassName이 지정되지 않으면, Default StorageClassName을 이용해 Dynamic Provisioning을 요청함. 
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: c-10-4-kubia
    spec:
      selector:
        matchLabels:
          app: c-10-4-kubia
      serviceName: c-10-4-kubia
      template:
        metadata:
          name: c-10-4-kubia
          labels:
            app: c-10-4-kubia
        spec:
          containers:
            - name: kubia
              image: luksa/kubia-pet
              ports:
                - name: http
                  containerPort: 8080
              volumeMounts:
                - mountPath: /var/data
                  name: data
      volumeClaimTemplates:
        - metadata:
            name: data
          spec:
            storageClassName: longhorn
            accessModes:
              - ReadWriteOnce
            resources:
              requests:
                storage: 1Mi

    여기서 생성된 StatefulSet의 Scale을 올렸을 때, 파드가 어떻게 생성되는지를 살펴보자.

    • 하나의 파드가 생성완료되면 다음 파드를 생성한다. 

    앞서 이야기 했던 것처럼 StatefulSet은 동일한 인스턴스(호스트 이름)가 하나의 PVC에만 마운트되도록 보장한다. 이것을 보장하기 위해서 StatefulSet은 파드를 생성하거나 삭제할 때, 반드시 하나의 파드씩만 생성/삭제하고 절대적으로 생성되거나 / 삭제되거나를 보장할 수 있는 경우에만 다음 스케일을 진행한다. 

    $ kubectl scale sts c-10-4-kubia --replicas=4
    $ kubectl get pod -w 
    >>>
    NAME             READY   STATUS              RESTARTS   AGE
    c-10-4-kubia-0   1/1     Running             0          24m
    c-10-4-kubia-1   0/1     ContainerCreating   0          4s
    c-10-4-kubia-1   1/1     Running             0          37s
    c-10-4-kubia-2   0/1     Pending             0          0s
    c-10-4-kubia-2   0/1     ContainerCreating   0          4s

    생성된 StatefulSet Pod 살펴보기

    생성된 StatefulSet Pod를 살펴보면 다음과 같이 지정되어있다. 

    • Volume에는 VolumeClaimTemplate으로 생성된 PVC가 'data'라는 이름으로 등록되어있다. PVC는 VolumeClaimTemplate의 Name + Pod Name으로 생성됨. 
    • VolumeMount에서는 동일하게 'data'라는 이름의 볼륨을 마운트한다. 

    살펴보면, Pod Template 자체에 Volume을 선언하는 대신 VolumeClaimTemplate에 선언한다. 그러면 Pod가 생성될 때 Volume에 <VolumeClaimTemplate 이름> + <파드 이름>의 PVC가 Volume으로 등록되고, 각 PVC가 각 Pod의 volumeMounts 되는 형식으로 구성된다. 

    spec:
      containers:
      - image: luksa/kubia-pet
      	...
        volumeMounts:
        - mountPath: /var/data
          name: data
      ...
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: data-c-10-4-kubia-1
    
     # 매니페스트 파일
    ---     
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: c-10-4-kubia
    spec:
      selector:
        matchLabels:
          app: c-10-4-kubia
      serviceName: c-10-4-kubia
      template:
        metadata:
          name: c-10-4-kubia
          labels:
            app: c-10-4-kubia
        spec:
          containers:
            - name: kubia
              image: luksa/kubia-pet
              ports:
                - name: http
                  containerPort: 8080
              volumeMounts:
                - mountPath: /var/data
                  name: data
      volumeClaimTemplates:
        - metadata:
            name: data
          spec:
            storageClassName: longhorn
            accessModes:
              - ReadWriteOnce
            resources:
              requests:
                storage: 1Mi

    10.3.3 파드 가지고 놀기

    앞서서 StatefulSet과 연결된 서비스를 Headless로 생성했다. Headless 서비스는 클러스터 IP가 없기 때문에 클러스터 내부에서는 사용할 수 없다. 대신 개별 파드에 직접 연결해야한다. 어떻게 하면 연결할 수 있을까? FQDN과 Kubernetes의 API 서버를 통해 파드와 통신할 수 있다. 여기서는 API 서버와 통신해본다.


    API 서버를 통해 파드와 통신하기 

    kube proxy를 이용하면, 복잡한 인증 절차를 거치지 않고 바로 쿠버네티스의 원하는 파드에 통신할 수 있다. 다음 형태로 접근하면 된다.

    # 프록시를 열고, API Server로 요청을 보냄. 
    $ kubectl proxy
    
    # 파드로 접근할 때
    $ <apiServerHost>:<port>/api/v1/namespaces/default/pods/<Pod name>/proxy/<path>
    
    # 서비스 접근할 때
    $ <apiServerHost>:<port>/api/v1/namespaces/default/services/<svc name>/proxy/<path>
    
    # 예시
    $ curl localhost:8001/api/v1/namespaces/default/pods/c-10-4-kubia-0/proxy
    $ curl -X POST -d "Hey There! This greeting was submitted to kubia-0." \
      localhost:8001/api/v1/namespaces/default/pods/c-10-4-kubia-0/proxy

    StatefulSet 스케일링 

    StatefulSet을 스케일링을 하게 되면 각각 다음과 같이 동작한다

    • Up : 0 → 1 → 2... 순서대로 새로운 파드가 생성된다.
    • Down : 10 → 9 → ... → 0 순서대로 파드가 삭제된다. 

    이 때 주의해야 할 점은 새로운 Pod가 생성되었을 때는, 이전에 파드가 있던 노드와는 별개의 노드에서 생성될 수 있다는 점이다. 예를 들면 아래 그림과 같이 동작할 수 있다는 것이다. 


    10.4 StatefulSet의 Peer Discovery

    클러스터링 된 어플리케이션의 요구사항 중 하나는 피어 디스커버리(클러스터의 다른 멤버를 찾는 기능)이다. 따라서 쿠버네티스의 StatefulSet으로 만들어진 Pod들은 다른 Pod들을 손쉽게 찾을 수 있어야 한다. 


    DNS 서비스와 SRV 레코드 소개

    쿠버네티스의 Headless 서비스를 사용하면, 쿠버네티스는 DNS에 특정 파드에만 접근할 수 있도록 각 파드에 FDQN을 제공해준다. 등록되는 FDQN의 양식은 다음과 같다. 만약 Headless 서비스가 아닌, 일반 서비스를 사용하는 경우 각 파드에 무작위로 접근하는 FDQN을 제공한다. 따라서 클러스터 어플리케이션이 피어 디스커버리를 손쉽게 하려면 Headless 서비스를 만들고, FDQN을 이용하면 된다. 

    # Headless 서비스가 제공하는 FDQN
    <파드이름>.<서비스이름>.<namespace>.svc.cluster.local
    
    # 일반 서비스가 제공하는 FDQN
    <서비스이름>.<namespace>.svc.cluster.local

    FDQN은 리눅스 쉘 환경에서 손쉽게 확인할 수 있다. nslookup이나 DIG 명령어에 Headless 서비스 이름을 명시해주면, 손쉽게 관련된 FDQN을 확인해 볼 수 있게 된다. 

    # 파드 접속
    $ kubectl run -it srvlookup --image=tutum/dnsutils --rm --restart=Never -- /bin/bash
    
    # NS Lookup으로 살펴보기 → 각 FDQN 확인 가능 
    $ nslookup c-10-4-kubia.default.svc.cluster.local
    >>>
    Server:         40.0.0.10
    Address:        40.0.0.10#53
    
    Name:   c-10-4-kubia.default.svc.cluster.local
    Address: 30.0.235.158
    Name:   c-10-4-kubia.default.svc.cluster.local
    Address: 30.0.235.171
    Name:   c-10-4-kubia.default.svc.cluster.local
    Address: 30.0.189.144
    Name:   c-10-4-kubia.default.svc.cluster.local
    Address: 30.0.182.1
    
    
    # dig SRV로 살펴보기 → 각 FDQN 확인 가능
    $ dig SRV c-10-4-kubia.default.svc.cluster.local
    >>>
    ;; ANSWER SECTION:
    c-10-4-kubia.default.svc.cluster.local. 30 IN SRV 0 25 80 c-10-4-kubia-3.c-10-4-kubia.default.svc.cluster.local.
    c-10-4-kubia.default.svc.cluster.local. 30 IN SRV 0 25 80 c-10-4-kubia-2.c-10-4-kubia.default.svc.cluster.local.
    c-10-4-kubia.default.svc.cluster.local. 30 IN SRV 0 25 80 c-10-4-kubia-1.c-10-4-kubia.default.svc.cluster.local.
    c-10-4-kubia.default.svc.cluster.local. 30 IN SRV 0 25 80 c-10-4-kubia-0.c-10-4-kubia.default.svc.cluster.local.
    
    ;; ADDITIONAL SECTION:
    c-10-4-kubia-1.c-10-4-kubia.default.svc.cluster.local. 30 IN A 30.0.182.1
    c-10-4-kubia-0.c-10-4-kubia.default.svc.cluster.local. 30 IN A 30.0.235.171
    c-10-4-kubia-2.c-10-4-kubia.default.svc.cluster.local. 30 IN A 30.0.235.158
    c-10-4-kubia-3.c-10-4-kubia.default.svc.cluster.local. 30 IN A 30.0.189.144

    클러스터링 어플리케이션은 피어 디스커버리를 위해서 Headless 서비스 이름에 대해서 DNS Resolve를 하면서 SRV 값만 읽어가면 된다. 그리고 SRV에서 받아낸 DNS 이름으로 피어 디스커버리를 할 수 있게 된다. 


    10.4.1 DNS를 통한 Peer Discovery

    다음은 하나의 클러스터 어플리케이션의 피어 디스커버리 적용했을 때의 상태를 살펴본다. 

    • 피어 디스커버리를 하지 않는 경우, Client가 보내는 요청은 서비스를 통해 무작위의 파드 하나에만 전달된다. 따라서 Client가 받는 응답은 클러스터 어플리케이션 중 파드 1개에 대한 데이터만 존재한다. 
    • 피어 디스커버리를 하는 경우, Client가 보내는 요청은 처음에는 무작위의 파드 하나에만 전달된다. 하지만 파드는 DNS Resolve를 통해 피어를 찾아내고, 모든 피어에게 요청을 보낸다. 그리고 피어들에게서 응답을 받아서 Aggregating을 한 후에 Client에게 응답해준다. 

    피어 디스커버리를 한다면 모든 피어의 데이터를 받을 수 있게 되지만, 피어 디스커버리를 하지 않는 경우라면 파드 1개의 데이터만 얻을 수 있게 된다. 


    10.4.2 StatefulSet 업데이트 (Update Default 설정값)

    StatefulSet은 업데이트 관점에서는 Deployment 보다는 ReplicaSet과 비슷하다.

    • Deployment는 Pod Template을 해시값으로 관리하며, 해시값이 바뀔 때 마다 새로운 ReplicaSet을 생성해 각 파드의 Update를 처리한다.
    • StatefulSet은 Pod Template을 해쉬값으로 관리하지 않는다. 따라서 Pod Template이 변한다고 해서 업데이트를 하지 않는다. 

    Deployment는 Pod Template의 해시값이 바뀔 때 마다 ReplicaSet을 만들어서 업데이트 하는 방식이었다. 하지만 StatefulSet은 Pod Template을 해쉬 값으로 관리하지도 않고, ReplicaSet을 만들지도 않는다. 따라서 StatefulSet은 Pod Template의 변화에 대해서 업데이트를 자동으로 진행해주지는 않는다.

    StatefulSet의 변경점을 반영하고 싶다면, 각 파드를 수동으로 삭제해주면 된다. 컨트롤러는 StatefulSet의 Template을 확인해서 새로운 파드를 생성해준다. 


    10.4.3 StatefulSet의 롤링 업데이트 (UpdateStrategy : RollingUpdate)

    기본적으로 StatefulSet은 Pod Template의 변화에 대해서 Update를 하지 않는다. 하지만 쿠버네티스 1.17부터 StatefulSet에서도 Rolling Update를 할 수 있도록 추가되었다. Rolling Update를 하기 위해서는 매니페스트 파일에서 updateStrategy.rollingUpdate를 설정해주면 된다. 

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: c-10-4-kubia
    spec:
      updateStrategy:
        rollingUpdate:
          maxUnavailable: 10%

    StatefulSet의 롤링 업데이트는 아래와 같은 순서대로 진행된다. 

    • 가장 높은 Suffix를 가진 Pod부터 재기동된다. 
      • 2 종료 → 2 생성 → 1 종료 → 1 생성 → 0 종료 → 0 생성
    $ kubectl edit sts c-10-4-kubia
    >>>
    statefulset.apps/c-10-4-kubia edited
    
    $ kubectl get pod -w 
    NAME             READY   STATUS              RESTARTS   AGE
    c-10-4-kubia-0   1/1     Running             0          96m
    c-10-4-kubia-1   1/1     Running             0          72m
    c-10-4-kubia-2   1/1     Running             0          70m
    c-10-4-kubia-2   1/1     Terminating         0          70m
    c-10-4-kubia-2   0/1     ContainerCreating   0          0s
    c-10-4-kubia-2   1/1     Running             0          17s
    c-10-4-kubia-1   1/1     Terminating         0          73m
    c-10-4-kubia-1   0/1     ContainerCreating   0          0s
    c-10-4-kubia-1   1/1     Running             0          8s
    c-10-4-kubia-0   1/1     Terminating         0          98m
    c-10-4-kubia-0   0/1     ContainerCreating   0          0s
    c-10-4-kubia-0   1/1     Running             0          11s

    10.5 StatefulSet이 노드 실패를 처리하는 과정 이해하기

    StatefulSet은 새로운 파드를 생성하기 전, 이전 파드가 종료되었음을 절대적으로 확신해야만 다음 파드를 생성한다. 그렇다면 일반적인 상황이 아니라 Worker Node가 실패한 상황을 고려해보자. Worker Node가 실패한 경우, Worker Node에서 파드가 정상적으로 동작하는지는 Worker Node의 Kubelet만 알고 있다. 

    따라서 마스터 노드는 StatefulSet의 파드가 절대적으로 죽었다는 것을 알 수 없다. 이런 경우라면, 사용자가 직접 워커 노드 / 파드를 삭제해줘야한다. 그러면 쿠버네티스는 파드가 삭제된 것으로 '절대적으로 확신'하고 다음 파드를 스케쥴링한다. 


    10.5.1 노드의 네트워크 연결 해제 시뮬레이션

    • 요약 정리
      • 노드가 연결이 끊기면, 노드 내의 파드는 어떻게 동작하고 있는지 확신할 수 없다. 
      • 이 경우 파드는 Unknown 상태가 되고, Unknown 상태로 몇 분이 흐르면 마스터 노드가 Pod를 삭제 요청을 함. 이 때, Terminating 상태로 표시 됨. 
      • 실제 워커 노드의 kubelet은 Pod 삭제를 완료한 뒤에, 마스터에 보고함. 이 때 Terminating이 종료됨. 
      • Terminating 시에는 파드가 종료되었는지 알 수 없기 때문에 새로운 파드를 생성하지 않음. 

    먼저 노드를 Not Ready로 해본다. 이를 위해서 특정 워커 노드를 꺼버리면 된다. 해당 노드를 꺼버리면 워커노드는 머지 않아 NotReady 상태가 된다. 이렇게 되면 워커 노드 위에서 동작하고 있는 파드는 '동작 중인지 / 아닌지' 알 수 없게 된다.

    $ kubectl get node
    >>> 
    NAME      STATUS     ROLES           AGE   VERSION
    master1   Ready      control-plane   15d   v1.27.1
    worker1   Ready      <none>          15d   v1.27.1
    worker2   NotReady   <none>          15d   v1.27.1
    worker3   Ready      <none>          15d   v1.27.1

    따라서 해당 노드에서 동작하고 있던 파드는 머지 않아서 Unknown 상태가 된다. 그리고 Unknown 상태가 몇 분 더 지속되면, 마스터 노드가 파드를 자동으로 삭제한다. 그런데 Terminating 상태로 유지가 되는 것을 알 수 있다. 

    $ kubectl get pod -w
    NAME             READY   STATUS    RESTARTS   AGE
    c-10-4-kubia-0   1/1     Running   0          31m
    c-10-4-kubia-1   1/1     Running   0          32m
    c-10-4-kubia-2   1/1     Running   0          33m
    c-10-4-kubia-3   1/1     Running   0          33m
    c-10-4-kubia-3   1/1     Running   0          35m
    c-10-4-kubia-2   1/1     Running   0          34m
    c-10-4-kubia-3   1/1     Terminating   0          35m
    c-10-4-kubia-2   1/1     Terminating   0          34m

    10.5.2 수동으로 파드 삭제하기

    앞선 실습에서 워커 노드의 연결이 끊어져, 마스터 노드의 Pod가 자동으로 삭제처리 되는 것을 살펴볼 수 있었다. 이 때 순서는 다음과 같이 진행된다.

    • 마스터 노드는 삭제를 위해 해당 파드의 Deletion을 표시함. 이 때, 파드는 Terminating 상태임.
    • kubelet은 이것을 읽고, 실제로 파드를 삭제함. 삭제가 완료되면 kubelet은 마스터에게 보고함.
    • 보고를 받은 마스터는 파드의 Terminating을 삭제함. 

    이런 단계로 파드를 삭제하는데, 현재 경우는 워커 노드의 연결이 끊겨있다. 따라서 워커 노드의 kubelet은 Deletetion을 읽지 않을 것이고, 따라서 Terminating은 절대로 없어지지 않을 것이다. 특정 파드의 종료가 '절대적'이지 않기 때문에 StatefulSet은 Terminating이 끝날 때 까지 새로운 파드를 만들지는 않을 것이다. 

    $ kubectl get pod -w
    NAME             READY   STATUS    RESTARTS   AGE
    c-10-4-kubia-0   1/1     Running   0          31m
    c-10-4-kubia-1   1/1     Running   0          32m
    c-10-4-kubia-2   1/1     Running   0          33m
    c-10-4-kubia-3   1/1     Running   0          33m
    c-10-4-kubia-3   1/1     Running   0          35m
    c-10-4-kubia-2   1/1     Running   0          34m
    c-10-4-kubia-3   1/1     Terminating   0          35m
    c-10-4-kubia-2   1/1     Terminating   0          34m

    파드 강제 삭제하기

    이런 상황일 때, 파드가 동작하지 않는 것이 명확한 상태라면 파드 강제 삭제가 좋은 방법이 될 수 있다. 파드의 강제 삭제는 이런 의미를 가진다.

    • 마스터 노드는 Kubelet이 파드가 더 이상 실행 중이지 않음을 확인해주는 것을 기다리지 않는다. 

    강제 삭제는 이런 의미를 가지고 있기 때문에 워커 노드에서 파드가 정상 동작하고 있는 상태라면 문제가 될 수 있으니 조심해야 한다. 삭제는 아래와 같이 처리할 수 있다. 

    $ kubectl delete pod kubia-0 --force --grace-period=0

    정리

    • ReplicaSet이 생성한 각 파드는 IP를 제외하고 모두 동일한 파드다. 이것은 가축과 같음. 
    • StatefulSet이 생성한 각 파드는 서로 다른 Identity(호스트)이름과 상태(PVC)를 가진다. 각 파드는 서로 다르다고 볼 수 있음. StatefulSet은 애완동물과 비슷함. 
    • Headless 서비스를 사용할 경우, 쿠버네티스는 각 파드에 대한 FDQN을 제공한다. 만약 Headless 서비스가 아니라면, 하나의 FDQN만 제공한다. 

    댓글

    Designed by JB FACTORY