Skip to main content

장애 대응은 '명령어 모음'이 아니라 순서의 문제다

운영 장애가 터지면 사람은 이상하게도 가장 익숙한 명령어부터 치게 된다. kubectl get pods, kubectl logs, kubectl describe pod. 문제는 명령어 자체가 아니라 순서다. 로그를 먼저 볼지, 이벤트를 먼저 볼지, 노드 상태를 먼저 볼지에 따라 원인 파악 시간이 5분이 될 수도 있고 50분이 될 수도 있다.

AI 워크로드에서는 이 차이가 더 커진다. 웹 API 서버 하나가 죽은 상황과, 70GB 모델을 로딩하던 vLLM 파드가 Pending 상태에 머무는 상황은 겉으로는 둘 다 "서비스가 안 뜬다"로 보인다. 하지만 실제 원인은 완전히 다를 수 있다. GPU가 노드에서 사라졌을 수도 있고, 네임스페이스 GPU 쿼터를 초과했을 수도 있고, PVC가 느려 모델 로딩이 끝나지 않았을 수도 있고, KV 캐시가 VRAM을 다 먹어 CUDA OOM이 났을 수도 있다.

그래서 이 장의 목표는 단순하다. 장애를 만났을 때 "어디부터 봐야 하지?"라는 시간을 줄이는 것이다. 여기서 다루는 플레이북은 다음 공통 구조를 따른다.

  1. 탐지: 어떤 알림 또는 증상으로 시작되었는가.

  2. 영향 범위 확인: 한 파드인가, 한 노드인가, 한 모델인가, 전체 클러스터인가.

  3. 임시 완화: 사용자를 살리기 위해 지금 당장 무엇을 줄이거나 우회할 것인가.

  4. 근본 원인 분석: 왜 발생했는가.

  5. 복구 검증: 정말 정상화되었는가.

  6. 사후 기록: 다음에는 더 빨리 잡기 위해 무엇을 남길 것인가.

이 구조는 전통적인 인시던트 대응 절차와 크게 다르지 않다. 다만 AI 쿠버네티스에서는 GPU, 모델 서버, 벡터 DB, 외부 LLM API, 에이전트 도구 호출 같은 새로운 층이 추가된다. 운영자는 이제 Pod만 보는 사람이 아니라, 토큰 지연 시간과 GPU 메모리, 외부 API 응답 시간까지 함께 읽는 사람이 되어야 한다.

장애 대응 사례와 운영 플레이북

첫 5분: 무조건 수집해야 하는 증거

장애 대응에서 가장 흔한 실수는 "고치기 전에 증거를 지워버리는 것"이다. 급한 마음에 파드를 재시작했는데, 그 순간 직전 로그와 이벤트가 사라진다. 노드를 드레인했는데, GPU 디바이스 플러그인이 왜 죽었는지 볼 단서가 날아간다. 자동화 에이전트가 리소스 limit을 올려 문제를 완화했지만, 어떤 값에서 어떤 값으로 바뀌었는지 기록이 없다.

따라서 먼저 해야 할 일은 수정이 아니라 수집이다.

# 1. 네임스페이스 전체 상태
kubectl get pods -n <namespace> -o wide
kubectl get events -n <namespace> --sort-by='.lastTimestamp' | tail -50

# 2. 문제 파드 상세 정보
kubectl describe pod <pod-name> -n <namespace>

# 3. 현재 로그와 직전 컨테이너 로그
kubectl logs <pod-name> -n <namespace> --tail=200 --timestamps
kubectl logs <pod-name> -n <namespace> --previous --tail=200 --timestamps

# 4. 노드 상태
kubectl describe node <node-name>
kubectl top node <node-name>
kubectl top pod -n <namespace> --containers

# 5. 최근 배포 이력
kubectl rollout history deploy/<deployment-name> -n <namespace>
kubectl get deploy <deployment-name> -n <namespace> -o yaml > incident-deploy.yaml

--previous는 특히 중요하다. CrashLoopBackOff 상태에서는 현재 컨테이너가 이미 죽고 새 컨테이너가 뜨는 중일 수 있다. 이때 일반 kubectl logs만 보면 아무것도 나오지 않거나, 재시작 후 초기 로그만 보인다. 직전에 죽은 컨테이너의 마지막 말은 --previous에 남아 있다.

AI 추론 서버라면 다음 정보도 함께 남긴다.

# vLLM 또는 SGLang 파드의 최근 로그
kubectl logs deploy/<llm-deployment> -n <namespace> --tail=300 --timestamps

# 서비스 엔드포인트 확인
kubectl get svc,endpoints,endpointslice -n <namespace>

# GPU 노드의 allocatable 확인
kubectl get nodes -o custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia.com/gpu

# GPU 관련 파드 확인
kubectl get pods -A | grep -E 'nvidia|gpu|dcgm|device-plugin'

이 단계에서 "아직 원인을 모른다"는 것은 실패가 아니다. 오히려 정상이다. 첫 5분의 목표는 원인 규명이 아니라, 나중에 원인을 규명할 수 있을 만큼의 흔적을 보존하는 것이다.

플레이북 1: GPU가 노드에서 사라졌다

가장 식은땀이 나는 장애부터 보자. 어제까지 잘 돌던 GPU 노드에서 갑자기 nvidia.com/gpu가 보이지 않는다. LLM 파드는 Pending에 빠지고, 스케줄러 이벤트에는 이런 메시지가 남는다.

0/6 nodes are available: 2 Insufficient nvidia.com/gpu, 4 node(s) didn't match Pod's node affinity/selector.

또는 노드 자체를 보면 GPU allocatable이 비어 있다.

kubectl get node <gpu-node> -o jsonpath='{.status.allocatable.nvidia.com/gpu}'

정상이라면 1, 4, 8 같은 숫자가 나와야 한다. 아무것도 나오지 않거나 0이면 쿠버네티스 스케줄러 입장에서는 그 노드에 GPU가 없는 것이다.

이 장애는 위에서 아래로 좁혀야 한다. 핵심 순서는 물리 GPU와 커널 드라이버 → NVIDIA Container Toolkit → device plugin → GPU Operator → kubelet과 Node Condition이다. NVIDIA Device Plugin은 GPU를 쿠버네티스의 스케줄 가능한 확장 리소스인 nvidia.com/gpu로 노출하는 역할을 하며, GPU Operator는 드라이버, 디바이스 플러그인, DCGM Exporter 같은 구성요소를 묶어 관리한다.1

