ZooKeeper 실행하기 — 분산 시스템 코디네이터

ZooKeeper 실행하기 — 분산 시스템 코디네이터 (Running ZooKeeper, A Distributed System Coordinator)

이 튜토리얼은 StatefulSets, PodDisruptionBudgets, PodAntiAffinity를 사용해 쿠버네티스에서 Apache Zookeeper를 실행하는 방법을 보여 줘요.

출처: 문서

본문

시작하기 전에

이 튜토리얼을 시작하기 전에 다음 쿠버네티스 개념에 익숙해야 해요:

  • Pods
  • Cluster DNS
  • Headless Services
  • PersistentVolumes
  • StatefulSets
  • PodDisruptionBudgets
  • PodAntiAffinity
  • kubectl CLI

최소 4개의 노드가 있는 클러스터가 있어야 하며, 각 노드는 최소 2 CPU와 4 GiB 메모리가 필요해요. 이 튜토리얼에서 클러스터의 노드를 코든(cordon)하고 드레인(drain)할 거예요. 이는 클러스터가 노드의 모든 파드를 종료하고 퇴거하며, 노드가 일시적으로 스케줄링 불가능해짐을 의미해요. 이 튜토리얼을 위해 전용 클러스터를 사용하거나, 일으키는 중단이 다른 테넌트를 방해하지 않도록 보장해야 해요.

이 튜토리얼은 PersistentVolumes를 동적으로 프로비저닝하도록 클러스터를 구성했다고 가정해요. 그렇게 구성되어 있지 않다면 이 튜토리얼을 시작하기 전에 20 GiB 볼륨 3개를 수동으로 프로비저닝해야 해요.

목표 (Objectives)

이 튜토리얼 후에 다음을 알게 될 거예요.

  • StatefulSet을 사용해 ZooKeeper 앙상블(ensemble)을 배포하는 방법.
  • 앙상블을 일관되게 구성하는 방법.
  • 앙상블의 ZooKeeper 서버 배포를 펼치는 방법.
  • 계획된 유지 관리 중 서비스 가용성을 보장하기 위해 PodDisruptionBudgets을 사용하는 방법.

ZooKeeper

Apache ZooKeeper는 분산 애플리케이션을 위한 분산 오픈 소스 코디네이션 서비스예요. ZooKeeper를 사용하면 데이터를 읽고, 쓰고, 업데이트 관찰을 할 수 있어요. 데이터는 파일시스템과 유사한 계층으로 구성되고 앙상블(일련의 ZooKeeper 서버)의 모든 ZooKeeper 서버에 복제돼요. 데이터에 대한 모든 작업은 원자적이고 순차적으로 일관돼요. ZooKeeper는 Zab 합의 프로토콜을 사용해 앙상블의 모든 서버에 상태 머신을 복제함으로써 이것을 보장해요.

앙상블은 Zab 프로토콜을 사용해 리더를 선출하고, 그 선거가 완료될 때까지 앙상블은 데이터를 쓸 수 없어요. 완료되면 앙상블은 Zab을 사용해 모든 쓰기를 쿼럼(quorum)에 복제한 후 승인하고 클라이언트에 보이게 해요. 가중치 있는 쿼럼을 무시하면, 쿼럼은 현재 리더를 포함한 앙상블의 과반수 컴포넌트예요. 예를 들어 앙상블에 세 대의 서버가 있다면, 리더와 다른 서버 하나를 포함한 컴포넌트가 쿼럼을 구성해요. 앙상블이 쿼럼을 달성할 수 없으면 데이터를 쓸 수 없어요.

ZooKeeper 서버는 전체 상태 머신을 메모리에 유지하고, 모든 변형을 저장 매체의 내구성 있는 WAL(Write Ahead Log)에 써요. 서버가 충돌하면 WAL을 재생해 이전 상태를 복구할 수 있어요. WAL이 무한정 커지는 것을 방지하기 위해 ZooKeeper 서버는 메모리 상태의 스냅샷을 주기적으로 저장 매체에 써요. 이러한 스냅샷은 메모리에 직접 로드될 수 있고, 스냅샷보다 앞선 모든 WAL 항목은 폐기될 수 있어요.

ZooKeeper 앙상블 만들기

아래 매니페스트는 Headless Service, Service, PodDisruptionBudget, StatefulSet을 포함해요.

