관리 메뉴

근묵자흑

Kubernetes Pattern: 예측 가능한 요구사항(Predictable Demands) 본문

k8s/kubernetes-pattern

Kubernetes Pattern: 예측 가능한 요구사항(Predictable Demands)

Luuuuu 2025. 8. 2. 19:43

Kubernetes에서 애플리케이션을 안정적으로 운영하려면 무엇이 필요할까요? 바로 예측 가능성입니다. Predictable Demands 패턴은 컨테이너화된 애플리케이션이 필요로 하는 리소스와 런타임 의존성을 명확히 선언하여, Kubernetes가 효율적으로 워크로드를 스케줄링하고 관리할 수 있도록 하는 기본 패턴입니다.

이 글에서는 실제 테스트 코드와 함께 Predictable Demands 패턴의 핵심 개념들을 살펴보겠습니다.

📚 핵심 개념 이해하기

1. 왜 Predictable Demands가 중요한가?

Kubernetes는 수많은 애플리케이션이 공존하는 멀티테넌트 환경입니다. 각 애플리케이션이 필요한 리소스를 명확히 선언하지 않으면:

  • 🚨 리소스 경쟁: 애플리케이션 간 예측 불가능한 리소스 경쟁 발생
  • 📉 성능 저하: 리소스 부족으로 인한 애플리케이션 성능 저하
  • 💥 시스템 불안정: OOM(Out of Memory) 킬, CPU 스로틀링 등의 문제
  • 🔄 비효율적 스케줄링: Kubernetes가 최적의 노드에 Pod을 배치하지 못함

2. 리소스 유형 이해하기

Kubernetes는 리소스를 두 가지로 분류합니다:

# 압축 가능한 리소스 (Compressible Resources)
# - CPU, 네트워크 대역폭
# - 부족해도 throttling으로 대응 가능
resources:
  limits:
    cpu: "1000m"  # 1 vCPU

# 압축 불가능한 리소스 (Incompressible Resources)  
# - 메모리, 스토리지
# - 부족하면 프로세스 종료 (OOMKilled)
resources:
  limits:
    memory: "1Gi"  # 1 GiB

🧪  Predictable Demands 테스트 해보기

테스트 코드를 통해 각 개념을 검증해보겠습니다. 전체 테스트 코드는 GitHub 저장소에서 확인할 수 있습니다.

테스트 1: 런타임 의존성 (Runtime Dependencies)

1.1 PVC 의존성 테스트

# test-dependencies.sh 중 일부
test_pvc_dependency() {
    # PVC 없이 Pod 생성 시도
    kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: test-missing-pvc
spec:
  containers:
  - name: app
    image: busybox
    volumeMounts:
    - name: data
      mountPath: /data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: non-existent-pvc  # 존재하지 않는 PVC
EOF

    # Pod이 Pending 상태에 머물러 있는지 확인
    pod_status=$(kubectl get pod test-missing-pvc -o jsonpath='{.status.phase}')

    if [ "$pod_status" = "Pending" ]; then
        # PVC 누락으로 인한 이벤트 확인
        kubectl get events --field-selector involvedObject.name=test-missing-pvc
        return 0
    fi
}

💡 핵심 포인트:

  • Pod은 필요한 PVC가 없으면 Pending 상태에서 대기합니다
  • 이를 통해 데이터 의존성을 명확히 선언하고 검증할 수 있습니다

1.2 ConfigMap 실시간 업데이트 테스트

# ConfigMap을 볼륨으로 마운트
spec:
  containers:
  - name: app
    volumeMounts:
    - name: config
      mountPath: /etc/config
  volumes:
  - name: config
    configMap:
      name: app-config

테스트 결과:

  • ConfigMap 업데이트 후 약 60-90초 내에 Pod에 반영됨
  • 환경변수로 주입된 경우는 Pod 재시작 필요
  • 볼륨 마운트 방식은 자동 업데이트 지원

1.3 네트워크 의존성과 Service Discovery

# network-dependencies.yaml 예제
apiVersion: v1
kind: Service
metadata:
  name: backend-service
spec:
  selector:
    app: backend
  ports:
  - port: 8080
    targetPort: 8080
---
apiVersion: v1
kind: Pod
metadata:
  name: frontend-pod
spec:
  containers:
  - name: frontend
    image: nicolaka/netshoot
    command: ["sh", "-c"]
    args:
      - |
        # DNS를 통한 서비스 발견
        nslookup backend-service

        # 환경변수를 통한 서비스 발견
        env | grep BACKEND_SERVICE

        # 서비스 접근 테스트
        curl -s http://backend-service:8080

Service Discovery 메커니즘:

  1. DNS 기반: service-name.namespace.svc.cluster.local
  2. 환경변수: SERVICE_NAME_SERVICE_HOST, SERVICE_NAME_SERVICE_PORT
  3. Headless Service: StatefulSet을 위한 개별 Pod DNS

테스트 2: QoS 클래스 (Quality of Service)

2.1 QoS 클래스 결정 규칙

# Guaranteed QoS - 최고 우선순위
apiVersion: v1
kind: Pod
metadata:
  name: guaranteed-pod
spec:
  containers:
  - name: app
    resources:
      requests:
        memory: "128Mi"
        cpu: "100m"
      limits:
        memory: "128Mi"  # requests와 동일
        cpu: "100m"      # requests와 동일

# Burstable QoS - 중간 우선순위
apiVersion: v1
kind: Pod
metadata:
  name: burstable-pod
spec:
  containers:
  - name: app
    resources:
      requests:
        memory: "64Mi"
        cpu: "50m"
      limits:
        memory: "128Mi"  # requests보다 큼
        cpu: "200m"

# BestEffort QoS - 최저 우선순위
apiVersion: v1
kind: Pod
metadata:
  name: besteffort-pod