1단계: 노드 안에서 GPU가 보이는가

먼저 쿠버네티스 바깥, 즉 노드 운영체제에서 GPU가 보이는지 확인한다.

ssh <gpu-node>
nvidia-smi
lsmod | grep nvidia
systemctl status nvidia-persistenced || true

nvidia-smi가 실패한다면 쿠버네티스 문제가 아닐 가능성이 높다. 드라이버가 커널과 맞지 않거나, 노드 재부팅 후 드라이버 모듈이 올라오지 않았거나, 클라우드 인스턴스에서 GPU 장치가 정상 연결되지 않은 상태일 수 있다.

이때 흔한 메시지는 다음과 같다.

NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver.

이 메시지가 나오면 바로 Deployment YAML을 고치면 안 된다. 앱 파드는 피해자일 뿐이다. 드라이버와 커널 모듈부터 복구해야 한다.

2단계: 컨테이너 런타임에서 GPU 접근이 가능한가

노드에서는 nvidia-smi가 되는데 파드에서는 안 된다면 NVIDIA Container Toolkit 쪽을 의심한다. 컨테이너 런타임이 GPU 디바이스와 라이브러리를 컨테이너 안으로 전달하지 못하는 상황이다.

# containerd 환경 예시
sudo crictl info | grep -i nvidia -A5
sudo journalctl -u containerd --since "30 min ago" | grep -i nvidia

GPU Operator를 쓰는 환경이라면 관련 파드 로그를 본다.

kubectl get pods -n gpu-operator
kubectl logs -n gpu-operator ds/nvidia-container-toolkit-daemonset --tail=100

런타임 설정이 꼬였을 때는 앱 파드를 재시작해도 해결되지 않는다. 컨테이너 런타임 설정을 복구한 뒤, 해당 노드의 GPU 워크로드를 안전하게 재스케줄링해야 한다.

3단계: NVIDIA device plugin이 GPU를 보고 있는가

쿠버네티스가 GPU를 스케줄링하려면 device plugin이 kubelet에 GPU 정보를 알려줘야 한다. device plugin 파드가 죽었거나, 노드와 플러그인 버전이 맞지 않거나, MIG 설정이 바뀌었는데 플러그인이 따라오지 못하면 allocatable이 0이 될 수 있다.

kubectl get pods -A -o wide | grep nvidia-device-plugin
kubectl logs -n kube-system ds/nvidia-device-plugin-daemonset --tail=200

# GPU Operator 설치 환경에서는 네임스페이스가 다를 수 있다.
kubectl get pods -n gpu-operator -o wide
kubectl logs -n gpu-operator -l app=nvidia-device-plugin-daemonset --tail=200

로그에서 다음 단서를 찾는다.

No devices found. Waiting indefinitely.
Incompatible strategy detected auto
Failed to initialize NVML

NVML은 NVIDIA Management Library의 약자로, GPU 상태를 조회하는 데 쓰인다. device plugin이 NVML 초기화에 실패하면 쿠버네티스는 GPU를 알 수 없다.

4단계: GPU Operator ClusterPolicy 확인

GPU Operator를 쓴다면 ClusterPolicy가 실제 원하는 상태를 선언한다. 드라이버 설치 여부, MIG 전략, DCGM Exporter, device plugin 설정이 여기에 묶인다.

kubectl get clusterpolicy -A
kubectl describe clusterpolicy <cluster-policy-name>

운영 중 자주 보는 사고는 "MIG 설정을 바꿨는데 기존 파드와 리소스 이름이 맞지 않는" 경우다. 예를 들어 기존 파드는 nvidia.com/gpu: 1을 요청하는데, 노드는 MIG 모드로 바뀌어 nvidia.com/mig-1g.10gb 같은 리소스만 노출할 수 있다. 이때 파드는 영원히 Pending 상태에 머문다.

kubectl describe node <gpu-node> | grep -A20 "Allocatable"

출력에서 실제 노출된 리소스 이름을 확인한 뒤, 워크로드가 요청하는 리소스 이름과 맞춰야 한다.

5단계: kubelet 로그와 Node Condition 확인

마지막으로 kubelet과 노드 상태를 본다.

kubectl describe node <gpu-node> | sed -n '/Conditions:/,/Addresses:/p'

ssh <gpu-node>
sudo journalctl -u kubelet --since "1 hour ago" | grep -Ei 'nvidia|device|plugin|allocatable|error'

MemoryPressure, DiskPressure, PIDPressure, NotReady가 함께 보이면 GPU 자체보다 노드 건강 문제가 먼저일 수 있다. 특히 모델 캐시가 노드 디스크를 가득 채우면 kubelet이 파드를 축출하거나 새 파드를 받지 못한다.

복구 후 CUDA 테스트 파드로 검증하기

복구가 끝났다고 해서 곧바로 LLM 서버를 올리면 안 된다. 모델 서버는 너무 무겁다. 먼저 아주 작은 CUDA 테스트 파드로 실제 디바이스 접근을 검증한다.

apiVersion: v1
kind: Pod
metadata:
  name: cuda-smoke-test
spec:
  restartPolicy: Never
  containers:
  - name: cuda
    image: nvidia/cuda:12.4.1-base-ubuntu22.04
    command: ["nvidia-smi"]
    resources:
      limits:
        nvidia.com/gpu: 1
kubectl apply -f cuda-smoke-test.yaml
kubectl logs cuda-smoke-test
kubectl delete pod cuda-smoke-test

검증 기준은 단순하다. 파드가 GPU 노드에 스케줄링되고, 컨테이너 안에서 nvidia-smi가 GPU를 보여주면 1차 복구는 성공이다. 그다음 vLLM이나 SGLang 같은 실제 모델 서버를 올린다.

플레이북 2: Pending Pod 결정 트리

Pending은 쿠버네티스에서 가장 정직하면서도 가장 답답한 상태다. 정직한 이유는 "아직 어디에도 배치되지 않았다"고 말해주기 때문이다. 답답한 이유는 원인이 너무 많기 때문이다.

Pending 대응의 시작점은 항상 kubectl describe pod의 Events다.

