[파드안티어피니티(PodAntiAffinity)](/docs/user-guide/node-selection/#inter-pod-affinity-and-anti-affinity-beta-feature)를 이용한 [Apache Zookeeper](https://zookeeper.apache.org) 실행을 설명한다.
최소한 4개의 노드가 있는 클러스터가 필요하며, 각 노드는 적어도 2 개의 CPU와 4 GiB 메모리가 필요하다. 이 튜토리얼에서 클러스터 노드를 통제(cordon)하고 비우게(drain) 할 것이다. **이것은 클러스터를 종료하여 노드의 모든 파드를 퇴출(evict)하는 것으로, 모든 파드는 임시적으로 언스케줄된다는 의미이다.** 이 튜토리얼을 위해 전용 클러스터를 이용하거나, 다른 테넌트에 간섭을 주는 혼란이 발생하지 않도록 해야합니다.
앙상블은 리더 선출을 위해 Zab 프로토콜을 사용하고, 리더 선출과 선거가 완료되기 전까지 앙상블은 데이터를 쓸 수 없다. 완료되면 앙상블은 Zab을 이용하여 확인하고 클라이언트에 보여지도록 모든 쓰기를 쿼럼(quorum)에 복제한다. 가중치있는 쿼럼과 관련없이, 쿼럼은 현재 리더를 포함하는 앙상블의 대다수 컴포넌트이다. 예를 들어 앙상블이 3개 서버인 경우, 리더와 다른 서버로 쿼럼을 구성한다. 앙상블이 쿼럼을 달성할 수 없다면, 앙상블은 데이터를 쓸 수 없다.
ZooKeeper는 전체 상태 머신을 메모리에 보존하고 모든 돌연변이를 저장 미디어의 내구성 있는 WAL(Write Ahead Log)에 기록한다. 서버 장애시 WAL을 재생하여 이전 상태를 복원할 수 있다. WAL이 무제한으로 커지는 것을 방지하기 위해 ZooKeeper는 주기적으로 저장 미디어에 메모리 상태의 스냅샷을 저장한다. 이 스냅샷은 메모리에 직접 적재할 수 있고 스냅샷 이전의 모든 WAL 항목은 삭제될 수 있다.
[`kubectl get`](/docs/reference/generated/kubectl/kubectl-commands/#get)을 사용하여
스테이트풀셋 컨트롤러가 스테이트풀셋 파드를 생성하는지 확인한다.
```shell
kubectl get pods -w -l app=zk
```
`zk-2` 파드가 Running and Ready 상태가 되면, `CTRL-C`를 눌러 kubectl을 종료하자.
```shell
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
```
스테이트풀셋 컨트롤러는 3개의 파드를 생성하고, 각 파드는
[ZooKeeper](http://www-us.apache.org/dist/zookeeper/stable/) 서버를 포함한 컨테이너를 가진다.
### 리더 선출 촉진
익명 네트워크에서 리더 선출을 위한 종료 알고리즘이 없기에, Zab은 리더 선출을 위해 명시적인 멤버 구성을 해야 한다. 앙상블의 각 서버는 고유 식별자를 가져야 하고, 모든 서버는 식별자 전역 집합을 알아야 하며, 각 식별자는 네트워크 주소에 연관되어야 한다.
[`kubectl exec`](/docs/reference/generated/kubectl/kubectl-commands/#exec)를 이용하여 `zk` 스테이트풀셋의 파드의
호스트네임을 알아내자.
```shell
for i in 0 1 2; do kubectl exec zk-$i -- hostname; done
```
스테이트풀셋 컨트롤러는 각 순번 인덱스에 기초하여 각 파드에 고유한 호스트네임을 부여한다. 각 호스트네임은 `<스테이트풀셋 이름>-<순번 인덱스>` 형식을 취한다. `zk` 스테이트풀셋의 `replicas` 필드는 `3`으로 설정되었기 때문에, 그 스테이트풀셋 컨트롤러는 3개 파드의 호스트네임을 `zk-0`, `zk-1`,
`zk-2`로 정한다.
```shell
zk-0
zk-1
zk-2
```
ZooKeeper 앙상블에 서버들은 고유 식별자로서 자연수를 이용하고 서버 데이터 디렉터리에 `my` 라는 파일로 서버 식별자를 저장한다.
각 서버에서 다음 명령어를 이용하여 `myid` 파일의 내용을 확인하자.
```shell
for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done
```
식별자는 자연수이고, 순번 인덱스들도 음수가 아니므로, 순번에 1을 더하여 순번을 만들 수 있다.
```shell
myid zk-0
1
myid zk-1
2
myid zk-2
3
```
`zk` 스테이트풀셋의 각 파드 Fully Qualified Domain Name (FQDN)을 얻기 위해 다음 명령어를 이용하자.
```shell
for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done
```
`zk-hs` 서비스는 모든 파드를 위한 도메인인
`zk-hs.default.svc.cluster.local`을 만든다.
```shell
zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local
```
[쿠버네티스 DNS](/docs/concepts/services-networking/dns-pod-service/)의 A 레코드는 FQDN을 파드의 IP 주소로 풀어낸다. 쿠버네티스가 파드를 리스케줄하면, 파드의 새 IP 주소로 A 레코드를 갱신하지만, A 레코드의 이름은 바뀌지 않는다.
ZooKeeper는 그것의 애플리케이션 환경설정을 `zoo.cfg` 파일에 저장한다. `kubectl exec`를 이용하여 `zk-0` 파드의 `zoo.cfg` 내용을 보자.
합의 프로토콜에서 각 참가자의 식별자는 유일해야 한다. Zab 프로토콜에서 동일한 고유 식별자를 요청하는 참가자는 없다. 이는 시스템 프로세스가 어떤 프로세스가 어떤 데이터를 커밋했는지 동의하게 하는데 필요하다. 2개 파드를 동일 순번으로 시작하였다면 두 대의 ZooKeeper 서버는 둘 다 스스로를 동일 서버로 식별한다.
합의 프로토콜에서 각 참여자의 식별자는 고유해야 한다. Zab 프로토콜에 두 참여자가 동일한 고유 식별자로 요청해서는 안된다. 이는 시스템 프로세스가 어떤 프로세스가 어떤 데이터를 커밋했는지 동의하도록 하기 위해 필수적이다. 동일 순번으로 두 개의 파드가 실행했다면 두 ZooKeeper 서버는 모두 동일한 서버로 식별된다.
```shell
kubectl get pods -w -l app=zk
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
```
각 파드의 A 레코드는 파드가 Ready 상태가 되면 입력된다. 따라서
ZooKeeper 서버의 FQDN은 단일 엔드포인트로 확인되고
해당 엔드포인트는 `myid` 파일에 구성된 식별자를 가진
고유한 ZooKeeper 서버가 된다.
```shell
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` 속성이 정확히 구성된 앙상블로 나타나는 것을 보증한다.
이는 컨테이너 내에서 안전하게 로깅하는 가장 단순한 방법이다. 표준 출력으로 애플리케이션 로그를 작성하면, 쿠버네티스는 로그 로테이션을 처리한다. 또한 쿠버네티스는 애플리케이션이 표준 출력과 표준 오류에 쓰여진 로그로 인하여 로컬 저장 미디어가 고갈되지 않도록 보장하는 정상적인 보존 정책을 구현한다.
파드의 마지막 20줄의 로그를 가져오는 [`kubectl logs`](/docs/reference/generated/kubectl/kubectl-commands/#logs) 명령을 이용하자.
```shell
kubectl logs zk-0 --tail 20
```
`kubectl logs`를 이용하거나 쿠버네티스 대시보드에서 표준 출력과 표준 오류로 쓰여진 애플리케이션 로그를 볼 수 있다.
```shell
2016-12-06 19:34:16,236 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52740
2016-12-06 19:34:16,237 [myid:1] - INFO [Thread-1136:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52740 (no session established for client)
2016-12-06 19:34:26,155 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52749
2016-12-06 19:34:26,155 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52749
2016-12-06 19:34:26,156 [myid:1] - INFO [Thread-1137:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52749 (no session established for client)
2016-12-06 19:34:26,222 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52750
2016-12-06 19:34:26,222 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52750
2016-12-06 19:34:26,226 [myid:1] - INFO [Thread-1138:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52750 (no session established for client)
2016-12-06 19:34:36,151 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO [Thread-1139:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52760 (no session established for client)
2016-12-06 19:34:36,230 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO [Thread-1140:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52761 (no session established for client)
2016-12-06 19:34:46,149 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO [Thread-1141:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52767 (no session established for client)
2016-12-06 19:34:46,230 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO [Thread-1142:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52768 (no session established for client)
다른 창에서 `zk-0` 파드의 파일시스템에서 `zkOk.sh` 스크립트를 삭제하기 위해 다음 명령어를 이용하자.
```shell
kubectl exec zk-0 -- rm /usr/bin/zookeeper-ready
```
ZooKeeper의 활성도 검사에 실패하면,
쿠버네티스는 자동으로 프로세스를 재시작하여 앙상블에 건강하지 않은 프로세스를
재시작하는 것을 보증한다.
```shell
kubectl get pod -w -l app=zk
NAME READY STATUS RESTARTS AGE
zk-0 1/1 Running 0 1h
zk-1 1/1 Running 0 1h
zk-2 1/1 Running 0 1h
NAME READY STATUS RESTARTS AGE
zk-0 0/1 Running 0 1h
zk-0 0/1 Running 1 1h
zk-0 1/1 Running 1 1h
```
### 준비도 테스트
준비도는 활성도와 동일하지 않다. 프로세스가 살아 있다면, 스케쥴링되고 건강하다.
프로세스가 준비되면 입력을 처리할 수 있다. 활성도는 필수적이나 준비도의 조건으로는
충분하지 않다. 몇몇의 경우
특별히 초기화와 종료 시에 프로세스는 살아있지만
준비되지 않을 수 있다.
준비도 검사를 지정하면, 쿠버네티스는 준비도가 통과할 때까지
애플리케이션 프로세스가 네트워크 트래픽을 수신하지 않게 한다.
ZooKeeper 서버에서는 준비도가 활성도를 내포한다. 그러므로 `zookeeper.yaml` 메니페스트에서
준비도 검사는 활성도 검사와 동일하다.
```yaml
readinessProbe:
exec:
command:
- sh
- -c
- "zookeeper-ready 2181"
initialDelaySeconds: 15
timeoutSeconds: 5
```
활성도와 준비도 검사가 동일함에도 둘 다 지정하는 것은 중요하다.
이는 ZooKeeper 앙상블에 건강한 서버만 아니라
네트워크 트래픽을 수신하는 것을 보장한다.
## 노드 실패 방지
ZooKeeper는 변조된 데이터를 성공적으로 커밋하기 위한 서버의 쿼럼이 필요하다.
3개의 서버 앙상블에서 성공적으로 저장하려면 2개 서버는 반드시 건강해야 한다.
쿼럼 기반 시스템에서, 멤버는 가용성을 보장하는
실패 영역에 걸쳐 배포된다.
중단을 방지하기 위해 개별 시스템의 손실로 인해 모범 사례에서는 동일한 시스템에
여러 인스턴스의 응용 프로그램을 함께 배치하는 것을 배제한다.
기본적으로 쿠버네티스는 동일 노드상에 `스테이트풀셋`의 파드를 위치시킬 수 있다. 생성한 3개의 서버 앙상블에서 2개의 서버가 같은 노드에 있다면, 그 노드는 실패하고 ZooKeeper 서비스 클라이언트는 그 파드들의 최소 하나가 재스케쥴링될 때까지 작동 중단을 경험할 것이다.
노드 실패하는 사건 중에도 중요 시스템의 프로세스가 재스케줄될 수 있게
항상 추가적인 용량을 프로비전해야 한다. 그렇게 하면 쿠버네티스 스케줄러가
ZooKeeper 서버 하나를 다시 스케줄하는 동안까지만 작동 중단될 것이다.
그러나 서비스에서 노드 실패로 인한 다운타임을 방지하려 한다면,
`파드안티어피니티`를 설정해야 한다.
`zk``스테이트풀셋`의 파드의 노드를 알아보기 위해 다음 명령어를 이용하자.
```shell
for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done
`kubectl drain`을 `PodDisruptionBudget`과 결합하면 유지보수중에도 서비스를 가용하게 할 수 있다. drain으로 노드를 통제하고 유지보수를 위해 노드를 오프라인하기 전에 파드를 추출하기 위해 사용한다면 서비스는 혼란 예산을 표기한 서비스는 그 예산이 존중은 존중될 것이다. 파드가 즉각적으로 재스케줄 할 수 있도록 항상 중요 서비스를 위한 추가 용량을 할당해야 한다.