[Kubernetes in Action] StatefulSets: deploying replicated stateful applications

2021. 12. 26. 13:03DevOps/Docker & Kubernetes

 

 

Replicating Stateful Pods

이전 포스팅에서 살펴본 ReplicaSet(Replication Controller도 있지만 비슷한 개념이므로 ReplicaSet만 언급하도록 하겠습니다.)은 하나의 파드 템플릿(pod template)으로부터 여러 개의 파드 복제본들을 생성합니다. 각각의 파드들은 name, IP주소를 제외하고는 모두 동일하며 만약 파드 템플릿이 PVC(Persistent Volume Claim)을 통한 스토리지를 가지고 있다면, 해당 파드 템플릿으로부터 생성된 모든 파드들은 동일한 PVC를 사용하게 되므로 동일한 스토리지를 사용하게 됩니다.(파드는 PVC를 레퍼런스 하고 있고 이 레퍼런스는 파드 템플릿에 명시되어 있기 때문에 동일한 참조를 갖게 되는 것입니다) 따라서 ReplicaSet의 파드들이 서로 다른 스토리지를 사용(Distributed DataStore) 하기 위해서는 다른 방법을 사용해야 합니다. 

 

 

 

 

Understanding StatefulSets

위에서 언급한 것과 같이, 개별 스토리지를 각각의 파드에 부착해서 파드를 "Stateful"하게 관리(파드마다 자신의 상태를 갖는)하기 위해서는 ReplicaSet 대신에 쿠버네티스에서 제공하는 StatefulSet을 사용하면 됩니다. 

 

 

Compairing StatefulSets with ReplicaSets

ReplicaSet에 의해 관리되는 파드의 복제본들은 상태를 가지고 있지 않기 때문(Stateless)에 파드에 문제가 생기면 해당 파드를 종료하고 완전히 새로운 파드를 생성하기만 하면 됩니다. 즉, 파드가 상태를 갖지 않으므로 언제든 다른 파드들로 대체가 가능하다는 의미입니다. 하지만 Stateful한 파드들의 경우에는 기존의 파드에 문제가 생겨서 새로운 파드를 생성하게 될 경우, 이전의 파드와 동일한 이름, IP주소 등의 네트워크 정보와 상태(Storage)를 가져야 합니다. 따라서 ReplicaSet을 사용하게 되면 이러한 Stateful 한 파드는 관리할 수가 없습니다.  StatefulSet을 사용하게 되면 Stateful 한 파드를 관리할 수 있게 되며, StatefulSet이 생성한 파드들은 서로의 exact copy가 아닌 자신만의 상태와 Volume을 갖게 됩니다.

 

 

Providing a stable network identity

StatefulSet을 사용해서 파드를 생성하게 되면 각각의 파드들은 zero-based index를 기반으로 파드의 이름이 생성됩니다. ReplicaSet이 random-generated-string으로 파드의 identity를 구분했던 것과 다르게 이름에 번호를 부여해서 특정 파드를 참조하기가 쉬워집니다.(The names of the pods are thus predictable.) 이렇게 하는 이유는 바로 뒤에 나올 Scaling시에 파드를 순서대로 생성하고 제거하기 위함이며, 특정 Stateful 한 파드에 문제가 생겼을 때 해당 파드를 다시 생성하는 과정에서 필요한 여러 정보들을 참조하기 위함입니다.

 

 

 

Introducing the Governing Service

일반적인 파드들(Stateless)과는 다르게, Stateful한 파드들은 때때로 서로를 참조해야 할 일이 생기게 됩니다. (각각의 파드들이 가지고 있는 상태가 다르기 때문에 특정 상태를 갖는 특정한 파드에 작업을 요청하는 경우가 있습니다.) 따라서 서로 다른 파드들 간에 hostname으로 요청이 가능해야 합니다.

 

따라서 StatefulSet을 사용할 경우, Governing Headless Service를 생성해서 각 파드에 네트워크 정보를 부여하는 것이 강제됩니다. 이 서비스를 사용하면 각각의 파드는 자신만의 DNS엔트리를 가지게 됩니다. 예를 들어 default namespace에 있는 foo라는 서비스의 A-0 파드를 가리키는 도메인 네임은 다음과 같습니다.

a-o.foo.default.svc.cluster.local

 

Replacing Lost Pets

StatefulSet으로 관리되는 파드 중 하나가 사라지면 (해당 파드가 생성되었던 노드에 문제가 생기는 등의 이슈로) StatefulSet은 자동으로 해당 파드를 Reschedule해줍니다. 이때, 위에서 언급했듯 ReplicaSet과는 다르게 새로 생긴 파드는 사라진 파드와 동일한 이름과 네트워크 정보, 스토리지 등을 갖도록 생성됩니다.

 

 

 

Scaling a Stateful Set

