[쿠버네티스 패턴] 10장 Singleton 서비스

bocopile·2025년 9월 27일

쿠버네티스 패턴

목록 보기
8/28

1. 개념 정리

싱글톤 서비스란?

쿠버네티스는 기본적으로 수평 확장(ReplicaSet, Deployment)을 전제로 설계되었습니다.

그러나 아래와 같이 오직 하나의 인스턴스만 존재해야 하는 워크로드가 있습니다.

  • 리더 선출(Leader Election)이 필요한 마스터 노드
  • 특정 리소스에 단독 접근해야 하는 크론 작업
  • 단일 인스턴스로만 실행해야 하는 배치·데몬

이러한 요구를 충족하기 위한 패턴이 바로 싱글톤 서비스(Singleton Service)입니다.

2. 해결 전략

방식특징
Deployment + Replica=1가장 단순하지만, 여러 노드에서 동시에 재스케줄될 가능성이 있음
StatefulSet + PersistentVolume스토리지와 안정적인 이름을 보장하며 단일 복제본 유지 가능
Leader Election여러 Pod가 실행되더라도 단 하나만 리더 역할 수행
CronJob / Job주기적 실행이 필요할 때 단일 실행 보장

3. 아키텍처 패턴

1) 리더 선출 기반

  • ConfigMap 또는 Lease API를 활용해 리더를 선출
  • 비리더 Pod는 대기 상태로 있다가 리더 장애 시 즉시 승격
  • 예: Controller Manager, Scheduler 등 쿠버네티스 핵심 컴포넌트도 이 방식 사용

2) 단일 ReplicaSet

  • Deployment에서 replicas: 1 설정
  • 노드 장애 시 다른 노드로 자동 스케줄
  • 단일 Pod가 반드시 필요한 경우 적합하지만, 일시적 중복 실행을 완전히 차단하기는 어려움

4. 애플리케이션 잠금(Locking) 전략

싱글톤 패턴을 안전하게 운영하려면 Pod 레벨을 넘어 애플리케이션 자체에서 중복 실행을 방지해야 합니다.

이를 내부 잠금(Internal Lock)과 외부 잠금(External Lock)으로 나눌 수 있습니다.

1) 내부 잠금 (Application-Level Lock)

  • 동작: 애플리케이션 프로세스에서 Mutex·세마포어 등을 활용해 단일 실행을 보장
  • 장점: 구현이 단순하고 쿠버네티스 리소스 의존도가 낮음
  • 단점: Pod 재시작이나 이중 스케줄 상황에서는 잠금 정보가 초기화될 수 있음
synchronized(lockObject) {
    // 크론 작업 단일 실행 보장
}

2) 외부 잠금 (Distributed Lock)

  • 동작: Redis, Etcd, Zookeeper, Consul 등 외부 시스템을 이용한 분산 락
  • 장점: Pod가 재시작되거나 여러 개가 떠도 외부 스토리지의 락으로 단일 실행 유지
  • 추천 사례: 데이터베이스 마이그레이션, 글로벌 스케줄러 등 강력한 단일성이 필요한 작업
# Redis Redlock 기반 분산 락
SETNX job-lock <uuid> EX 30

5. PodDisruptionBudget(PDB)로 고가용성 확보

단일 Pod 서비스는 노드 업그레이드나 유지보수 시 중단 위험이 큽니다.

이를 최소화하려면 PodDisruptionBudget(PDB)을 설정해 강제 중단을 방지해야 합니다.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: singleton-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: singleton
  • minAvailable: 1 : 최소 한 개의 Pod는 항상 유지
  • 단일 Pod라 해도 kubectl drain 등의 관리 작업 중 자동 중단을 방지
  • 주의: replicas가 1개라면 minAvailable: 1이 강제되어, drain 작업이 거부될 수 있디.

6. 운영 팁

  • 가용성 확보: 단일 Pod 운영이라도 PDB로 강제 중단을 방지
  • 잠금 방식 선택
    • 트래픽이 적고 단순한 경우 → 내부 잠금
    • 다중 노드·강력한 일관성이 필요한 경우 → 외부 잠금(분산 락)
  • 데이터 일관성: StatefulSet + PersistentVolume을 조합해 복구 시 데이터 손실 최소화