kubectl describe pod <pod-name> -n <namespace>

아래쪽 Events에 스케줄러가 남긴 메시지가 있다. 여기서부터 분기한다.

원인 A: 자원 부족

대표 메시지는 다음과 같다.

0/5 nodes are available: 3 Insufficient memory, 2 Insufficient nvidia.com/gpu.

이 경우 확인할 것은 세 가지다.

kubectl top nodes
kubectl describe nodes | grep -A8 "Allocated resources"
kubectl get pods -A -o wide --field-selector spec.nodeName=<node-name>

GPU 워크로드라면 nvidia.com/gpu가 실제로 남아 있는지 본다.

kubectl get nodes -o custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia.com/gpu
kubectl describe node <gpu-node> | grep -A20 "Allocated resources"

임시 완화는 선택지에 따라 달라진다. 중요하지 않은 배치 작업을 중단하거나, 낮은 우선순위 파드를 축출하거나, GPU 노드풀을 확장하거나, 모델 서버의 replica를 줄인다. 단, 추론 서비스라면 replica를 줄이는 순간 지연 시간이 더 악화될 수 있으므로 큐 길이와 P95/P99 지연을 함께 봐야 한다.

원인 B: nodeSelector 또는 node affinity 불일치

대표 메시지는 다음과 같다.

0/5 nodes are available: 5 node(s) didn't match Pod's node affinity/selector.

확인 명령은 간단하다.

kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.nodeSelector}'
kubectl get pod <pod-name> -n <namespace> -o yaml | yq '.spec.affinity'
kubectl get nodes --show-labels | grep -E 'gpu|a100|h100|l40|workload'

AI 클러스터에서는 GPU Feature Discovery가 붙인 라벨을 많이 사용한다. 예를 들어 특정 모델을 H100 노드에만 배치하려고 다음처럼 썼다고 하자.

nodeSelector:
  nvidia.com/gpu.product: "NVIDIA-H100-SXM5-80GB"

그런데 실제 라벨 값이 NVIDIA-H100-80GB-HBM3처럼 다르면 절대 스케줄링되지 않는다. 라벨은 사람이 기억하는 값이 아니라 클러스터가 실제로 가진 값을 기준으로 써야 한다.

원인 C: taint와 toleration 누락

GPU 노드는 보통 일반 워크로드가 올라오지 못하게 taint를 건다.

kubectl describe node <gpu-node> | grep -i taints

예를 들어 노드에 다음 taint가 있다면,

workload=llm-inference:NoSchedule

파드에는 대응하는 toleration이 있어야 한다.

tolerations:
- key: "workload"
  operator: "Equal"
  value: "llm-inference"
  effect: "NoSchedule"

이 실수는 의외로 자주 발생한다. 특히 Helm values를 복사해 새 모델을 배포할 때 nodeSelector는 바꿨지만 toleration을 빼먹는 경우가 많다.

원인 D: 네임스페이스 ResourceQuota 초과

팀별 GPU 사용량을 제한하기 위해 ResourceQuota를 쓰면, 파드는 생성되었지만 실제 스케줄링 전에 막힐 수 있다.

kubectl get resourcequota -n <namespace>
kubectl describe resourcequota -n <namespace>

예를 들어 한 네임스페이스에 GPU 2개 제한이 있는데, 이미 2개를 쓰고 있다면 새 추론 파드는 Pending 상태가 된다. 이때 해결책은 쿼터를 무작정 올리는 것이 아니다. 먼저 어떤 파드가 GPU를 잡고 있는지 확인한다.

kubectl get pods -n <namespace> -o json \
  | jq -r '.items[] | [.metadata.name, (.spec.containers[].resources.limits["nvidia.com/gpu"] // "0")] | @tsv'

장기적으로는 팀별 쿼터, PriorityClass, Kueue 큐 정책을 함께 설계해야 한다. 추론 서비스와 실험용 노트북이 같은 GPU 쿼터를 공유하면, 가장 중요한 시간에 실험 파드가 프로덕션 replica를 밀어내는 일이 생긴다.

원인 E: PriorityClass와 preemption 실패

고우선순위 파드가 낮은 우선순위 파드를 밀어내도록 설정했더라도 항상 성공하는 것은 아니다.

preemption: 0/5 nodes are available: 5 No preemption victims found for incoming pod.

이 메시지는 "쫓아낼 파드를 찾아도 새 파드가 들어갈 만큼 자원이 생기지 않는다"는 뜻일 수 있다. 예를 들어 새 파드는 GPU 4개가 한 노드에 필요하지만, 각 노드에 1개씩만 비울 수 있다면 preemption으로도 해결되지 않는다.

확인할 것은 새 파드의 요구량과 노드 단위 가용량이다.

kubectl get pod <pod-name> -n <namespace> -o yaml | yq '.spec.priorityClassName, .spec.containers[].resources'
kubectl describe nodes | grep -A20 "Allocated resources"

Tensor Parallel 모델은 "GPU 총량"보다 "한 노드 안의 연속된 GPU 수"가 더 중요하다. 클러스터 전체에 GPU 8개가 남아 있어도, 한 노드에 4개가 모여 있지 않으면 tensor-parallel-size=4 모델은 뜨지 못한다.

원인 F: DRA ResourceClaim 미할당

Dynamic Resource Allocation(DRA)을 쓰는 환경에서는 파드가 직접 nvidia.com/gpu를 요청하는 대신 ResourceClaim을 통해 장치를 할당받을 수 있다. 이때 claim이 할당되지 않으면 파드는 Pending 상태로 남는다.

kubectl get resourceclaim -A
kubectl describe resourceclaim <claim-name> -n <namespace>
kubectl describe pod <pod-name> -n <namespace> | grep -i resourceclaim -A5

DRA 장애에서는 세 가지를 확인한다. ResourceClaimTemplate이 올바른지, DeviceClass가 실제 노드 장치와 매칭되는지, 드라이버가 claim 상태를 갱신하고 있는지다. 단순히 파드만 삭제하면 같은 claim 문제로 다시 Pending이 된다.

플레이북 3: OOMKilled, 노드 메모리 압박, CUDA OOM 구분하기

운영 중 "메모리 부족입니다"라는 말을 들으면 먼저 되물어야 한다. "어떤 메모리요?"