apiVersion: v1
kind: Service
metadata:
  name: zk-hs
  labels:
    app: zk
spec:
  ports:
  - port: 2888
    name: server
  - port: 3888
    name: leader-election
  clusterIP: None
  selector:
    app: zk
---
apiVersion: v1
kind: Service
metadata:
  name: zk-cs
  labels:
    app: zk
spec:
  ports:
  - port: 2181
    name: client
  selector:
    app: zk
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  selector:
    matchLabels:
      app: zk
  maxUnavailable: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: zk
spec:
  selector:
    matchLabels:
      app: zk
  serviceName: zk-hs
  replicas: 3
  updateStrategy:
    type: RollingUpdate
  podManagementPolicy: OrderedReady
  template:
    metadata:
      labels:
        app: zk
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"
      containers:
      - name: kubernetes-zookeeper
        imagePullPolicy: Always
        image: "registry.k8s.io/kubernetes-zookeeper:1.0-3.4.10"
        resources:
          requests:
            memory: "1Gi"
            cpu: "0.5"
        ports:
        - containerPort: 2181
          name: client
        - containerPort: 2888
          name: server
        - containerPort: 3888
          name: leader-election
        command:
        - sh
        - -c
        - "start-zookeeper \
          --servers=3 \
          --data_dir=/var/lib/zookeeper/data \
          --data_log_dir=/var/lib/zookeeper/data/log \
          --conf_dir=/opt/zookeeper/conf \
          --client_port=2181 \
          --election_port=3888 \
          --server_port=2888 \
          --tick_time=2000 \
          --init_limit=10 \
          --sync_limit=5 \
          --heap=512M \
          --max_client_cnxns=60 \
          --snap_retain_count=3 \
          --purge_interval=12 \
          --max_session_timeout=40000 \
          --min_session_timeout=4000 \
          --log_level=INFO"
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/zookeeper
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
  volumeClaimTemplates:
  - metadata:
      name: datadir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

터미널을 열고 kubectl apply 명령을 사용해 매니페스트를 만들어요.

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

이것은 zk-hs Headless Service, zk-cs Service, zk-pdb PodDisruptionBudget, zk StatefulSet을 만들어요.

service/zk-hs created
service/zk-cs created
poddisruptionbudget.policy/zk-pdb created
statefulset.apps/zk created

kubectl get을 사용해 StatefulSet 컨트롤러가 StatefulSet의 파드를 만드는 것을 지켜봐요.

kubectl get pods -w -l app=zk

zk-2 파드가 Running과 Ready가 되면 CTRL-C로 kubectl을 종료해요.

NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s

StatefulSet 컨트롤러는 세 개의 파드를 만들고, 각 파드는 ZooKeeper 서버가 있는 컨테이너를 가져요.

리더 선거 촉진

익명 네트워크에서 리더를 선출하는 종료 알고리즘이 없으므로, Zab은 리더 선거를 수행하기 위해 명시적인 멤버십 구성을 요구해요. 앙상블의 각 서버는 고유한 식별자를 가져야 하고, 모든 서버는 식별자의 전체 집합을 알아야 하며, 각 식별자는 네트워크 주소와 연관되어야 해요.

kubectl exec을 사용해 zk StatefulSet의 파드 호스트 이름을 가져와요.

for i in 0 1 2; do kubectl exec zk-$i -- hostname; done

StatefulSet 컨트롤러는 각 파드에 서수 인덱스에 기반한 고유한 호스트 이름을 제공해요. 호스트 이름은 <statefulset name>-<ordinal index> 형태를 취해요. zk StatefulSet의 replicas 필드가 3으로 설정되어 있으므로, Set의 컨트롤러는 호스트 이름이 zk-0, zk-1, zk-2로 설정된 세 개의 파드를 만들어요.

zk-0
zk-1
zk-2

ZooKeeper 앙상블의 서버는 자연수를 고유 식별자로 사용하고, 각 서버의 식별자를 서버의 데이터 디렉터리의 myid라는 파일에 저장해요.

각 서버의 myid 파일 내용을 검사하려면 다음 명령을 사용해요.

for i in 0 1 2; do echo "myid zk-$i"; kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done

식별자가 자연수이고 서수 인덱스가 음이 아닌 정수이므로, 서수에 1을 더해 식별자를 생성할 수 있어요.

