[챌린지 #1] 게임 서버 K8s 배포 - Part 5: 나머지 서비스
🎯 핵심 개념
로비 서비스를 띄웠으니 이제 나머지 서비스들을 배포할 차례다. 하지만 모든 서비스를 똑같이 배포하면 안 된다. 각 서비스의 특성이 다르기 때문이다.
- 게임 룸: CPU를 많이 쓴다 (게임 로직 계산)
- 채팅: 메모리를 많이 쓴다 (메시지 버퍼)
- 랭킹: 단일 인스턴스면 충분하다
식당으로 비유하면, 주방장(게임 룸)은 화력 좋은 곳에, 바텐더(채팅)는 냉장고 큰 곳에, 계산대(랭킹)는 하나만 두는 것과 같다.
💡 왜 워크로드를 분리하나
k3d로 클러스터를 만들 때 워커 노드 2개에 라벨을 달아뒀었다.
kubectl label nodes k3d-k3s-local-agent-0 workload=compute
kubectl label nodes k3d-k3s-local-agent-1 workload=backend
이제 nodeSelector를 써서 서비스를 적절한 노드에 배치할 수 있다.
spec:
nodeSelector:
workload: compute # compute 라벨 가진 노드에만 배치
실무에서는 이렇게 노드를 나눈다:
- CPU 집약적 워크로드 → 고성능 CPU 노드
- 메모리 집약적 워크로드 → 고용량 메모리 노드
- GPU 필요한 워크로드 → GPU 노드
비용 최적화를 위해 워크로드 특성에 맞는 인스턴스 타입을 쓰는 거다.
📌 게임 룸 서비스 (CPU 집약적)
게임 로직을 계산하는 서비스다. CPU를 많이 쓴다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: game-room
namespace: game-prod
labels:
app: game-room
tier: backend
spec:
replicas: 2
selector:
matchLabels:
app: game-room
template:
metadata:
labels:
app: game-room
tier: backend
spec:
# CPU 집약적이므로 compute 노드에 배치
nodeSelector:
workload: compute
containers:
- name: room-server
image: nginx:alpine
ports:
- containerPort: 80
name: http
envFrom:
- configMapRef:
name: game-common-config
- configMapRef:
name: gameroom-config
resources:
requests:
memory: "256Mi"
cpu: "500m" # CPU 많이 할당
limits:
memory: "512Mi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: game-room
namespace: game-prod
labels:
app: game-room
spec:
type: ClusterIP
selector:
app: game-room
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
CPU requests/limits가 로비보다 5배 크다. 게임 로직 계산에 필요한 만큼 할당했다.
📌 채팅 서비스 (메모리 집약적)
실시간 메시지를 처리하는 서비스다. 메모리를 많이 쓴다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: game-chat
namespace: game-prod
labels:
app: game-chat
tier: backend
spec:
replicas: 2
selector:
matchLabels:
app: game-chat
template:
metadata:
labels:
app: game-chat
tier: backend
spec:
# 일반 backend 노드에 배치
nodeSelector:
workload: backend
containers:
- name: chat-server
image: nginx:alpine
ports:
- containerPort: 80
name: http
envFrom:
- configMapRef:
name: game-common-config
- configMapRef:
name: chat-config
resources:
requests:
memory: "512Mi" # 메모리 많이 할당
cpu: "100m"
limits:
memory: "1024Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: game-chat
namespace: game-prod
labels:
app: game-chat
spec:
type: ClusterIP
selector:
app: game-chat
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
메모리 requests/limits가 크다. 메시지 버퍼를 메모리에 올려두기 때문이다.
📌 랭킹 서비스 (단일 인스턴스)
랭킹 데이터를 관리하는 서비스다. 일관성을 위해 하나만 띄운다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: game-ranking
namespace: game-prod
labels:
app: game-ranking
tier: backend
spec:
replicas: 1 # 단일 인스턴스
selector:
matchLabels:
app: game-ranking
template:
metadata:
labels:
app: game-ranking
tier: backend
spec:
nodeSelector:
workload: backend
containers:
- name: ranking-server
image: nginx:alpine
ports:
- containerPort: 80
name: http
envFrom:
- configMapRef:
name: game-common-config
- configMapRef:
name: ranking-config
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: game-ranking
namespace: game-prod
labels:
app: game-ranking
spec:
type: ClusterIP
selector:
app: game-ranking
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
replicas가 1이다. 실무에서는 DB를 쓰겠지만, 이번 챌린지에서는 단순하게 갔다.
📌 배포 및 확인
# ConfigMap 먼저 생성 (각 서비스별)
kubectl apply -f 02-configmap.yaml
# 모든 서비스 배포
kubectl apply -f 05-gameroom-deployment.yaml
kubectl apply -f 06-chat-deployment.yaml
kubectl apply -f 07-ranking-deployment.yaml
# 전체 Pod 확인
kubectl get pods -n game-prod -o wide
# 노드별 Pod 배치 확인
kubectl get pods -n game-prod -o wide | grep compute
kubectl get pods -n game-prod -o wide | grep backend
주목할 점:
- game-room 3개가 전부 agent-0에 배치됨 → nodeSelector: compute 작동 ✅
- game-chat 2개가 전부 agent-1에 배치됨 → nodeSelector: backend 작동 ✅
nodeSelector로 의도한 대로 워크로드를 분리했다.
⚠️ 주의사항
nodeSelector로 인한 Pending
노드 라벨이 없으면 Pod가 Pending 상태로 남는다.
$ kubectl get pods -n game-prod
NAME READY STATUS RESTARTS AGE
game-room-xxx 0/1 Pending 0 1m
$ kubectl describe pod game-room-xxx -n game-prod
Events:
Warning FailedScheduling pod didn't match node selector
이럴 때는 노드 라벨을 확인한다.
# 라벨 확인
kubectl get nodes --show-labels
# 라벨 추가
kubectl label nodes k3d-k3s-local-agent-0 workload=compute
리소스 부족
노드 리소스가 부족하면 Pod가 안 뜬다.
$ kubectl describe pod game-room-xxx -n game-prod
Events:
Warning FailedScheduling Insufficient cpu
이럴 때는 requests를 줄이거나, 노드를 추가해야 한다.
replicas=1의 위험성
랭킹 서비스는 replicas=1이라 Pod가 죽으면 서비스 전체가 중단된다. 실무에서는 이렇게 하면 안 되고, DB를 써서 상태를 분리하고 replicas를 2개 이상으로 가져간다.
정리
게임 룸, 채팅, 랭킹 서비스를 배포했다. nodeSelector로 워크로드 특성에 맞게 노드를 분리했고, 각 서비스에 적절한 리소스를 할당했다.
다음 글에서는 HPA로 부하에 따라 자동으로 Pod를 늘리고 줄이는 걸 해볼 예정이다.
💭 생각해볼 점
Q: nodeSelector 대신 더 유연한 방법은 없을까?
힌트: nodeAffinity를 쓰면 “선호하는 노드”를 지정할 수 있다. nodeSelector는 “반드시 이 라벨”이지만, nodeAffinity는 “가능하면 이 라벨, 안 되면 다른 곳”도 가능하다. 더 복잡하지만 유연하다.
🎯 추가 학습
- nodeAffinity와 podAffinity 차이
- Taint와 Toleration으로 노드 격리하기
- PriorityClass로 중요한 Pod 우선 배치