StatefulSet은 스케일링을 인덱스에 기반하여 "순서대로" 처리합니다. 앞서, StatefulSet을 통해 생성된 파드들은 파드의 이름에 zero-based index가 붙는(A-0, A-1...)다는 것을 설명했습니다. 이 인덱스를 기준으로 새로운 파드를 추가할 때에는 마지막 인덱스보다 하나 큰 값을 이름으로 하는 파드를 생성하고, 기존의 파드를 제거할 때는 제일 마지막 인덱스를 이름으로 갖는 파드부터 "순서대로" 제거합니다. 이렇게 인덱스 기반으로 파드를 생성하고 제거하므로 어떤 파드가 삭제될지를 사전에 알 수 있게 됩니다.

 

 

StatefulSets 안의 파드들은 각각 독립적인 상태를 가지고 있기 때문에 모든 파드를 한번에 제거하게 되면 데이터 유실의 위험이 생기게 됩니다. 따라서 위와 같이 하나씩 Scaling 하는 방법을 통해 데이터를 백업할 기회를 제공합니다. 또한 이러한 이유로 인해  임의의 인스턴스가 정상이 아닌 경우 Scale down은 불가능합니다.

 

 

Providing stable dedicated storage to each stateful instance

Stateful한 파드들은 각자 고유한 State를 관리하기 위해 스토리지를 사용하기도 합니다. 이 경우 특정 파드에 문제가 생겨서 삭제되었다가 Rescheduled 되었을 때, 이전의 파드가 가지고 있던 스토리지를 그대로 연결해주어야 합니다. (the new instance must have the same storage attached to it) StatefulSet은 파드와 스토리지를 분리시켜(decouple) 이를 관리합니다.

 

 

Teaming up pod templates with volume claim templates

StatefulSet은 pod template뿐만 아니라 volume claim template도 갖습니다. 즉, StatefulSet이 PVC(Persistent Volume Claim)을 생성한다는 것입니다. 클러스터 관리자가 클러스터 운영환경에 맞는 PV(Persistent Volume)을 생성하면 StatefulSet은 설정파일에 명시된 Volume Claim Template을 기반으로 PVC를 생성해서 파드를 띄울 때 해당 파드에 PV를 연결해 줍니다. (실제로 Volume Claim Template을 어떻게 명시하는지는 이후 직접 StatefulSet을 생성하는 yaml파일에서 소개합니다.)

 

 

 

StatefulSet(Pod Template, Volume Claim Template이 명시된)이 Scale up하는 경우에는 새로운 파드를 생성하고(인덱스에 맞게), 새로운 PVC를 생성해서 PV와 파드를 연결해줍니다. 하지만 Scale down 하는 경우에는 조금 다르게 동작하는데, 앞서 잠깐 언급했듯 파드와 볼륨을 분리하여 파드만 제거하고 PVC는 그대로 유지하는 방식으로 동작합니다. 즉, 파드만 지워지고 볼륨과 볼륨의 데이터는 그대로 유지하는 것입니다. 이는 파드가 다시 scale up 되어 Reschedule 될 때, 기존 파드의 데이터를 그대로 유지하기 위함입니다. 따라서 StatefulSet을 Scale down 할 때 PV까지 release 하기 위해서는 PVC를 수동으로 지워주어야 합니다.

 

 

 

 

 

Understanding StatefulSet Guarantees

StatefulSet's at-most-one semantics

만약 쿠퍼네티스가 새로운 파드를 생성했는데, 기존 파드가 제거되지 않은 상태라면 "동일한 상태"를 갖는 파드가 2개가 생기게 되고, 이 2개의 파드가 동시에 동일한 스토리지에 접근해서 Write를 할 수 있는 상황이 생기게 됩니다. 따라서 쿠버네티스에서는 StatefulSet에서 같은 정보를 가지고 같은 PVC에 바인딩된 파드가 중복되어 생기지 않도록 보장하며 이를 at-most-one semantics라고 합니다. 

 

 

 

Using a StatefulSet

StatefulSet을 사용해서 애플리케이션을 배포하기 위해서는 PV(Persistent Volumes)와 Governing Service를 생성해주어야 합니다.

 

Creating the Persistent Volumes

우선 테스트를 위해 필요한 PV를 다음과 같이 리스트 형식으로 3개 생성해줍니다.

List를 사용하면 동일한 YAML파일에 여러 개의 Object들을 명시할 수 있습니다. 
이렇게 List를 사용할 수도 있고 three-dash line ('---')를 사용해서 구분할 수도 있습니다.
kind: List
apiVersion: v1
items:
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-a
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    hostPath:
      path: /tmp/pv-a
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-b
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    hostPath:
      path: /tmp/pv-b
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-c
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    hostPath:
      path: /tmp/pv-c

 

Governing Service

서비스를 생성할 때 clusterIP를 명시하지 않고 "None"으로 설정하게 되면 headless 서비스가 됩니다.
apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  clusterIP: None   # headless
  selector:
    app: kubia
  ports:
  - name: http
    port: 80

 

StatefulSet

