
여러 대의 Jetson 디바이스를 쿠버네티스 클러스터로 묶었다면, 이제 이 자원들을 어떻게 효율적으로 활용할지 테스트해 볼 차례다. 이번 글에서는 Jetson 노드들을 활용한 GPU 분산 처리 테스트 방법과, 클러스터 구성 시 흔하게 마주칠 수 있는 트러블슈팅 사례들을 정리해 본다.
10대의 Jetson 노드에 각각 Pod를 띄워 동시에 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
단순히 병렬로 띄우는 것을 넘어, 분산 학습 메커니즘을 흉내 낸 간단한 태스크를 수행해 보자.
# 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
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
실행 및 결과 확인
kubectl apply -f reducer-deployment.yaml → kubectl logs -f deployment/reducer-deploymentkubectl apply -f mapper-job.yaml → kubectl get pods -w/submit POST 요청이 들어오는 것을 확인kubectl port-forward svc/reducer-svc 8080:80
# 브라우저 또는 curl로 http://localhost:8080/results 접속
{
"hello": 530,
"jetson": 488
}
Jetson에서 쿠버네티스 클러스터를 세팅하기까지 수많은 에러를 마주할 수 있다. 구축 과정에서 발생한 주요 트러블슈팅 사례를 정리했다.
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
sudo -i
swapoff -a
sed -i '/ swap / s/^/#/' /etc/fstab # 재부팅 시 스왑 활성화 방지
fstab을 수정해도 재부팅 할 때마다 자동으로 swap이 다시 켜지는 경우가 많다. 따라서 부팅 시마다 swapoff -a를 실행하도록 서비스에 등록하거나 수동으로 꺼주어야 한다.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에 빠지는 경우, 로그를 보면 크게 두 가지 케이스가 있다.
# 초기화 시 --pod-network-cidr 플래그를 반드시 추가해야 함
kubeadm init --pod-network-cidr=10.244.0.0/16
kube-flannel.yml 매니페스트 파일을 열고, 컨테이너 환경 변수(env) 설정 부분에 KUBERNETES_SERVICE_HOST와 PORT를 직접 명시해 준 뒤 다시 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'
여기까지 연구실에서 진행했던 테스크 오프로드를 위한 엣지 환경에서의 쿠버네티스 클러스터 구축에 관한 시리즈였다. 당시 중구난방했던 내용들을 다시 정리하며 돌아보니 비효율적인 방식을 취했던 게 눈에 보였고, 그럼에도 이러한 방식을 거쳤기에 얻은 것 또한 많다고도 생각한다. 쿠버네티스를 내가 다시 사용할 일이 있을지는 모르겠지만, 밑거름이 되길 바라본다.