0. Contents

1. Overview

Kubernetes의 인스턴스 수 제어 방향

대상제어 방향
ReplicaSet (replicas: N)여러 개 동시 실행 (Active-Active)
Singleton Service딱 하나만 Active ← 이번 챕터!
PodDisruptionBudget최소 N개는 항상 유지

패턴의 목적

  • Singleton Service 패턴은 동시에 활성화되는 서비스 인스턴스를 하나로 제한하기 위한 패턴이다
  • 스케일 아웃이 기본인 Kubernetes에서, 역설적으로 “하나만 실행"을 보장하는 메커니즘을 다룬다

2. Problem

레플리카의 강점

  • Kubernetes의 강점 중 하나는 Pod를 여러 개(레플리카) 띄워 처리량과 가용성을 높이는 것이다
  • 레플리카를 늘리면
    • 처리량 ↑ (여러 Pod가 동시에 요청 처리)
    • 가용성 ↑ (하나 죽어도 나머지가 처리)

반대의 요구사항

  • 그런데 현실에는 이런 요구사항도 있다

“여러 인스턴스가 동시에 실행되면 안 된다.”

예시

  • 주기적 작업(Scheduler): 3개 Pod가 떠 있으면 같은 배치 작업이 3번 실행된다
  • 폴링(Polling): 파일시스템/DB를 여러 Pod가 동시에 읽고 처리하면 중복 처리 발생
  • 순서 보장 메시지 컨슈머: 단일 스레드로 순서대로 소비해야 하는데 여러 인스턴스가 나눠 가져가면 순서가 깨진다

필요한 것: Active 인스턴스 1개

  • 이런 경우 동시에 Active인 인스턴스를 정확히 1개로 제한해야 한다

3. Solution

싱글톤 구현 방법: 두 가지 수준

  • 근본적으로 싱글톤은 두 가지 수준에서 구현할 수 있다
방식제어 주체특징
Out-of-Application Locking앱 외부(Kubernetes)가 제어앱은 자신이 싱글톤인지 모름
In-Application Locking앱 코드 내부에서 직접 제어분산 락으로 스스로 보장

3.1 Active-Active vs Active-Passive

토폴로지 차이

  • 일반 레플리카는 모든 인스턴스가 동시에 요청을 처리하는 Active-Active 토폴로지다
  • 싱글톤이 필요한 경우에는 Active-Passive 토폴로지가 필요하다
Active-Active (일반 레플리카)Active-Passive (싱글톤 목표)ServicePod1ActivePod2ActivePod3Active모두 동시 처리Pod1 (Active)실제 작업 수행Pod2 (Passive)대기Pod3 (Passive)대기
일반 레플리카는 모두 Active, 싱글톤은 하나만 Active

4. Out-of-Application Locking

외부 관리 프로세스의 보장

  • 이름 그대로 애플리케이션 외부의 관리 프로세스가 싱글톤을 보장한다
  • 앱 코드는 자신이 싱글톤으로 실행되는지조차 모른다

비유: Spring 싱글톤 빈

  • Spring Framework의 싱글톤 빈과 같은 원리다
  • 클래스 코드에는 “나는 싱글톤이야"라는 코드가 없다
    • Spring이 외부에서 딱 하나만 생성해 관리한다

Out-of-Application 방식의 핵심

  • 앱 코드 → “그냥 실행될 뿐, 싱글톤인지 모름”
  • Kubernetes → “이 앱은 1개만 실행해야 해” (외부에서 강제)

관심사 분리 (Separation of Concerns)

  • 앱 코드 → 비즈니스 로직만 집중
  • 외부 관리자 → 인스턴스 수 제어

4.1 ReplicaSet + replicas: 1

가장 단순한 방법: replicas: 1

  • Kubernetes에서 Out-of-Application 싱글톤을 구현하는 가장 단순한 방법은 replicas: 1로 Pod를 시작하는 것이다
  • 단, 이것만으로는 고가용성이 보장되지 않는다
단순 Pod 1개 실행Pod1 💀죽으면 끝 ❌아무도 살려주지 않음ReplicaSet + replicas: 1Pod1 💀ReplicaSet이 즉시 감지"1개 있어야 하는데 0개네!"Pod2 자동 생성 ✅
Pod 하나만 띄우면 끝, ReplicaSet 이 있으면 다시 만든다

