[EKS] EKS GPU 트러블슈팅: 3. 장애 재현 - 3. 분산학습과 NCCL 통신 - 2. SG 차단 네트워크 장애 재현
정영준님의 AWS EKS Workshop Study(AEWS) 5주차 학습 내용을 기반으로 합니다.
TL;DR
이전 글에서 정리한 실험 설계(Pod 2개 + headless Service, torchrun 기반 2-node all_reduce)를 바탕으로, EKS node SG의 ephemeral self-ref(1025-65535/tcp) 제거로 분산학습 네트워크 장애를 재현한다.
- 장애 재현: Terraform 토글 2개 플립 → SG rule 8개 제거(ephemeral self-ref + webhook 5개 + egress + aux VPC allow) → 2-node torchrun이 120초 후 실패
- 핵심 발견: 실패 지점이 예상(NCCL)과 다르다. NCCL이 아니라 PyTorch c10d TCPStore에서 rendezvous timeout이 먼저 발생.
NCCL_DEBUG=INFO로그가 한 줄도 출력되지 않는다 - 진단 공식:
NCCL_DEBUG=INFO로그가 비어 있고 Python traceback에socket.cpp/TCPStore/DistNetworkError가 보이면 → NCCL이 아니라 c10d 층. SG/방화벽/MASTER_PORT접근성을 먼저 확인 - 복구: Terraform SG 원복(8 rule 재생성, 4초) → all_reduce 재성공(21초)
- 운영 함정: “NCCL 이슈”라고 불리는 것의 상당수는 NCCL 바깥이다. EKS managed node SG의 recommended rule 덩어리는 한 묶음으로 움직인다
전제 환경
이전 글에서 설계한 실험 환경을 그대로 이어받는다.
| 항목 | 값 |
|---|---|
| 클러스터 | myeks5w, K8s 1.35 |
| GPU 노드 | g5.xlarge × 2, AZ ap-northeast-2a, private subnet |
| GPU Operator | v26.3.1, ClusterPolicy status.state: ready |
| Device Plugin DS | 2/2/2 Ready |
| 컨테이너 이미지 | nvcr.io/nvidia/pytorch:24.10-py3 (NCCL 2.22.3+cuda12.6 내장) |
| NCCL transport | NET/Socket (IB 없음, NCCL_IB_DISABLE=1) |
| 실험 구조 | Pod 2개 + headless Service, torchrun 기반 2-node all_reduce |
Baseline 스냅샷
장애 주입 전, SG가 온전한 상태에서 2-node all_reduce가 정상 동작함을 먼저 증명한다. 이것이 있어야 “차단 후 실패”를 SG 때문이라고 귀속할 수 있다.
SG rule 상태
| SG | ingress 수 | egress 수 | 핵심 rule |
|---|---|---|---|
| node SG | 10 | 1 (0.0.0.0/0 all) |
self-ref ephemeral 1025-65535/tcp, cluster→node webhook 5개(4443/6443/8443/9443/10251), kubelet 10250, DNS 53/tcp+udp self, cluster→node 443 |
| aux SG | 1 (192.168.0.0/16 all-traffic) |
1 (0.0.0.0/0 all) |
VPC 대역 전체 허용 |
2-node all_reduce 성공
정상 상태에서 torchrun 기반 2-node all_reduce를 실행한 결과, 핵심 로그를 발췌하면 다음과 같다.
NCCL INFO Bootstrap : Using eth0:192.168.xx.xx<0>
NCCL INFO NET/Socket : Using [0]eth0:192.168.xx.xx<0>
NCCL INFO Using network Socket
NCCL INFO ncclCommInitRank ... - Init COMPLETE
NCCL INFO Connected all rings
[rank 0] iter 0 sum-first=3.0
[rank 0] iter 1 sum-first=6.0
[rank 0] iter 2 sum-first=12.0
[rank 0] iter 3 sum-first=24.0
[rank 0] iter 4 sum-first=48.0
- NCCL 2.22.3+cuda12.6, NET/Socket transport(IB 없음)
Init COMPLETE→Connected all rings→ rank 0/1 양쪽 sum 일치(3→6→12→24→48)- Pod 상태: 양쪽
Completed
Master(rank 0) 전체 NCCL 로그
nccl-master-0:1:1 [0] NCCL INFO NCCL_SOCKET_IFNAME set by environment to eth0
nccl-master-0:1:1 [0] NCCL INFO Bootstrap : Using eth0:192.168.xx.xx<0>
nccl-master-0:1:1 [0] NCCL INFO cudaDriverVersion 13000
nccl-master-0:1:1 [0] NCCL INFO NCCL version 2.22.3+cuda12.6
nccl-master-0:1:84 [0] NCCL INFO Plugin Path : /opt/hpcx/nccl_rdma_sharp_plugin/lib/libnccl-net.so
nccl-master-0:1:84 [0] NCCL INFO P2P plugin v8 IBext_v8
nccl-master-0:1:84 [0] NCCL INFO NET/IB : No device found. # ← IB 디바이스 탐색 실패 (우선순위 2 탈락)
nccl-master-0:1:84 [0] NCCL INFO NCCL_IB_DISABLE set by environment to 1. # ← 환경변수로 IB 명시적 비활성화
nccl-master-0:1:84 [0] NCCL INFO NET/Socket : Using [0]eth0:192.168.xx.xx<0> # ← TCP Socket으로 fallback (우선순위 3)
nccl-master-0:1:84 [0] NCCL INFO Using network Socket # ← 최종 transport 확정: TCP Socket
nccl-master-0:1:84 [0] NCCL INFO ncclCommInitRank comm 0x56223d711810 rank 0 nranks 2 cudaDev 0 nvmlDev 0 busId 1e0 commId 0xef1a7a6ce2d1f588 - Init START
nccl-master-0:1:84 [0] NCCL INFO comm 0x56223d711810 rank 0 nRanks 2 nNodes 2 localRanks 1 localRank 0 MNNVL 0
nccl-master-0:1:84 [0] NCCL INFO Channel 00/02 : 0 1
nccl-master-0:1:84 [0] NCCL INFO Channel 01/02 : 0 1
nccl-master-0:1:84 [0] NCCL INFO Trees [0] 1/-1/-1->0->-1 [1] -1/-1/-1->0->1
nccl-master-0:1:84 [0] NCCL INFO ncclCommInitRank comm 0x56223d711810 rank 0 nranks 2 cudaDev 0 nvmlDev 0 busId 1e0 commId 0xef1a7a6ce2d1f588 - Init COMPLETE
nccl-master-0:1:87 [0] NCCL INFO Channel 00/0 : 1[0] -> 0[0] [receive] via NET/Socket/0
nccl-master-0:1:87 [0] NCCL INFO Channel 01/0 : 1[0] -> 0[0] [receive] via NET/Socket/0
nccl-master-0:1:87 [0] NCCL INFO Channel 00/0 : 0[0] -> 1[0] [send] via NET/Socket/0
nccl-master-0:1:87 [0] NCCL INFO Channel 01/0 : 0[0] -> 1[0] [send] via NET/Socket/0
nccl-master-0:1:87 [0] NCCL INFO Connected all rings
NCCL transport 선택 과정을 정리하면 다음과 같다. NCCL은 우선순위가 높은 transport부터 시도하여, 사용 가능한 첫 번째 경로를 선택한다.
| 우선순위 | transport | 이 실험에서 | 로그 |
|---|---|---|---|
| 1 | NVLink | 해당 없음 (노드 간 통신, 같은 서버 내 GPU 간만 가능) | - |
| 2 | GPUDirect RDMA / IB | g5.xlarge에 EFA/IB 없음 → No device found + IB_DISABLE=1 |
line 7-8 |
| 3 | TCP Socket | 선택됨 → NET/Socket : Using [0]eth0 |
line 9-10 |
g5.xlarge에는 NVLink도 EFA도 없으므로 TCP Socket이 유일한 노드 간 NCCL 통신 경로다. 이 TCP Socket 통신이 node SG의 ephemeral self-ref(1025-65535/tcp) rule을 통해 허용된다. 즉, 이 rule이 제거되면 NCCL의 유일한 통신 경로가 막힌다.
장애 주입
Terraform SG rule 차단
Terraform 변수 2개를 false로 플립하여 SG rule 8개를 제거한다.
terraform plan \
-var gpu_desired_size=2 \
-var enable_aux_sg_vpc_allow=false \
-var node_sg_enable_recommended_rules=false \
-out=tfplan-c3-block
# Plan: 0 to add, 0 to change, 8 to destroy
제거되는 SG rule 8개
| # | rule | 의미 | 실험상 역할 |
|---|---|---|---|
| 1 | aux SG VPC 192.168.0.0/16 all-traffic ingress |
노드 간 숨은 허용 경로 | 분산학습 통신 차단 (핵심) |
| 2 | node SG self-ref 1025-65535/tcp ingress |
노드 간 ephemeral 포트 허용 | 분산학습 통신 차단 (핵심) |
| 3 | node SG egress 0.0.0.0/0 |
노드 outbound all | 부수 효과 (aux_egress_all이 보완) |
| 4 | cluster→node 4443/tcp | webhook | 부수 효과 |
| 5 | cluster→node 6443/tcp | webhook | 부수 효과 |
| 6 | cluster→node 8443/tcp | Karpenter webhook | 부수 효과 |
| 7 | cluster→node 9443/tcp | aws-lb-controller, cert-manager webhook | 부수 효과 |
| 8 | cluster→node 10251/tcp | webhook | 부수 효과 |
1번과 2번이 분산학습 통신의 실제 차단 대상이다. 3번(egress)은 aux_egress_all이 커버하고, 4~8번은 webhook 부수 효과다.
“1번과 2번만 골라서 제거하면 되지 않나?”라고 생각할 수 있지만, EKS Terraform 모듈의 node_security_group_enable_recommended_rules는 단일 boolean이다. 이 토글 하나로 ephemeral self-ref, webhook 5개, egress가 한 묶음으로 켜지거나 꺼진다. 개별 rule만 골라 빼는 옵션이 모듈에 없다. aws ec2 revoke-security-group-ingress CLI로 수동 제거할 수는 있지만, 실무에서 이 장애가 발생하는 경로는 “누군가 PR에서 토글을 false로 바꾸거나, 모듈 업그레이드 시 기본값이 바뀌면서 8개가 한꺼번에 내려가는 것”이다. 이 실험은 그 실무 사고 경로를 그대로 재현한다.
차단 후 SG 상태
| SG | baseline ingress | 차단 후 ingress | 남는 rule |
|---|---|---|---|
| node SG | 10 | 4 | kubelet 10250, DNS 53/tcp+udp self, cluster→node 443 |
| aux SG | 1 | 0 | (egress 0.0.0.0/0만 유지) |
디버깅 경로
장애 원인을 모르는 상태에서 분산학습 실패를 만났다고 가정하고, 단계별로 원인을 좁혀간다.
1단계: Pod 상태 확인
차단 상태에서 2-node all_reduce를 실행하면, 약 2분 23초 후 양쪽 Pod가 Error로 전이한다.
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE NODE
nccl-master-0 0/1 Error 0 2m23s ip-192-168-aa-aa...
nccl-worker-0 0/1 Error 0 2m23s ip-192-168-bb-bb...
podAntiAffinity에 의해 두 Pod가 서로 다른 GPU 노드에 분산되었다. 120초 PG timeout + container teardown 시간을 합치면 약 2분 23초다.
2단계: kubectl logs — 에러 메시지 확인
Master (rank 0)
[init] host=nccl-master-0 rank=0 ws=2 master=nccl-master:29500
Traceback (most recent call last):
File "/tmp/allreduce.py", line 4, in <module>
dist.init_process_group(backend="nccl", timeout=datetime.timedelta(seconds=120))
...
torch.distributed.DistStoreError: Timed out after 121 seconds waiting for clients. 1/2 clients joined.
rank 0은 MASTER_PORT(29500)에서 TCP listen을 열고 나머지 rank가 접속하기를 기다렸지만, 121초가 지나도 1/2만 참가(자기 자신)했다.
Worker (rank 1)
[init] host=nccl-worker-0 rank=1 ws=2 master=nccl-master:29500
[E419 13:27:42.301344821 socket.cpp:1011] [c10d] The client socket has timed out after 120000ms
while trying to connect to (nccl-master, 29500).
torch.distributed.DistNetworkError: The client socket has timed out after 120000ms
while trying to connect to (nccl-master, 29500).
rank 1은 MASTER_ADDR:MASTER_PORT(nccl-master:29500)에 TCP connect를 시도했지만, 120초 후 timeout이 발생했다.
Worker(rank 1) 전체 traceback
[E419 13:27:42.301344821 socket.cpp:1011] [c10d] The client socket has timed out after 120000ms while trying to connect to (nccl-master, 29500).
[E419 13:27:42.317323202 TCPStore.cpp:390] [c10d] TCP client failed to connect/validate to host nccl-master:29500 - timed out (try=0, timeout=120000ms): The client socket has timed out after 120000ms while trying to connect to (nccl-master, 29500).
Exception raised from throwTimeoutError at /opt/pytorch/pytorch/torch/csrc/distributed/c10d/socket.cpp:1013 (most recent call first):
frame #0: c10::Error::Error(...) in libc10.so
frame #7: c10d::TCPStore::TCPStore(...) in libtorch_cpu.so
...
Traceback (most recent call last):
File "/tmp/allreduce.py", line 4, in <module>
dist.init_process_group(backend="nccl", timeout=datetime.timedelta(seconds=120))
...
torch.distributed.DistNetworkError: The client socket has timed out after 120000ms while trying to connect to (nccl-master, 29500).
NCCL INFO 라인: 0줄
양쪽 로그 모두 NCCL 관련 출력이 한 줄도 없다. baseline 성공 시와 동일한 Pod 매니페스트, 동일한 NCCL_DEBUG=INFO 환경변수를 사용했는데도, baseline에서 출력되었던 NCCL INFO Bootstrap, NCCL INFO NET/Socket, Init START 등의 라인이 전혀 나타나지 않는다.
같은 설정에서 한쪽은 NCCL 로그가 나오고 한쪽은 0줄이므로, 로그 레벨 설정 문제가 아니다. NCCL_DEBUG=INFO는 NCCL 코드가 한 줄이라도 실행되면 bootstrap·transport 선택·communicator init 로그를 반드시 출력하는 레벨이다. 0줄이라는 것은 NCCL 코드 경로에 아예 진입하지 못했다는 뜻이다.
3단계: 층 분석 — 왜 NCCL이 아닌가
이전 글에서 정리한 초기화 시퀀스를 다시 보자.
1. c10d TCPStore rendezvous ← 여기서 실패 (120s timeout)
rank 0: MASTER_PORT에 TCP listen
rank 1: MASTER_ADDR:MASTER_PORT로 TCP connect
→ ephemeral 범위(1025-65535) 차단으로 connect 불가
2. NCCL bootstrap ← 도달하지 못함
3. NCCL communicator 구성 ← 도달하지 못함
init_process_group(backend="nccl")이 내부적으로 c10d TCPStore를 먼저 생성한다. TCPStore는 MASTER_ADDR:MASTER_PORT(nccl-master:29500)에 대한 TCP 연결이다. 29500은 ephemeral 범위(1025-65535)에 속하므로, self-ref rule이 제거된 상태에서는 이 TCP connect가 차단된다.
c10d TCPStore 단계에서 먼저 죽기 때문에, NCCL bootstrap 단계에 도달하지 못한다. 그래서 NCCL_DEBUG=INFO 로그가 한 줄도 출력되지 않는 것이다.
4단계: SG rule 확인
차단 후 SG 상태를 조회하면, baseline 대비 정확히 8개 rule이 제거되었음을 확인할 수 있다.
aws ec2 describe-security-groups --group-ids <NODE_SG_ID> <AUX_SG_ID> \
--region ap-northeast-2 --output json
- node SG: 10 → 4 ingress (ephemeral self-ref + webhook 5개 + egress 제거 확인)
- aux SG: 1 → 0 ingress (VPC all-traffic 제거 확인)
부수 효과 관찰
SG 차단 3분 48초 구간 동안 webhook/controller 상태를 관찰했다.
| 컴포넌트 | 상태 | 비고 |
|---|---|---|
| cert-manager | Running 유지, 재시작 0회 | 차단 구간에 신규 Certificate 생성을 하면 9443 차단으로 실패했을 것 (미검증) |
| gpu-operator | Running 유지, 재시작 0회 | validator Job은 이미 Completed 상태라 webhook 재호출 없음 |
ClusterPolicy status.state |
ready 유지 |
operator reconcile은 노드 내부 경로라 SG 영향 없음 |
| aws-lb-controller | N/A | 본 클러스터에 미설치 |
차단 구간이 짧고(3분 48초), 이 동안 신규 admission 호출이 없었기 때문에 부수 효과가 표면화되지 않았다. 운영 환경에서 이 시간이 길어지면 webhook 장애로 이어질 수 있다.
예상 vs 실측
| 항목 | 예상 | 실측 | 일치 |
|---|---|---|---|
| terraform plan | 8 destroy | 8 destroy (정확 일치) | O |
| 차단 후 node SG ingress | recommended 외 rule만 남음 | 4개 (kubelet/DNS/443) | O |
| torchrun 실패 지점 | NCCL bootstrap 후 peer TCP connect hang | c10d TCPStore rendezvous timeout (NCCL 이전) | X (층이 다름) |
NCCL_DEBUG=INFO 출력 |
Bootstrap, NET/Socket 등 | 0줄 (NCCL 초기화 미도달) | X |
| Pod 상태 | Error | Error (2m23s) | O |
| webhook 영향 | 일시적 admission 실패 가능 | Running 유지 (호출 미발생) | △ |
핵심 불일치는 실패 층이다. NCCL이 아니라 그 앞의 c10d TCPStore에서 먼저 막혔다. 이 불일치 자체가 “NCCL 이슈인지 아닌지 판정하는 기준”을 제시하는 가장 강력한 실측 근거가 된다.
해결/복구
Terraform SG 원복
# SG rule 8개 재생성 (기본값 true/true 복귀)
terraform plan -var gpu_desired_size=2 -out=tfplan-c3-restore
# Plan: 8 to add, 0 to change, 0 to destroy
terraform apply "tfplan-c3-restore"
# Apply complete! Resources: 8 added (4초 내)
all_reduce 재성공
SG 원복 후 동일 매니페스트로 all_reduce를 재실행하면, 21초 내 양쪽 Completed로 전이한다.
NCCL INFO Bootstrap : Using eth0:192.168.xx.xx<0>
NCCL INFO NET/Socket : Using [0]eth0:192.168.xx.xx<0>
NCCL INFO ncclCommInitRank ... - Init COMPLETE
NCCL INFO Connected all rings
[rank 0] iter 0 sum-first=3.0
[rank 0] iter 1 sum-first=6.0
[rank 0] iter 2 sum-first=12.0
[rank 0] iter 3 sum-first=24.0
[rank 0] iter 4 sum-first=48.0
Baseline과 100% 동일한 출력이다.
복구 검증
| 항목 | baseline | 복구 후 | 일치 |
|---|---|---|---|
| node SG ingress 수 | 10 | 10 | O |
| aux SG ingress VPC allow | 존재 | 존재 | O |
| sum-first (iter 0~4) | 3.0/6.0/12.0/24.0/48.0 | 동일 | O |
NCCL Init COMPLETE + Connected all rings |
O | O | O |
| Pod phase | Completed | Completed (21s, 이미지 캐시) | O |
차단→복구 사이클 닫힘 확인: baseline 성공 → 차단 후 실패 → 복구 apply → 동일 매니페스트 재실행으로 baseline과 동일 값 재현.
정리
진단 공식
NCCL_DEBUG=INFO로그가 stdout에 아예 없고, Python traceback이torch.distributed.socket.cpp/TCPStore/DistNetworkError키워드를 포함하면 → NCCL이 아니라 PyTorch c10d 층의 rendezvous 단계(MASTER_ADDR:MASTER_PORT TCP connect)가 원인이다. SG/방화벽/MASTER_PORT접근성을 먼저 확인한다.
이 공식은 NCCL communicator lazy init 디버깅(관련 글)과 함께 읽으면, “NCCL 로그가 비어 있을 때 어디를 봐야 하는가”에 대한 판단 근거가 된다.
디버깅 경로 비교
| 03-01: Device Plugin 비활성화 | 03-02: vLLM 기동 실패 | 03-03-02: SG 차단 (이번 글) | |
|---|---|---|---|
| 장애 층 | 인프라 층 (GPU Operator) | 앱 층 (vLLM) | 네트워크 층 (SG/c10d) |
| Pod STATUS | Pending | CrashLoopBackOff | Error |
| 핵심 도구 | kubectl get/describe |
kubectl logs --previous |
kubectl logs + aws ec2 describe-security-groups |
| GPU 인프라 | 비정상 (Allocatable 0) | 정상 | 정상 |
| NCCL 관여 | 없음 | 없음 | 예상했으나 c10d에서 먼저 실패 |
같은 “GPU 워크로드가 안 된다”는 증상이지만, Pod STATUS와 에러 메시지의 층위가 디버깅 경로의 분기점이 된다.
운영 함정 2가지
1. “NCCL 이슈”라고 불리는 것의 상당수는 NCCL 바깥이다
NCCL_DEBUG=INFO를 켰는데도 NCCL INFO 라인이 한 줄도 없다면, 원인은 그 아래 계층이다: PyTorch c10d TCPStore, Kubernetes Service DNS, 방화벽/SG, kube-proxy. NCCL 자체를 의심하기 전에 네트워크 연결성부터 확인해야 한다.
2. EKS managed node SG의 “recommended rule 덩어리”는 한 묶음으로 움직인다
node_security_group_enable_recommended_rules=false 토글 하나가 self-ref ephemeral(분산학습)과 webhook(4443/6443/8443/9443/10251)을 동시에 내린다. 분산학습만 끊기는 것이 아니라 admission webhook도 영향을 받을 수 있다. IaC 가드레일로 이 토글을 잠그는 것이 재발 방지의 핵심이다.
요약
| 항목 | 장애 상태 | 복구 상태 |
|---|---|---|
| node SG ephemeral self-ref | 제거됨 | 존재 (1025-65535/tcp) |
| aux SG VPC allow | 제거됨 | 존재 (192.168.0.0/16) |
| c10d TCPStore rendezvous | timeout 120s | 성공 |
NCCL Init COMPLETE |
미도달 (NCCL 로그 0줄) | Init COMPLETE |
| all_reduce sum | 실패 | 3.0/6.0/12.0/24.0/48.0 |
| Pod phase | Error (2m23s) | Completed (21s) |
| 차단→복구 총 소요 | — | SG apply 4초 + all_reduce 21초 |
다음 단계
이번 글까지 인프라 층(Device Plugin), 앱 층(vLLM), 네트워크 층(SG 차단) 장애를 각각 재현했다. 다음 글에서는 EKS 관리형 환경에서 재현할 수 없는 3개 주제(CUDA XID, Auto Mode, EFA)를 사례 탐구로 다룬다.
댓글남기기