--- # reviewers: # - enisoc # - erictune # - foxish # - janetkuo # - kow3ns # - smarterclayton title: 복제 스테이트풀 애플리케이션 실행하기 content_type: tutorial weight: 30 --- 이 페이지에서는 {{< glossary_tooltip term_id="statefulset" >}} 으로 복제 스테이트풀 애플리케이션을 실행하는 방법에 대해 소개한다. 이 애플리케이션은 복제 MySQL 데이터베이스이다. 이 예제의 토폴로지는 단일 주 서버와 여러 복제 서버로 이루어져있으며, row-based 비동기 복제 방식을 사용한다. {{< note >}} **해당 설정은 프로덕션 설정이 아니다**. 쿠버네티스에서 스테이트풀한 애플리케이션을 실행하기 위한 일반적인 패턴에 집중하기 위해 MySQL 세팅이 안전하지 않은 기본 설정으로 되어 있다. {{< /note >}} ## {{% heading "prerequisites" %}} * {{< include "task-tutorial-prereqs.md" >}} {{< version-check >}} * {{< include "default-storage-class-prereqs.md" >}} * 이 튜토리얼은 [퍼시스턴트볼륨](/ko/docs/concepts/storage/persistent-volumes/) 그리고 [스테이트풀셋](/ko/docs/concepts/workloads/controllers/statefulset/), [파드](/ko/docs/concepts/workloads/pods/), [서비스](/ko/docs/concepts/services-networking/service/), [컨피그맵(ConfigMap)](/docs/tasks/configure-pod-container/configure-pod-configmap/)와 같은 핵심 개념들에 대해 알고 있다고 가정한다. * MySQL에 대한 지식이 있으면 도움이 되지만, 이 튜토리얼은 다른 시스템을 활용하였을 때도 도움이 되는 일반적인 패턴을 다루는데 중점을 둔다. * default 네임스페이스를 사용하거나, 다른 오브젝트들과 충돌이 나지 않는 다른 네임스페이스를 사용한다. ## {{% heading "objectives" %}} * 스테이트풀셋을 이용한 복제 MySQL 토폴로지를 배포한다. * MySQL 클라이언트에게 트래픽을 보낸다. * 다운타임에 대한 저항력을 관찰한다. * 스테이트풀셋을 확장/축소한다. ## MySQL 배포하기 MySQL 디플로이먼트 예시는 컨피그맵과, 2개의 서비스, 그리고 스테이트풀셋으로 구성되어 있다. ### 컨피그맵 생성하기 {#configmap} 다음 YAML 설정 파일로부터 컨피그맵을 생성한다. {{< codenew file="application/mysql/mysql-configmap.yaml" >}} ```shell kubectl apply -f https://k8s.io/examples/application/mysql/mysql-configmap.yaml ``` 이 컨피그맵은 당신이 독립적으로 주 MySQL 서버와 레플리카들의 설정을 컨트롤할 수 있도록 `my.cnf` 을 오버라이드한다. 이 경우에는, 주 서버는 복제 로그를 레플리카들에게 제공하고 레플리카들은 복제를 통한 쓰기가 아닌 다른 쓰기들은 거부하도록 할 것이다. 컨피그맵 자체가 다른 파드들에 서로 다른 설정 영역이 적용되도록 하는 것이 아니다. 스테이트풀셋 컨트롤러가 제공해주는 정보에 따라서, 각 파드들은 초기화되면서 설정 영역을 참조할지 결정한다. ### 서비스 생성하기 {#services} 다음 YAML 설정 파일로부터 서비스를 생성한다. {{< codenew file="application/mysql/mysql-services.yaml" >}} ```shell kubectl apply -f https://k8s.io/examples/application/mysql/mysql-services.yaml ``` 헤드리스 서비스는 스테이트풀셋 {{< glossary_tooltip text="컨트롤러" term_id="controller" >}}가 집합의 일부분인 파드들을 위해 생성한 DNS 엔트리들(entries)의 위한 거점이 된다. 헤드리스 서비스의 이름이 `mysql`이므로, 파드들은 같은 쿠버네티스 클러스터나 네임스페이스에 존재하는 다른 파드들에게 `.mysql`라는 이름으로 접근될 수 있다. `mysql-read`라고 불리우는 클라이언트 서비스는 고유의 클러스터 IP를 가지며, Ready 상태인 모든 MySQL 파드들에게 커넥션을 분배하는 일반적인 서비스이다. 잠재적인 엔드포인트들의 집합은 주 MySQL 서버와 해당 레플리카들을 포함한다. 오직 읽기 쿼리들만 로드-밸런싱된 클라이언트 서비스를 이용할 수 있다는 사실에 주목하자. 하나의 주 MySQL 서버만이 존재하기 떄문에, 클라이언트들은 쓰기 작업을 실행하기 위해서 주 MySQL 파드에 (헤드리스 서비스 안에 존재하는 DNS 엔트리를 통해) 직접 접근해야 한다. ### 스테이트풀셋 생성하기 {#statefulset} 마지막으로, 다음 YAML 설정 파일로부터 스테이트풀셋을 생성한다. {{< codenew file="application/mysql/mysql-statefulset.yaml" >}} ```shell kubectl apply -f https://k8s.io/examples/application/mysql/mysql-statefulset.yaml ``` 다음을 실행하여, 초기화되는 프로세스들을 확인할 수 있다. ```shell kubectl get pods -l app=mysql --watch ``` 잠시 뒤에, 3개의 파드들이 `Running` 상태가 되는 것을 볼 수 있다. ``` NAME READY STATUS RESTARTS AGE mysql-0 2/2 Running 0 2m mysql-1 2/2 Running 0 1m mysql-2 2/2 Running 0 1m ``` **Ctrl+C**를 입력하여 watch를 종료하자. {{< note >}} 만약 아무 진행 상황도 보이지 않는다면, [시작하기 전에](#before-you-begin) 에 언급된 동적 퍼시스턴트볼륨 프로비저너(provisioner)가 활성화되어 있는지 확인한다. {{< /note >}} 해당 메니페스트에는 스테이트풀셋의 일부분인 스테이트풀 파드들을 관리하기 위한 다양한 기법들이 적용되어 있다. 다음 섹션에서는 스테트풀셋이 파드들을 생성할 때 일어나는 일들을 이해할 수 있도록 일부 기법들을 강조하여 설명한다. ## 스테이트풀 파드 초기화 이해하기 스테이트풀셋 컨트롤러는 파드들의 인덱스에 따라 순차적으로 시작시킨다. 컨트롤러는 다음 파드 생성 이전에 각 파드가 Ready 상태가 되었다고 알려줄 때까지 기다린다. 추가적으로, 컨트롤러는 각 파드들에게 `<스테이트풀셋 이름>-<순차적 인덱스>` 형태의 고유하고 안정적인 이름을 부여하는데, 결과적으로 파드들은 `mysql-0`, `mysql-1`, 그리고 `mysql-2` 라는 이름을 가지게 된다. 스테이트풀셋 매니페스트의 파드 템플릿은 해당 속성들을 통해 순차적인 MySQL 복제의 시작을 수행한다. ### 설정 생성하기 파드 스펙의 컨테이너를 시작하기 전에, 파드는 순서가 정의되어 있는 [초기화 컨테이너](/ko/docs/concepts/workloads/pods/init-containers/)들을 먼저 실행시킨다. `init-mysql`라는 이름의 첫 번째 초기화 컨테이너는, 인덱스에 따라 특별한 MySQL 설정 파일을 생성한다. 스크립트는 인덱스를 `hostname` 명령으로 반환되는 파드 이름의 마지막 부분에서 추출하여 결정한다. 그리고 인덱스(이미 사용된 값들을 피하기 위한 오프셋 숫자와 함께)를 MySQL의 `conf.d` 디렉토리의 `server-id.cnf` 파일에 저장한다. 이는 스테이트풀셋에게서 제공된 고유하고, 안정적인 신원을 같은 속성을 필요로 하는 MySQL 서버 ID의 형태로 바꾸어준다. 또한 `init-mysql` 컨테이너의 스크립트는 컨피그맵을 `conf.d`로 복사하여, `primary.cnf` 또는 `replica.cnf`을 적용한다. 이 예제의 토폴로지가 하나의 주 MySQL 서버와 일정 수의 레플리카들로 이루어져 있기 때문에, 스크립트는 `0` 인덱스를 주 서버로, 그리고 나머지 값들은 레플리카로 지정한다. 스테이트풀셋 컨트롤러의 [디플로이먼트와 스케일링 보증](/ko/docs/concepts/workloads/controllers/statefulset/#디플로이먼트와-스케일링-보증)과 합쳐지면, 복제를 위한 레플리카들을 생성하기 전에 주 MySQL 서버가 Ready 상태가 되도록 보장할 수 있다. ### 기존 데이터 복제(cloning) 일반적으로, 레플리카에 새로운 파드가 추가되는 경우, 주 MySQL 서버가 이미 데이터를 가지고 있다고 가정해야 한다. 또한 복제 로그가 첫 시작점부터의 로그들을 다 가지고 있지는 않을 수 있다고 가정해야 한다. 이러한 보수적인 가정들은 스테이트풀셋이 초기 크기로 고정되어 있는 것보다, 시간에 따라 확장/축소하게 할 수 있도록 하는 중요한 열쇠가 된다. `clone-mysql`라는 이름의 두 번째 초기화 컨테이너는, 퍼시스턴트볼륨에서 처음 초기화된 레플리카 파드에 복제 작업을 수행한다. 이 말은 다른 실행 중인 파드로부터 모든 데이터들을 복제하기 때문에, 로컬의 상태가 충분히 일관성을 유지하고 있어서 주 서버에서부터 복제를 시작할 수 있다는 의미이다. MySQL 자체가 이러한 메커니즘을 제공해주지는 않기 때문에, 이 예제에서는 XtraBackup이라는 유명한 오픈소스 도구를 사용한다. 복제 중에는, 복제 대상 MySQL 서버가 성능 저하를 겪을 수 있다. 주 MySQL 서버의 이러한 충격을 최소화하기 위해, 스크립트는 각 파드가 자신의 인덱스보다 하나 작은 파드로부터 복제하도록 지시한다. 이것이 정상적으로 동작하는 이유는 스테이트풀셋 컨트롤러가 파드 `N+1`을 실행하기 전에 항상 파드 `N`이 Ready 상태라는 것을 보장하기 때문이다. ### 복제(replication) 시작하기 초기화 컨테이너들의 성공적으로 완료되면, 일반적인 컨테이너가 실행된다. MySQL 파드는 `mysqld` 서버를 구동하는 `mysql` 컨테이너로 구성되어 있으며, `xtrabackup` 컨테이너는 [사이드카(sidecar)](/blog/2015/06/the-distributed-system-toolkit-patterns)로서 작동한다. `xtrabackup` 사이드카는 복제된 데이터 파일들을 보고 레플리카에 MySQL 복제를 시작해야 할 필요가 있는지 결정한다. 만약 그렇다면, `mysqld`이 준비될 때까지 기다린 후 `CHANGE MASTER TO`, 그리고 `START SLAVE`를 XtraBackup 복제(clone) 파일들에서 추출한 복제(replication) 파라미터들과 함께 실행시킨다. 레플리카가 복제를 시작하면, 먼저 주 MySQL 서버를 기억하고, 서버가 재시작되거나 커넥션이 끊어지면 다시 연결한다. 또한 레플리카들은 주 서버를 안정된 DNS 이름 (`mysql-0.mysql`)으로 찾기 때문에, 주 서버가 리스케쥴링에 의해 새로운 파드 IP를 받아도 주 서버를 자동으로 찾는다. 마지막으로, 복제를 시작한 후에는, `xtrabackup` 컨테이너는 데이터 복제를 요청하는 다른 파드들의 커넥션을 리스닝한다. 이 서버는 스테이트풀셋이 확장하거나, 다음 파드가 퍼시스턴트볼륨클레임을 잃어서 다시 복제를 수행해햐 할 경우를 대비하여 독립적으로 존재해야 한다. ## 클라이언트 트래픽 보내기 임시 컨테이너를 `mysql:5.7` 이미지로 실행하고 `mysql` 클라이언트 바이너리를 실행하는 것으로 테스트 쿼리를 주 MySQL 서버(`mysql-0.mysql` 호스트네임)로 보낼 수 있다. ```shell kubectl run mysql-client --image=mysql:5.7 -i --rm --restart=Never --\ mysql -h mysql-0.mysql <`를 이전 단계에서 찾았던 노드의 이름으로 바꾸자. {{< caution >}} 노드 드레인은 해당 노드에서 실행 중인 다른 워크로드와 애플리케이션들에게 영향을 줄 수 있다. 테스트 클러스터에만 다음 단계를 수행하자. {{< /caution >}} ```shell # 위에 명시된 다른 워크로드들이 받는 영향에 대한 주의사항을 확인한다. kubectl drain --force --delete-emptydir-data --ignore-daemonsets ``` 이제 파드가 다른 노드에 리스케줄링되는 것을 관찰할 수 있다. ```shell kubectl get pod mysql-2 -o wide --watch ``` 출력은 다음과 비슷할 것이다. ``` NAME READY STATUS RESTARTS AGE IP NODE mysql-2 2/2 Terminating 0 15m 10.244.1.56 kubernetes-node-9l2t [...] mysql-2 0/2 Pending 0 0s kubernetes-node-fjlm mysql-2 0/2 Init:0/2 0 0s kubernetes-node-fjlm mysql-2 0/2 Init:1/2 0 20s 10.244.5.32 kubernetes-node-fjlm mysql-2 0/2 PodInitializing 0 21s 10.244.5.32 kubernetes-node-fjlm mysql-2 1/2 Running 0 22s 10.244.5.32 kubernetes-node-fjlm mysql-2 2/2 Running 0 30s 10.244.5.32 kubernetes-node-fjlm ``` 그리고, 서버 ID `102`가 `SELECT @@server_id` 루프 출력에서 잠시 사라진 후 다시 보이는 것을 확인할 수 있을 것이다. 이제 노드의 스케줄 방지를 다시 해제(uncordon)해서 정상으로 돌아가도록 조치한다. ```shell kubectl uncordon ``` ## 레플리카 스케일링하기 MySQL 레플리케이션을 사용하면, 레플리카를 추가하는 것으로 읽기 쿼리 용량을 키울 수 있다. 스테이트풀셋을 사용하면, 단 한 줄의 명령으로 달성할 수 있다. ```shell kubectl scale statefulset mysql --replicas=5 ``` 명령을 실행시켜서 새로운 파드들이 올라오는 것을 관찰하자. ```shell kubectl get pods -l app=mysql --watch ``` 파드들이 올라오면, `SELECT @@server_id` 루프 출력에 서버 ID `103` 과 `104`가 나타나기 시작할 것이다. 그리고 해당 파드들이 존재하기 전에 추가된 데이터들이 해당 새 서버들에게도 존재하는 것을 확인할 수 있다. ```shell kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\ mysql -h mysql-3.mysql -e "SELECT * FROM test.messages" ``` ``` Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false +---------+ | message | +---------+ | hello | +---------+ pod "mysql-client" deleted ``` 축소하는 것도 간단하게 할 수 있다. ```shell kubectl scale statefulset mysql --replicas=3 ``` {{< note >}} 확장은 퍼시스턴트볼륨클레임을 자동으로 생성하지만, 축소에서는 해당 PVC들이 자동으로 삭제되지 않는다. 이로써 확장을 빠르게 하기 위해 초기화된 PVC들을 보관해 두거나, 삭제하기 전에 데이터를 추출하는 선택을 할 수 있다. {{< /note >}} 다음 명령을 실행하여 확인할 수 있다. ```shell kubectl get pvc -l app=mysql ``` 스테이트풀셋을 3으로 축소했음에도 PVC 5개가 아직 남아있음을 보여준다. ``` NAME STATUS VOLUME CAPACITY ACCESSMODES AGE data-mysql-0 Bound pvc-8acbf5dc-b103-11e6-93fa-42010a800002 10Gi RWO 20m data-mysql-1 Bound pvc-8ad39820-b103-11e6-93fa-42010a800002 10Gi RWO 20m data-mysql-2 Bound pvc-8ad69a6d-b103-11e6-93fa-42010a800002 10Gi RWO 20m data-mysql-3 Bound pvc-50043c45-b1c5-11e6-93fa-42010a800002 10Gi RWO 2m data-mysql-4 Bound pvc-500a9957-b1c5-11e6-93fa-42010a800002 10Gi RWO 2m ``` 만약 여분의 PVC들을 재사용하지 않을 것이라면, 이들을 삭제할 수 있다. ```shell kubectl delete pvc data-mysql-3 kubectl delete pvc data-mysql-4 ``` ## {{% heading "cleanup" %}} 1. `SELECT @@server_id` 루프를 끝내기 위해, 터미널에 **Ctrl+C**를 입력하거나, 해당 명령을 다른 터미널에서 실행시키자. ```shell kubectl delete pod mysql-client-loop --now ``` 1. 스테이트풀셋을 삭제한다. 이것은 파드들 또한 삭제할 것이다. ```shell kubectl delete statefulset mysql ``` 1. 파드들의 삭제를 확인한다. 삭제가 완료되기까지 시간이 걸릴 수 있다. ```shell kubectl get pods -l app=mysql ``` 위와 같은 메세지가 나타나면 파드들이 삭제되었다는 것을 알 수 있다. ``` No resources found. ``` 1. 컨피그맵, 서비스, 그리고 퍼시스턴트볼륨클레임들을 삭제한다. ```shell kubectl delete configmap,service,pvc -l app=mysql ``` 1. 만약 수동으로 퍼시스턴스볼륨들을 프로비저닝했다면, 수동으로 삭제하면서, 그 밑에 존재하는 리소스들을 또한 삭제해야 한다. 만약 동적 프로비저너를 사용했다면, 당신이 퍼시스턴트볼륨클레임으로 삭제하면 자동으로 퍼시스턴트볼륨을 삭제한다. 일부 (EBS나 PD와 같은) 동적 프로비저너들은 퍼시스턴트볼륨을 삭제 하면 그 뒤에 존재하는 리소스들도 삭제한다. ## {{% heading "whatsnext" %}} * [스테이트풀셋(StatefulSet) 확장하기](/ko/docs/tasks/run-application/scale-stateful-set/)에 대해 더 배워보기. * [스테이트풀셋 디버깅하기](/ko/docs/tasks/debug/debug-application/debug-statefulset/)에 대해 더 배워보기. * [스테이트풀셋(StatefulSet) 삭제하기](/ko/docs/tasks/run-application/delete-stateful-set/)에 대해 더 배워보기. * [스테이트풀셋(StatefulSet) 파드 강제 삭제하기](/ko/docs/tasks/run-application/force-delete-stateful-set-pod/)에 대해 더 배워보기. * [Helm Charts 저장소](https://artifacthub.io/)를 통해 다른 스테이트풀 애플리케이션 예제들을 확인해보기.