ReplicaSet: 목표값을 지키는 컨트롤러

  • ReplicaSet은 replicas라는 목표값을 24시간 감시하며 유지하는 관리자다
  • Pod 자체가 아니라 Kubernetes 안에서 동작하는 별도의 컨트롤러 오브젝트다
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: my-singleton
spec:
  replicas: 1          # 외부에서 1개로 제한 — 앱 코드 수정 없음
  selector:
    matchLabels:
      app: my-singleton
  template:
    ...

Active-Passive와 같은 효과

  • 이 토폴로지는 정확히 Active-Passive는 아니다(Passive 인스턴스가 없다)
  • 그러나 Kubernetes가 항상 1개의 Pod가 실행되도록 보장하기 때문에 동일한 효과를 낸다

헬스 체크와 결합

  • 헬스 체크(Chapter 4, Health Probe)와 결합하면 Pod 장애 시 자동으로 치유(healing)된다
    • 고가용성 싱글톤에 가까워진다

4.2 ReplicaSet 방식의 한계

주의할 점 두 가지

  • 주의해야 할 점이 두 가지 있다

첫째, 실수로 레플리카 수가 늘어날 수 있다

  • 누군가 kubectl scale --replicas=3 실행
    • Kubernetes가 막아주지 않음
    • 싱글톤 즉시 깨짐 ❌
  • 플랫폼 수준에서 레플리카 수 변경을 막는 메커니즘이 없다

둘째, “항상 정확히 1개"는 사실이 아니다

  • ReplicaSet은 가용성(Availability)을 일관성(Consistency)보다 우선시한다
  • 즉, at most 1이 아니라 at least 1 의미론을 적용한다
at most 1 (ReplicaSet이 하지 않는 것)Pod1 완전히 죽은 거 확인 후 → Pod2 생성Pod1Pod2 생성그 사이 잠깐 0개 감수at least 1 (ReplicaSet이 실제 하는 것)일단 Pod2 먼저 생성 → 나중에 Pod1 제거Pod1 (나중에 제거)Pod2 먼저 생성잠깐 2개 동시 실행 감수
at most 1 은 빈틈을, at least 1 은 겹침을 감수한다

가장 흔한 케이스: 노드 네트워크 단절

정상클러스터Node APod1 (Active) ✅Node A 단절클러스터Node APod1 (Active)살아있지만 연락 안됨"죽었다고 판단"Node BPod2 (새로 생성) ✅결과: Pod1 + Pod2 일시적 동시 실행 ⚠️
연락이 끊긴 Pod 는 죽은 것으로 판단되어 새 Pod 가 하나 더 뜬다

그 밖의 상황과 영향

  • 이 외에도 레플리카 수 변경, Pod 노드 이동 시에도 일시적으로 2개가 실행될 수 있다
  • 이는 무상태(Stateless) 앱에는 문제없다
    • 하지만 싱글톤이 필요한 앱에는 치명적일 수 있다

4.3 StatefulSet

느슨한 보장 vs 엄격한 보장

  • ReplicaSet이 느슨한 싱글톤 보장을 제공한다면, StatefulSet은 엄격한 싱글톤 보장을 제공한다
ReplicaSetStatefulSet
우선순위가용성 우선 → 잠깐 2개 허용 (at least 1)일관성 우선 → 잠깐 0개 허용 (at most 1)
노드 단절 시구 Pod 종료 확인 안 하고 새 Pod 생성구 Pod 완전히 종료 확인 후 새 Pod 생성

고르는 기준

  • 중복 실행이 더 무서운가? → StatefulSet
  • 서비스 중단이 더 무서운가? → ReplicaSet

StatefulSet의 기능과 복잡도

  • StatefulSet은 Stateful 애플리케이션을 위해 설계되었다
  • 더 강력한 싱글톤 보장을 포함한 많은 기능을 제공한다
  • 단, 복잡도도 함께 올라간다
  • 자세한 내용은 Chapter 11에서 다룬다

들어오는 연결: Headless Service

  • 싱글톤 Pod가 외부로부터 들어오는 연결(Incoming Connection) 을 수락해야 할 때는 Service 리소스가 필요하다
  • StatefulSet으로 관리되는 싱글톤 Pod처럼 Pod가 하나뿐이고 안정적인 네트워크 ID가 있는 경우
    • 일반 ClusterIP Service 대신 Headless Service(clusterIP: None)가 더 적합하다
일반 Service (ClusterIP)로드밸런싱, 중간 레이어 존재[가상 IP]kube-proxyPodHeadless Service중간 레이어 없음, 직접 연결[DNS 조회]Pod IP 직접 반환Pod
Headless Service 는 중간 레이어 없이 Pod IP 를 바로 준다

