Quantcast
Channel: 개발 노트
Viewing all articles
Browse latest Browse all 299

[Kubernetes] 헷갈렸던 StatefulSet 정리

$
0
0
<p data-ke-size="size16">이 게시글은 <span data-token-index="0">D</span>atabase <span data-token-index="2">O</span>perator <span data-token-index="4">I</span>n <span data-token-index="6">K</span>ubernetes study (=<span data-token-index="8">DOIK</span>) 2기에서 스터디한 내용 중 Database Operator를 위해 필수적으로 필요한 StatefulSet에 대한 내용을 공부하며 기록한 내용입니다.</p> <h2 data-ke-size="size26">StatefulSet은?</h2> <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>StatefulSet은 Pod의 순서와 고유성에 대해 보장</li> <li>각 Pod에 영구적인 식별자인 고정 ID를 유지하기 때문에 PV를 사용할 경우 Pod가 제거되고 새로운 Pod가 실행되더라도 동일한 PV에 mount할 수 있도록 함</li> <li>N개의 replica가 설정된 StatefulSet은 각 Pod에 0 ~ N-1까지의 정수가 순서대로 할당됨 (순서 인덱싱) <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>시작 인덱스 값은 .spec.ordinals 값으로 변경 가능하지만 변경할 일은 거의 없을 것으로 예상</li> </ul> </li> <li>StatefulSet 사용이 필요한 요건 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>고유한 네트워크 식별자가 필요한 경우</li> <li>지속적인 스토리지가 필요한 경우</li> <li>순차적으로 실행/확장이 필요한 경우</li> <li>순차적인 롤링 업데이트가 필요한 경우</li> </ul> </li> </ul> <h3 data-ke-size="size23">네트워크 Identity</h3> <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>각 Pod 별로 도메인을 제어하기 위해서는 헤드리스 서비스 사용 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>$(Pod명).$(StatefulSet의 서비스명).$(네임스페이스명).svc.cluster.local</li> </ul> </li> </ul> <h3 data-ke-size="size23">PV 연동</h3> <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>replicaSet의 경우 Pod가 증가하더라도 연결된 동일한 PVC를 통해 동일한 볼륨에 mount하는 반면 StatefulSet은 생성되는 Pod마다 각자의 PVC를 갖게됨 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>StatefulSet에서 볼륨 설정에는 Deployment와 달리 volumeClaimTemplate로 설정</li> <li>Elasticsearch나 MySQL과 같이 클러스터에 속한 각 서버가 각자의 스토리지를 가져야하는 경우 적합</li> </ul> </li> </ul> <h3 data-ke-size="size23">Pod 스케일링</h3> <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>Pod 배포 시 {0..N-1}의 순서대로 생성 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>모든 선행 Pod가 Running 및 Ready 상태여야함</li> </ul> </li> <li>Pod 삭제 시 {N-1..0}의 순서대로(역순) 종료 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>모든 후속 Pod가 완전히 종료되어야 함</li> </ul> </li> </ul> <h3 data-ke-size="size23">주의 사항</h3> <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>StatefulSet에 보존할 데이터는 퍼스턴트 볼륨과 연동 되어야함</li> <li>StatefulSet이 삭제되거나 스케일다운 되어도 볼륨이 삭제되면 안됨</li> <li>StatefulSet으로 생성된 Pod의 네트워크를 식별할 수 있는 헤드리스 서비스 필요</li> <li>롤링업데이트 전략을 사용할 경우 Pod management policy(OrderedReady)를 함께 사용하면 복구를 위한 수동 개입이 필요한 파손 상태에 빠질 수 있음 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>minReadySeconds의 설정값에 따라 롤아웃 진행 상황을 확인하는데 사용되는데 기본값이 0</li> </ul> </li> <li>StatefulSet의 오브젝트명은 DNS 서브도메인 이름이 되기 때문에 적절한 이름 지정 필요</li> <li>StatefulSet의 Pod는 종료 시에도 데이터를 안전하게 처리해야 하기 때문에 pod.Spec.TerminationGracePeriodSeconds 값을 0으로 설정하면 안됨</li> <li>StatefulSet은 강제로 삭제할 경우를 만들면 안됨 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>StatefulSet 컨트롤러가 멤버 생성, 스케일링, 삭제를 담당하는데 생성 시 Pod 별로 고유한 Identity가 부여되고 StatefulSet 컨트롤러는 각 Identity 당 최대 1개의 Pod가 실행 중인지를 지속적으로 확인</li> <li>만약 동일한 Identity를 가진 Pod가 여러개 생성될 경우 데이터 손실로 이어질 수 있음 (Split-brain)</li> <li>StatefulSet의 Pod를 강제 삭제(delete 명령 시 —force 옵션 사용)할 경우 Pod가 정상 종료되었다는 kubelet의 확인을 기다리지 않고, apiserver에서 즉시 이름을 해제하는데 이 경우 StatefulSet 컨트롤러가 동일한 Identity를 가진 대체 Pod를 생성할 수 있기 때문에 1개 이상의 Pod가 실행될 수 있는 가능성이 생김</li> </ul> </li> </ul> <h2 data-ke-size="size26">StatefulSet 테스트 해보기</h2> <p data-ke-size="size16">실습 환경은 Amazon EKS 클러스터를 사용 중이며 EBS CSI Driver가 이미 설치되었다는 전제 하에 진행합니다.</p> <p data-ke-size="size16">먼저 아래 yaml 파일을 적용하여 StatefulSet을 생성합니다.</p> <pre class="yaml"><code>cat << EOF > statefulset.yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql namespace: default labels: app: mysql spec: replicas: 1 selector: matchLabels: app: mysql serviceName: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: "public.ecr.aws/docker/library/mysql:5.7" args: - "--ignore-db-dir=lost+found" imagePullPolicy: IfNotPresent env: - name: MYSQL_ROOT_PASSWORD value: root - name: MYSQL_DATABASE value: demo ports: - name: mysql containerPort: 3306 protocol: TCP volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: gp2 resources: requests: storage: 30Gi EOF kubectl apply -f statefulset.yaml </code></pre> <p data-ke-size="size16"> </p> <p data-ke-size="size16">EBS 볼륨이 동적으로 프로비저닝되는 시간이 소요되기 때문에 Pod 실행까지 약간의 시간이 소요됩니다. 이 후 아래 명령으로 mysql Pod에 EBS 볼륨이 마운트 되었는지 확인합니다.</p> <pre class="cpp"><code>kubectl exec --stdin mysql-0 -- bash -c "df -h" </code></pre> <p data-ke-size="size16"> </p> <p data-ke-size="size16">만약 EBS 볼륨은 AZ a에 생성이 되었는데 Pod는 AZ c에 생성되었다면 어떻게 될까요? EBS 볼륨이 AZ에 의존적이기 때문에 c존의 Pod에서는 해당 볼륨에 마운트할 수 없어 오류가 발생합니다. 하지만 StatefulSet으로 EBS 볼륨을 동적 프로비저닝하면 항상 실패하지 않고 잘 마운트 됩니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">그 이유는 먼저 Kubernetes의 스케쥴러에 의해 Pod가 배치될 노드가 결정되는데 Pod를 생성하기 전에 먼저 EBS CSI Driver를 통해 볼륨을 준비하기 때문입니다. 즉, 노드와 동일한 AZ에 EBS 볼륨을 먼저 생성한 후 Pod를 실행하며 마운트 합니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">그럼 이제 퍼시스턴트 볼륨으로 인해 데이터가 삭제되지 않고 보존이 잘 되는지 확인하겠습니다.</p> <pre class="crystal"><code># 파일 생성 kubectl exec mysql-0 -- bash -c "echo 123 > /var/lib/mysql/test.txt" # 파일 확인 kubectl exec mysql-0 -- ls -larth /var/lib/mysql/ | grep -i test # Pod 제거 kubectl delete pods mysql-0 # Pod가 제거되면 StatefulSet에 의해 재생성되므로 Ready 상태가 될 때까지 대기 후 Pod 확인 kubectl wait --for=condition=Ready pod \\ -l app=mysql --timeout=60s kubectl get pods -l app=mysql # 재생성 된 후에도 파일이 남아 있는지 확인 kubectl exec mysql-0 -- ls -larth /var/lib/mysql/ | grep -i test kubectl exec mysql-0 -- cat /var/lib/mysql/test.txt </code></pre> <p data-ke-size="size16"> </p> <p data-ke-size="size16">정상 실행에 대해 테스트 해봤으니 이제 본격적으로 궁금했던 부분들에 대해 테스트해보겠습니다.</p> <h3 data-ke-size="size23">StatefulSet Pod는 매번 동일한 노드와 PV에 마운트 될까?</h3> <p data-ke-size="size16">EBS 볼륨은 기본적으로 동일 리전의 인스턴스에서만 마운트가 가능한데 별도의 설정 없이도 Pod 재생성시 매번 동일 노드로 마운트가 되는지 확인해보겠습니다.</p> <pre class="crystal"><code>cat << EOF > run.sh #!/bin/bash i=0 while [ "$i" -lt 10 ] do kubectl delete pods mysql-0 kubectl wait --for=condition=Ready pod -l app=mysql --timeout=60s kubectl exec mysql-0 -- ls -larth /var/lib/mysql/ | grep -i test kubectl exec mysql-0 -- cat /var/lib/mysql/test.txt a=$(expr $a + 1) done EOF sh run.sh </code></pre> <p data-ke-size="size16"> </p> <p data-ke-size="size16">Pod는 재생성 될 때 다른 노드에 배치될 수 있고 그 의미는 EBS 볼륨가 다른 존에 생성될 수도 있다는 얘기이기도 합니다. 그럼 마운트가 실패하는 경우도 있을 것 같은데 왜 문제 없이 Pod가 재생성될까요?</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">Deployment로 생성된 Pod의 경우에는 스케쥴러에 의해 마운트 된 PV와 상관없이 최적의 노드를 찾은 후 배치하기 때문에 EBS 볼륨과 다른 AZ의 노드로 배치될 수 있지만 StatefulSet으로 생성된 Pod는 고유한 Identity를 가지며 Scheduler는 가능한 동일 노드에 Pod를 배치하려고 합니다. 하지만 노드 리소스가 부족하거나 노드가 Not Ready 상태인 경우에는 다른 노드에 배치될 수 있습니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">그럼에도 노드에 Pod가 배치 되기 전 먼저 PV가 연결된 후에 Pod가 배치되기 때문에 새로 생성된 노드가 같은 AZ에 있다면 기존의 EBS 볼륨을 그대로 사용하기 위해 detach/attach하는 절차가 수행되어 동일 PV를 사용할 수 있습니다. 물론 AZ가 달라져 버리면 EBS 볼륨의 경우에는 동일 AZ에 대한 제약 때문에 마운트에 실패하겠죠.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">Scheduler는 사실 우리가 정보를 주지 않으면 PV 마운트를 위해 Pod를 어떤 노드에 배치해야할 지 모릅니다. 만약 다른 AZ에 있는 노드가 선택되지 않도록 강제하고 싶다면 Scheduler가 노드 레이블의 ‘<b>topology.kubernetes.io/zone</b>’ 정보를 참고해서 특정 AZ의 노드에만 Pod를 배치할 수 있도록 설정해야합니다. 즉, nodeSelector나 아래와 같이 <a href="https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/">nodeAffinity</a> 설정이 필요하다는 것입니다.</p> <pre class="yaml"><code>spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - ap-northeast-2a </code></pre> <p data-ke-size="size16">그러면 아래와 같이 PV에도 기존에 없던 nodeAffinity 정보가 추가 됩니다.</p> <pre class="bash" data-ke-language="bash"><code>VolumeMode: Filesystem Capacity: 30Gi Node Affinity: Required Terms: Term 0: topology.kubernetes.io/zone in [ap-northeast-2a] topology.kubernetes.io/region in [ap-northeast-2]</code></pre> <h3 data-ke-size="size23">StatefulSet Pod를 실행하고 있는 노드가 제거될 경우 Pod는 어떻게 될까? 다른 노드에 실행 시 Zone이 달라진다면?</h3> <p data-ke-size="size16">Pod가 배치될 노드가 없다면 스케쥴러는 해당 Pod가 실행될 수 있는 다른 노드를 선택하게 되는데 어떤 Zone에 배치해야하는지는 알 수 없습니다. 따라서 새로 배치된 노드가 다른 Zone에 배치되었다면 기존의 EBS 볼륨에는 마운트할 수 없습니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">만약 동일 Zone에 다른 노드로 Pod가 배치되는 경우에는 StatefulSet의 Pod가 노드에 배치되기 전에 볼륨이 사용 가능한 상태가 되어야 합니다. EBS는 한 번에 1개의 노드에서만 사용할 수 있으므로 기존의 노드에 attach된 EBS 볼륨을 detach하고 Pod가 새로 실행된 노드로 attach를 하게 되는데 시간이 소요됩니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">이런 위험 요소들을 방지하려면 앞서 설명한 것 처럼 nodeSelector 또는 nodeAffinity 설정으로 Pod가 동일한 Zone에 실행 될 수 있도록 해야합니다.</p> <h3 data-ke-size="size23">StatefulSet Pod의 고가용성을 위해 multi-AZ로 구성할 수는 없을까?</h3> <p data-ke-size="size16">일반적으로 StatefulSet Pod와 EBS 볼륨을 특정 AZ에 바인딩하는데 이 방식의 단점은 애플리케이션의 가용성이 높지 않다는 것입니다. AZ에 장애가 발생하면 AZ가 다시 정상화될 때까지 Pod가 보류 상태로 유지됩니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">만약 AZ 장애 시 기존 AZ에서 다른 AZ로 PVC를 자동으로 장애 조치해야 하는데 이를 위한 방법은 EBS 스냅샷에서 생성된 EBS 볼륨에서 PVC를 수동으로 생성하는 것입니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">자동으로 고가용성을 달성하려면 세개의 AZ에 대해 각각의 StatefulSet를 생성할 수 있습니다. 하지만 이 경우에는 MySQL의 read replica와 같이 복제본 데이터의 경우에는 문제가 없겠지만 Pod 마다 고유한 데이터를 보관한다면 장애난 AZ의 Pod가 복구 될 때까지 해당 데이터를 사용하지 못한다는 위험이 있습니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">AZ 장애에도 자동으로 가용성을 유지하기 위해 EBS 대신 EFS를 사용하는 방법도 고려해볼 수 있습니다. EFS는 여러 AZ에 걸쳐서 고가용성을 제공하기 때문에 데이터 유실에 대한 걱정은 덜 수 있지만 비용이 더 비싸고, 인스턴스와 같은 AZ 내에서 고속의 성능을 제공하는 EBS와 달리 EFS는 고가용성을 위해 다른 AZ에 있는 스토리지로 접근 해야할 수 있기 때문에 성능에 문제가 있을 수 있습니다.</p> <h3 data-ke-size="size23">StatefulSet의 컨테이너 이미지 업데이트 시 순단이 발생할 수 있을까?</h3> <pre class="jboss-cli"><code># 새 터미널에서 아래 명령으로 watch watch -n1 "kubectl get pods" # 기존 터미널에서 아래 명령으로 이미지 업데이트 kubectl patch statefulset mysql --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"public.ecr.aws/docker/library/mysql:5.6"}]' </code></pre> <p data-ke-size="size16"> </p> <p data-ke-size="size16">실행해보면 기존 Pod를 Terminate 시키고, 완전히 제거될 때까지 대기 한 후 새로운 버전으로 Pod를 재생성합니다. 이 때 새로운 이미지이므로 pull 받는 시간과 Pod가 정상 실행될 때까지의 시간 동안은 사용 불가능 상태이므로 순단이 발생할 수 있습니다.</p> <p data-ke-size="size16"> </p> <p data-ke-size="size16">아래 내용을 참고하여 사전에 준비가 필요합니다.</p> <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>시간 단축을 위해 각 노드에 대상 컨테이너 이미지를 미리 pull 받아놓기 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>미리 업데이트 할 이미지로 DaemonSet을 생성하여 사전에 모든 노드에 미리 이미지 pulling</li> </ul> </li> <li>장애 방지를 위해 한개의 Pod가 종료되어도 클러스터 전체에 영향이 가지 않도록 가용성 확보 <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li>예를들어 MySQL의 경우 read replica의 수를 증설 후 업데이트, master의 경우 single Pod로 운영되기 때문에 클라이언트 측에서 retry 로직 구현</li> </ul> </li> </ul> <h2 data-ke-size="size26">참고</h2> <ul style="list-style-type: disc;" data-ke-list-type="disc"> <li><a href="https://kubernetes.io/ko/docs/concepts/workloads/controllers/statefulset/#스테이트풀셋-사용">https://kubernetes.io/ko/docs/concepts/workloads/controllers/statefulset/#스테이트풀셋-사용</a></li> <li><a href="https://bcho.tistory.com/1306">https://bcho.tistory.com/1306</a></li> <li><a href="https://malwareanalysis.tistory.com/598">https://malwareanalysis.tistory.com/598</a></li> <li><a href="https://zerobig-k8s.tistory.com/18">https://zerobig-k8s.tistory.com/18</a></li> <li><a href="https://malwareanalysis.tistory.com/338">https://malwareanalysis.tistory.com/338</a></li> <li><a href="https://aws.amazon.com/ko/blogs/containers/scaling-kubernetes-with-karpenter-advanced-scheduling-with-pod-affinity-and-volume-topology-awareness/">https://aws.amazon.com/ko/blogs/containers/scaling-kubernetes-with-karpenter-advanced-scheduling-with-pod-affinity-and-volume-topology-awareness/</a></li> </ul>

Viewing all articles
Browse latest Browse all 299

Trending Articles