쿠버네티스의 메모리 장애에는 적어도 세 종류가 있다.

증상부족한 자원대표 원인주로 보는 곳
OOMKilled, exit code 137컨테이너 RAM limit컨테이너 메모리 limit 초과Pod status, kube-state-metrics
Evicted, MemoryPressure노드 RAM노드 전체 메모리 압박Node Condition, kubelet event
CUDA out of memoryGPU VRAM모델, KV cache, batch size, TP 설정 문제앱 로그, DCGM, 모델 서버 메트릭

이 셋을 뭉뚱그리면 해결책이 틀어진다. CUDA OOM인데 컨테이너 memory limit만 올려도 소용없다. 노드 MemoryPressure인데 vLLM --gpu-memory-utilization만 낮춰도 해결되지 않는다.

컨테이너 OOMKilled

먼저 Pod 상태를 본다.

kubectl describe pod <pod-name> -n <namespace>

다음과 같은 흔적이 있으면 컨테이너가 메모리 limit을 넘은 것이다.

Last State: Terminated
Reason: OOMKilled
Exit Code: 137

exit code 137은 SIGKILL로 종료되었다는 뜻이며, 컨테이너가 지정된 메모리 한도를 넘었을 때 흔히 나타난다.2

확인할 값은 현재 limit과 실제 사용량이다.

kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[*].resources}'
kubectl top pod <pod-name> -n <namespace> --containers

Prometheus가 있다면 다음을 본다.

container_memory_working_set_bytes{namespace="<namespace>", pod="<pod-name>"}

container_memory_working_set_bytes{namespace="<namespace>", pod="<pod-name>"}
/
container_spec_memory_limit_bytes{namespace="<namespace>", pod="<pod-name>"}

임시 완화는 memory limit을 올리는 것이다. 하지만 이것만으로 끝내면 같은 장애가 다시 온다. 재발 방지를 위해 request와 limit을 함께 조정하고, VPA를 recommendation 모드로 켜서 실제 사용량 기반 권장치를 받는다.

resources:
  requests:
    memory: "16Gi"
    cpu: "4"
  limits:
    memory: "24Gi"
    cpu: "8"

LLM 서버의 CPU 메모리는 모델 가중치보다 작아 보일 수 있지만, 토크나이저, HTTP 서버, 요청 큐, 로그 버퍼, Python 런타임, 프롬프트 전처리 캐시가 함께 올라간다. "GPU 모델이니까 RAM은 조금만 줘도 되겠지"라는 생각이 OOMKilled의 출발점이 된다.

노드 MemoryPressure와 Evicted

파드가 OOMKilled가 아니라 Evicted라면 노드 전체 압박을 봐야 한다.

kubectl describe pod <pod-name> -n <namespace>
kubectl describe node <node-name> | grep -A10 Conditions
kubectl get events --all-namespaces --sort-by='.lastTimestamp' | grep -i evict

노드에 MemoryPressure=True가 보이면 kubelet이 노드 보호를 위해 파드를 축출한다. 이때 BestEffort 파드가 가장 먼저 희생된다. critical 추론 서비스라면 최소한 requests를 설정해 Burstable 이상으로 올리고, 정말 중요한 구성요소는 requests와 limits를 같게 설정해 Guaranteed QoS를 고려한다.

resources:
  requests:
    cpu: "4"
    memory: "24Gi"
  limits:
    cpu: "4"
    memory: "24Gi"

Guaranteed QoS는 비용 면에서는 덜 유연하지만, 노드 압박 상황에서 생존 우선순위를 높인다. 모든 파드에 적용할 필요는 없지만, 게이트웨이, 인증, 라우터, 핵심 추론 replica에는 적용할 가치가 있다.

CUDA out of memory

CUDA OOM은 파드 상태만 보면 일반 CrashLoopBackOff처럼 보일 수 있다. 진짜 단서는 앱 로그에 있다.

kubectl logs <pod-name> -n <namespace> --previous --tail=200 | grep -i "cuda|out of memory|vram"

예시는 다음과 같다.

torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB

이 경우 조정 대상은 컨테이너 RAM이 아니라 모델 서빙 파라미터다. vLLM에서는 --gpu-memory-utilization, --max-model-len, --max-num-batched-tokens, --max-num-seqs, --tensor-parallel-size 같은 값이 직접 영향을 준다. vLLM의 쿠버네티스 배포 예시에서도 GPU 리소스 요청, PVC 기반 모델 캐시, /dev/shm emptyDir, readiness/liveness probe 설정이 함께 등장한다.3

예를 들어 긴 컨텍스트와 큰 batch를 동시에 허용하면 KV 캐시가 VRAM을 빠르게 채운다. KV 캐시는 모델이 이전 토큰의 계산 결과를 저장해 다음 토큰 생성을 빠르게 하는 메모리 영역이다. 사용자가 긴 프롬프트를 많이 보내거나 동시 요청이 늘어나면 KV 캐시가 폭증한다.

임시 완화는 다음 순서로 시도한다.

args:
  - "--gpu-memory-utilization=0.85"
  - "--max-model-len=4096"
  - "--max-num-batched-tokens=4096"
  - "--max-num-seqs=64"

기존 값이 0.95, 8192, 8192, 128이었다면 위 조정은 처리량을 일부 포기하고 안정성을 얻는 선택이다. 사용자는 약간 느려질 수 있지만, 서버가 반복적으로 죽는 것보다는 낫다.

근본 해결은 모델 크기, 양자화, tensor parallel, GPU 종류를 다시 맞추는 것이다. 70B 모델을 40GB GPU 한 장에 무리하게 올리려 하면 파라미터 몇 개를 만져도 한계가 있다. 이때는 4-bit 양자화 모델을 쓰거나, 더 작은 모델로 라우팅하거나, tensor parallel 구성을 늘려야 한다.

플레이북 4: CPU throttling과 "느린데 죽지는 않는" 장애

장애는 항상 빨간색으로 오지 않는다. 파드는 Running이고, readiness도 통과하고, 에러율도 낮은데 사용자는 "너무 느리다"고 말한다. 이때 CPU throttling을 봐야 한다.