DNS 레코드 자동 갱신

  • Headless Service는 Pod IP가 바뀌어도 Kubernetes가 DNS 레코드를 자동으로 업데이트해준다
  • 그래서 클라이언트는 항상 현재 Pod에 접근할 수 있다

5. In-Application Locking

분산 락으로 직접 보장

  • 앱 코드 내부에서 분산 락(Distributed Lock) 을 이용해 직접 싱글톤을 보장하는 방식이다
시작: 모두 락 획득 시도Pod1 시작락 획득 시도성공!Active ✅실제 작업 수행Pod2 시작락 획득 시도실패Passive ⏳락 해제 감시Pod3 시작락 획득 시도실패Passive ⏳락 해제 감시장애: 락 만료·해제 후 전환Pod1 장애락 만료 또는 해제세션·리스 타임아웃 후Pod2 락 획득 성공Active ✅ (새 Pod 생성 없음)
락을 얻은 하나만 Active, 나머지는 락 해제를 기다린다

Out-of-Application과의 결정적 차이

  • Passive 인스턴스들이 이미 살아서 대기 중이다
  • Active 장애 시 새 Pod를 생성할 필요 없이, 락이 만료되거나 해제되면 대기 중인 Pod가 넘겨받는다
Out-of-ApplicationIn-Application
실행 방식Pod 자체를 1개로 제한Pod는 여러 개 실행 중, 락을 가진 1개만 Active
장애 시새 Pod 생성 시간 필요락 만료·해제 후 대기 Pod가 인계 ✅

클래식 OOP 싱글톤과 비교

  • 클래식 OOP의 싱글톤 패턴과 비교하면 이해가 쉽다
  • 클래식 싱글톤 (단일 프로세스)
    • private static instance; — 정적 변수에 저장
    • private 생성자 → 외부 생성 차단
    • → “이 프로세스 안에서” 1개 보장
  • 분산 싱글톤 (여러 Pod)
    • 같은 코드가 Pod마다 따로 돌아감
    • → 클래식 싱글톤 패턴은 무의미
    • → 분산 락으로 “클러스터 전체에서” 1개 보장 필요

5.1 Apache ZooKeeper: Ephemeral Node

임시 노드를 락으로 사용

  • ZooKeeper를 이용한 구현은 임시 노드(Ephemeral Node) 를 분산 락으로 사용한다

임시 노드의 특성

  • 클라이언트 세션이 살아있는 동안만 존재
  • 세션 종료(또는 Pod 장애) 시 → 자동 삭제
  • 이것이 핵심!
    • Active Pod가 갑자기 죽어도 락이 자동 해제됨
    • 데드락(Deadlock) 없음 ✅

ZooKeeper 분산 락 흐름

락 획득Pod1Active ✅세션 생성 → /lock 임시 노드 생성 성공Pod2Passive (감시 중) ⏳/lock 생성 시도 → 이미 존재!Pod3Passive (감시 중) ⏳/lock 생성 시도 → 이미 존재!ZooKeeper/lock임시 노드Pod1 장애 시Pod1 💀세션 종료/lock 자동 삭제Pod2, Pod3 감지동시에 생성 시도먼저 성공한 쪽이Active ✅
ZooKeeper 임시 노드는 세션과 함께 사라져 락이 자동으로 풀린다

5.2 Kubernetes 환경에서의 선택: Etcd + ConfigMap

ZooKeeper 별도 운영의 부담

  • ZooKeeper 클러스터를 락킹만을 위해 별도로 운영하는 것은 오버헤드다
  • Kubernetes는 이미 내부적으로 Etcd를 쓰고 있다
ZooKeeper 사용 시Etcd 사용 시
운영Kubernetes 클러스터 + ZooKeeper 클러스터 따로 관리Kubernetes가 이미 내장 → 추가 설치 불필요
결과관리 포인트 2개, 복잡도 ↑Kubernetes API로 바로 접근 가능 ✅

Etcd: Raft 기반 키-값 저장소

  • Etcd는 Raft 프로토콜을 사용해 분산 일관성을 보장하는 키-값 저장소다
  • 리더 선출(Leader Election) 구현에 필요한 기본 요소를 제공한다

