EKS Bottlerocket AMI에서 DCGM 오류로 GPU 노드 반복 교체 문제 해결기
안녕하세요, 데브옵스 엔지니어 포카입니다.
최근 인프런의 ”클린 코더스: 실전 객체 지향 프로그래밍과 TDD 마스터 클래스” 강의 영상 업스케일링 작업을 위해 GPU 클러스터를 운영하던 중, 예상치 못한 문제에 직면했습니다. GPU 노드가 정상적으로 생성되었음에도 불구하고 약 10-11분 후 갑자기 교체되는 현상이 반복되었습니다.
이번 글에서는 Amazon EKS Bottlerocket AMI 환경에서 발생한 NVIDIA DCGM 관련 오류로 인한 GPU 노드 반복 교체 문제를 해결한 과정을 공유하겠습니다.
배경
현재 인프런의 GPU 워크로드 환경
인프런에서는 강의 영상의 품질 향상을 위해 AI 기반 업스케일링 작업을 수행하고 있습니다. 이를 위해 다음과 같은 환경을 구축했습니다:
- Amazon EKS 1.32 클러스터
- Bottlerocket AMI (
amazon/bottlerocket-aws-k8s-1.32-nvidia-aarch64-v1.39.1-3a880b44
) - NVIDIA GPU 인스턴스 (
g5g.xlarge
) - Karpenter v1.4.0을 통한 노드 자동 관리
Node Auto-Repair 기능 도입 배경
GPU 워크로드를 안정적으로 운영하기 위해 Karpenter의 Node Auto-Repair 기능을 도입했습니다. 이 기능을 도입한 이유는 다음과 같습니다:
기존 문제: 노드 좀비 상태
Bottlerocket AMI 환경에서 메모리 부족으로 인해 kubelet이 OOM으로 종료되면, EC2 인스턴스는 살아있지만 Kubernetes에서는 Unknown
상태로 인식되는 좀비 노드 문제가 있었습니다.
Node Auto-Repair로 해결
이를 해결하기 위해 Karpenter의 Node Auto-Repair 기능을 활성화했습니다:
settings:
clusterName: "{{ .Environment.Name }}"
clusterEndpoint: "{{ .Values.clusterEndpoint }}"
interruptionQueue: "{{ .Values.karpenter.interruptionQueue }}"
featureGates:
spotToSpotConsolidation: true
nodeRepair: true # Karpenter 설정에서 nodeRepair 기능 활성화
참고: 현재는 메모리 4GB 이상 인스턴스 타입 사용으로 완화했습니다. (관련 이슈)
그런데 왜 GPU 노드가 계속 교체되지?
Node Auto-Repair 기능을 도입한 후, 예상치 못한 새로운 문제가 발생했습니다.
문제 현상
GPU를 사용하는 Pod를 실행하자 다음과 같은 현상이 반복되었습니다:
- GPU 노드가 정상적으로 생성되고
Ready
상태가 됨 - 약 10-11분 후, 노드가 갑자기 비정상으로 간주되어 Karpenter에 의해 교체됨
- 새로 생성된 노드에서도 동일한 문제가 반복됨
- 결과적으로 GPU Pod들이 지속적으로 재시작되어 작업이 제대로 진행되지 않음
문제 원인 분석
핵심 오류 메시지 발견
Node Event 분석 결과, 다음과 같은 메시지를 발견했습니다:
Normal AcceleratedHardwareReady (karpenter, 12분 전):
Status: True → False
Reason: DCGMError
Message: failed to initialize DCGM: libdcgm.so not Found
NVIDIA DCGM(Data Center GPU Manager) 라이브러리 누락이 문제의 핵심이었습니다.
libdcgm.so
은 DCGM 기반 모니터링에서 사용되는 GPU의 상태, 이벤트 등을 수집하는 라이브러리입니다. 따라서 GPU 연산 자체에 영향을 주진 않지만 DCGM을 사용한 GPU 상태 모니터링은 불가능하다고 볼 수 있습니다..
그림 1: 시간순 이벤트 로그 - DCGM 오류 발생부터 Karpenter의 노드 교체까지의 전체 과정
에러 발생 타임라인
이벤트 로그를 시간 순으로 정리하면 다음과 같은 흐름을 확인할 수 있었습니다:
1. 노드 초기화 및 준비 완료 (약 13분 전)
Normal Starting (kube-proxy): kube-proxy가 노드에서 정상적으로 시작
Normal NodeAllocatableEnforced: 노드 리소스 할당 정책 적용
Normal Ready (karpenter): kubelet is posting ready status
Normal NodeReady (kubelet): 노드가 준비되었음을 알림
이 시점까지는 모든 것이 정상적으로 진행됩니다.
2. DCGM 오류 발생 (약 12분 전)
Normal AcceleratedHardwareReady (karpenter, 12분 전):
Status: True → False
Reason: DCGMError
Message: failed to initialize DCGM: libdcgm.so not Found
노드가 Ready
상태가 된 지 약 1분 후, AcceleratedHardwareReady
상태가 True
에서 False
로 변경됩니다. 이는 GPU 하드웨어를 사용할 수 없는 상태임을 의미합니다.
3. Karpenter의 노드 복구 시도
Warning NodeRepairBlocked (karpenter, 11분 전):
Message: more than 20% nodes are unhealthy in the nodepool
Warning FailedDraining (karpenter, 43초 전):
Message: Failed to drain node, 1 pods are waiting to be evicted
Warning InstanceTerminating (karpenter, 42초 전):
Message: Instance is terminating
AcceleratedHardwareReady=False
상태가 되자 Karpenter는 해당 노드를 비정상으로 판단하고 복구 절차를 시작합니다.
문제의 근본 원인: DCGM 라이브러리 누락
DCGM이란 무엇인가?
NVIDIA DCGM(Data Center GPU Manager)은 데이터센터 환경에서 GPU를 체계적으로 관리하기 위한 종합 솔루션입니다.
DCGM의 주요 기능:
-
실시간 GPU 모니터링
- GPU 온도, 메모리 사용량, 전력 소비량 추적
- GPU 오류 및 성능 저하 감지
- 하드웨어 장애 예측 및 알림
-
Kubernetes 통합
AcceleratedHardwareReady
Node Condition 관리- GPU 노드의 스케줄링 가능 여부 결정
- 클러스터 레벨에서 GPU 리소스 상태 제공
-
정책 기반 관리
- GPU 사용률 임계값 설정
- 자동 스케일링 트리거 제공
- 워크로드 배치 최적화 지원
문제 발생 과정
Bottlerocket AMI의 libdcgm.so
라이브러리 누락으로 인해 다음과 같은 연쇄 반응이 일어났습니다:
- GPU 노드의
AcceleratedHardwareReady
Node Condition이False
로 설정됨 - Kubernetes 및 Karpenter가 해당 노드를 비정상으로 판단함
- 결과적으로 노드가 자동으로 교체되는 무한 루프 발생
Karpenter가 노드를 교체한 이유
DCGM 오류를 발견했지만, 왜 Karpenter가 이를 문제로 인식하고 노드를 교체했는지 이해해야 합니다.
Node Auto-Repair 기능 개요
Karpenter의 Node Auto-Repair는 다음과 같이 동작합니다:
- 목적: 비정상 노드를 자동 감지하고 교체하여 클러스터 안정성 유지
- 버전: Karpenter v1.1.0부터 지원 (현재 알파 기능)
- 장점: 수동 개입 없이 노드 문제 자동 해결
비정상 노드 판단 기준
Karpenter는 다음 Node Condition들을 기준으로 노드 상태를 판단합니다:
- Ready=False: kubelet이 응답하지 않거나 노드가 스케줄링 불가능한 상태
- Ready=Unknown: kubelet과의 통신이 끊어진 상태 (앞서 언급된 좀비 노드 상황)
- MemoryPressure=True: 노드에 메모리가 부족한 상태
- DiskPressure=True: 노드에 디스크 공간이 부족한 상태
- PIDPressure=True: 노드에 프로세스 ID가 부족한 상태
- NetworkUnavailable=True: 노드의 네트워크를 사용할 수 없는 상태
- AcceleratedHardwareReady=False: GPU 등 가속 하드웨어를 사용할 수 없는 상태 (바로 이번 사례의 원인)
이 중 AcceleratedHardwareReady=False
상태가 저희가 겪은 DCGM 라이브러리 누락 문제의 직접적인 원인이었습니다.
Karpenter의 노드 복구 로직
약 10-11분 후 노드가 교체되는 이유를 이해하기 위해 Karpenter의 실제 코드를 살펴보겠습니다:
// Reconcile은 노드 상태 변경에 대한 응답으로 호출됩니다.
// 비정상 노드를 감지하고 복구 로직을 수행합니다.
func (c *Controller) Reconcile(ctx context.Context, node *corev1.Node) (reconcile.Result, error) {
ctx = injection.WithControllerName(ctx, "node.health")
// 해당 노드가 Karpenter에 의해 관리되는지 확인합니다.
nodeClaim, err := nodeutils.NodeClaimForNode(ctx, c.kubeClient, node)
if err != nil {
return reconcile.Result{}, nodeutils.IgnoreDuplicateNodeClaimError(nodeutils.IgnoreNodeClaimNotFoundError(err))
}
ctx = log.IntoContext(ctx, log.FromContext(ctx).WithValues("NodeClaim", klog.KObj(nodeClaim)))
// 노드 풀 레이블이 있는 경우, 해당 노드 풀 내의 노드 클레임 상태를 확인합니다.
// 독립형 노드 클레임의 경우, 클러스터 전체의 노드 상태를 확인하여 복구를 진행합니다.
nodePoolName, found := nodeClaim.Labels[v1.NodePoolLabelKey]
if found {
nodePoolHealthy, err := c.isNodePoolHealthy(ctx, nodePoolName)
if err != nil {
return reconcile.Result{}, client.IgnoreNotFound(err)
}
if !nodePoolHealthy {
return reconcile.Result{}, c.publishNodePoolHealthEvent(ctx, node, nodeClaim, nodePoolName)
}
} else {
clusterHealthy, err := c.isClusterHealthy(ctx)
if err != nil {
return reconcile.Result{}, err
}
if !clusterHealthy {
c.recorder.Publish(NodeRepairBlockedUnmanagedNodeClaim(node, nodeClaim, fmt.Sprintf("more then %s nodes are unhealthy in the cluster", allowedUnhealthyPercent.String()))...)
return reconcile.Result{}, nil
}
}
// unhealthyNodeCondition은 비정상 상태와 정책에 따른 종료 기간을 찾습니다.
unhealthyNodeCondition, policyTerminationDuration := c.findUnhealthyConditions(node)
if unhealthyNodeCondition == nil {
return reconcile.Result{}, nil
}
// 노드가 비정상이지만 아직 전체 허용 장애 시간에 도달하지 않은 경우,
// 비정상 노드의 종료 시간에 맞춰 재조정(requeue)합니다.
terminationTime := unhealthyNodeCondition.LastTransitionTime.Add(policyTerminationDuration)
if c.clock.Now().Before(terminationTime) {
return reconcile.Result{RequeueAfter: terminationTime.Sub(c.clock.Now())}, nil
}
// 허용 장애 시간을 초과한 비정상 노드는 강제로 종료할 수 있습니다.
// 종료 유예 기간을 어노테이션으로 추가합니다.
if err := c.annotateTerminationGracePeriod(ctx, nodeClaim); err != nil {
return reconcile.Result{}, client.IgnoreNotFound(err)
}
return c.deleteNodeClaim(ctx, nodeClaim, node, unhealthyNodeCondition)
}
로직 설명:
- 노드 풀 상태 확인:
isNodePoolHealthy()
메서드로 노드 풀 내 비정상 노드 비율이 임계값을 초과하는지 확인 - 비정상 조건 검사:
findUnhealthyConditions()
메서드로AcceleratedHardwareReady=False
같은 비정상 조건 탐지 - 유예 시간 적용:
terminationTime
계산으로 즉시 제거하지 않고 설정된 시간만큼 대기 (약 10분) - 재조정 메커니즘:
RequeueAfter
로 유예 시간 동안 주기적으로 상태 재확인
저희 사례에서는 AcceleratedHardwareReady=False
조건이 감지되었지만, GPU를 사용하지 않는 다른 Pod들이 정상 동작 중이었기에 Karpenter는 유예 시간 동안 대기한 후 최종적으로 노드를 제거했습니다.
해결 방안 검토와 최종 선택
고려했던 해결 방안들
문제를 해결하기 위해 다음과 같은 방안들을 검토했습니다:
방안 | 장점 | 단점 |
---|---|---|
DCGM 라이브러리 수동 설치 | 근본 원인 해결 | Bottlerocket 불변성으로 어려움 |
다른 AMI 사용 | 검증된 해결책 | 기존 인프라 변경 비용 큼 |
Node Auto-Repair 비활성화 | 즉시 적용 가능, 안전함 | 임시 해결책 |
최종 선택: Node Auto-Repair 비활성화
현재 상황에서는 Node Auto-Repair 기능을 비활성화하는 것이 최적의 해결책이라고 판단했습니다.
settings:
clusterName: "{{ .Environment.Name }}"
clusterEndpoint: "{{ .Values.clusterEndpoint }}"
interruptionQueue: "{{ .Values.karpenter.interruptionQueue }}"
featureGates:
spotToSpotConsolidation: true
nodeRepair: false # Karpenter 설정에서 nodeRepair 기능 비활성화
이 처럼 Karpenter Node Auto-Repair 기능을 활성화 한 후 GPU 인스턴스의 잦은 교체를 경험하신 분들은 Node Auto-Repair 기능을 비활성화해보시길 바랍니다.
추가 참고 자료
- AWS Containers Roadmap #2555 - EKS Node Auto-Repair 관련 이슈
- Bottlerocket OS #4075 - Bottlerocket 메모리 관리 이슈
- Karpenter Node Auto-Repair 공식 문서