CPU throttling은 컨테이너가 CPU limit에 막혀 실행 시간을 빼앗기는 현상이다. CPU 사용률이 80%라서 괜찮아 보이는데도 실제로는 limit 때문에 대기 시간이 길어질 수 있다.

Prometheus에서는 다음 비율을 본다.

rate(container_cpu_cfs_throttled_periods_total{namespace="<namespace>", pod="<pod-name>"}[5m])
/
rate(container_cpu_cfs_periods_total{namespace="<namespace>", pod="<pod-name>"}[5m])

이 값이 지속적으로 높으면 CPU limit을 낮게 잡은 것이다. LLM 서버에서는 GPU가 핵심처럼 보이지만, CPU도 바쁘다. 토크나이징, JSON 직렬화, 스트리밍 응답, 라우팅, 로깅, 권한 검사, 프롬프트 템플릿 조립이 모두 CPU에서 일어난다.

대응은 세 가지 중 하나다.

resources:
  requests:
    cpu: "4"
    memory: "16Gi"
  limits:
    cpu: "8"
    memory: "24Gi"

첫째, CPU limit을 올린다. 둘째, limit을 제거하고 request만 둔다. 셋째, replica를 늘려 요청을 분산한다. 프로덕션에서는 보통 request는 정확히 잡고 limit은 너무 빡빡하게 두지 않는 쪽이 LLM 서버에 유리하다. 단, 멀티테넌트 클러스터에서는 한 파드가 노드 CPU를 독점하지 않도록 상한선을 둘 필요가 있다.

플레이북 5: LLM 추론이 갑자기 느려졌다

LLM 추론 장애는 전통적인 웹 서비스 장애보다 더 교묘하다. CPU와 메모리만 보면 정상인데, 사용자가 첫 토큰을 받기까지 10초가 걸린다. 또는 첫 토큰은 빠른데 이후 토큰이 느릿느릿 나온다. 이때 봐야 할 지표가 다르다.

핵심 지표는 네 가지다.

지표의미나빠질 때의 느낌
TTFTTime To First Token, 첫 토큰까지 걸린 시간사용자가 "응답 시작이 느리다"고 느낌
ITLInter-Token Latency, 토큰 사이 간격스트리밍이 끊기듯 느림
tokens/sec초당 생성 토큰 수전체 생성 완료가 늦음
queue depth대기 중 요청 수요청이 밀리고 타임아웃 증가

HPA가 CPU 기준으로만 움직이면 이런 장애를 놓친다. CPU는 낮은데 GPU KV 캐시가 포화되었거나, 배치 큐가 밀렸거나, 외부 검색 API가 느려졌을 수 있다. 2026년의 LLM 운영에서는 queue depth, KV cache utilization, 토큰 처리량 같은 추론 특화 지표를 오토스케일링과 알림에 넣는 흐름이 강해지고 있다.4

1단계: cold start인지 확인한다

증상이 배포 직후 또는 scale-from-zero 직후에만 발생한다면 cold start를 의심한다. 모델 가중치 다운로드, PVC attach, 모델 로딩, CUDA graph 준비, 워밍업 요청이 모두 첫 사용자 요청 앞에 놓일 수 있다.

확인할 것:

kubectl get pods -n <namespace> -o wide
kubectl describe pod <pod-name> -n <namespace> | grep -A5 Started
kubectl logs <pod-name> -n <namespace> --tail=200 | grep -Ei 'download|loading|warm|ready|cuda'

대응은 warm pool을 두는 것이다. 완전한 scale-to-zero가 비용에는 좋지만, 사용자 인터랙션이 중요한 서비스라면 최소 1개 replica 또는 예열된 standby replica가 필요할 수 있다.

spec:
  minReplicaCount: 1
  maxReplicaCount: 10

비용이 정말 중요하다면 낮 시간에는 minReplicaCount: 1, 새벽에는 0으로 바꾸는 스케줄 기반 정책을 둘 수 있다.

2단계: batching 포화인지 확인한다

vLLM과 SGLang은 요청을 묶어 처리해 GPU 효율을 높인다. 하지만 batch가 너무 크면 개별 요청 대기 시간이 늘어난다. 특히 TTFT가 급증하면 prefill 단계가 밀리는지 봐야 한다.

확인할 지표:

# 예시: 실제 메트릭 이름은 배포한 exporter에 따라 다르다.
vllm:num_requests_waiting
vllm:time_to_first_token_seconds_bucket
vllm:inter_token_latency_seconds_bucket
vllm:gpu_cache_usage_perc

임시 완화는 max batch 관련 값을 낮추는 것이다. 처리량은 줄지만 대기 시간이 안정된다.

args:
  - "--max-num-batched-tokens=4096"
  - "--max-num-seqs=64"

반대로 GPU 사용률이 낮고 queue만 쌓인다면 batch가 너무 작거나 replica 라우팅이 비효율적일 수 있다. 이 경우 batch를 키우거나 라우터가 요청을 너무 잘게 흩뿌리지 않는지 본다.

3단계: KV cache miss와 prefix cache 미사용 확인

RAG나 에이전트 서비스에서는 비슷한 시스템 프롬프트와 긴 컨텍스트가 반복된다. prefix cache를 활용하면 같은 prefix의 계산 결과를 재사용할 수 있다. 그런데 라우터가 매번 다른 replica로 요청을 보내면 캐시 재사용률이 떨어진다.

증상은 묘하다. 트래픽은 비슷한데 TTFT가 늘고, GPU 계산량이 증가하고, 캐시 hit rate가 낮아진다. 해결은 세션 또는 prefix 기반 라우팅, 모델별 sticky routing, 캐시 친화적 게이트웨이 설정이다.

쿠버네티스 관점에서는 Service의 단순 라운드로빈만으로 충분하지 않을 수 있다. 앞 장에서 다룬 Gateway API Inference Extension이나 모델 인식 라우팅 계층이 필요한 이유가 여기에 있다.

4단계: GPU utilization과 SM occupancy를 함께 본다

GPU utilization이 95%라고 해서 항상 좋은 것은 아니다. 큐가 길고 ITL이 나쁘면서 GPU가 95%라면 포화다. 반대로 queue가 긴데 GPU utilization이 30%라면 GPU가 아니라 CPU, 네트워크, 스토리지, 라우터, 외부 API가 병목일 수 있다.