spec:
  containers:
  - name: app
    image: nginx
    # resources 섹션 없음

테스트 검증 코드:

# QoS 클래스 확인
kubectl get pod guaranteed-pod -o jsonpath='{.status.qosClass}'
# 출력: Guaranteed

kubectl get pod burstable-pod -o jsonpath='{.status.qosClass}'
# 출력: Burstable

kubectl get pod besteffort-pod -o jsonpath='{.status.qosClass}'
# 출력: BestEffort

2.2 리소스 제한 동작 테스트

CPU Throttling 테스트:

spec:
  containers:
  - name: cpu-stress
    image: polinux/stress
    command: ["stress"]
    args: ["--cpu", "2", "--timeout", "30s"]
    resources:
      requests:
        cpu: "50m"
      limits:
        cpu: "100m"  # 0.1 CPU로 제한

실행 결과:

  • CPU 사용이 100m(0.1 vCPU)로 제한됨
  • 초과 사용 시 throttling 발생
  • 애플리케이션은 계속 실행되지만 속도가 느려짐

Memory OOM 테스트:

spec:
  containers:
  - name: memory-stress
    image: polinux/stress
    command: ["stress"]
    args: ["--vm", "1", "--vm-bytes", "150M"]
    resources:
      limits:
        memory: "128Mi"  # 128MiB로 제한

실행 결과:

  • 메모리 사용이 128MiB를 초과하면 OOMKilled
  • Pod은 재시작 정책에 따라 재시작됨

테스트 3: Pod 우선순위와 선점(Preemption)

3.1 우선순위 클래스 정의

# 높은 우선순위 클래스
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000
description: "높은 우선순위 워크로드"

# 낮은 우선순위 클래스
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 100
description: "낮은 우선순위 워크로드"

3.2 선점 시나리오 테스트

# 1단계: 낮은 우선순위 Pod으로 클러스터 채우기
for i in {1..8}; do
  kubectl run low-priority-$i --image=nginx \
    --overrides='{"spec":{"priorityClassName":"low-priority"}}' \
    --requests='cpu=50m,memory=64Mi'
done

# 2단계: 높은 우선순위 Pod 추가 (선점 발생)
kubectl run high-priority --image=nginx \
  --overrides='{"spec":{"priorityClassName":"high-priority"}}' \
  --requests='cpu=300m,memory=256Mi'

# 3단계: 선점 이벤트 확인
kubectl get events --sort-by='.lastTimestamp' | grep Preempted

실행 결과:

  • 높은 우선순위 Pod을 위해 낮은 우선순위 Pod들이 evict됨
  • Kubernetes는 최소한의 Pod만 제거하여 공간 확보

테스트 4: 네임스페이스 리소스 관리

4.1 ResourceQuota 설정

apiVersion: v1
kind: ResourceQuota
metadata:
  name: namespace-quota
  namespace: development
spec:
  hard:
    requests.cpu: "4"
    requests.memory: "4Gi"
    limits.cpu: "8"
    limits.memory: "8Gi"
    pods: "20"
    persistentvolumeclaims: "10"

테스트 검증:

# 할당량 상태 확인
kubectl describe resourcequota namespace-quota -n development

# 출력 예시:
# Name:                   namespace-quota
# Resource                Used    Hard
# --------                ----    ----
# limits.cpu              2       8
# limits.memory           2Gi     8Gi
# persistentvolumeclaims  2       10
# pods                    5       20
# requests.cpu            1       4
# requests.memory         1Gi     4Gi

4.2 LimitRange 자동 적용

apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: development
spec:
  limits:
  - type: Container
    default:
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    max:
      cpu: "2"
      memory: "2Gi"

테스트 결과:

  • 리소스를 명시하지 않은 Pod에 자동으로 기본값 적용
  • 최대값을 초과하는 요청은 거부됨

📊 테스트 요약

우리의 테스트 스위트 실행 결과:

✅ Runtime Dependencies Tests 

  • PVC 의존성 검증
  • ConfigMap 실시간 업데이트
  • Secret 마운트 권한
  • Service Discovery
  • EmptyDir 볼륨 공유

✅ Resource Limits and QoS Tests

  • QoS 클래스 자동 할당
  • CPU throttling 동작
  • Memory OOM 처리
  • 리소스 사용량 모니터링
  • Multi-container Pod QoS
  • 리소스 경쟁 시뮬레이션

✅ Pod Priority and Preemption Tests

  • PriorityClass 생성 및 적용
  • 우선순위 기반 스케줄링
  • 선점(Preemption) 시나리오
  • PodDisruptionBudget 연동
  • 우선순위 기반 리소스 할당
  • 시스템 Pod 보호

✅ ResourceQuota and LimitRange Tests

  • 기본 ResourceQuota 동작
  • 할당량 초과 시 거부
  • LimitRange 자동 적용
  • 제한 범위 위반 검증
  • 객체 수 제한
  • 스토리지 할당량
  • 범위별(Scoped) ResourceQuota
  • 복합 제한 테스트

🚀 실습 환경 구성

Minikube 환경 설정

# Minikube 시작 (충분한 리소스 할당)
minikube start --cpus=2 --memory=4096 --driver=docker

# 애드온 활성화
minikube addons enable metrics-server
minikube addons enable dashboard

# 테스트 실행
cd foundational/predictable-demands/05-testing
chmod +x *.sh
./run-all-tests.sh

주요 확인 명령어

# Pod 리소스 사용량 확인
kubectl top pods

# QoS 클래스 확인
kubectl get pods -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass

# ResourceQuota 사용량 확인
kubectl describe resourcequota -A

# 이벤트 모니터링
kubectl get events --sort-by='.lastTimestamp' -w