myid zk-0
1
myid zk-1
2
myid zk-2
3

zk StatefulSet의 각 파드의 FQDN(정규화된 도메인 이름)을 얻으려면 다음 명령을 사용해요.

for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done

zk-hs Service는 모든 파드에 대한 도메인 zk-hs.default.svc.cluster.local을 만들어요.

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

Kubernetes DNS의 A 레코드는 FQDN을 파드의 IP 주소로 해석해요. 쿠버네티스가 파드를 재스케줄링하면 A 레코드를 파드의 새 IP 주소로 업데이트하지만, A 레코드 이름은 변경되지 않아요.

ZooKeeper는 애플리케이션 구성을 zoo.cfg라는 파일에 저장해요. kubectl exec을 사용해 zk-0 파드의 zoo.cfg 파일 내용을 봐요.

kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg

파일 맨 아래의 server.1, server.2, server.3 속성에서 1, 2, 3은 ZooKeeper 서버의 myid 파일의 식별자에 해당해요. 그것들은 zk StatefulSet의 파드에 대한 FQDN으로 설정돼요.

clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
tickTime=2000
initLimit=10
syncLimit=2000
maxClientCnxns=60
minSessionTimeout= 4000
maxSessionTimeout= 40000
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

합의 달성

합의(consensus) 프로토콜은 각 참가자의 식별자가 고유해야 해요. Zab 프로토콜의 두 참가자가 같은 고유 식별자를 주장해서는 안 돼요. 이것은 시스템의 프로세스가 어떤 프로세스가 어떤 데이터를 커밋했는지 합의할 수 있도록 필요해요. 두 파드가 같은 서수로 시작되면 두 ZooKeeper 서버가 둘 다 자신을 같은 서버로 식별할 거예요.

kubectl get pods -w -l app=zk

각 파드에 대한 A 레코드는 파드가 Ready가 될 때 입력돼요. 따라서 ZooKeeper 서버의 FQDN은 단일 엔드포인트로 해석되고, 그 엔드포인트는 myid 파일에 구성된 정체성을 주장하는 고유한 ZooKeeper 서버가 돼요.

zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local

이것은 ZooKeeper의 zoo.cfg 파일의 servers 속성이 올바르게 구성된 앙상블을 나타냄을 보장해요.

server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888

서버가 Zab 프로토콜을 사용해 값을 커밋하려고 할 때, (리더 선거가 성공하고 적어도 두 파드가 Running과 Ready라면) 합의를 달성하고 값을 커밋하거나, (조건 중 하나라도 충족되지 않으면) 그렇게 하지 못할 거예요. 한 서버가 다른 서버를 대신해 쓰기를 승인하는 상태는 발생하지 않아요.

앙상블 무결성 테스트

가장 기본적인 무결성 테스트는 한 ZooKeeper 서버에 데이터를 쓰고 다른 서버에서 데이터를 읽는 것이에요.

아래 명령은 zkCli.sh 스크립트를 실행해 앙상블의 zk-0 파드의 /hello 경로에 world를 써요.

kubectl exec zk-0 -- zkCli.sh create /hello world
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
Created /hello

zk-1 파드에서 데이터를 가져오려면 다음 명령을 사용해요.

kubectl exec zk-1 -- zkCli.sh get /hello

zk-0에서 만든 데이터는 앙상블의 모든 서버에서 사용할 수 있어요.

WATCHER::
WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

내구성 있는 저장소 제공

ZooKeeper 기초 섹션에서 언급했듯이 ZooKeeper는 모든 항목을 내구성 있는 WAL에 커밋하고, 주기적으로 메모리 상태의 스냅샷을 저장 매체에 써요. 복제된 상태 머신을 달성하기 위해 합의 프로토콜을 사용하는 애플리케이션에서 내구성을 제공하기 위해 WAL을 사용하는 것은 일반적인 기술이에요.

kubectl delete 명령을 사용해 zk StatefulSet을 삭제해요.

kubectl delete statefulset zk
statefulset.apps "zk" deleted

StatefulSet의 파드 종료를 지켜봐요.

kubectl get pods -w -l app=zk

zk-0이 완전히 종료되면 CTRL-C로 kubectl을 종료해요.

zk-2      1/1       Terminating   0         9m
zk-0      1/1       Terminating   0         11m
zk-1      1/1       Terminating   0         10m
zk-2      0/1       Terminating   0         9m
...