Apache Camel: ConfigMap을 분산 락으로

  • Apache Camel은 한 단계 더 나아가 Etcd API 대신 Kubernetes의 ConfigMap을 분산 락으로 재활용한다
  • ConfigMap의 본래 목적: 앱 설정값 저장
  • Apache Camel의 활용: ConfigMap을 락 저장소로 재활용
  • ConfigMap (락 역할)
    • data.leader: "Pod1" ← 현재 리더 기록
    • metadata.resourceVersion: "42" ← 낙관적 락킹용 버전 (API 서버가 관리)

핵심: 낙관적 락킹(Optimistic Locking)

  • 핵심은 Kubernetes의 낙관적 락킹(Optimistic Locking) 보장이다
Pod1ConfigMapPod2resourceVersion: 42읽음 (resourceVersion: 42)읽음 (resourceVersion: 42)업데이트 시도성공! (resourceVersion: 43)resourceVersion: 43업데이트 시도실패! (이미 43)→ 재시도
먼저 쓴 쪽만 성공하고, 늦은 쪽은 버전이 달라 실패한다

추가 인프라 없는 리더 선출

  • 한 번에 하나의 Pod만 ConfigMap을 업데이트할 수 있다는 Kubernetes 보장이 있다
  • 덕분에 추가 인프라 없이 Kubernetes API만으로 리더 선출을 구현할 수 있다

Camel 동작 결과

  • Pod1 (Active): Camel 라우트 실행 ✅
  • Pod2 (Passive): 대기 ⏳
  • Pod3 (Passive): 대기 ⏳
  • Pod1 장애 → ConfigMap 갱신 중단
    • → 락(리스) 만료 후 Pod2 또는 Pod3 락 획득 → Active ✅

구현과 무관한 공통 원리

  • ZooKeeper, Etcd, Redis, Consul 등 어떤 구현을 쓰든 원리는 같다
  • 락을 가진 1개만 Active, 나머지는 Passive 대기
  • Pod 수가 아무리 많아도 비즈니스 로직은 1개만 수행

6. PodDisruptionBudget

상한선 제어와 하한선 제어

  • 싱글톤이 “너무 많으면 안 된다"는 상한선 제어다
  • PodDisruptionBudget(PDB) 은 “너무 적으면 안 된다"는 하한선 제어다
많음 ↑적음 ↓싱글톤 / 리더 선출이 제한 (상한선)PDB가 제한 (하한선)이 범위 안에서 운영
싱글톤은 상한선, PDB 는 하한선을 정한다

PDB의 대상: 자발적 중단

  • PDB는 자발적 중단(Voluntary Disruption) 시 동시에 내려가는 Pod 수를 제한한다
자발적 중단 (PDB가 제어 가능)비자발적 중단 (PDB가 제어 불가)
예시kubectl drain (노드 유지보수), 클러스터 스케일 다운, 노드 업그레이드노드 하드웨어 장애, 커널 패닉, 네트워크 단절
결과예측 가능 → PDB로 지연/제한 ✅예측 불가 → PDB로 막을 수 없음 ❌

Example 10-1. PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: random-generator-pdb
spec:
  selector:
    matchLabels:
      app: random-generator   # 이 라벨을 가진 Pod에 적용
  minAvailable: 2             # 최소 2개는 항상 Running 상태 유지
                              # "80%"처럼 백분율도 가능
Pod 5개 운영 중, minAvailable: 2 설정 시kubectl drain node-1 실행Pod 축출 시도"최소 2개는 살아있어야 함"PodPodPodPodPod5PodPodPodPod4PodPodPod3PodPod2PDB 하한선minAvailable: 2여기서 멈춤!
drain 은 PDB 하한선에서 멈춘다

minAvailable과 maxUnavailable

  • minAvailable 대신 maxUnavailable을 쓸 수도 있다
  • 단, 두 필드를 동시에 지정할 수 없다
  • 결과는 같지만 표현 방식이 다름
  • 둘 중 하나만 선택 — 동시 지정 시 충돌 가능성 있으므로 금지
설정의미같은 뜻
minAvailable: 3“최소 3개 유지”최대 2개 동시 중단 가능
maxUnavailable: 2“최대 2개 중단”최소 3개 유지

적용 범위: 컨트롤러가 관리하는 Pod

  • PDB는 컨트롤러(ReplicaSet, Deployment, StatefulSet 등)가 관리하는 Pod에만 완전히 적용된다
  • 컨트롤러 없이 직접 생성한 Bare/Naked Pod에도 쓸 수 있지만 제약이 있다
    • minAvailable에 정수 값만 쓸 수 있다
    • maxUnavailable과 퍼센트 값은 컨트롤러가 관리하는 Pod에서만 쓸 수 있다
    • 축출 후 자동 생성이 없으므로 PDB의 의미가 퇴색된다