DCGM Exporter를 쓰면 GPU 메모리, 온도, 전력, utilization 같은 지표를 Prometheus로 가져올 수 있다. GPU 장애 대응에서는 노드 메트릭, 파드 메트릭, 모델 서버 메트릭을 함께 봐야 한다.

DCGM_FI_DEV_GPU_UTIL{gpu="0"}
DCGM_FI_DEV_FB_USED{gpu="0"}
DCGM_FI_DEV_FB_FREE{gpu="0"}

5단계: 외부 의존성 지연을 분리한다

AI 서비스는 모델 서버만으로 끝나지 않는다. 벡터 DB, MongoDB, Redis, 외부 LLM API, 결제 API, 검색 API가 한 요청 경로에 묶인다. 사용자는 "AI가 느리다"고 말하지만, 실제로는 Pinecone 검색이 느리거나 Azure OpenAI fallback 호출이 지연될 수 있다.

분리 방법은 요청 타임라인을 쪼개는 것이다.

request_received
  -> auth_checked
  -> mongo_read_done
  -> vector_search_done
  -> prompt_built
  -> llm_first_token
  -> llm_last_token
  -> response_completed

각 구간의 시간을 trace나 structured log로 남기면 "LLM이 느린지, LLM 앞단이 느린지"가 보인다. 이 구분이 없으면 GPU replica를 늘려도 외부 API 병목은 그대로다.

공통 복구 검증 체크리스트

장애를 고친 뒤 가장 위험한 말은 "이제 되는 것 같은데요"다. 복구는 느낌이 아니라 검증으로 끝나야 한다.

AI 쿠버네티스 서비스의 복구 검증은 다음을 확인한다.

# 파드 상태
kubectl get pods -n <namespace> -o wide

# 최근 이벤트에 같은 실패가 반복되는지
kubectl get events -n <namespace> --sort-by='.lastTimestamp' | tail -30

# 배포 상태
kubectl rollout status deploy/<deployment-name> -n <namespace>

# 서비스 엔드포인트
kubectl get endpoints <service-name> -n <namespace>

# 간단한 헬스 체크
curl -s http://<service>/health

# 실제 추론 요청
curl -s http://<service>/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"<model>","messages":[{"role":"user","content":"ping"}],"max_tokens":16}'

GPU 복구라면 CUDA smoke test를 통과해야 하고, LLM 복구라면 실제 OpenAI 호환 API 호출이 성공해야 한다. readiness probe만 통과했다고 사용자 요청이 정상이라는 뜻은 아니다. readiness는 "서버가 받을 준비가 되었는가"를 보는 최소 조건이고, 실제 추론은 모델 로딩, 토크나이저, GPU 메모리, 라우팅, 인증, 응답 스트리밍까지 모두 지나야 한다.

AI 보조 SRE를 안전하게 쓰는 법

2026년에는 AI가 운영자의 옆자리에 앉는 것이 자연스러워졌다. 알림을 읽고, 로그를 요약하고, 비슷한 과거 장애를 찾고, runbook 후보를 제안한다. 일부 조직은 승인된 범위 안에서 자동 복구까지 시도한다. AI 보조 SRE와 agentic operations의 핵심은 사람이 모든 명령을 직접 치는 구조에서, AI가 진단과 제안을 맡고 사람은 승인과 정책을 관리하는 구조로 이동한다는 점이다.5

하지만 운영 자동화에서 가장 중요한 문장은 이것이다.

AI가 빠르게 고칠 수 있는 권한은, AI가 빠르게 망가뜨릴 수 있는 권한이기도 하다.

따라서 AI 보조 SRE는 다음 안전장치 안에서 움직여야 한다.

읽기 전용에서 시작한다

처음부터 kubectl patch, kubectl delete, helm upgrade 권한을 주지 않는다. 첫 단계는 읽기 전용이다.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ai-sre-readonly
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log", "events", "nodes", "services", "endpoints"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
  verbs: ["get", "list", "watch"]

AI는 이 권한으로 증거를 모으고, 요약하고, 원인 후보를 제시한다. 운영자가 결과를 신뢰하기 시작하면 다음 단계로 넘어간다.

변경은 dry-run과 GitOps PR로 보낸다

AI가 직접 운영 클러스터에 patch를 날리는 대신, 먼저 dry-run을 실행한다.

kubectl apply -f proposed-fix.yaml --dry-run=server
kubectl diff -f proposed-fix.yaml

그다음 GitOps 저장소에 PR을 만든다.

incident-2026-05-08-vllm-oom
  ├─ helm/values-prod.yaml 수정
  ├─ runbook 링크
  ├─ 예상 영향
  ├─ 롤백 방법
  └─ 검증 쿼리

Argo CD나 Flux가 운영 배포를 담당한다면, AI는 "클러스터를 직접 바꾸는 존재"가 아니라 "변경 제안을 만드는 존재"가 된다. 이 차이가 중요하다.

자동 실행은 낮은 위험 작업부터 허용한다

자동화 범위는 점진적으로 넓힌다.

단계허용 작업승인 방식
1단계로그 요약, 이벤트 분류, 유사 장애 검색자동
2단계runbook 후보, YAML patch 제안사람 승인
3단계비프로덕션 재시작, 임시 scale up조건부 자동
4단계프로덕션 변경GitOps PR 승인 후 적용

AI에게 가장 먼저 맡기기 좋은 일은 "지루하지만 위험하지 않은 일"이다. 예를 들어 장애 티켓 초안 작성, 타임라인 정리, 관련 로그 묶기, 과거 유사 장애 검색은 효과가 크고 위험은 낮다. 반대로 kubectl delete pod, terraform destroy, 데이터베이스 마이그레이션, 시크릿 변경은 끝까지 엄격한 승인 경로를 둔다.

롤백 기준을 먼저 정한다

AI가 제안한 변경이든 사람이 한 변경이든, 적용 전에 롤백 기준이 있어야 한다.

변경: vLLM max-num-batched-tokens 8192 -> 4096
기대 효과: TTFT P95 8초 -> 3초 이하
허용 부작용: tokens/sec 최대 15% 감소
롤백 조건:
  - 10분 내 5xx 비율 2% 초과
  - tokens/sec 30% 이상 감소
  - queue depth가 변경 전보다 2배 이상 증가