zookeeper.yaml의 매니페스트를 다시 적용해요.

kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml

이것은 zk StatefulSet 객체를 만들지만, 이미 존재하므로 매니페스트의 다른 API 객체는 수정되지 않아요.

StatefulSet 컨트롤러가 StatefulSet의 파드를 재생성하는 것을 지켜봐요.

kubectl get pods -w -l app=zk

zk-2 파드가 Running과 Ready가 되면 CTRL-C로 kubectl을 종료해요.

아래 명령을 사용해 무결성 테스트 중 입력한 값을 zk-2 파드에서 가져와요.

kubectl exec zk-2 zkCli.sh get /hello

zk StatefulSet의 모든 파드를 종료하고 재생성했지만, 앙상블은 여전히 원래 값을 제공해요.

zk StatefulSet의 spec의 volumeClaimTemplates 필드는 각 파드에 대해 프로비저닝된 PersistentVolume을 지정해요.

StatefulSet 컨트롤러는 StatefulSet의 각 파드에 대해 PersistentVolumeClaim을 생성해요.

다음 명령을 사용해 StatefulSet의 PersistentVolumeClaims를 가져와요.

kubectl get pvc -l app=zk

StatefulSet이 파드를 재생성할 때 파드의 PersistentVolumes를 다시 마운트해요.

StatefulSet의 컨테이너 템플릿의 volumeMounts 섹션은 PersistentVolumes를 ZooKeeper 서버의 데이터 디렉터리에 마운트해요.

zk StatefulSet의 파드가 (재)스케줄링될 때, 항상 같은 PersistentVolume이 ZooKeeper 서버의 데이터 디렉터리에 마운트돼요. 파드가 재스케줄링될 때에도 ZooKeeper 서버의 WAL에 대한 모든 쓰기와 모든 스냅샷이 내구성 있게 유지돼요.

일관된 구성 보장

리더 선거 촉진과 합의 달성 섹션에서 언급했듯이, ZooKeeper 앙상블의 서버는 리더를 선출하고 쿼럼을 형성하기 위해 일관된 구성이 필요해요. 또한 프로토콜이 네트워크에서 올바르게 작동하려면 Zab 프로토콜의 일관된 구성이 필요해요. 우리 예시에서는 구성을 매니페스트에 직접 포함시켜 일관된 구성을 달성해요.

zk StatefulSet을 가져와요.

kubectl get sts zk -o yaml

ZooKeeper 서버를 시작하는 데 사용된 명령은 구성을 명령줄 파라미터로 전달했어요. 환경 변수를 사용해 앙상블에 구성을 전달할 수도 있어요.

로깅 구성

zkGenConfig.sh 스크립트가 생성하는 파일 중 하나가 ZooKeeper의 로깅을 제어해요. ZooKeeper는 Log4j를 사용하며, 기본적으로 로깅 구성에 시간과 크기 기반 회전 파일 추가자(rolling file appender)를 사용해요.

아래 명령을 사용해 zk StatefulSet의 파드 중 하나에서 로깅 구성을 가져와요.

kubectl exec zk-0 cat /usr/etc/zookeeper/log4j.properties

아래 로깅 구성은 ZooKeeper 프로세스가 모든 로그를 표준 출력 파일 스트림에 쓰게 해요.

zookeeper.root.logger=CONSOLE
zookeeper.console.threshold=INFO
log4j.rootLogger=${zookeeper.root.logger}
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=${zookeeper.console.threshold}
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n

이것은 컨테이너 안에서 안전하게 로그하는 가장 간단한 가능한 방법이에요. 애플리케이션이 표준 출력에 로그를 쓰기 때문에 쿠버네티스가 로그 회전을 처리해 줘요. 쿠버네티스는 또한 표준 출력과 표준 오류에 쓰여진 애플리케이션 로그가 로컬 저장 매체를 소진하지 않도록 보장하는 합리적인 보존 정책을 구현해요.

kubectl logs를 사용해 파드 중 하나에서 마지막 20개의 로그 줄을 검색해요.

kubectl logs zk-0 --tail 20

kubectl logs와 쿠버네티스 대시보드에서 표준 출력이나 표준 오류에 쓰여진 애플리케이션 로그를 볼 수 있어요.

