7.4.1 상태 유지 애플리케이션의 특징
앞서 우리는 디플로이먼트와 레플리카셋을 통해 주로 ‘상태가 없는(stateless)’ 애플리케이션을 다루는 방법을 익혔습니다. 이러한 애플리케이션들은 각 파드가 서로 대체 가능하며, 어느 파드가 요청을 처리하든 동일한 결과를 반환하고, 파드가 재시작되거나 교체되어도 서비스 자체에는 큰 영향을 주지 않습니다. 웹 서버나 간단한 API 서버들이 대표적인 예시죠. 이들은 마치 이름 없는 가축(cattle)처럼, 필요에 따라 쉽게 늘리거나 줄일 수 있습니다.
하지만 우리가 실제 운영하는 많은 시스템들은 데이터를 저장하고, 그 데이터에 기반하여 동작하는 ‘상태를 유지하는(stateful)’ 애플리케이션들입니다. 이러한 애플리케이션들은 각 인스턴스(파드)가 단순한 복제품 이상으로 고유한 정체성과 데이터를 가지며, 마치 이름과 개성이 뚜렷한 애완동물(pets)처럼 특별한 관리가 필요합니다. 예를 들어, 고객 데이터베이스의 특정 샤드(shard)를 담당하는 파드는 아무 파드로 대체될 수 없으며, 그 파드가 재시작되더라도 이전에 저장했던 데이터를 그대로 유지해야 합니다.
이러한 상태 유지 애플리케이션들은 일반적인 상태 없는 애플리케이션과는 다른 독특한 요구사항들을 가지고 있습니다. 디플로이먼트만으로는 이러한 요구사항들을 효과적으로 충족시키기 어렵기 때문에, 쿠버네티스는 ‘스테이트풀셋(StatefulSet)’이라는 특별한 컨트롤러를 제공합니다. 스테이트풀셋을 제대로 이해하기 위해서는 먼저 상태 유지 애플리케이션이 어떤 특징들을 가지는지 명확히 파악하는 것이 중요합니다.
7.4.1.1 안정적인 고유 식별자 필요 (네트워크 ID)
상태 유지 애플리케이션의 가장 중요한 특징 중 하나는 각 인스턴스, 즉 쿠버네티스 환경에서의 각 파드가 안정적이고 고유한 식별자를 가져야 한다는 점입니다. 여기서 ‘안정적’이라는 것은 파드가 재시작되거나 다른 노드로 스케줄링되더라도 그 식별자가 변하지 않아야 함을 의미합니다. 이 식별자는 주로 네트워크 ID의 형태로 나타나며, 다른 파드나 클라이언트가 특정 파드를 정확하게 찾아 통신하는 데 사용됩니다.
상상해 봅시다. 우리가 여러 개의 복제본으로 구성된 데이터베이스 클러스터를 운영한다고 가정해 보겠습니다. 이 클러스터에는 보통 하나의 주(Primary 또는 Master) 데이터베이스와 여러 개의 복제(Replica 또는 Slave) 데이터베이스가 존재합니다. 복제 데이터베이스는 주 데이터베이스로부터 데이터를 동기화해야 하며, 때로는 클라이언트가 특정 복제본에 읽기 요청을 보낼 수도 있습니다. 만약 주 데이터베이스 파드의 네트워크 주소(IP 주소나 호스트명)가 재시작될 때마다 바뀐다면, 복제 데이터베이스들은 주 데이터베이스를 찾지 못해 데이터 동기화에 실패할 것입니다. 또한, 클러스터 내부의 다른 구성원들이 서로를 식별하고 통신해야 하는 분산 시스템(예: Apache Kafka 브로커, Elasticsearch 노드)에서도 각 멤버의 고유하고 안정적인 네트워크 ID는 필수적입니다.
디플로이먼트에 의해 관리되는 파드들은 기본적으로 랜덤한 이름(예: my-app-deployment-ab12cd34ef-gh56i)을 가지며, 재시작 시 IP 주소도 변경될 수 있습니다. 이는 각 파드가 서로 대체 가능하고 익명성을 가지는 상태 없는 애플리케이션에는 적합하지만, 각 파드가 고유한 역할을 수행하고 서로를 정확히 인지해야 하는 상태 유지 애플리케이션에는 큰 장애물이 됩니다.
따라서 상태 유지 애플리케이션은 다음과 같은 특성을 가진 네트워크 ID를 필요로 합니다.
- 고유성(Uniqueness): 클러스터 내의 다른 어떤 파드와도 중복되지 않는 식별자여야 합니다.
- 안정성(Stability): 파드가 재시작되거나 다른 노드로 옮겨가더라도 이 식별자는 변하지 않아야 합니다.
- 예측 가능성(Predictability): 파드의 이름이나 네트워크 주소가 일정한 패턴을 가져서 다른 시스템이 이를 쉽게 예측하고 참조할 수 있어야 합니다. (예: my-db-0, my-db-1, my-db-2 와 같은 순서 있는 호스트명)
이러한 안정적인 고유 식별자는 상태 유지 애플리케이션의 파드들이 서로를 발견하고, 신뢰성 있는 통신 채널을 구축하며, 클러스터링 및 데이터 복제와 같은 복잡한 작업을 수행하는 데 있어 핵심적인 기반이 됩니다.
7.4.1.2 안정적인 퍼시스턴트 스토리지 필요
상태 유지 애플리케이션의 또 다른 핵심적인 요구사항은 각 파드가 자신만의 안정적인 퍼시스턴트 스토리지(Persistent Storage)를 가져야 한다는 것입니다. 여기서 ‘안정적’이라는 의미는 파드가 종료되거나 재시작되더라도 저장된 데이터가 유실되지 않아야 함을 뜻하며, ‘자신만의’라는 의미는 특정 파드 인스턴스와 그 스토리지가 1:1로 연결되어야 함을 의미합니다.
데이터베이스를 예로 들어보겠습니다. 각 데이터베이스 인스턴스는 자신이 담당하는 데이터 파일들을 특정 스토리지 볼륨에 저장합니다. 만약 데이터베이스 파드가 어떤 이유로 재시작된다면, 재시작된 파드는 이전에 사용하던 바로 그 스토리지 볼륨에 다시 접근하여 데이터를 읽고 쓸 수 있어야 합니다. 만약 재시작 시 새로운 빈 볼륨이 할당되거나, 다른 파드가 사용하던 볼륨에 연결된다면 데이터 유실이나 심각한 데이터 불일치 문제가 발생할 것입니다.
디플로이먼트가 관리하는 파드들은 기본적으로 임시(ephemeral) 스토리지를 사용하거나, 모든 파드가 공유하는 하나의 퍼시스턴트 볼륨을 사용할 수 있습니다. 임시 스토리지는 파드가 삭제되면 데이터도 함께 사라지므로 상태 유지에는 적합하지 않습니다. 모든 파드가 하나의 볼륨을 공유하는 방식은 일부 시나리오(예: 여러 웹 서버 파드가 정적 콘텐츠를 공유)에는 유용할 수 있지만, 각 파드가 독립적인 데이터를 가져야 하는 데이터베이스나 메시지 큐 같은 경우에는 적절하지 않습니다. 예를 들어, db-pod-0은 0번 샤드의 데이터를, db-pod-1은 1번 샤드의 데이터를 각각 별도의 스토리지에 저장하고 관리해야 합니다.
따라서 상태 유지 애플리케이션의 각 파드는 다음과 같은 특징을 가진 스토리지를 필요로 합니다.
- 영속성(Persistence): 파드의 생명주기와 관계없이 데이터가 안전하게 보존되어야 합니다.
- 고유성(Uniqueness): 각 파드 인스턴스는 자신에게 할당된 고유한 스토리지 볼륨을 가져야 합니다. 즉, pod-0은 volume-0에, pod-1은 volume-1에 연결되어야 하며, pod-0이 재시작되어도 여전히 volume-0에 연결되어야 합니다.
- 안정적인 연결(Stable Binding): 파드의 고유 식별자와 그 파드가 사용하는 스토리지 볼륨 간의 연결이 안정적으로 유지되어야 합니다.
이러한 안정적인 퍼시스턴트 스토리지는 상태 유지 애플리케이션이 데이터를 안전하게 보호하고, 장애 발생 시에도 데이터의 일관성을 유지하며 서비스를 지속할 수 있도록 하는 핵심 요소입니다. 쿠버네티스에서는 퍼시스턴트 볼륨(PV)과 퍼시스턴트 볼륨 클레임(PVC)을 통해 이를 구현하며, 스테이트풀셋은 각 파드에게 고유한 PVC를 자동으로 프로비저닝하고 연결해주는 기능을 제공합니다.
7.4.1.3 순차적, 점진적 배포 및 스케일링 필요
상태 유지 애플리케이션, 특히 클러스터 형태로 구성되는 시스템들은 파드의 생성, 삭제, 업데이트 순서가 매우 중요한 경우가 많습니다. 모든 파드가 동시에 시작되거나 무작위 순서로 업데이트되면 클러스터 전체가 불안정해지거나 데이터 정합성에 문제가 생길 수 있습니다. 따라서 이러한 애플리케이션들은 순차적이고 점진적인 방식으로 배포되고 스케일링되어야 합니다.
몇 가지 구체적인 예를 들어보겠습니다.
- 데이터베이스 클러스터 초기화: 주(Primary) 데이터베이스가 먼저 완전히 시작되고 준비 상태가 된 후에 복제(Replica) 데이터베이스들이 순차적으로 시작되어 주 데이터베이스에 연결하고 데이터를 동기화해야 하는 경우가 많습니다. 만약 모든 파드가 동시에 시작하려고 하면, 복제본들이 아직 준비되지 않은 주 데이터베이스에 접속을 시도하거나, 주 데이터베이스 선출 과정에서 경쟁이 발생하여 클러스터가 정상적으로 구성되지 않을 수 있습니다.
- 클러스터 멤버십 관리: 분산 시스템에서는 새로운 노드가 클러스터에 참여하거나 기존 노드가 탈퇴할 때, 클러스터 전체의 멤버십 정보를 업데이트하고 데이터 재분배 등의 작업을 수행해야 합니다. 이러한 작업은 보통 한 번에 하나씩, 순차적으로 진행되어야 시스템의 안정성을 해치지 않습니다.
- 애플리케이션 버전 업데이트: 데이터베이스 스키마 변경과 같이 민감한 업데이트를 수행할 때는, 보통 복제본들을 먼저 새로운 버전으로 업데이트하고, 모든 복제본이 안정화된 것을 확인한 후에 주 데이터베이스를 업데이트하는 방식을 사용합니다. 이렇게 하면 업데이트 중에도 서비스 중단을 최소화하고, 문제 발생 시 롤백도 용이해집니다.
- 스케일 다운(축소): 파드의 수를 줄일 때도 특정 순서로 파드를 종료해야 할 수 있습니다. 예를 들어, 가장 최근에 추가된 파드부터 제거하거나, 특정 역할을 수행하지 않는 파드부터 안전하게 종료시켜야 데이터 유실이나 서비스 중단을 방지할 수 있습니다. 또한, 파드를 종료하기 전에 현재 처리 중인 요청을 완료하고, 필요한 데이터를 다른 노드로 이전하는 등의 ‘정리(graceful shutdown)’ 절차가 필요할 수 있습니다.
디플로이먼트는 기본적으로 파드들을 병렬적이고 무작위적인 순서로 생성하거나 삭제할 수 있습니다 (롤링 업데이트 시에는 순서가 있지만, 각 파드의 고유한 정체성을 고려하지는 않습니다). 이는 상태 없는 애플리케이션에는 효율적이지만, 위에서 언급한 상태 유지 애플리케이션의 요구사항을 만족시키기는 어렵습니다.
따라서 상태 유지 애플리케이션은 다음과 같은 배포 및 스케일링 방식을 필요로 합니다.
- 순서 있는 생성(Ordered Creation): 파드는 정해진 순서(예: 0번 파드, 1번 파드, 2번 파드 순)로 하나씩 생성되고, 이전 파드가 완전히 준비될 때까지 다음 파드의 생성이 지연될 수 있어야 합니다.
- 순서 있는 종료(Ordered Termination): 파드는 생성 순서의 역순(예: 2번 파드, 1번 파드, 0번 파드 순)으로 하나씩 종료되어야 합니다.
- 순서 있는 업데이트(Ordered Updates): 파드 템플릿이 변경되어 업데이트가 필요할 때도, 파드들은 정해진 순서(보통 종료 순서와 동일)로 하나씩 업데이트되어야 합니다.
이러한 순차적이고 점진적인 접근 방식은 상태 유지 애플리케이션이 클러스터 상태를 안정적으로 유지하고, 데이터 일관성을 보장하며, 예기치 않은 문제를 방지하는 데 매우 중요합니다.
7.4.1.4 예: 데이터베이스, 메시지 큐 등
이론적인 설명만으로는 감이 잘 오지 않을 수 있으니, 우리 주변에서 흔히 볼 수 있는 상태 유지 애플리케이션의 구체적인 예를 통해 위에서 언급된 특징들이 어떻게 나타나는지 살펴보겠습니다.
- 데이터베이스 (예: MySQL, PostgreSQL, MongoDB, Cassandra):
- 안정적인 고유 식별자: MySQL의 주-복제(Primary-Replica) 구성에서 복제본은 주 데이터베이스의 안정적인 네트워크 주소를 알아야 데이터를 복제할 수 있습니다. MongoDB나 Cassandra와 같은 NoSQL 데이터베이스 클러스터에서도 각 노드는 고유한 ID를 가지고 서로를 식별하며 데이터를 샤딩(분산 저장)하거나 복제합니다.
- 안정적인 퍼시스턴트 스토리지: 데이터베이스의 핵심은 ‘데이터’입니다. 각 데이터베이스 인스턴스는 자신이 저장하는 데이터 파일, 로그 파일 등을 위한 전용 퍼시스턴트 스토리지를 가져야 하며, 파드가 재시작되어도 이 데이터는 반드시 유지되어야 합니다.
- 순차적 배포 및 스케일링: 새로운 데이터베이스 클러스터를 구성할 때, 보통 주 노드를 먼저 설정하고 초기화한 후, 복제 노드들이 순차적으로 참여하여 데이터를 동기화합니다. 클러스터에서 노드를 제거할 때도, 해당 노드가 가지고 있던 데이터를 다른 노드로 안전하게 이전한 후 제거하는 순서가 중요할 수 있습니다.
- 메시지 큐 (예: Apache Kafka, RabbitMQ):
- 안정적인 고유 식별자: Kafka 클러스터에서 각 브로커(Broker)는 고유한 ID(Broker ID)를 가집니다. 이 ID는 주키퍼(ZooKeeper)와 같은 코디네이션 서비스에 등록되어 클러스터 멤버십을 관리하고, 토픽의 파티션 리더를 선출하는 데 사용됩니다. RabbitMQ 클러스터에서도 노드들은 서로를 이름으로 식별하고 통신합니다.
- 안정적인 퍼시스턴트 스토리지: 메시지 큐는 생산자(Producer)가 보낸 메시지를 소비자가 가져갈 때까지 안전하게 보관해야 합니다. 따라서 각 브로커는 자신이 담당하는 메시지 로그나 큐 데이터를 위한 퍼시스턴트 스토리지를 필요로 합니다.
- 순차적 배포 및 스케일링: Kafka 클러스터에 새로운 브로커를 추가하거나 제거할 때, 파티션 리밸런싱(partition rebalancing)과 같은 작업이 순차적으로 이루어져야 메시지 유실 없이 안정적으로 서비스를 확장하거나 축소할 수 있습니다.
- 기타 예시:
- 분산 코디네이션 서비스 (예: ZooKeeper, etcd): 이들은 분산 시스템에서 리더 선출, 설정 정보 공유, 분산 락 등의 기능을 제공합니다. 각 멤버는 고유 ID를 가지며, 앙상블(ensemble)을 구성하고 쿼럼(quorum)을 유지하기 위해 안정적인 스토리지와 순서 있는 관리가 필요합니다.
- 검색 엔진 (예: Elasticsearch): Elasticsearch 클러스터의 각 노드는 고유 ID를 가지며, 인덱스 샤드를 저장하기 위한 퍼시스턴트 스토리지가 필요합니다. 노드의 추가/제거 시 샤드 재분배가 발생하므로 순서 있는 관리가 중요합니다.
- 자체 개발한 상태 유지 애플리케이션: 특정 비즈니스 로직을 수행하면서 중요한 상태 정보를 로컬 파일 시스템이나 메모리에 저장하고, 클러스터 멤버 간에 이 상태를 공유하거나 동기화해야 하는 모든 종류의 맞춤형 애플리케이션도 여기에 해당될 수 있습니다.
이처럼 우리가 흔히 사용하는 많은 핵심 시스템들이 상태 유지 애플리케이션의 범주에 속합니다. 이러한 애플리케이션들의 안정적인 운영은 전체 서비스의 품질과 직결되기 때문에, 이들의 고유한 특징을 이해하고 그에 맞는 관리 방안을 마련하는 것은 매우 중요합니다. 쿠버네티스의 스테이트풀셋은 바로 이러한 고민을 해결해주기 위해 설계된 도구라고 할 수 있습니다.