검증 대시보드:
  - Grafana: LLM Serving / Latency
  - Grafana: GPU / DCGM

롤백 기준이 없으면 "좋아진 것 같다"와 "나빠진 것 같다"가 회의에서 싸운다. 기준이 있으면 시스템이 말한다.

TILNOTE식 소규모 AI 서비스 인시던트 타임라인

대규모 엔터프라이즈만 장애를 겪는 것은 아니다. 오히려 1인 개발자나 소규모 팀은 한 사람이 백엔드, 인프라, 데이터베이스, LLM 비용, 고객 문의를 모두 본다. 이 상황에서는 완벽한 SRE 조직보다 간결한 플레이북이 더 중요하다.

다음은 TILNOTE 같은 소규모 AI 콘텐츠 서비스에서 충분히 일어날 수 있는 인시던트 예시다. 구성은 Next.js 프론트엔드, Express API, MongoDB, Pinecone 벡터 DB, Azure OpenAI, Redis 캐시, 쿠버네티스 기반 워커라고 가정한다.

09:02 — CPU 알람 발생

Prometheus 알림이 울린다.

HighCPUUsage: api-server CPU usage > 85% for 10m

처음에는 단순 트래픽 증가로 보인다. 대시보드를 보니 API 서버 CPU가 높고, 응답 시간이 조금씩 증가한다.

kubectl top pod -n prod
kubectl top pod -n prod --containers

확인 결과 API 서버의 CPU throttling 비율이 높다. 최근 배포에서 콘텐츠 생성 요청마다 토큰 수 계산과 프롬프트 템플릿 렌더링을 동기적으로 수행하도록 바뀌었다.

임시 대응:

kubectl scale deploy/api-server -n prod --replicas=4

장기 개선:

- 프롬프트 렌더링 결과 캐시
- 토큰 수 계산 비동기화
- CPU request/limit 재조정
- 부하 테스트에 콘텐츠 생성 시나리오 추가

09:18 — 캐시 미스 급증

CPU를 늘렸는데도 응답 시간이 완전히 내려가지 않는다. Redis hit rate가 평소 85%에서 40%로 떨어졌다. 원인은 배포 과정에서 캐시 key prefix가 바뀐 것이다. 기존 캐시가 모두 무효화되면서 MongoDB와 Pinecone 호출이 동시에 늘었다.

확인:

cache_hit_rate{service="api"}
mongodb_query_duration_seconds_bucket
pinecone_request_duration_seconds_bucket

임시 대응은 캐시 워밍업이다. 인기 문서와 최근 생성 템플릿을 미리 로드한다.

kubectl create job cache-warmup-$(date +%s) \
  --from=cronjob/cache-warmup -n prod

장기 개선은 캐시 key 버전 변경을 배포 체크리스트에 넣는 것이다. 캐시 key를 바꾸는 배포는 기능 배포가 아니라 인프라 이벤트다.

09:31 — Pinecone 지연 증가

이제 API 서버는 안정됐지만 콘텐츠 생성 요청 일부가 여전히 느리다. trace를 보니 벡터 검색 구간이 길다.

request_received -> mongo_read_done: 45ms
mongo_read_done -> vector_search_done: 2400ms
vector_search_done -> llm_first_token: 1100ms

사용자는 "AI 응답이 느리다"고 느끼지만, 실제로는 LLM보다 Pinecone 검색이 병목이다. 이때 GPU나 vLLM replica를 늘리면 비용만 오른다.

임시 대응:

- 검색 top_k를 20에서 8로 낮춤
- 오래된 문서 컬렉션 검색을 임시 비활성화
- Pinecone timeout을 3초로 두고 fallback 요약 검색 사용

장기 개선:

- 벡터 DB 지연 시간 SLO 추가
- 검색 결과 캐시 도입
- tenant별 인덱스 크기와 쿼리 비용 관찰
- 외부 의존성 장애 시 degraded mode 설계

09:47 — Azure OpenAI fallback 비용 급증

일부 자체 모델 요청이 실패하면서 fallback으로 Azure OpenAI 호출이 늘었다. 서비스는 살아 있지만 비용 알람이 울린다.

AzureOpenAIRequestsHigh: requests increased 300% in 15m

이런 장애는 가용성 장애이면서 비용 장애다. 사용자는 계속 서비스를 쓰고 있으므로 더 위험할 수 있다. 조용히 돈이 새기 때문이다.

임시 대응:

- 무료 사용자 fallback 제한
- 긴 컨텍스트 요청 max_tokens 축소
- 동일 프롬프트 재시도 차단
- 관리자에게 비용 알림 전송

장기 개선:

- provider별 circuit breaker
- 사용자 등급별 fallback 정책
- 비용 예산 초과 시 graceful degradation
- OpenCost/Kubecost와 외부 LLM 비용을 같은 대시보드에서 보기

10:05 — 인시던트 종료와 사후 기록

서비스 P95 latency가 정상 범위로 돌아오고, CPU throttling이 낮아지고, Pinecone timeout 비율이 안정화되었다. 여기서 끝내면 다음 장애 때 다시 같은 일을 반복한다. 기록해야 한다.

# Incident: 2026-05-08 콘텐츠 생성 지연 및 외부 API 비용 증가

## 요약
09:02부터 10:05까지 콘텐츠 생성 API의 P95 latency가 2.1초에서 9.8초로 증가했다.
직접 원인은 캐시 key prefix 변경으로 인한 캐시 미스 급증과 Pinecone 검색 지연이었다.
부수적으로 자체 모델 실패 요청이 Azure OpenAI fallback으로 이동하며 비용 알람이 발생했다.

## 영향 범위
- 전체 사용자 중 약 18%의 콘텐츠 생성 요청 지연
- 5xx 에러율 최대 1.2%
- Azure OpenAI 호출량 평소 대비 3배 증가

## 타임라인
- 09:02 CPU 알람 발생
- 09:08 API 서버 CPU throttling 확인
- 09:12 API 서버 replica 2 -> 4 확장
- 09:18 Redis cache hit rate 하락 확인
- 09:24 cache warmup job 실행
- 09:31 Pinecone latency 증가 확인
- 09:38 top_k 20 -> 8 임시 조정
- 09:47 Azure OpenAI fallback 비용 알람 발생
- 09:52 무료 사용자 fallback 제한 적용
- 10:05 주요 지표 정상화