쿠버네티스는 많은 로깅 솔루션과 통합돼요. 클러스터와 애플리케이션에 가장 잘 맞는 로깅 솔루션을 선택할 수 있어요. 클러스터 레벨 로깅과 집계를 위해 로그를 회전하고 보내는 사이드카 컨테이너 배포를 고려하세요.

비특권 사용자 구성

애플리케이션이 컨테이너 안에서 특권 사용자로 실행되도록 허용하는 모범 사례는 논쟁의 여지가 있어요. 조직이 애플리케이션이 비특권 사용자로 실행되도록 요구한다면 SecurityContext를 사용해 엔트리 포인트가 실행되는 사용자를 제어할 수 있어요.

zk StatefulSet의 파드 템플릿은 SecurityContext를 포함해요.

zk-0 파드에서 ZooKeeper 프로세스 정보를 가져와요.

securityContext 객체의 runAsUser 필드가 1000으로 설정되어 있으므로, root로 실행되는 대신 ZooKeeper 프로세스가 zookeeper 사용자로 실행돼요.

기본적으로 파드의 PersistentVolumes가 ZooKeeper 서버의 데이터 디렉터리에 마운트되면 root 사용자만 접근할 수 있어요. 이 구성은 ZooKeeper 프로세스가 WAL에 쓰고 스냅샷을 저장하는 것을 방지해요.

아래 명령을 사용해 zk-0 파드의 ZooKeeper 데이터 디렉터리의 파일 권한을 가져와요.

kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data

securityContext 객체의 fsGroup 필드가 1000으로 설정되어 있으므로 파드의 PersistentVolumes의 소유권이 zookeeper 그룹으로 설정되고, ZooKeeper 프로세스가 데이터를 읽고 쓸 수 있어요.

ZooKeeper 프로세스 관리

ZooKeeper 문서는 "각 ZooKeeper 서버 프로세스(JVM)를 관리하는 감독 프로세스(supervisory process)를 원할 것"이라고 언급해요. 분산 시스템에서 실패한 프로세스를 재시작하기 위해 워치독(감독 프로세스)을 활용하는 것은 일반적인 패턴이에요. 쿠버네티스에 애플리케이션을 배포할 때는 외부 유틸리티를 감독 프로세스로 사용하는 대신 쿠버네티스를 애플리케이션의 워치독으로 사용해야 해요.

앙상블 업데이트

zk StatefulSet은 RollingUpdate 업데이트 전략을 사용하도록 구성돼요.

kubectl patch을 사용해 서버에 할당된 CPU 수를 업데이트할 수 있어요.

kubectl patch sts zk --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/cpu", "value":"0.3"}]'

kubectl rollout status을 사용해 업데이트 상태를 지켜봐요.

kubectl rollout status sts/zk

이것은 파드를 역순으로 한 번에 하나씩 종료하고 새 구성으로 재생성해요. 이것은 롤링 업데이트 중 쿼럼이 유지되도록 보장해요.

kubectl rollout history 명령을 사용해 이전 구성의 히스토리를 보세요.

kubectl rollout history sts/zk

kubectl rollout undo 명령을 사용해 수정을 롤백해요.

kubectl rollout undo sts/zk

프로세스 실패 처리

재시작 정책(Restart Policies)은 쿠버네티스가 파드의 컨테이너 엔트리 포인트의 프로세스 실패를 어떻게 처리하는지 제어해요. StatefulSet의 파드에 대해 유일하게 적절한 RestartPolicy는 Always이며, 이것이 기본값이에요. 상태가 있는(stateful) 애플리케이션의 경우 기본 정책을 재정의해서는 안 돼요.

다음 명령을 사용해 zk-0 파드에서 실행되는 ZooKeeper 서버의 프로세스 트리를 검사해요.

kubectl exec zk-0 -- ps -ef

컨테이너의 엔트리 포인트로 사용된 명령은 PID 1이고, 엔트리 포인트의 자식인 ZooKeeper 프로세스는 PID 27이에요.

다른 터미널에서 다음 명령으로 zk StatefulSet의 파드를 지켜봐요.

kubectl get pod -w -l app=zk

다른 터미널에서 다음 명령으로 zk-0 파드의 ZooKeeper 프로세스를 종료해요.

kubectl exec zk-0 -- pkill java

