Jetson TroubleShooting 및 분산 처리 [完]

inuit·6일 전

All about 쿠버네티스

목록 보기
26/26
post-thumbnail

최근 업데이트일 2025-01-20

여러 대의 Jetson 디바이스를 쿠버네티스 클러스터로 묶었다면, 이제 이 자원들을 어떻게 효율적으로 활용할지 테스트해 볼 차례다. 이번 글에서는 Jetson 노드들을 활용한 GPU 분산 처리 테스트 방법과, 클러스터 구성 시 흔하게 마주칠 수 있는 트러블슈팅 사례들을 정리해 본다.

Jetson GPU 병렬 분산 처리 테스트

10대의 Jetson 노드에 각각 Pod를 띄워 동시에 GPU 추론 작업을 시뮬레이션하고 결과를 모으는 테스트를 진행해 보자.


기본 GPU 병렬 처리

  • 단순히 10개의 Pod를 10대의 노드에 분산시켜 GPU 작업을 실행하는 기본 Job 이다.
apiVersion: batch/v1
kind: Job
metadata:
  name: jetson-parallel-gpu-job
spec:
  parallelism: 10 # 동시에 실행할 Pod의 최대 개수 (Jetson 10대)
  completions: 10 # 총 완료되어야 하는 Pod의 개수
  backoffLimit: 4 # Pod 실패 시 재시도 횟수
  template:
    metadata:
      labels:
        app: gpu-job-pod
    spec:
      restartPolicy: OnFailure
      # GPU가 있는 노드에만 스케줄링되도록 강제
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: [nvidia.com/gpu](https://nvidia.com/gpu)
                operator: Exists
      containers:
      - name: cuda-test-container
        # Jetson Orin(L4T) 환경에 맞는 CUDA 이미지 사용
        image: "nvcr.io/nvidia/l4t-pytorch:r35.4.1-pth2.1-py3"
        command: [ "bash", "-c" ]
        args:
          - |
            echo "=========================================="
            echo "Starting GPU Test on node: $NODE_NAME"
            env | grep NVIDIA
            echo "---"
            python3 -c "import torch; print(f'PyTorch version: {torch.__version__}'); print(f'CUDA available: {torch.cuda.is_available()}'); print(f'Device count: {torch.cuda.device_count()}'); print(f'Current device: {torch.cuda.current_device()}'); print(f'Device name: {torch.cuda.get_device_name(0)}');"
            echo "---"
            echo "Simulating 30 seconds of GPU work..."
            sleep 30
            echo "Work complete. Exiting."
            echo "=========================================="
        resources:
          limits:
            [nvidia.com/gpu](https://nvidia.com/gpu): 1 # 각 Pod가 1개의 GPU 요청
        env:
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName

분산 워드 카운트

단순히 병렬로 띄우는 것을 넘어, 분산 학습 메커니즘을 흉내 낸 간단한 태스크를 수행해 보자.

  • 분산 워드 카운트(Distributed Word Count)는 하둡이나 스파크 같은 빅데이터 시스템의 "Hello, World!" 예제이며, 분산 학습의 Parameter Server 방식과 매우 유사한 Scatter-Gather(분산-취합) 패턴을 사용한다.
  • 테스트 시나리오: 10대의 Jetson이 각자 데이터를 처리한 뒤, 그 결과를 하나의 중앙 서버(Reducer)로 전송하여 최종 결과를 취합한다.
  • 이를 위해 결과 취합 서버(Deployment + Service)와 작업 워커(Job) 두 가지 리소스가 필요하다.
  1. 결과 취합 서버 (Reducer) 배포
    • 먼저 워커들로부터 결과를 수신할 중앙 서버를 띄운다. (간단한 Python Flask 앱 사용)
# 1. ClusterIP Service (내부 DNS 이름 'reducer-svc' 생성)
apiVersion: v1
kind: Service
metadata:
  name: reducer-svc
spec:
  selector:
    app: reducer
  ports:
  - protocol: TCP
    port: 80
    targetPort: 5000

---
# 2. Deployment (Flask 서버 본체)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reducer-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reducer
  template:
    metadata:
      labels:
        app: reducer
    spec:
      containers:
      - name: reducer
        image: "python:3.9-slim"
        command: ["/bin/sh", "-c"]
        args:
        - |
          pip install Flask
          export FLASK_APP=server.py
          cat <<EOF > server.py
          import threading
          from flask import Flask, request, jsonify

          app = Flask(__name__)
          total_counts = {}
          lock = threading.Lock()

          @app.route('/submit', methods=['POST'])
          def submit():
              data = request.json
              with lock:
                  for word, count in data.items():
                      total_counts[word] = total_counts.get(word, 0) + count
              return jsonify({"status": "received"}), 200

          @app.route('/results', methods=['GET'])
          def results():
              with lock:
                  return jsonify(total_counts)

          if __name__ == '__main__':
              app.run(host='0.0.0.0', port=5000)
          EOF
          flask run
  1. 작업 수행 워커 (Mapper) 배포
    • 이제 10대의 Jetson에서 실제 GPU 작업을 수행하고 위 서버(reducer-svc)로 결과를 전송할 Job을 만든다.
apiVersion: batch/v1
kind: Job
metadata:
  name: jetson-wordcount-job
spec:
  parallelism: 10
  completions: 10
  template:
    spec:
      restartPolicy: OnFailure
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: [nvidia.com/gpu](https://nvidia.com/gpu)
                operator: Exists
      containers:
      - name: mapper-container
        image: "nvcr.io/nvidia/l4t-pytorch:r35.4.1-pth2.1-py3"
        command: ["/bin/sh", "-c"]
        args:
        - |
          # 1. 모의 작업: GPU를 사용한 간단한 CUDA 연산
          echo "Node $NODE_NAME: Running mock GPU task..."
          python3 -c "import torch; a=torch.rand(10000,10000).cuda(); b=torch.rand(10000,10000).cuda(); c=a*b; print('GPU task done.')"
          
          # 2. 모의 결과 생성: 각 Pod가 'hello'와 'jetson'을 랜덤하게 카운트
          HELLO_COUNT=$((RANDOM % 100))
          JETSON_COUNT=$((RANDOM % 100))
          NODE_RESULT="{\"hello\": $HELLO_COUNT, \"jetson\": $JETSON_COUNT}"
          echo "Node $NODE_NAME: My result is $NODE_RESULT"

          # 3. K8s Service로 결과 전송
          pip install requests
          python3 -c "import requests; requests.post('http://reducer-svc/submit', json=$NODE_RESULT)"
          echo "Node $NODE_NAME: Submit complete. Job finished."
        resources:
          limits:
            [nvidia.com/gpu](https://nvidia.com/gpu): 1
        env:
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
  1. 실행 및 결과 확인

    • 터미널을 여러 개 띄워 흐름을 모니터링해 보자.
    1. [터미널 1] Reducer 배포: kubectl apply -f reducer-deployment.yamlkubectl logs -f deployment/reducer-deployment
    2. [터미널 2] Mapper 배포: kubectl apply -f mapper-job.yamlkubectl get pods -w
    3. [터미널 1] 로그 확인: 10개의 워커 Pod로부터 /submit POST 요청이 들어오는 것을 확인
    4. [터미널 3] 결과 확인: 포트 포워딩을 통해 최종 결과를 로컬에서 확인
    kubectl port-forward svc/reducer-svc 8080:80
    # 브라우저 또는 curl로 http://localhost:8080/results 접속
    • 결과 : 10개 Pod의 랜덤 카운트가 모두 합산된 결과를 볼 수 있다.
    {
      "hello": 530,  
      "jetson": 488 
    }

Jetson K8s 클러스터 구축 트러블슈팅

Jetson에서 쿠버네티스 클러스터를 세팅하기까지 수많은 에러를 마주할 수 있다. 구축 과정에서 발생한 주요 트러블슈팅 사례를 정리했다.

kubectl Config 에러

  • ※ The connection to the server localhost:8080 was refused - did you specify the right host or port
  • kubeadm init 직후, 혹은 워커 노드에서 kubectl 명령어를 쳤을 때 발생하는 권한/설정 파일 부재 에러다.
  • 해결 방법
    • 마스터 노드에서 쿠버네티스 관리자 설정 파일을 유저 디렉토리로 복사해야 한다. 워커 노드에서 사용하고 싶다면 해당 파일을 scp로 넘겨주면 된다.
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config 

# 워커 노드에서 kubectl을 사용하고 싶다면 마스터 노드에서 아래 명령어 실행
scp /etc/kubernetes/admin.conf <USER>@<WORKER_NODE_IP>:~/.kube/config

API Server 연결 에러 (Swap 메모리 이슈)

  • ※ The connection to the server 192.168.0.42:6443 was refused - did you specify the right host or port
  • 해결 방법
    • 쿠버네티스는 안정적인 성능 보장을 위해 Swap 메모리(RAM 용량이 부족할 때 하드 디스크 일부를 메모리처럼 활용하는 기능)가 비활성화되어 있어야 한다.
sudo -i
swapoff -a
sed -i '/ swap / s/^/#/' /etc/fstab # 재부팅 시 스왑 활성화 방지
  • ※ Jetson은 위처럼 fstab을 수정해도 재부팅 할 때마다 자동으로 swap이 다시 켜지는 경우가 많다. 따라서 부팅 시마다 swapoff -a를 실행하도록 서비스에 등록하거나 수동으로 꺼주어야 한다.

Calico 초기화 오류 (tigera-operator)

  • kubeadm reset 후 다시 init을 진행했을 때 이전 인증서가 꼬이거나 tigera-operator 관련 Pod들이 무수히 많은 에러를 뿜는 경우가 있다.
  • 해결 방법
    • 에러가 발생한 tigera-operator 관련 Pod를 일괄 삭제하여 재시작을 유도한다.
kubectl get pods -A | grep "tigera-operator" | awk '{print $1 " " $2}' | while read namespace pod; do kubectl delete pod $pod -n $namespace; done

Flannel CNI CrashLoopBackOff 에러

Flannel CNI를 배포했는데 파드가 정상적으로 뜨지 않고 CrashLoopBackOff에 빠지는 경우, 로그를 보면 크게 두 가지 케이스가 있다.

  • Pod CIDR 미할당
    • ※ Error registering network: failed to acquire lease: node "pnudtn" pod cidr not assigned
    • 클러스터 초기화 시 Pod 네트워크 대역을 명시하지 않아서 발생한다.
# 초기화 시 --pod-network-cidr 플래그를 반드시 추가해야 함
kubeadm init --pod-network-cidr=10.244.0.0/16
  • API 서버 접근 권한 없음
    • ※ Failed to create SubnetManager: error retrieving pod spec for 'kube-system/kube-flannel-ds-sb3k4': the server does not allow access to the requested resource
    • Flannel이 API 서버와 통신할 위치를 찾지 못하는 이슈다.
    • kube-flannel.yml 매니페스트 파일을 열고, 컨테이너 환경 변수(env) 설정 부분에 KUBERNETES_SERVICE_HOSTPORT를 직접 명시해 준 뒤 다시 apply 한다.
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            # 아래 두 환경 변수 명시적 추가
            - name: KUBERNETES_SERVICE_HOST
              value: '<Master_Node_IP>' # 예: 192.168.0.42
            - name: KUBERNETES_SERVICE_PORT
              value: '6443'

여기까지 연구실에서 진행했던 테스크 오프로드를 위한 엣지 환경에서의 쿠버네티스 클러스터 구축에 관한 시리즈였다. 당시 중구난방했던 내용들을 다시 정리하며 돌아보니 비효율적인 방식을 취했던 게 눈에 보였고, 그럼에도 이러한 방식을 거쳤기에 얻은 것 또한 많다고도 생각한다. 쿠버네티스를 내가 다시 사용할 일이 있을지는 모르겠지만, 밑거름이 되길 바라본다.

profile
It’s always white night here.

0개의 댓글