PDB가 특히 유용한 상황

상황이유
Etcd, ZooKeeper 클러스터쿼럼(과반수) 유지 필수
Kafka 브로커최소 복제본 수 보장
결제/금융 서비스SLA 기준 처리량 유지

예: Etcd 5노드 클러스터, 쿼럼 = 3개

  • PDB 없이 drain
    • 3개 동시 축출 가능 → 쿼럼 미달 → 클러스터 중단 ❌
  • PDB (minAvailable: 3)
    • 항상 3개 이상 유지 → 쿼럼 항상 보장 ✅

7. Discussion

강한 싱글톤 보장이 필요할 때

ReplicaSet의 한계: at-most-one 미보장

  • ReplicaSet은 가용성 우선 설계라 at-most-one을 보장하지 않는다
  • 짧은 시간 2개가 동시에 실행되는 시나리오가 실제로 존재한다

2개가 될 수 있는 상황

  1. 노드 네트워크 단절 → 구 Pod 살아있는데 새 Pod 생성
  2. 레플리카 수 변경 도중
  3. Pod 다른 노드로 이동 도중

허용되지 않을 때의 선택지

  • StatefulSet 사용
    • 인프라 수준에서 at most 1 보장 (같은 이름의 Pod는 최대 1개)
    • 복잡도 ↑
  • In-Application Locking 사용
    • 코드 수준에서 at most 1 보장 (락을 가진 1개만 Active)
    • 추가 장점: replicas를 실수로 늘려도 싱글톤 유지 (락은 1개만 획득 가능하므로)

일부 컴포넌트만 싱글톤이어야 할 때

스케일 가능한 부분과 싱글톤 부분의 공존

  • 하나의 애플리케이션 안에 스케일 가능한 부분과 싱글톤이어야 하는 부분이 함께 있는 경우가 있다

예시: 이커머스 서비스

컴포넌트요구
HTTP API (주문 조회, 상품 검색)스케일 가능 ✅
재고 동기화 폴링싱글톤 필요 ⚠️

replicas: 1의 한계

  • Out-of-Application 방식(replicas: 1)으로는 전체를 1개로 제한해버려 HTTP API도 스케일 불가다
해결책 1: 컴포넌트 분리 배포HTTP servicereplicas: Npollingreplicas: 1장점깔끔한 분리단점배포 복잡도 ↑오버헤드 ↑항상 실용적이지는 않음해결책 2: In-Application Locking (권장)replicas: N으로 스케일 아웃Pod1HTTP ✅+폴링 Active ✅(락 보유)Pod2HTTP ✅+폴링 Passive ⏳Pod3HTTP ✅+폴링 Passive ⏳→ HTTP는 3개가 처리 (스케일 아웃 효과) ✅→ 폴링은 1개만 수행 (싱글톤 보장) ✅→ Pod1 장애 시 락 만료 후 폴링 인계 ✅
컴포넌트를 나누거나, 같은 Pod 안에서 폴링만 락으로 묶는다

방식별 최종 선택 기준

잠깐 2개 실행이 허용되는가?ReplicaSet + replicas: 1(단순, 빠름, 고가용성에 가까움)YESNO엄격한 싱글톤이 필요하다인프라 수준에서 해결하고 싶다코드 수준에서 해결하거나실수로 인한 스케일링도 막고 싶다StatefulSet(at most 1 보장, 복잡도 중간)In-Application Locking(ZooKeeper / Etcd / Redis / ConfigMap)(가장 강력, 복잡도 높음, 락 만료 후 인계)
싱글톤 방식 선택 가이드
ReplicaSetStatefulSetIn-Application
우선순위가용성일관성일관성
싱글톤 보장느슨함엄격함엄격함
Passive 인스턴스없음없음있음 (대기 중)
장애 시 복구새 Pod 생성새 Pod 생성락 만료 후 대기 Pod 인계 ✅
실수 스케일링 방지❌❌✅
복잡도낮음중간높음

Singleton Service 패턴의 핵심은 “실행 중인 인스턴스 수"와 “Active 인스턴스 수"를 분리해서 생각하는 것이다. 여러 Pod가 떠 있어도 비즈니스 로직을 수행하는 것은 단 하나. In-Application Locking이 이 분리를 가장 명확하게 실현한다.