ZooKeeper 프로세스의 종료로 인해 부모 프로세스가 종료됐어요. 컨테이너의 RestartPolicy가 Always이므로 부모 프로세스가 재시작됐어요.

애플리케이션이 스크립트(zkServer.sh 같은)를 사용해 애플리케이션의 비즈니스 로직을 구현하는 프로세스를 시작한다면, 스크립트는 자식 프로세스와 함께 종료되어야 해요. 이것은 애플리케이션의 비즈니스 로직을 구현하는 프로세스가 실패할 때 쿠버네티스가 애플리케이션의 컨테이너를 재시작하도록 보장해요.

활성 상태(liveness) 테스트

애플리케이션을 구성해 실패한 프로세스를 재시작하는 것은 분산 시스템을 건강하게 유지하기에 충분하지 않아요. 시스템의 프로세스가 살아 있으면서도 응답하지 않거나, 어쩌면 불건전한 시나리오가 있어요. liveness 프로브를 사용해 애플리케이션의 프로세스가 불건전함을 쿠버네티스에 알리고 재시작해야 해요.

zk StatefulSet의 파드 템플릿은 liveness 프로브를 지정해요.

프로브는 ZooKeeper ruok 네 글자 단어를 사용해 서버의 상태를 테스트하는 bash 스크립트를 호출해요.

OK=$(echo ruok | nc 127.0.0.1 $1)
if [ "$OK" == "imok" ]; then
    exit 0
else
    exit 1
fi

한 터미널 창에서 다음 명령을 사용해 zk StatefulSet의 파드를 지켜봐요.

kubectl get pod -w -l app=zk

다른 창에서 다음 명령을 사용해 zk-0 파드의 파일시스템에서 zookeeper-ready 스크립트를 삭제해요.

kubectl exec zk-0 -- rm /opt/zookeeper/bin/zookeeper-ready

ZooKeeper 프로세스의 liveness 프로브가 실패하면 쿠버네티스가 자동으로 프로세스를 재시작해, 앙상블의 불건전한 프로세스가 재시작되도록 보장해요.

준비 상태(readiness) 테스트

준비 상태(readiness)는 활성 상태(liveness)와 같지 않아요. 프로세스가 살아 있으면 스케줄링되고 건강한 것이에요. 프로세스가 준비되었으면 입력을 처리할 수 있어요. Liveness는 readiness의 필요 조건이지만 충분 조건은 아니에요. 특히 초기화와 종료 중에 프로세스가 살아 있지만 준비되지 않을 수 있는 경우가 있어요.

readiness 프로브를 지정하면 쿠버네티스는 애플리케이션의 프로세스가 준비 상태 검사를 통과할 때까지 네트워크 트래픽을 받지 않도록 보장해요.

ZooKeeper 서버의 경우 liveness는 readiness를 의미해요. 따라서 zookeeper.yaml 매니페스트의 readiness 프로브는 liveness 프로브와 동일해요.

liveness와 readiness 프로브가 동일하더라도 둘 다 지정하는 것이 중요해요. 이것은 ZooKeeper 앙상블의 건강한 서버만 네트워크 트래픽을 받도록 보장해요.

노드 장애 견디기

ZooKeeper는 데이터에 대한 변형을 성공적으로 커밋하기 위해 서버의 쿼럼이 필요해요. 세 서버 앙상블의 경우 쓰기가 성공하려면 두 서버가 건강해야 해요. 쿼럼 기반 시스템에서 멤버는 가용성을 보장하기 위해 장애 도메인에 걸쳐 배포돼요. 개별 머신의 손실로 인한 중단을 피하기 위해, 모범 사례는 같은 머신에 애플리케이션의 여러 인스턴스를 공동 배치하지 않는 것을 금지해요.

기본적으로 쿠버네티스는 StatefulSet의 파드를 같은 노드에 공동 배치할 수 있어요. 만든 세 서버 앙상블의 경우 두 서버가 같은 노드에 있고 그 노드가 실패하면, 파드 중 적어도 하나가 재스케줄링될 때까지 ZooKeeper 서비스의 클라이언트가 중단을 경험할 거예요.

중요한 시스템의 프로세스가 노드 장애 시 재스케줄링되도록 추가 용량을 항상 프로비저닝해야 해요. 그렇게 하면 중단은 쿠버네티스 스케줄러가 ZooKeeper 서버 중 하나를 재스케줄링할 때까지만 지속돼요. 그러나 서비스가 다운타임 없이 노드 실패를 견디기를 원한다면 podAntiAffinity를 설정해야 해요.