7. 적용 사례

  • 쿠버네티스 컨트롤 플레인: Controller Manager, Scheduler → Leader Election + Lease
  • 메시지 큐 컨슈머: 특정 토픽 단일 컨슈머
  • 데이터 마이그레이션: Redis 기반 외부 락으로 실행 충돌 방지

8. 내용 보강 – Kubernetes 1.34 최신 동향 및 주의점

1) 주요 변화와 영향

기능/변화상태싱글톤 서비스 영향
Coordinated Leader ElectionBeta리더 선출 알고리즘을 결정적·예측 가능하게 개선. 업그레이드나 멀티 버전 환경에서 리더 충돌을 줄여 안정성 강화
Scheduler 비동기 API 호출Beta스케줄러 응답성이 향상되어 리더 장애 시 failover 시간을 단축
ContainerRestartRulesAlphaPod 내 개별 컨테이너 재시작 정책 세밀 제어 → 사이드카 장애 격리로 메인 프로세스 안정성 확보
Dynamic Resource Allocation(DRA)StableGPU 등 특수 자원을 단일 Pod가 독점해야 할 때 자원 예약·할당 정확도 향상
Ordered Namespace DeletionStable네임스페이스 삭제 시 리소스 정리 순서 보장 → Lease·Lock 제거 충돌 완화

2) 최신 고려 사항

  • Feature Gate 점검: 1.34의 신규 기능 다수는 베타·알파 단계이므로 운영 전 활성화 여부와 롤백 전략을 반드시 검토
  • Leader Election 튜닝: renewDeadline, retryPeriod 등을 클러스터 규모·네트워크 지연에 맞게 조정
  • PDB 제약: replicas=1 + minAvailable: 1은 노드 드레인을 거부 → maxUnavailable: 0 등의 대안을 함께 고려
  • 외부 잠금 내성 강화: Redis·Etcd 장애 대비 타임아웃·재시도·백오프 로직 필수
  • 리더 전환 시 상태 동기화: Snapshot·커밋 로그 등을 활용해 상태 인수 전략 마련
  • 스케줄링 지연 대비: Scheduler 비동기 API, NodeSelector/Taint/Toleration 등을 통해 우선순위 확보

3) 단점 및 주의점

  • 단일 장애점(SPOF): 리더 Pod 장애 시 서비스 전체가 중단될 수 있음
  • 운영 복잡도: 리더 선출·분산 락·PDB 등 다층 설계가 필요
  • 유지보수 제약: drain 거부로 업그레이드·유지보수 작업이 지연될 수 있음
  • 락 경합/지연: 외부 잠금 시스템 병목·장애 시 전체 서비스가 대기 상태가 될 수 있음
  • 리더 전환 지연 및 상태 불일치: 장애 감지·리더 선출·상태 동기화가 늦으면 데이터 불일치 가능성 발생
  • 업그레이드 호환성: Leader Election API·Feature Gate 변경 시 사전 검증 필요

9. 실습 - 진행중

1) Go 애플리케이션 코드 (Leader Election)

  • main.go

    package main
    
    import (
        "context"
        "fmt"
        "os"
        "time"
    
        coordinationv1 "k8s.io/api/coordination/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/client-go/kubernetes"
        "k8s.io/client-go/rest"
        "k8s.io/client-go/tools/leaderelection"
        "k8s.io/client-go/tools/leaderelection/resourcelock"
    )
    
    func main() {
        // Kubernetes in-cluster config
        config, err := rest.InClusterConfig()
        if err != nil {
            panic(err.Error())
        }
    
        clientset, err := kubernetes.NewForConfig(config)
        if err != nil {
            panic(err.Error())
        }
    
        id, _ := os.Hostname()
    
        lock, err := resourcelock.New(
            resourcelock.LeasesResourceLock,
            "default",
            "singleton-leader",
            clientset.CoreV1(),
            clientset.CoordinationV1(),
            resourcelock.ResourceLockConfig{
                Identity: id,
            },
        )
        if err != nil {
            panic(err)
        }
    
        // coordinationv1 타입을 직접 참조해 사용
        //    (단순히 변수에 할당하고 _ 로 무시해도 import는 유효 처리됨)
        _ = coordinationv1.Lease{}
    
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()
    
        leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
            Lock:            lock,
            ReleaseOnCancel: true,
            LeaseDuration:   15 * time.Second,
            RenewDeadline:   10 * time.Second,
            RetryPeriod:     2 * time.Second,
            Callbacks: leaderelection.LeaderCallbacks{
                OnStartedLeading: func(ctx context.Context) {
                    for {
                        fmt.Printf("%s is leader: running singleton task...\n", id)
    
                        // metav1 실제 사용 예
                        now := metav1.Now()
                        fmt.Printf("Current metav1 time: %s\n", now.String())
    
                        time.Sleep(10 * time.Second)
                    }
                },
                OnStoppedLeading: func() {
                    fmt.Printf("%s lost leadership\n", id)
                },
            },
        })
    }