## 근본 원인
캐시 key prefix 변경이 고위험 변경으로 분류되지 않았고,
배포 후 cache hit rate와 외부 의존성 latency를 확인하는 체크가 없었다.

## 잘한 점
- trace 구간 분해로 LLM 병목이 아님을 빠르게 확인했다.
- replica 확장으로 API 서버 CPU 병목을 먼저 완화했다.
- 비용 알람이 fallback 폭증을 조기에 잡았다.

## 개선할 점
- 캐시 key 변경 시 사전 warmup 필요
- Pinecone degraded mode 필요
- fallback 정책에 사용자 등급과 예산 조건 반영 필요

## 액션 아이템
- [ ] 배포 체크리스트에 cache key 변경 항목 추가
- [ ] cache hit rate 알림 추가
- [ ] Pinecone timeout/fallback 정책 코드화
- [ ] Azure OpenAI 예산 기반 circuit breaker 구현
- [ ] 인시던트 재현 부하 테스트 추가

사후 기록의 목표는 비난이 아니라 압축이다. 오늘 63분 걸린 장애를 다음에는 15분 안에 끝내기 위해, 판단 과정을 문서로 압축해 두는 것이다.

증상별 빠른 분류표

장애 중에는 긴 문서를 읽을 시간이 없다. 다음 표는 첫 분류용이다.

증상먼저 볼 것흔한 원인첫 대응
Pod Pendingkubectl describe pod Events자원 부족, affinity 불일치, toleration 누락, quota 초과이벤트 메시지 기준으로 분기
OOMKilledLast State, exit code 137컨테이너 memory limit 초과limit 임시 상향, 사용량 추이 확인
EvictedNode Condition노드 MemoryPressure/DiskPressure노드 압박 완화, BestEffort 제거
CUDA OOM앱 로그VRAM 부족, KV cache 과다batch/model len/gpu utilization 조정
GPU allocatable 0node allocatable, device plugin 로그드라이버, toolkit, plugin, MIG 설정 문제CUDA smoke test까지 검증
TTFT 급증queue, prefill, cold start모델 로딩, batching 포화, cache misswarm pool, batch 조정
ITL 악화tokens/sec, GPU utildecode 포화, GPU 병목replica/TP/배치 조정
ImagePullBackOffEvents이미지 태그 오류, secret 누락imagePullSecret, registry 확인
CrashLoopBackOfflogs --previous설정 오류, 의존성 실패, OOM직전 로그 기준 수정

이 표는 외워야 할 답안지가 아니다. 장애를 올바른 플레이북으로 보내는 라우터다.

운영 플레이북을 팀의 자산으로 만드는 법

플레이북은 한 번 쓰고 끝나는 문서가 아니다. 실제 장애를 겪을 때마다 더 짧고 정확해져야 한다. 좋은 플레이북에는 명령어만 있는 것이 아니라 판단 기준이 있다.

나쁜 플레이북은 이렇게 쓴다.

문제가 생기면 로그를 확인한다.
필요하면 파드를 재시작한다.

좋은 플레이북은 이렇게 쓴다.

조건: vLLM TTFT P95가 5분 이상 5초를 초과하고 queue depth가 20 이상일 때
확인:
  1. cold start 여부: 최근 10분 내 pod 생성 여부 확인
  2. GPU utilization 90% 이상인지 확인
  3. vLLM waiting requests 증가 여부 확인
완화:
  - cold start면 minReplicaCount 1로 임시 변경
  - GPU 포화면 max-num-batched-tokens 8192 -> 4096
  - queue 포화면 KEDA maxReplicaCount 2배 상향
검증:
  - 10분 내 TTFT P95 3초 이하
  - 5xx 1% 이하
롤백:
  - tokens/sec 30% 이상 하락 시 batch 설정 원복

플레이북은 "누가 봐도 같은 판단을 하게 만드는 장치"다. 사람이 보든 AI 에이전트가 보든, 같은 조건에서는 같은 수집, 같은 완화, 같은 검증을 하도록 만든다.

마지막으로: 장애는 없앨 수 없지만 MTTR은 줄일 수 있다

AI 시대의 쿠버네티스 운영은 복잡하다. GPU 드라이버, device plugin, DRA, Kueue, KEDA, Gateway, vLLM, SGLang, 벡터 DB, 외부 LLM API, 에이전트 도구 호출이 한 요청 경로에 엮인다. 이 복잡성을 없앨 수는 없다.

하지만 장애 대응 시간을 줄일 수는 있다. 핵심은 세 가지다.

첫째, 증상별로 첫 관찰 지점을 정한다. Pending은 scheduler event, OOM은 Last State와 Node Condition, GPU 미인식은 nvidia-smi부터 device plugin까지, 느린 추론은 TTFT와 queue depth부터 본다.

둘째, 임시 완화와 근본 해결을 구분한다. replica를 늘리는 것은 완화일 수 있지만, 캐시 key 변경으로 인한 miss 폭증을 고치지는 못한다. memory limit을 올리는 것은 급한 불을 끄지만, KV cache 정책이나 메모리 누수를 해결하지는 못한다.

셋째, AI 보조 SRE를 쓰되 안전장치를 먼저 둔다. AI는 로그 요약과 유사 장애 검색에서 매우 강력하다. 하지만 운영 변경은 RBAC, dry-run, GitOps PR, 승인, 롤백 기준 안에서 움직여야 한다.

장애 대응의 목표는 영웅적인 밤샘이 아니다. 같은 장애가 두 번 왔을 때 두 번째는 더 차분하고, 더 빠르고, 더 안전하게 끝내는 것이다. 이 장의 플레이북이 그 두 번째 밤을 조금 덜 길게 만들어 주기를 바란다.

참고

1NVIDIA GPU Operator Documentation

2How to Fix OOMKilled in Kubernetes: We Resolved 500+ Cases (2026 Guide)

3Using Kubernetes - vLLM

4Deploying LLMs on Kubernetes: vLLM, Ray Serve & GPU Scheduling Guide (2026)

5Agentic Operations for Kubernetes: AI Agents Replacing Manual K8s Management

책 목록

AI 시대의 쿠버네티스: LLM 추론과 에이전트를 운영하는 인프라 전략

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기