아래 명령을 사용해 zk StatefulSet의 파드에 대한 노드를 가져와요.

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

zk StatefulSet의 모든 파드가 다른 노드에 배포돼 있어요.

이것은 zk StatefulSet의 파드에 PodAntiAffinity가 지정되었기 때문이에요.

requiredDuringSchedulingIgnoredDuringExecution 필드는 쿠버네티스 스케줄러에게 topologyKey로 정의된 도메인에서 app 레이블이 zk인 두 파드를 절대 공동 배치하지 말라고 알려줘요. topologyKey kubernetes.io/hostname은 도메인이 개별 노드임을 나타내요. 다른 규칙, 레이블, 셀렉터를 사용해 이 기술을 확장해 앙상블을 물리적, 네트워크, 전력 장애 도메인에 걸쳐 펼칠 수 있어요.

유지 관리에서 살아남기

이 섹션에서는 노드를 코든하고 드레인할 거예요. 공유 클러스터에서 이 튜토리얼을 사용한다면 이것이 다른 테넌트에 부정적인 영향을 주지 않도록 하세요.

이전 섹션은 계획되지 않은 노드 실패에서 살아남기 위해 파드를 노드에 걸쳐 펼치는 방법을 보여 줬지만, 계획된 유지 관리로 인해 발생하는 일시적인 노드 실패에 대해서도 계획해야 해요.

이 명령을 사용해 클러스터의 노드를 가져와요.

kubectl get nodes

이 튜토리얼은 최소 4개의 노드가 있는 클러스터를 가정해요. 클러스터에 4개보다 많다면, kubectl cordon을 사용해 4개를 제외한 모든 노드를 코든해요. 4개 노드로 제한하면 쿠버네티스가 다음 유지 관리 시뮬레이션에서 zookeeper 파드를 스케줄링할 때 어피니티와 PodDisruptionBudget 제약을 만나도록 보장해요.

kubectl cordon <node-name>

이 명령을 사용해 zk-pdb PodDisruptionBudget을 가져와요.

kubectl get pdb zk-pdb

max-unavailable 필드는 쿠버네티스에게 zk StatefulSet에서 한 번에 최대 한 개의 파드만 사용 불가능할 수 있음을 나타내요.

한 터미널에서 이 명령을 사용해 zk StatefulSet의 파드를 지켜봐요.

kubectl get pods -w -l app=zk

다른 터미널에서 이 명령을 사용해 파드가 현재 스케줄링된 노드를 가져와요.

for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done

zk-0 파드가 스케줄링된 노드를 코든하고 드레인하려면 kubectl drain을 사용해요.

kubectl drain $(kubectl get pod zk-0 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

클러스터에 4개의 노드가 있으므로 kubectl drain이 성공하고 zk-0이 다른 노드로 재스케줄링돼요.

첫 번째 터미널에서 StatefulSet의 파드를 계속 지켜보고 zk-1이 스케줄링된 노드를 드레인해요.

kubectl drain $(kubectl get pod zk-1 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

zk-1 파드는 zk StatefulSet에 파드의 공동 배치를 방지하는 PodAntiAffinity 규칙이 있고 스케줄링 가능한 노드가 두 개뿐이므로 스케줄링될 수 없으며, 파드는 Pending 상태로 남아 있을 거예요.

StatefulSet의 파드를 계속 지켜보고 zk-2가 스케줄링된 노드를 드레인해요.

kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data

이 출력에 오류가 있을 거예요:

There are pending pods when an error occurred: Cannot evict pod as it would violate the pod's disruption budget.

이 출력은 계획된 유지 관리를 수행하는 동안 두 파드가 스케줄링될 수 없을 때 PodDisruptionBudget이 중단으로부터 보호한다는 것을 보여 줘요. 노드가 네 개일 때 PDB로 인해 사용 불가능한 파드 수가 하나로 제한되므로, 드레인할 노드에 있던 파드가 드레인될 수 없어요. 드레인이 성공하려면 드레인하고 있는 노드가 다른 파드가 스케줄링을 계속할 수 있는 유휴 노드 하나를 제공하는 네 개 이상의 노드에 의존할 수 있어야 해요.

더 알아보기 (Learn more)