이 코드는 coordination.k8s.io/v1의 Lease 객체를 사용해, 여러 Pod 중 하나만 리더로 선출되어 주기 작업을 수행합니다.

2) Dockerfile

  • go 빌드

    go mod init singleton
    go mod tidy
  • 빌드 작업

    # -------- Build Stage --------
    FROM golang:1.25 AS builder
    WORKDIR /app
    
    # CGO 비활성화 + 정적 빌드
    ENV CGO_ENABLED=0 GOOS=linux GOARCH=arm64
    
    COPY go.mod go.sum* ./
    RUN go mod download || true
    
    COPY . .
    RUN go build -ldflags="-s -w" -o singleton .
    
    # -------- Runtime Stage --------
    # glibc 없는 경량 이미지
    FROM gcr.io/distroless/static:nonroot
    COPY --from=builder /app/singleton /singleton
    USER nonroot:nonroot
    ENTRYPOINT ["/singleton"]
    
  • 빌드 및 이미지 푸시

    docker build -t gjrjr4545/singleton:latest .
    docker push gjrjr4545/singleton:latest

3) 쿠버네티스 매니페스트

(1) RBAC (Lease 객체 사용 권한)

  • rbac.yaml

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: singleton-sa
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: singleton-role
    rules:
      - apiGroups: ["coordination.k8s.io"]
        resources: ["leases"]
        verbs: ["get", "create", "update"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: singleton-rb
    subjects:
      - kind: ServiceAccount
        name: singleton-sa
    roleRef:
      apiGroup: rbac.authorization.k8s.io   # ← 들여쓰기 바로 roleRef 밑으로
      kind: Role
      name: singleton-role
    

(2) Deployment + PDB

  • deploy.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: singleton-app
    spec:
      replicas: 2               # 2개 이상 띄워도 리더는 1개만!
      selector:
        matchLabels:
          app: singleton
      template:
        metadata:
          labels:
            app: singleton
        spec:
          serviceAccountName: singleton-sa
          containers:
          - name: singleton
            image: <YOUR_REPO>/singleton:1.0
            imagePullPolicy: IfNotPresent
            resources:
              requests:
                cpu: 100m
                memory: 128Mi
    ---
    apiVersion: policy/v1
    kind: PodDisruptionBudget
    metadata:
      name: singleton-pdb
    spec:
      minAvailable: 1
      selector:
        matchLabels:
          app: singleto
  • 특이사항 replicas를 2로 설정했지만, Leader Election 덕분에 동시에 하나의 Pod만 실질 작업을 수행

4) 테스트 방법

  1. 리소스 생성
kubectl apply -f rbac.yaml
kubectl apply -f deploy.yaml
  1. 리더 확인
kubectl get lease singleton-leader -o yaml
kubectl logs -l app=singleton

  1. 리더 Pod 강제 삭제
kubectl delete pod -l app=singleton --force

→ 다른 Pod가 즉시 리더를 인계받고 작업을 계속하는지 확인합니다.

  • holderIdenty가 변경 된것을 확인

10. 출처

profile
DevOps Engineer

3개의 댓글

comment-user-thumbnail
2025년 9월 27일

client-go 이용해서 리더선출 방식의 싱글턴을 구현하셨는데, 굿잡!

답글 달기
comment-user-thumbnail
2025년 9월 28일

안녕하세요. 작성하신 글 잘 읽었습니다. 중간에 단일 장애점(SPOF)에 대해 이야기 해주셨는데 이 방식은 Leader 선출 방식이 아닌 외부 락과 같이 단일 Pod를 생성하는 경우에만 해당되는건지 여쭤보고자 합니다.

1개의 답글