ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kubernetes 리소스 최적화: ARM64 환경에서 효율적으로 운영하기
    실제 경험과 인사이트를 AI와 함께 정리한 글 2025. 10. 22. 00:03

    Kubernetes 리소스 최적화: ARM64 환경에서 효율적으로 운영하기

    작성일: 2025-10-21
    태그: Kubernetes, ARM64, Performance, HPA
    난이도: 중급~고급

    들어가며

    imprun.dev는 Oracle Cloud의 ARM64 기반 Ampere 프로세서 클러스터에서 운영됩니다. 각 노드는 4 cores, 24GB RAM이라는 제약 조건 속에서 MongoDB, Redis, API 서버, Web Console, Runtime Exporter를 모두 수용해야 합니다.

    이 글에서는 제한된 리소스 환경에서 안정적인 서비스를 운영하기 위한 실전 최적화 기법을 소개합니다.

    환경 소개

    하드웨어 스펙

    클러스터 구성:
      노드 수: 3대
      CPU: 4 cores (ARM64 Ampere Altra)
      메모리: 24GB RAM
      스토리지: 200GB Block Storage
      네트워크: 10Gbps
    
    총 가용 리소스:
      CPU: 12 cores
      메모리: 72GB

    애플리케이션 스택

    ┌─────────────────────────────────────────────┐
    │ imprun-system Namespace                     │
    ├─────────────────────────────────────────────┤
    │ • MongoDB (Replica Set x1)                  │
    │ • Redis (Single Instance)                   │
    │ • imprun-server (API Server)                │
    │ • imprun-console (Next.js Web Console)      │
    │ • imp-runtime-exporter (Metrics Exporter)   │
    │                                             │
    │ + 사용자 Runtime Pods (동적 생성/삭제)      │
    └─────────────────────────────────────────────┘

    Part 1: ARM64 아키텍처 이해

    ARM64 vs x86_64 차이점

    특성 ARM64 x86_64
    전력 효율 🟢 우수 (같은 성능에 전력 50% 절감) 🟡 보통
    가격 🟢 저렴 (Oracle Cloud Free Tier) 🔴 비쌈
    Docker 이미지 호환성 🟡 제한적 (명시적 빌드 필요) 🟢 완벽
    성능 🟢 정수 연산 우수 🟢 부동소수점 우수
    생태계 🟡 성장 중 🟢 성숙

    ARM64 전용 이미지 빌드

    Multi-platform 빌드 필수!

    # Dockerfile (ARM64 호환 확인)
    FROM node:18-alpine  # ✅ ARM64 지원
    
    # ❌ 나쁜 예: x86_64 전용 바이너리
    # COPY ./bin/x86_64/app /app
    
    # ✅ 좋은 예: 소스 빌드
    COPY package*.json ./
    RUN npm ci --only=production

    Docker Buildx로 멀티 플랫폼 빌드:

    # Buildx 활성화 (한 번만)
    docker buildx create --name multiarch --use
    docker buildx inspect --bootstrap
    
    # ARM64 + AMD64 동시 빌드 & 푸시
    docker buildx build \
      --platform linux/amd64,linux/arm64 \
      -t junsik/imprun-server:latest \
      --push \
      .

    Manifest 확인:

    docker manifest inspect junsik/imprun-server:latest | jq '.manifests[].platform'
    
    # 출력:
    # {
    #   "architecture": "amd64",
    #   "os": "linux"
    # }
    # {
    #   "architecture": "arm64",
    #   "os": "linux",
    #   "variant": "v8"
    # }

    Node Selector 설정

    ARM64 노드에만 배포:

    # charts/imprun-server/templates/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    spec:
      template:
        spec:
          nodeSelector:
            kubernetes.io/arch: arm64  # ARM64 노드 선택
    
          # 또는 특정 노드 타입 지정
          # cloud.google.com/gke-nodepool: arm-pool

    혼합 클러스터 (ARM + x86):

    # values-production.yaml
    imprun-server:
      nodeSelector:
        kubernetes.io/arch: arm64  # ARM64 선호
    
      tolerations:  # x86도 허용 (필요시)
      - key: kubernetes.io/arch
        operator: Equal
        value: amd64
        effect: NoSchedule

    Part 2: Resource Requests/Limits 튜닝

    리소스 설정의 중요성

    Requests: "이만큼은 보장해주세요" (스케줄링 기준)
    Limits: "이 이상은 절대 안 돼요" (제한)

    resources:
      requests:
        cpu: 200m      # 0.2 core 보장
        memory: 512Mi  # 512MB 보장
      limits:
        cpu: 1000m     # 최대 1 core
        memory: 2Gi    # 최대 2GB (초과 시 OOMKilled)

    컴포넌트별 리소스 전략

    1. MongoDB (Stateful, 메모리 집약적)

    초기 설정 (실패 사례):

    # ❌ 너무 높은 설정
    mongodb:
      resources:
        requests:
          cpu: 500m
          memory: 2Gi  # 노드당 24GB인데 2Gi x 3 = 6GB
        limits:
          cpu: 2000m
          memory: 4Gi
    
    # 문제: 다른 Pod 스케줄링 실패!

    최적화 후:

    # ✅ 실제 사용량 기반 조정
    mongodb:
      replicas: 1  # ARM64 환경에서는 단일 인스턴스
    
      resources:
        requests:
          cpu: 200m     # 실제 사용량: 평균 150m
          memory: 512Mi # 실제 사용량: 평균 400Mi
        limits:
          cpu: 500m     # 버스트 허용
          memory: 1Gi   # OOM 방지
    
      # WiredTiger 캐시 크기 제한
      extraEnvVars:
      - name: MONGO_INITDB_ARGS
        value: "--wiredTigerCacheSizeGB 0.5"  # 512MB 캐시

    모니터링으로 검증:

    # 실제 사용량 확인
    kubectl top pod mongodb-mongodb-0 -n imprun-system
    
    # 출력:
    # NAME                CPU    MEMORY
    # mongodb-mongodb-0   120m   380Mi  ← requests보다 훨씬 낮음!

    2. Redis (메모리 제한 중요)

    redis:
      resources:
        requests:
          cpu: 100m
          memory: 256Mi
        limits:
          cpu: 500m
          memory: 512Mi  # ⚠️ 중요: Redis는 메모리 제한 필수!
    
      # Redis 자체 메모리 제한
      config:
        maxmemory: "400mb"  # limits보다 20% 낮게
        maxmemory-policy: "allkeys-lru"  # 메모리 부족 시 LRU 제거

    왜 낮게 설정?

    • Redis는 메모리 제한 초과 시 OOMKilled → 데이터 손실
    • 여유분 확보로 안전 마진 제공

    3. API Server (CPU 집약적)

    imprun-server:
      resources:
        requests:
          cpu: 200m     # Node.js 단일 스레드 기준
          memory: 512Mi
        limits:
          cpu: 1000m    # 버스트 허용 (NestJS 부팅 시)
          memory: 2Gi   # 메모리 릭 방지
    
      # Node.js 최적화
      env:
      - name: NODE_OPTIONS
        value: "--max-old-space-size=1536"  # 1.5GB (limit의 75%)

    부팅 시 CPU 버스트 패턴:

    부팅 시:   800m ~ 1000m (TypeScript 컴파일, 초기화)
    정상 운영: 150m ~ 300m (요청 처리)

    4. Web Console (Next.js)

    imprun-console:
      resources:
        requests:
          cpu: 100m     # SSR이 아닌 정적 서빙
          memory: 256Mi
        limits:
          cpu: 500m
          memory: 512Mi
    
      # Next.js Standalone 빌드 (메모리 절약)
      env:
      - name: NEXT_TELEMETRY_DISABLED
        value: "1"

    5. Runtime Exporter (경량)

    imp-runtime-exporter:
      resources:
        requests:
          cpu: 50m      # 메트릭 수집만
          memory: 128Mi
        limits:
          cpu: 200m
          memory: 256Mi

    리소스 튜닝 프로세스

    1단계: 보수적 시작

    resources:
      requests:
        cpu: 100m
        memory: 256Mi
      limits:
        cpu: 500m
        memory: 512Mi

    2단계: 모니터링 (1주일)

    # VictoriaMetrics 쿼리 (또는 Prometheus)
    # CPU 사용률 (P95)
    histogram_quantile(0.95,
      rate(container_cpu_usage_seconds_total{pod=~"imprun-server.*"}[5m])
    )
    
    # 메모리 사용률 (MAX)
    max_over_time(
      container_memory_working_set_bytes{pod=~"imprun-server.*"}[7d]
    )

    3단계: 조정

    # CPU P95 = 180m → requests: 200m (10% 여유)
    # 메모리 MAX = 450Mi → requests: 512Mi (15% 여유)
    
    resources:
      requests:
        cpu: 200m
        memory: 512Mi
      limits:
        cpu: 1000m     # 버스트 허용 (5배)
        memory: 1Gi    # OOM 방지 (2배)

    4단계: 검증

    # OOMKilled 이벤트 확인
    kubectl get events -n imprun-system \
      --field-selector reason=OOMKilled
    
    # CPU Throttling 확인
    kubectl top pod -n imprun-system --containers

    Part 3: Horizontal Pod Autoscaler (HPA)

    HPA 기본 개념

    목표: 부하에 따라 Pod 수 자동 조절

    트래픽 증가 → CPU 사용률 상승 → HPA가 Pod 추가
    트래픽 감소 → CPU 사용률 하락 → HPA가 Pod 제거

    Metrics Server 설치 (필수)

    kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
    
    # ARM64 노드에서 TLS 오류 시
    kubectl patch deployment metrics-server -n kube-system --type='json' \
      -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}]'
    
    # 동작 확인
    kubectl top nodes
    kubectl top pods -n imprun-system

    API Server HPA 설정

    # charts/imprun-server/templates/hpa.yaml
    {{- if .Values.autoscaling.enabled }}
    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: {{ include "imprun-server.fullname" . }}
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: {{ include "imprun-server.fullname" . }}
    
      minReplicas: {{ .Values.autoscaling.minReplicas }}
      maxReplicas: {{ .Values.autoscaling.maxReplicas }}
    
      metrics:
      # CPU 기반 스케일링
      - type: Resource
        resource:
          name: cpu
          target:
            type: Utilization
            averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
    
      # 메모리 기반 스케일링 (선택)
      - type: Resource
        resource:
          name: memory
          target:
            type: Utilization
            averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
    
      # 스케일링 동작 제어
      behavior:
        scaleUp:
          stabilizationWindowSeconds: 60  # 1분간 안정화 후 스케일 업
          policies:
          - type: Percent
            value: 50   # 한 번에 50% 증가
            periodSeconds: 60
          - type: Pods
            value: 2    # 또는 최대 2개 추가
            periodSeconds: 60
          selectPolicy: Min  # 둘 중 작은 값 선택
    
        scaleDown:
          stabilizationWindowSeconds: 300  # 5분간 안정화 후 스케일 다운
          policies:
          - type: Percent
            value: 25   # 한 번에 25%만 감소
            periodSeconds: 60
    {{- end }}

    Values 설정:

    # values-production.yaml
    imprun-server:
      autoscaling:
        enabled: true
        minReplicas: 2      # 최소 2개 (HA)
        maxReplicas: 6      # 최대 6개 (4 cores x 3 nodes = 12 cores 고려)
        targetCPUUtilizationPercentage: 70     # CPU 70% 시 스케일 업
        targetMemoryUtilizationPercentage: 80  # 메모리 80% 시 스케일 업

    HPA 동작 시뮬레이션

    부하 테스트:

    # Apache Bench로 부하 생성
    ab -n 10000 -c 100 https://app.imprun.dev/v1/health/liveness
    
    # HPA 상태 모니터링
    kubectl get hpa -n imprun-system -w

    예상 동작:

    NAME                REFERENCE                      TARGETS   MINPODS   MAXPODS   REPLICAS
    imprun-server-hpa   Deployment/imprun-server       45%/70%   2         6         2
    
    # 부하 증가 (1분 후)
    imprun-server-hpa   Deployment/imprun-server       85%/70%   2         6         3  ← +1 Pod
    
    # 부하 지속 (2분 후)
    imprun-server-hpa   Deployment/imprun-server       78%/70%   2         6         4  ← +1 Pod
    
    # 부하 감소 (5분 안정화 후)
    imprun-server-hpa   Deployment/imprun-server       40%/70%   2         6         3  ← -1 Pod

    커스텀 메트릭 기반 HPA (고급)

    사용 사례: API 요청 수(RPS) 기반 스케일링

    1. Prometheus Adapter 설치:

    helm install prometheus-adapter prometheus-community/prometheus-adapter \
      -n monitoring \
      --set prometheus.url=http://vmsingle-vm.monitoring.svc:8428

    2. 커스텀 메트릭 정의:

    # prometheus-adapter-values.yaml
    rules:
    - seriesQuery: 'http_requests_total{namespace="imprun-system",pod=~"imprun-server.*"}'
      resources:
        overrides:
          namespace: {resource: "namespace"}
          pod: {resource: "pod"}
      name:
        matches: "^(.*)_total$"
        as: "${1}_per_second"
      metricsQuery: 'rate(<<.Series>>{<<.LabelMatchers>>}[2m])'

    3. HPA에서 사용:

    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    spec:
      metrics:
      - type: Pods
        pods:
          metric:
            name: http_requests_per_second
          target:
            type: AverageValue
            averageValue: "100"  # Pod당 100 RPS 초과 시 스케일 업

    Part 4: 리소스 최적화 실전 팁

    1. QoS (Quality of Service) 클래스 이해

    Kubernetes는 Pod를 3가지 QoS 클래스로 분류:

    # Guaranteed (최고 우선순위)
    # requests == limits
    resources:
      requests:
        cpu: 500m
        memory: 1Gi
      limits:
        cpu: 500m
        memory: 1Gi
    
    # Burstable (중간 우선순위) ← 권장!
    # requests < limits
    resources:
      requests:
        cpu: 200m
        memory: 512Mi
      limits:
        cpu: 1000m
        memory: 2Gi
    
    # BestEffort (최저 우선순위)
    # requests/limits 없음
    resources: {}

    리소스 부족 시 제거 우선순위:

    BestEffort (먼저 죽음) > Burstable > Guaranteed (마지막까지 살아남음)

    권장: Stateful 워크로드(MongoDB)는 Guaranteed, Stateless(API)는 Burstable

    2. PodDisruptionBudget (PDB)

    목적: 자발적 중단 시 최소 가용성 보장

    # charts/imprun-server/templates/pdb.yaml
    apiVersion: policy/v1
    kind: PodDisruptionBudget
    metadata:
      name: imprun-server-pdb
    spec:
      minAvailable: 1  # 최소 1개 Pod는 항상 Running
      selector:
        matchLabels:
          app.kubernetes.io/name: imprun-server
    
      # 또는 maxUnavailable 사용
      # maxUnavailable: 1  # 최대 1개만 동시 중단 가능

    효과:

    • kubectl drain 실행 시 1개 Pod는 보존
    • 클러스터 업그레이드 시 무중단 배포

    3. Vertical Pod Autoscaler (VPA)

    HPA vs VPA:

    • HPA: Pod 개수 조절 (수평 확장)
    • VPA: Pod 크기 조절 (수직 확장)
    apiVersion: autoscaling.k8s.io/v1
    kind: VerticalPodAutoscaler
    metadata:
      name: mongodb-vpa
    spec:
      targetRef:
        apiVersion: apps/v1
        kind: StatefulSet
        name: mongodb-mongodb
    
      updatePolicy:
        updateMode: "Auto"  # 자동으로 Pod 재시작 & 리소스 조정
    
      resourcePolicy:
        containerPolicies:
        - containerName: mongodb
          minAllowed:
            cpu: 200m
            memory: 512Mi
          maxAllowed:
            cpu: 2000m
            memory: 4Gi

    주의: VPA + HPA 동시 사용 시 충돌 가능! (동일 메트릭 사용 금지)

    4. Node Affinity로 워크로드 분산

    시나리오: Stateful(MongoDB)과 Stateless(API) 분리

    # MongoDB: 특정 노드에 고정
    mongodb:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: node-role
                operator: In
                values:
                - database  # database 역할 노드에만 배포
    
    # API Server: MongoDB와 같은 노드 피하기 (선호)
    imprun-server:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels:
                  app: mongodb
              topologyKey: kubernetes.io/hostname

    5. 리소스 쿼터 설정

    네임스페이스 전체 리소스 제한:

    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: imprun-system-quota
      namespace: imprun-system
    spec:
      hard:
        requests.cpu: "8"        # 총 8 cores 요청 가능
        requests.memory: "16Gi"  # 총 16GB 메모리 요청 가능
        limits.cpu: "20"
        limits.memory: "48Gi"
        persistentvolumeclaims: "10"  # PVC 최대 10개
        pods: "50"  # Pod 최대 50개

    효과: 실수로 과도한 리소스 할당 방지


    실전 시나리오

    시나리오 1: OOMKilled 디버깅

    증상:

    kubectl get pods -n imprun-system
    # NAME                        READY   STATUS      RESTARTS
    # imprun-server-7b78c-6zzzd   0/1     OOMKilled   3

    원인 파악:

    # 1. 이벤트 확인
    kubectl describe pod imprun-server-7b78c-6zzzd -n imprun-system | grep -A5 "Last State"
    
    # 출력:
    # Last State:     Terminated
    #   Reason:       OOMKilled
    #   Exit Code:    137
    
    # 2. 메모리 사용량 히스토리 (VictoriaMetrics)
    # container_memory_working_set_bytes{pod="imprun-server-7b78c-6zzzd"}

    해결:

    # limits 증가
    resources:
      limits:
        memory: 2Gi  # 1Gi → 2Gi
    
    # Node.js 힙 크기 조정
    env:
    - name: NODE_OPTIONS
      value: "--max-old-space-size=1536"  # 1.5GB

    시나리오 2: CPU Throttling 최소화

    증상: API 응답 속도 느림, CPU throttled 높음

    확인:

    # Throttling 비율 확인 (Prometheus)
    rate(container_cpu_cfs_throttled_seconds_total{pod=~"imprun-server.*"}[5m])
    / rate(container_cpu_cfs_periods_total{pod=~"imprun-server.*"}[5m])
    
    # 30% 이상이면 문제!

    해결:

    # CPU limits 증가 또는 제거
    resources:
      limits:
        cpu: 2000m  # 1000m → 2000m
    
    # 또는 limits 제거 (Burstable QoS 유지)
    # limits:
    #   cpu: null  # CPU throttling 없음 (주의: 노이지 네이버 가능)

    시나리오 3: HPA가 작동하지 않음

    증상:

    kubectl get hpa -n imprun-system
    # NAME        REFERENCE             TARGETS         MINPODS   MAXPODS   REPLICAS
    # api-hpa     Deployment/api-server <unknown>/70%   2         6         2

    원인 & 해결:

    # 1. Metrics Server 확인
    kubectl top nodes
    # Error: Metrics API not available
    
    # → Metrics Server 설치 필요
    
    # 2. Pod에 resources.requests 없음
    kubectl get pod api-server-xxx -o yaml | grep -A5 resources
    # resources: {}  ← 문제!
    
    # → requests 추가 필수
    
    # 3. HPA 버전 불일치
    kubectl api-versions | grep autoscaling
    # autoscaling/v1  ← v2 필요!
    
    # → Kubernetes 버전 업그레이드

    모니터링 대시보드

    Grafana 대시보드 (VictoriaMetrics)

    CPU 사용률 패널:

    # 컨테이너별 CPU 사용률
    sum(rate(container_cpu_usage_seconds_total{namespace="imprun-system"}[5m])) by (pod)
    /
    sum(container_spec_cpu_quota{namespace="imprun-system"}/container_spec_cpu_period{namespace="imprun-system"}) by (pod)
    * 100

    메모리 사용률 패널:

    # 컨테이너별 메모리 사용률
    container_memory_working_set_bytes{namespace="imprun-system"}
    /
    container_spec_memory_limit_bytes{namespace="imprun-system"}
    * 100

    HPA 스케일링 이벤트:

    # Replica 변경 히스토리
    kube_horizontalpodautoscaler_status_current_replicas{namespace="imprun-system"}

    체크리스트

    리소스 설정

    • 모든 컨테이너에 requests/limits 설정
    • requests는 실제 사용량의 110~120%
    • limits는 requests의 2~5배 (버스트 허용)
    • Stateful 워크로드는 Guaranteed QoS 고려

    ARM64 최적화

    • 멀티 플랫폼 이미지 빌드 (amd64 + arm64)
    • Node Selector로 아키텍처 지정
    • ARM64 전용 바이너리 없는지 확인

    HPA 설정

    • Metrics Server 설치 및 동작 확인
    • CPU 기반 HPA 우선 적용
    • minReplicas ≥ 2 (고가용성)
    • scaleDown 안정화 시간 충분히 확보 (5분+)

    모니터링

    • VictoriaMetrics/Prometheus 연동
    • Grafana 대시보드 구성
    • OOMKilled/CPU Throttling 알림 설정
    • 주간 리소스 사용량 리포트

    결론

    제한된 리소스 환경에서 안정적인 서비스 운영의 핵심:

    1. 정확한 측정: 추측이 아닌 메트릭 기반 의사결정
    2. 점진적 조정: 작게 시작해서 실제 사용량 기반 확장
    3. 자동화: HPA로 부하 대응, VPA로 리소스 최적화
    4. 방어적 설계: QoS, PDB로 장애 영향 최소화

    ARM64 환경의 비용 효율성과 성능을 최대한 활용하여 imprun.dev는 월 $0의 인프라 비용으로 운영되고 있습니다.

    다음 글 예고

    • CI/CD 파이프라인 구축: GitHub Actions로 빌드부터 배포까지 완전 자동화

    참고 자료


    질문이나 피드백은 GitHub Issues로!
    📧 GitHub Issues

Designed by Tistory.