위에서 언급한 것 처럼 volumeClaimTemplate를 명시하여 PVC Template을 사용하였습니다. 

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kubia
spec:
  serviceName: kubia
  replicas: 2
  selector:
    matchLabels:
      app: kubia       
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia-pet
        ports:
        - name: http
          containerPort: 8080
        volumeMounts:
        - name: data
          mountPath: /var/data  
  volumeClaimTemplates:         
  - metadata:
      name: data
    spec:
      resources:
        requests:
          storage: 1Mi
      accessModes:
      - ReadWriteOnce

 

위의 3가지 서비스를 모두 생성한 뒤에 상태를 확인해보면 다음과 같습니다. (다만 실습 환경을 Minikube로 구성할 경우에 PV는 Minikube가 알아서 새로 만든 PV를 사용합니다.)

 

 

확인해보면 Cluster-IP가 None인 Headless Governing Service가 생성되었고, 파드가 zero-based index롤 가지고 생성된 것을 확인할 수 있습니다.

 

 

Playing with your Pods

실습을 위한 환경이 마련되었으므로 파드에 접속해서 실제로 파드가 Stateful한지등을 이것저것 확인해보도록 하겠습니다. 주의할 점은 해당 파드들을 관리하는 서비스는 Headless이기 때문에 따로 Cluster IP를 통해 접속할 수 없다는 점입니다. 파드에 요청을 보내기 위해서는 서비스를 통해서가 아니라 Stateful Pod에 직접 요청을 보내야 합니다.

 

Communicating with pods through the API Server

다음 URL로 요청을 보내면 개별 파드에 직접 요청을 보낼 수 있습니다.

 

<apiServerHost>:<port>/api/v1/namespaces/default/pods/<pod-name>/proxy/<path>

다만 이전 포스팅에서 살펴보았듯, API Server에 직접 요청을 보내는 것은 복잡한 인증 절차들을 거쳐야 하므로 kubectl proxy를 사용해서 요청을 보내보도록 하겠습니다. 터미널을 새로 열고 kubectl proxy를 실행해줍니다.

 

 

다음과 같이 요청하면 kubia-0 파드에서 응답을 받을 수 있게 됩니다

 

 

실제로 요청을 보내면 터미널에서 파드에 이르기까지 2개의 프록시를 거치게 되는데 하나는 kubectl proxy이고 하나는 kubernetes API Server입니다. proxy를 거쳐 인증을 처리하고, 이를 API Server로 넘겨서 개별 파드에 요청을 보내게 되는 것입니다.

 

 

 

이번엔 Post 요청을 통해 해당 파드(0번 파드)에만 데이터를 저장해보겠습니다. 실제로 0번 파드에만 데이터가 저장이되고 다른 파드들에는 데이터가 저장이 되지 않은 것을 확인할 수 있습니다. 즉 파드가 "Stateful"하다는 것을 확인한 것입니다.

 

 

파드와 스토리지의 decoupling을 확인하기 위해 데이터를 Post 했던 0번 파드를 지워보겠습니다. 33초 정도 뒤에 해당 파드가 제거되었고, StatefulSet의 config에 의해 kubia-0 파드가 다시 생성된 것을 확인할 수 있습니다. 해당 파드에 curl 요청을 보내면 이전 파드와 동일한 응답을 보내는 것을 확인할 수 있습니다. 즉 reattachment가 잘 일어나고 있는 것입니다.

 

 

 

 

Discovering Peers in a StatefulSet

앞서 StatefulSet을 사용하면 특정 파드가 다른 파드를 발견할 수 있어야 한다고 언급했습니다. 물론 APIServer를 통해서도 발견하고 통신할 수 있지만 쿠버네티스에 대한 것을 아무것도 모른 상태로 애플리케이션이 운영될 수 있어야 한다는 쿠버네티스에 철학에는 맞지 않다는 단점이 있습니다. 따라서 각 파드에 접근할 수 있는 DNS를 통해 이를 해결합니다.

 

 

SRV Records

이 레코드는 특정 서비스를 제공하는 hostname, port를 알려주기 위한 record입니다. 아래의 커맨드를 터미널에 입력한 뒤 조금 기다리면 다음과 같이 파드들의 SRV Record정보가 나오게 됩니다.

kubectl run -it srvlookup --image=tutum/dnsutils --rm \
    --restart=Never -- dig SRV kubia.default.svc.cluster.local

 

 

확인 결과 ANSWER SECTION에 아까 생성한 2개의 파드의 Record를 확인할 수 있습니다. 각각

  • kubia-0.kubia.default.svc.cluster.local
  • kubia-1.kubia.default.svc.cluster.local

이라는 DNS name을 가지고 있습니다. 따라서 파드가 StatefulSet내의 다른 파드에 대한 정보를 얻고 싶다면 SRV DNS Lookup을 한번 수행해주면 됩니다. 이를테면 Node.js에서는 다음과 같이 DNS Lookup을 수행할 수 있습니다.

 

dns.resolveSrv("kubia.default.svc.cluster.local", callbackFn);

 

 

반응형