13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[작성자:] admin

  • 홈랩에 DevStack으로 OpenStack 올리기 (1) — 설치 준비와 CPU x86-64-v2 함정

    프라이빗 클라우드를 제대로 공부하려면 OpenStack을 직접 깔아봐야 합니다. 가장 빠른 길이 DevStack — 스크립트 하나로 올인원 OpenStack을 올려주는 개발/학습용 도구죠. 저는 이걸 Proxmox 홈랩에 올려봤습니다. 결론부터 솔직히 말하면 한 방에 안 됐고, 여러 번 깨졌습니다. 이번 편은 설치 준비부터 첫 번째 벽까지의 실전 기록입니다.

    1. 먼저 정한 것 — OS는 Ubuntu

    제 홈랩엔 Rocky 9 설치용 VM이 있었지만, DevStack엔 안 썼습니다. DevStack은 사실상 Ubuntu 전용이거든요(공식 지원·테스트가 우분투 중심). Rocky/CentOS는 지원이 불안정합니다. 그래서 깨끗한 Ubuntu 24.04로 갔습니다.

    2. VM 준비 — cloud-init 템플릿 클론

    예전에 만들어둔 Ubuntu cloud-init 템플릿을 클론해 30초 만에 노드를 준비했습니다.

    # 템플릿(9000)에서 DevStack용 VM(200) 클론
    qm clone 9000 200 --full --name devstack
    qm resize 200 scsi0 +37G          # 디스크 ~40G
    qm set 200 --memory 12288 --cores 6 --sshkeys ~/.ssh/id_rsa.pub --ipconfig0 ip=dhcp
    qm start 200

    사양은 6코어 / 12GB / 40GB. DevStack 올인원의 현실적 최소선입니다(8GB로도 되지만 빠듯). 호스트 RAM을 보호하려 16GB가 아닌 12GB로 잡았습니다.

    3. DevStack 설치 준비

    DevStack은 root가 아니라 전용 stack 유저로 돌려야 합니다.

    # stack 유저 + passwordless sudo
    sudo useradd -s /bin/bash -d /opt/stack -m stack
    echo "stack ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/stack
    
    # devstack 클론 (stack 유저로)
    sudo -u stack -H git clone https://opendev.org/openstack/devstack /opt/stack/devstack

    설정은 local.conf 하나입니다. 최소 구성으로 시작했습니다.

    [[local|localrc]]
    ADMIN_PASSWORD=devstack123
    DATABASE_PASSWORD=devstack123
    RABBIT_PASSWORD=devstack123
    SERVICE_PASSWORD=devstack123
    HOST_IP=192.168.x.x
    VOLUME_BACKING_FILE_SIZE=8G   # Cinder 백킹 축소로 디스크 절약
    cd /opt/stack/devstack && ./stack.sh

    4. 첫 번째 벽 — NumPy가 CPU를 걸고 넘어졌다

    패키지 설치가 한참 돌더니 nova-novncproxy 서비스에서 죽었습니다. 로그를 파보니 범인은 엉뚱하게도 NumPy였습니다.

    RuntimeError: NumPy was built with baseline optimizations:
    (X86_V2) but your machine doesn't support: (X86_V2).

    원인은 가상 CPU 타입이었습니다. Proxmox VM의 기본 CPU 타입(kvm64)은 호환성을 위해 최신 명령어셋(SSE4.2 등, x86-64-v2)을 감춥니다. 그런데 요즘 NumPy 휠은 x86-64-v2를 전제로 빌드돼서, 그 명령어가 없으면 임포트 단계에서 크래시합니다. nova의 콘솔 프록시가 NumPy를 쓰다 죽은 거죠.

    해결은 지난 KVM 글에서 다룬 바로 그 포인트 — CPU 타입을 host로 바꿔 물리 CPU 기능을 그대로 노출하는 것이었습니다.

    qm stop 200
    qm set 200 --cpu host      # 물리 i5의 명령어셋 그대로 노출
    qm start 200
    # VM 안에서 확인 — 이제 노출됨
    grep -o -m1 sse4_2 /proc/cpuinfo   # sse4_2
    grep -o -m1 avx2 /proc/cpuinfo     # avx2

    이건 일반 DevStack 튜토리얼엔 없는 함정입니다. 물리 서버나 워크스테이션에선 안 생기고, “기본 CPU 타입 가상머신”에서만 터지거든요. 홈랩에서 VM으로 OpenStack·AI 라이브러리를 돌린다면 --cpu host는 기본으로 챙기세요.

    다음 편

    CPU를 고치고 다시 ./stack.sh를 돌렸습니다. 그런데 이번엔 네트워크(Neutron)에서, 그다음엔 Glance에서, 또 placement에서 연달아 막혔습니다. 다음 편에서 이 “서비스는 뜨는데 안 되는” 연속 삽질과 각각의 원인을 낱낱이 분석합니다. DevStack이 홈랩에서 왜 만만치 않은지, 제대로 보여드리겠습니다.

  • Proxmox 네트워크 기초 — Linux Bridge와 VLAN 이해하기

    Proxmox를 설치하면 vmbr0라는 게 자동으로 생깁니다. VM·컨테이너가 다 여기에 붙는데, 정작 이게 뭔지 모르고 쓰는 분이 많습니다. Linux Bridge와 VLAN — Proxmox 네트워크의 두 핵심을 실제 제 구성과 함께 정리합니다.

    1. Linux Bridge — 가상 스위치

    vmbr0는 소프트웨어로 만든 네트워크 스위치입니다. 물리 랜포트(NIC)와 VM/컨테이너들을 이 가상 스위치에 꽂아 서로·외부와 통신하게 합니다.

    실제 제 홈랩의 설정은 이렇게 단순합니다.

    # /etc/network/interfaces
    auto vmbr0
    iface vmbr0 inet static
        address 192.168.20.100/24
        bridge-ports enp1s0     # 물리 NIC를 브리지에 연결
        bridge-stp off
        bridge-fd 0

    즉 물리 NIC enp1s0가 vmbr0에 물려 있고, 모든 게스트가 이 브리지를 통해 192.168.20.x 실제 네트워크에 직접 참여합니다. 그래서 VM에 공유기가 주는 IP가 그대로 붙는 겁니다.

    2. 게스트를 브리지에 연결

    # VM/LXC 네트워크를 vmbr0에 virtio로 연결
    qm set 100 --net0 virtio,bridge=vmbr0
    pct set 113 --net0 name=eth0,bridge=vmbr0,ip=dhcp

    이게 전부입니다. 게스트는 마치 물리 스위치에 랜선을 꽂은 것처럼 동작합니다.

    3. VLAN — 하나의 랜을 여러 개로 나누기

    네트워크를 용도별로 분리하고 싶을 때(예: 서버망 / IoT망 / 게스트망) VLAN을 씁니다. 물리 케이블은 하나인데 논리적으로 여러 망으로 쪼개는 기술입니다.

    Proxmox에서는 브리지를 VLAN-aware로 만들고, 게스트마다 VLAN 태그를 지정합니다.

    # VLAN 인식 브리지
    auto vmbr0
    iface vmbr0 inet static
        address 192.168.20.100/24
        bridge-ports enp1s0
        bridge-vlan-aware yes
        bridge-vids 2-4094
    # 게스트를 특정 VLAN(예: 30)에 배치
    qm set 100 --net0 virtio,bridge=vmbr0,tag=30

    이러면 tag=30인 게스트들끼리만 같은 망에 묶이고, 다른 VLAN과는 L3 라우팅(방화벽)을 거쳐야만 통신합니다. 보안 분리에 효과적이죠.

    4. 솔직한 현실 — 내 홈랩은 아직 플랫

    고백하자면 제 홈랩은 아직 VLAN 없이 단일 플랫 네트워크(192.168.20.x)입니다. 서비스가 늘면서 “IoT·실험 VM은 분리하는 게 맞는데” 싶은 순간이 오는데, 저도 그 숙제를 남겨둔 상태입니다. 홈랩이 커지면 VLAN 분리는 결국 마주치는 과제입니다.

    5. 실전 팁

    • bridge-stp off: 단일 스위치 환경에선 STP 꺼도 됩니다(루프 위험 없을 때).
    • 본딩(bond): NIC가 여러 개면 본딩으로 이중화·대역폭 확장 가능.
    • 관리망 분리: 규모가 커지면 Proxmox 관리 접속용 망을 따로 두는 걸 권장.

    6. 정리

    Proxmox 네트워크의 기본은 Linux Bridge(가상 스위치)이고, 분리가 필요하면 VLAN(논리 분할)을 얹습니다. 대부분은 vmbr0 하나로 시작해 충분하고, 서비스가 많아지면 VLAN으로 넘어가면 됩니다. 브리지가 어떻게 물리 NIC와 게스트를 잇는지만 이해하면, Proxmox 네트워크는 더 이상 미스터리가 아닙니다.

  • [k8s] Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    [k8s] Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    Kubernetes 프로브 트러블슈팅: Liveness/Readiness 실패

    Kubernetes 프로브 트러블슈팅이 중요한 이유

    Kubernetes 프로브 트러블슈팅은 단순히 /health 경로가 200을 반환하는지 확인하는 일이 아닙니다. 운영에서는 이 설정 하나가 재시작 루프, 트래픽 블랙홀, 배포 지연, 의존성 장애 전파를 만들기도 하고 막기도 하거든요. 13년 넘게 서비스를 운영하면서 느낀 건, 프로브는 애플리케이션의 상태를 보는 기능이라기보다 쿠버네티스가 장애에 어떻게 반응할지 정하는 정책에 가깝다는 점입니다.

    대표적인 장면은 비슷합니다. 앱 로그만 보면 큰 문제가 없는데 kubectl get pods의 RESTARTS가 계속 올라갑니다. 또는 배포는 끝났는데 Service 뒤에서 파드가 빠져 502, 503이 발생합니다. 이때 무작정 앱을 재배포하거나 노드를 의심하면 시간이 정말 빨리 녹습니다. 먼저 봐야 할 것은 kubelet 이벤트입니다. Liveness probe failed 뒤에 Killing container가 붙는지, Readiness probe failed만 반복되는지에 따라 원인이 완전히 갈립니다.

    위 흐름에서 핵심은 하나입니다. Liveness 실패는 재시작을 부르고, Readiness 실패는 트래픽 제외를 부릅니다. 둘 다 헬스 체크처럼 보이지만 운영 결과는 다릅니다. 그래서 프로브를 설계할 때는 “정상인가?”보다 “실패했을 때 쿠버네티스가 무엇을 해야 하는가?”를 먼저 정해야 합니다.

    Liveness Probe 실패와 Readiness Probe 오류의 차이

    Liveness Probe는 컨테이너가 복구 불가능한 상태에 빠졌는지 판단합니다. 실패가 임계치에 도달하면 kubelet이 해당 컨테이너를 재시작합니다. Readiness Probe는 지금 요청을 받아도 되는지 판단합니다. 실패해도 컨테이너는 계속 실행되지만, 해당 파드는 Service 엔드포인트에서 제외됩니다. Startup Probe는 초기 기동 중인 앱을 보호합니다. Startup Probe가 설정되면 성공하기 전까지 Liveness와 Readiness가 실행되지 않으므로, 기동이 느린 앱에서 조기 재시작을 막는 데 유용합니다.

    운영에서 많이 터지는 실수는 외부 의존성까지 Liveness에 넣는 겁니다. DB가 잠깐 느려졌을 뿐인데 모든 파드가 동시에 재시작되면, 커넥션 재생성·캐시 워밍업·JIT/클래스 로딩 같은 비용이 한꺼번에 몰립니다. 장애를 복구하는 게 아니라 장애에 연료를 붓는 모양이 되더라고요. DB, Redis, 메시지 브로커, 외부 API는 대부분 Readiness에서 다루는 편이 안전합니다.

    구분 Liveness Probe Readiness Probe Startup Probe 운영 판단 기준
    목적 컨테이너를 재시작해야 하는지 판단 트래픽을 받을 준비가 됐는지 판단 초기 기동이 끝났는지 판단 실패 시 원하는 조치가 재시작인지, 트래픽 차단인지부터 정합니다.
    실패 시 동작 컨테이너 재시작 Service 엔드포인트에서 제외 성공 전까지 다른 프로브 보류, 실패 시 컨테이너 재시작 RESTARTS, Events, EndpointSlice를 같이 봐야 오판이 줄어듭니다.
    적합한 검사 이벤트 루프 멈춤, 데드락, 기본 핸들러 무응답 DB 연결, 캐시 준비, 필수 설정 로딩, 마이그레이션 대기 JVM/Node.js/.NET 앱의 긴 부팅, 모델 로딩, 초기 캐시 구성 외부 의존성 실패가 곧 프로세스 사망을 의미하지는 않습니다.
    과도하게 엄격할 때 재시작 루프, 콜드 스타트 비용 증가 정상 파드가 트래픽을 못 받아 가용 용량 감소 기동 실패 감지가 늦어짐 엄격함은 안정성이 아니라 운영 정책입니다. 비용을 함께 계산해야 합니다.
    주로 확인할 증거 Killing container, BackOff, --previous 로그 Ready 0/1, Endpoint/EndpointSlice 누락 기동 로그, 초기화 완료 시점, Startup 실패 이벤트 로그보다 먼저 Events를 보면 방향을 빨리 잡습니다.

    쿠버네티스 헬스 체크 기본 설정 예시

    아래 예시는 세 프로브를 분리한 Deployment입니다. 실제 애플리케이션 이미지가 /livez, /readyz, /startupz 엔드포인트를 제공한다는 전제로 봐주세요. /livez는 프로세스와 HTTP 서버가 응답 가능한지만 확인하고, /readyz는 실제 요청 처리 준비 상태를 확인합니다. /startupz는 초기화가 끝나기 전 Liveness가 끼어들지 않도록 막습니다. 운영 매니페스트에서는 이 정도 분리가 나중에 장애 분석 시간을 꽤 줄여줍니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: probe-demo
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: probe-demo
      template:
        metadata:
          labels:
            app: probe-demo
        spec:
          terminationGracePeriodSeconds: 30
          containers:
            - name: app
              image: your-registry.example.com/probe-demo:1.0.0
              ports:
                - name: http
                  containerPort: 8080
              startupProbe:
                httpGet:
                  path: /startupz
                  port: http
                periodSeconds: 5
                timeoutSeconds: 2
                failureThreshold: 30
              livenessProbe:
                httpGet:
                  path: /livez
                  port: http
                periodSeconds: 10
                timeoutSeconds: 2
                failureThreshold: 3
              readinessProbe:
                httpGet:
                  path: /readyz
                  port: http
                periodSeconds: 5
                timeoutSeconds: 2
                successThreshold: 1
                failureThreshold: 3

    값을 고를 때는 감이 아니라 앱의 실제 시간을 기준으로 잡습니다. 기동 로그에서 “서버 포트 바인딩 완료”, “마이그레이션 완료”, “캐시 로딩 완료”, “요청 처리 가능” 시점을 분리해 보세요. initialDelaySeconds를 길게 늘리는 방식은 단순하지만, 앱이 빨리 떠도 무조건 기다립니다. 반면 startupProbe는 성공하는 순간 다음 단계로 넘어가므로, 느린 기동과 빠른 회복을 같이 다루기 좋습니다.

    프로브 파라미터는 서로 곱해져 운영 결과를 만듭니다. 예를 들어 periodSeconds: 10, failureThreshold: 3, timeoutSeconds: 2라면 실패를 확정하기까지 여러 번의 검사 주기가 필요합니다. 대략 30초 안팎으로 생각할 수 있지만, 정확한 감지 시간은 스케줄링과 응답 지연에 영향을 받습니다. 고정 수치처럼 외우기보다 “얼마나 빨리 빼고 얼마나 늦게 죽일 것인가”라는 정책으로 보는 편이 낫습니다.

    매니페스트에서 확인할 필드는 httpGet.path, port, periodSeconds, timeoutSeconds, failureThreshold, successThreshold입니다. 참고로 successThreshold는 Liveness와 Startup에서는 1이어야 하고, Readiness에서만 1보다 크게 둘 수 있습니다. Readiness 복귀를 일부러 보수적으로 만들고 싶을 때만 조정하세요.

    실전 디버깅: Kubernetes 프로브 트러블슈팅 명령어

    장애가 났을 때 저는 애플리케이션 로그보다 먼저 쿠버네티스 이벤트를 봅니다. 이벤트는 kubelet이 왜 컨테이너를 죽였는지, 왜 Ready에서 제외했는지 비교적 솔직하게 남깁니다. 아래 순서대로 보면 “앱 문제인지, 프로브 설정 문제인지, Service 라우팅 문제인지”가 빠르게 갈립니다.

    1. 파드의 Ready 상태와 재시작 횟수를 확인합니다.
    2. Events에서 Liveness probe failed, Readiness probe failed, Startup probe failed를 찾습니다.
    3. 현재 로그와 이전 컨테이너 로그를 비교합니다.
    4. Service, Endpoint, EndpointSlice에 파드 IP가 들어갔는지 확인합니다.
    5. 클러스터 내부에서 DNS, 포트, HTTP 경로를 직접 호출합니다.
    NS=default
    APP=probe-demo
    POD=$(kubectl get pod -n "$NS" -l app="$APP" -o jsonpath='{.items[0].metadata.name}')
    
    kubectl get pods -n "$NS" -l app="$APP" -o wide
    kubectl describe pod "$POD" -n "$NS"
    kubectl logs "$POD" -n "$NS" --all-containers=true --tail=200
    kubectl logs "$POD" -n "$NS" --previous --tail=200
    kubectl get events -n "$NS" --sort-by=.lastTimestamp
    kubectl get svc "$APP" -n "$NS"
    kubectl get endpoints "$APP" -n "$NS"
    kubectl get endpointslice -n "$NS" -l kubernetes.io/service-name="$APP"

    해석은 이렇게 합니다. Liveness probe failed 다음에 Killing container가 이어지면 재시작 원인은 Liveness입니다. Readiness probe failed만 반복되고 RESTARTS가 늘지 않으면 파드는 살아 있지만 Service 트래픽에서 제외된 겁니다. connection refused는 포트 미오픈, 잘못된 포트명, 앱 기동 전 검사 시작, 또는 앱이 127.0.0.1에만 바인딩된 경우를 의심합니다. HTTP probe failed with statuscode: 404는 대개 경로 오타입니다. context deadline exceeded나 timeout 계열 메시지는 앱 내부 지연, CPU throttling, I/O 대기, GC 지연까지 같이 봐야 합니다.

    kubectl run curl-debug \
      -n default \
      --image=curlimages/curl:8.5.0 \
      --restart=Never \
      --rm -it \
      -- sh
    
    # 디버그 파드 안에서 실행
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local/readyz
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local/livez
    curl -sv --max-time 3 http://probe-demo.default.svc.cluster.local:80/readyz

    여기서 중요한 건 “Service DNS로 되는가”와 “Pod IP로 직접 되는가”를 나눠 보는 겁니다. Service DNS는 실패하는데 Pod IP 직접 호출은 성공하면 Service selector나 EndpointSlice 쪽을 봅니다. Pod IP 직접 호출도 실패하면 컨테이너 포트, 앱 바인딩 주소, 경로, 응답 시간을 봅니다. 프로브는 kubelet이 Pod 네트워크의 Pod IP로 직접 보내는 검사라서, Ingress에서 보이는 증상과 다를 수 있습니다.

    재현 가능한 시나리오: Readiness Probe 오류 만들고 확인하기

    테스트 클러스터에서 일부러 잘못된 Readiness 경로를 넣어보면 동작이 선명해집니다. 아래 Pod는 nginx를 띄우지만 존재하지 않는 /not-ready를 Readiness로 검사합니다. 컨테이너는 실행되지만 Ready가 되지 않고, Service 엔드포인트에도 들어가지 않는 상태를 만들 수 있습니다. 이거 한 번 직접 보면 실장애 때 감이 훨씬 빨리 옵니다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: bad-readiness
      labels:
        app: bad-readiness
    spec:
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /not-ready
              port: http
            initialDelaySeconds: 3
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: bad-readiness
    spec:
      selector:
        app: bad-readiness
      ports:
        - name: http
          port: 80
          targetPort: http
    kubectl apply -f bad-readiness.yaml
    kubectl get pod bad-readiness -w
    kubectl describe pod bad-readiness
    kubectl get endpoints bad-readiness
    kubectl get endpointslice -l kubernetes.io/service-name=bad-readiness
    kubectl delete -f bad-readiness.yaml

    정상적인 관찰 결과는 이렇습니다. kubectl get pod에서 READY가 0/1로 보이고, describe Events에는 Readiness 실패가 찍힙니다. 하지만 RESTARTS는 증가하지 않습니다. Service의 Endpoint 또는 EndpointSlice도 비어 있거나 해당 파드 IP가 빠져 있어야 합니다. 이 작은 실험을 해두면 “왜 컨테이너는 Running인데 트래픽이 안 가지?”라는 질문에 훨씬 빨리 답할 수 있습니다.

    반대로 Liveness 경로를 잘못 지정하면 결과가 달라집니다. READY 이전에 컨테이너가 재시작될 수 있고, Events에는 Liveness 실패와 컨테이너 종료 메시지가 같이 나타납니다. 같은 404라도 Readiness에서는 트래픽 제외, Liveness에서는 재시작입니다. 이 차이를 머리에 넣어두면 디버깅 방향이 흔들리지 않습니다.

    제가 겪은 Liveness Probe 실패 패턴

    가장 위험했던 패턴은 종합 건강검진식 Liveness였습니다. /health 요청 하나에 DB 쿼리, Redis ping, 외부 API 호출, 디스크 체크를 전부 넣는 방식입니다. 겉보기에는 꼼꼼합니다. 그런데 DB가 순간적으로 느려지면 모든 파드가 Liveness 실패로 재시작됩니다. 재시작된 파드는 다시 DB 커넥션을 만들고, 캐시를 채우고, 준비되지 않은 상태에서 또 검사를 받습니다. 문제의 뿌리는 DB 지연인데 증상은 애플리케이션 재시작 폭풍으로 바뀝니다.

    제가 쓰는 기준은 꽤 엄격합니다. Liveness는 “이 프로세스를 죽이면 나아지는가?”에 예라고 답할 수 있을 때만 실패시킵니다. 외부 의존성이 실패했지만 프로세스가 요청 큐를 비우고 회복할 수 있다면 Liveness가 아니라 Readiness입니다. 이 기준이 약간 보수적으로 보여도, 운영에서는 불필요한 재시작을 줄이는 쪽이 대체로 더 싸고 안전했습니다.

    • Liveness: 기본 HTTP 핸들러, 이벤트 루프, 내부 상태 플래그처럼 가볍고 로컬인 검사만 둡니다.
    • Readiness: DB 연결, 브로커 구독, 필수 설정 로딩, 캐시 워밍업처럼 트래픽 처리 조건을 둡니다.
    • Startup: 앱 부팅이 느리거나 초기화 단계가 여러 개인 서비스에서 Liveness 개입을 늦춥니다.
    • 외부 API: 핵심 경로가 아니라면 Readiness에서도 과하게 엄격하게 묶지 않습니다. 서킷 브레이커와 graceful degradation이 더 나은 경우가 많습니다.
    startupProbe:
      httpGet:
        path: /startupz
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 30
    livenessProbe:
      httpGet:
        path: /livez
        port: 8080
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /readyz
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 3

    이 구성을 그대로 복사하기보다, 먼저 앱의 실제 기동 경로를 관찰하세요. 포트가 열리는 시점과 요청을 안전하게 처리할 수 있는 시점은 다를 수 있습니다. 특히 Java, .NET, 대형 Node.js 앱처럼 초기 로딩이 긴 서비스는 포트가 먼저 열리고 내부 초기화가 나중에 끝나는 경우가 있습니다. 이때 Liveness를 빨리 켜면 멀쩡히 뜨는 중인 앱을 kubelet이 반복해서 죽입니다.

    검증/결과: 무엇을 보고 정상이라고 판단할까

    프로브 설정을 바꾼 뒤 “배포 성공”만 보고 끝내면 위험합니다. 제가 확인하는 기준은 네 가지입니다. 파드가 Ready인지, 재시작이 멈췄는지, Service 엔드포인트에 포함됐는지, 그리고 실패 이벤트가 더 이상 증가하지 않는지입니다. 이 확인 루틴은 짧지만, 운영에서는 진짜 편하더라고요.

    NS=default
    APP=probe-demo
    
    kubectl rollout status deployment/$APP -n $NS
    kubectl get pods -n $NS -l app=$APP -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount,PHASE:.status.phase,NODE:.spec.nodeName'
    kubectl get endpoints $APP -n $NS -o wide
    kubectl get endpointslice -n $NS -l kubernetes.io/service-name=$APP -o wide
    kubectl describe deployment $APP -n $NS
    kubectl get events -n $NS --field-selector involvedObject.kind=Pod --sort-by=.lastTimestamp

    판단은 이렇게 가져갑니다. READY가 모든 컨테이너 수와 일치하면 요청을 받을 준비가 된 상태입니다. RESTARTS가 계속 늘면 Liveness 실패 또는 애플리케이션 크래시를 봅니다. EndpointSlice에 파드 IP가 없으면 Readiness 실패, Service selector 불일치, 포트명 불일치, 또는 라벨 문제를 봅니다. connection refused는 포트와 바인딩 주소, timeout은 앱 지연과 리소스 압박, 404는 경로 불일치부터 확인하세요.

    Kubernetes 프로브 트러블슈팅을 위한 kubectl 진단 결과 화면

    운영 대시보드가 있다면 프로브 실패율만 따로 보는 것도 좋습니다. 다만 프로브 자체가 너무 자주 실행되면 노드와 애플리케이션에 부하가 됩니다. 특히 exec 프로브는 컨테이너 안에서 프로세스를 실행하므로, 파드 수가 많고 주기가 짧으면 비용이 눈에 띄게 늘 수 있습니다. HTTP나 TCP로 충분한 경우라면 굳이 셸 명령을 매번 실행하지 않는 편이 낫습니다.

    Kubernetes 프로브 모범 사례와 선택 기준

    제가 팀에 리뷰할 때 가장 많이 보는 항목은 세 가지입니다. 첫째, Liveness가 외부 의존성을 검사하지 않는가. 둘째, Readiness가 실제 트래픽 처리 가능성을 반영하는가. 셋째, 느린 기동을 initialDelaySeconds 땜질이 아니라 Startup Probe로 풀었는가. 이 세 가지만 잡아도 Kubernetes 프로브 트러블슈팅의 절반은 줄어듭니다.

    상황별 추천

    • 애플리케이션 시작이 느리다면: Startup Probe를 추가하고 Liveness의 initialDelaySeconds를 과하게 늘리지 않습니다.
    • DB가 잠깐 끊길 수 있다면: DB 검사는 Readiness Probe에 둡니다. Liveness에서 DB를 때리지 않습니다.
    • 프로세스가 가끔 멈춘다면: 로컬 핸들러 기반의 가벼운 Liveness Probe로 재시작하게 합니다.
    • 경로가 자주 바뀐다면: 비즈니스 API와 헬스 체크 엔드포인트를 분리하고, 라우팅 리팩터링에 영향받지 않는 고정 경로를 씁니다.
    • 서비스 메시나 Ingress를 쓴다면: kubelet의 Pod 프로브, Service 라우팅, 외부 로드밸런서 헬스 체크를 서로 다른 계층으로 보고 분리해서 검증합니다.
    • 파드 수가 많다면: 짧은 periodSeconds와 무거운 exec 프로브 조합을 피합니다. 헬스 체크도 트래픽입니다.

    자주 묻는 질문

    Q. Liveness와 Readiness를 같은 경로로 써도 되나요?
    가능은 하지만 운영 기본값으로 추천하지 않습니다. 같은 경로가 DB나 외부 API를 검사하면 Readiness 실패로 끝날 문제가 Liveness 재시작으로 커질 수 있습니다. 정말 같은 경로를 써야 한다면 그 경로는 로컬 상태만 검사해야 합니다.

    Q. TCP Probe와 HTTP Probe 중 뭘 써야 하나요?
    HTTP 서비스라면 HTTP Probe가 보통 더 낫습니다. 상태 코드와 경로로 의도를 표현할 수 있기 때문입니다. TCP Probe는 포트가 열렸는지만 확인하므로, 앱이 내부 초기화 중이어도 성공할 수 있습니다.

    Q. Exec Probe는 언제 쓰나요?
    HTTP 엔드포인트를 만들기 어려운 배치성 프로세스나 레거시 앱에서 컨테이너 내부 상태 파일·명령 결과를 확인할 때 씁니다. 다만 명령 실행 비용, 이미지 안의 바이너리 존재 여부, 셸 의존성을 반드시 확인해야 합니다.

    Q. gRPC 서비스는 어떻게 하나요?
    gRPC Health Checking Protocol을 구현했다면 Kubernetes v1.27부터 안정화된 gRPC probe를 검토할 수 있습니다. gRPC probe는 HTTP/TCP probe와 달리 named port를 지원하지 않으므로 숫자 포트를 명시해야 합니다. 서비스 이름과 포트 설정을 명확히 하고, HTTP 게이트웨이와 혼동하지 않도록 매니페스트 리뷰를 거치는 편이 좋습니다.

    Liveness Probe Readiness Probe Startup Probe 선택 기준 요약

    공식 동작 기준은 Kubernetes 문서의 Configure Liveness, Readiness and Startup Probes와 Liveness, Readiness, and Startup Probes를 기준으로 확인하되, 실제 값은 서비스의 기동 로그와 장애 패턴에 맞춰 조정하세요. 문서의 예시는 출발점이지 운영 정책 그 자체는 아닙니다. 관련 배포 전략이나 장애 대응 문서가 있다면 내부 링크로 함께 묶어두면 독자가 다음 글로 자연스럽게 이동합니다.

    마무리: 운영에서 덜 아프게 쓰는 결론

    프로브 설계의 기준은 명확합니다. 재시작하면 좋아지는 문제만 Liveness에 넣고, 트래픽만 빼면 되는 문제는 Readiness에 넣고, 느린 기동은 Startup Probe로 보호하세요. 이 원칙을 어기면 프로브는 장애 감지 장치가 아니라 장애 증폭 장치가 됩니다.

    일반 웹 서비스라면 저는 이렇게 시작합니다. Liveness는 /livez에서 프로세스와 기본 핸들러만 확인합니다. Readiness는 /readyz에서 DB 연결, 필수 설정, 내부 큐 상태처럼 요청 처리 조건을 확인합니다. 기동이 느린 서비스는 /startupz를 두고, Startup Probe가 성공하기 전까지 Liveness가 개입하지 않게 합니다. 배포 후에는 kubectl describe pod, kubectl logs --previous, kubectl get endpoints, kubectl get endpointslice를 함께 확인합니다. 이 네 가지를 습관처럼 보면 Kubernetes 프로브 트러블슈팅에서 가장 흔한 삽질을 꽤 많이 줄일 수 있습니다.

  • 컨테이너는 어떻게 격리되나 — namespaces와 cgroups 원리

    “컨테이너는 가벼운 VM”이라고들 하지만, 사실 둘은 원리가 완전히 다릅니다. 컨테이너는 가상 머신이 아니라 “격리된 프로세스”일 뿐입니다. 그 격리를 만드는 두 가지 리눅스 커널 기능 — namespaces와 cgroups — 를 이해하면 LXC·Docker가 왜 그렇게 가벼운지, 그리고 왜 Docker-in-LXC에 특별한 설정이 필요한지가 보입니다.

    1. 컨테이너 vs VM — 근본 차이

    VM 컨테이너
    격리 방식 하드웨어 가상화 커널 기능(격리된 프로세스)
    커널 게스트마다 별도 호스트와 공유
    부팅 OS 부팅(수십 초) 프로세스 시작(즉시)
    오버헤드 큼 거의 없음

    컨테이너는 호스트 커널을 그대로 공유하면서, “이 프로세스는 자기만의 세상에 있는 것처럼” 보이게 속입니다. 그 속임수의 두 축이 namespaces와 cgroups입니다.

    2. namespaces — “무엇을 볼 수 있는가”

    namespace는 프로세스가 보는 시야를 격리합니다. 종류별로 각각 다른 자원을 가립니다.

    namespace 격리 대상
    PID 프로세스 목록(자기 PID 1)
    NET 네트워크(자기 IP·인터페이스)
    MNT 파일시스템 마운트
    UTS 호스트명
    IPC 프로세스 간 통신
    USER 사용자·권한(UID 매핑)

    그래서 컨테이너 안에서 ps를 치면 자기 프로세스만 보이고, 자기만의 IP를 갖습니다. 실제로는 호스트의 한 프로세스일 뿐인데도요. USER namespace가 바로 “unprivileged 컨테이너”의 핵심 — 컨테이너의 root를 호스트의 비특권 사용자로 매핑해 보안을 높입니다.

    3. cgroups — “얼마나 쓸 수 있는가”

    namespace가 “시야”라면, cgroups(control groups)는 “자원 사용량 제한”입니다.

    • CPU: 이 컨테이너는 코어 2개까지
    • 메모리: 512MB 넘으면 제한
    • I/O: 디스크 대역폭 제한

    Proxmox에서 LXC에 cores·memory를 지정하면, 내부적으로 cgroups가 그 한도를 강제합니다. vCPU 오버커밋이 가능한 것도 cgroups가 실제 사용량을 조율하기 때문입니다.

    4. 그래서 Docker-in-LXC가 까다롭다

    컨테이너가 이 커널 기능들에 의존하다 보니, 컨테이너 안에서 또 컨테이너(LXC 안 Docker)를 돌리려면 커널 기능 접근을 열어줘야 합니다.

    • nesting=1: 중첩된 namespace 생성 허용
    • keyctl=1: Docker가 쓰는 커널 키링 접근 허용

    원리를 알고 나면 이 플래그들이 왜 필요한지 자연스럽게 이해됩니다 — 컨테이너의 격리 메커니즘을 한 겹 더 쌓는 것이니까요.

    5. 정리

    컨테이너 = namespaces(시야 격리) + cgroups(자원 제한)로 만든 “격리된 프로세스”입니다. VM처럼 커널을 통째로 복제하지 않으니 가볍고 빠른 거죠. 이 원리 하나를 잡으면 LXC·Docker의 동작, unprivileged의 의미, nesting 플래그의 이유가 전부 하나로 꿰어집니다.

  • Proxmox 백업 vzdump 완전 정리 — 스냅샷 말고 진짜 백업 걸기

    지난 ZFS 글에서 “스냅샷은 백업이 아니다”라고 했습니다. 그럼 Proxmox에서 진짜 백업은 어떻게 할까요? 답은 vzdump입니다. VM·컨테이너를 통째로 백업하는 Proxmox 내장 도구인데, 솔직히 저도 아직 예약 백업을 안 걸어둔 상태라 이번 기회에 제대로 정리합니다.

    1. vzdump이 하는 일

    vzdump은 VM/LXC 전체를 하나의 아카이브로 백업합니다. 스냅샷(같은 디스크 안의 되돌리기)과 달리, 별도 스토리지(NAS 등)에 복제본을 만드는 진짜 백업입니다.

    • 디스크가 통째로 죽어도 복구 가능
    • 다른 노드·다른 서버로 이전 가능
    • 보관 개수를 정해 자동 로테이션

    2. 백업 모드 3가지

    모드 다운타임 특징
    snapshot 거의 없음 실행 중 백업(권장)
    suspend 잠깐 정지 중간 안전성
    stop 완전 정지 가장 일관적

    대부분 snapshot 모드면 충분합니다. 실행 중인 서비스를 멈추지 않고 백업하니까요(단, DB처럼 일관성이 중요하면 stop 고려).

    3. 수동 백업 — 한 줄

    # LXC 113을 NAS 스토리지에 snapshot 모드로 백업
    vzdump 113 --mode snapshot --storage nas-backup --compress zstd
    
    # 여러 개 한 번에
    vzdump 112 113 124 --mode snapshot --storage nas-backup --compress zstd

    --compress zstd는 빠르고 압축률도 좋아 요즘 표준입니다. 백업 파일은 지정한 스토리지(예: NAS)에 .vma.zst(VM) / .tar.zst(LXC)로 쌓입니다.

    4. 예약 백업 (여기가 진짜 핵심)

    수동은 잊어버립니다. Proxmox 웹UI Datacenter → Backup → Add에서 예약을 겁니다. 핵심 설정은 —

    • 대상: 백업할 VM/LXC 선택(또는 전체)
    • 스토리지: 반드시 다른 디스크/NAS(같은 풀에 백업하면 의미 없음)
    • 스케줄: 예) 매일 새벽 3시
    • 보관(Retention): 일 7 / 주 4 / 월 3 등
    # /etc/pve/jobs.cfg 에 생성되는 예약 백업(예시)
    vzdump: backup-daily
    	schedule 03:00
    	storage nas-backup
    	mode snapshot
    	compress zstd
    	prune-backups keep-daily=7,keep-weekly=4

    5. 복구

    백업이 있으면 복구는 간단합니다. 웹UI에서 백업 파일 선택 → Restore, 또는 CLI로:

    # LXC 복구 (새 ID 113으로)
    pct restore 113 /mnt/nas-backup/dump/vzdump-lxc-113-....tar.zst --storage local-zfs

    중요: 백업은 복구 테스트까지 해봐야 진짜 백업입니다. 한 번쯤 테스트 ID로 복구해 정상 부팅을 확인해 두세요.

    6. 정리 — 스냅샷 + vzdump = 완성

    스냅샷은 위험한 작업 직전의 빠른 되돌리기, vzdump은 다른 스토리지에 두는 진짜 백업입니다. 둘은 역할이 다르니 둘 다 써야 합니다. 저처럼 “스냅샷은 하는데 예약 백업은 아직”인 분이라면, 오늘 Datacenter → Backup에서 매일 백업 하나만 걸어두세요. 미래의 내가 고마워할 겁니다.

  • [Game] 게임 리뷰 분석 가이드: 성능·안정성·모딩 체크

    [Game] 게임 리뷰 분석 가이드: 성능·안정성·모딩 체크

    게임 리뷰 분석 가이드: 성능·안정성·모딩 체크

    게임 리뷰 분석을 기술 글답게 만들려면 “재미있다”, “최적화가 별로다”에서 멈추면 아쉽습니다. 독자가 실제로 알고 싶은 건 더 구체적이거든요. 내 PC에서 버틸지, 특정 구간의 끊김이 그래픽 카드 문제인지 저장 장치 문제인지, 튕겼을 때 원인을 좁힐 수 있는지, 모드를 깔았다가 업데이트 후 망가져도 되돌릴 수 있는지 말이죠.

    저는 서버실에서 장애 보고서를 쓰고, 홈랩에서 이런저런 시스템을 굴리며 13년 가까이 로그와 지표를 붙들고 살았습니다. 그 습관으로 게임을 보면 리뷰의 초점이 조금 달라집니다. 게임은 감상 대상이기도 하지만, 동시에 CPU, GPU, 저장 장치, 드라이버, 런타임, 파일 구조가 같이 움직이는 작은 시스템입니다. 좋은 기술 리뷰는 그 시스템이 언제 안정적이고, 언제 무너지고, 어디까지 독자가 직접 회복할 수 있는지를 보여줘야 합니다.

    그래서 이 글에서는 성능, 안정성, 모딩 지원을 따로 보되 마지막에는 하나의 판단으로 묶겠습니다. 숫자를 꾸며내지는 않겠습니다. 대신 실제 리뷰에서 바로 복사해 쓸 수 있는 명령어, 설정 파일, 로그 해석 기준, 실패 패턴을 최대한 구체적으로 남기겠습니다. 관련 벤치마크 글을 운영한다면 이 글을 기준 문서로 내부 링크해두면 독자 흐름도 훨씬 좋아집니다.

    게임 리뷰 분석 전체 흐름 다이어그램

    캡션: 게임 리뷰 분석을 성능, 안정성, 모딩 지원 관점으로 나눠 보는 전체 흐름입니다.

    1. 게임 리뷰 분석은 세 줄 평이 아니라 관측 설계입니다

    기술 블로거가 먼저 정해야 할 것은 도구가 아니라 질문입니다. “이 게임이 빠른가?”보다 “어떤 조건에서 체감이 무너지는가?”가 낫고, “잘 튕기는가?”보다 “같은 조건에서 재현되는 장애인가?”가 낫습니다. 이 차이가 글의 신뢰도를 가릅니다.

    제가 쓰는 기본 축은 세 가지입니다. Performance는 평균 FPS보다 프레임타임의 일관성을 봅니다. Stability는 크래시 자체보다 재현 조건과 로그의 반복성을 봅니다. Modding Support는 모드 개수보다 설치, 비활성화, 롤백, 업데이트 후 복구 가능성을 봅니다.

    분석 축 리뷰어가 확인할 것 좋은 질문 피해야 할 단정
    게임 성능 리뷰 FPS, 프레임타임, 1% low 성격의 하위 구간, CPU/GPU 사용률, VRAM, 디스크 I/O 평균은 높아도 특정 이벤트에서 끊기는가? 평균 FPS 하나로 최적화 전체를 평가
    게임 안정성 평가 크래시 시점, 이벤트 로그, Proton 로그, 드라이버 오류, 저장 실패, 반복 재현성 같은 장면과 옵션에서 다시 발생하는가? 한 번 튕긴 경험을 게임 전체 문제로 일반화
    모딩 지원 분석 모드 폴더, 설정 파일 분리, 로더, 충돌 관리, 세이브 백업, 업데이트 대응 모드 제거 후 원래 상태로 돌아갈 수 있는가? 모드가 많다는 이유만으로 모딩 친화적이라고 평가

    세 축을 분리하는 이유는 원인이 섞이기 쉽기 때문입니다. 예를 들어 자동 저장 순간 끊김은 GPU 옵션 문제가 아니라 디스크 쓰기, 압축, 세이브 직렬화 문제일 수 있습니다. 모드 적용 후 크래시는 게임 자체 안정성보다 DLL 주입, 스크립트 로더 버전, 원본 파일 덮어쓰기 문제일 수 있고요. 리뷰에서 이 구분을 해주면 독자는 “내가 겪는 문제와 같은 종류인지”를 훨씬 빨리 판단합니다.

    2. 게임 성능 리뷰는 평균 FPS보다 프레임타임을 먼저 봅니다

    FPS는 결과값이고 프레임타임은 흐름입니다. 평균 FPS가 높아도 프레임타임 그래프가 톱니처럼 솟으면 체감은 거칠어집니다. 반대로 평균은 아주 높지 않아도 프레임 간격이 일정하면 플레이는 안정적으로 느껴질 수 있습니다. 저는 리뷰에서 평균 FPS를 제목처럼 쓰지 않고, 프레임타임을 본문 판단의 중심에 둡니다.

    Linux, Steam Deck, Proton 환경에서는 MangoHud가 리뷰용 관측 도구로 꽤 좋습니다. 다만 처음부터 센서를 전부 켜면 오버레이가 스크린샷을 잡아먹습니다. 리뷰 목적이라면 화면 캡처용 설정과 로그 수집용 설정을 나눠 두는 편이 낫습니다. 이거 진짜 편하더라고요.

    # Steam 실행 옵션: 오버레이를 켭니다.
    mangohud %command%
    
    # Proton 로그까지 함께 남길 때
    PROTON_LOG=1 mangohud %command%
    
    # 비 Steam 실행 파일 테스트
    mangohud ./GameExecutable
    
    # MangoHud 설정 디렉터리
    mkdir -p ~/.config/MangoHud
    nano ~/.config/MangoHud/MangoHud.conf

    아래 설정은 리뷰 초안 단계에서 쓰기 좋은 최소형입니다. FPS, 프레임타임, CPU/GPU, RAM/VRAM만 보고, 로그는 별도 폴더에 남깁니다. MangoHud 설정 파일의 output_folder에는 셸의 ~가 항상 기대대로 풀린다고 가정하지 말고, 실제 홈 경로를 적는 편이 안전합니다.

    fps
    frametime
    cpu_stats
    gpu_stats
    ram
    vram
    frame_timing=1
    histogram
    output_folder=/home/YOURUSER/mangohud_logs
    log_duration=60
    autostart_log=1

    여기서 주의할 점이 있습니다. 로그 길이를 무작정 길게 잡으면 비교가 어려워집니다. 같은 저장 지점에서 60초, 같은 이동 루트에서 90초처럼 구간을 고정해야 합니다. 리뷰마다 측정 시간이 다르면 “이 게임은 평균이 높다/낮다”보다 “서로 다른 장면을 비교했다”가 되어버립니다.

    상황 먼저 볼 지표 의심할 원인 리뷰 문장 방향
    평균 FPS는 높은데 순간적으로 끊김 프레임타임 스파이크, VRAM 사용량, 저장 시점 셰이더 컴파일, 스트리밍, 자동 저장, VRAM 압박 평균 성능보다 일관성이 문제라고 설명
    GPU 사용률이 계속 높고 옵션을 낮추면 개선 GPU 사용률, 해상도, 업스케일링, 그림자/반사 옵션 GPU 병목 그래픽 옵션 조정 효과가 있는 게임으로 평가
    CPU 일부 코어만 높고 GPU가 놀고 있음 CPU 코어별 사용률, 프레임타임, 군중/AI 구간 메인 스레드 병목, 시뮬레이션 부하 해상도를 낮춰도 개선이 작을 수 있다고 안내
    자동 저장 아이콘과 함께 끊김 iostat await, 디스크 쓰기, 프레임타임 저장 처리, 압축, 동기식 파일 쓰기 저장 장치와 세이브 처리 영향을 분리해서 설명

    3. 리뷰용 측정 루틴: 같은 조건을 반복해야 글이 단단해집니다

    제가 가장 많이 고친 리뷰 초안은 “여러 번 해봤는데 대충 비슷했다”는 식의 글이었습니다. 독자는 그 말을 믿고 싶어도 판단할 재료가 없습니다. 기술 리뷰라면 테스트 루틴을 간단히라도 남겨야 합니다. 고급 장비보다 중요한 건 반복 가능한 절차입니다.

    1. 해상도, 화면 모드, 그래픽 프리셋, 업스케일링 옵션을 고정합니다.
    2. 드라이버, OS, 커널, Proton 또는 런타임 버전을 기록합니다.
    3. 게임 내 같은 저장 지점, 같은 이동 경로, 같은 전투나 로딩 구간을 씁니다.
    4. 첫 실행과 두 번째 실행을 구분합니다. 셰이더 캐시 때문에 양상이 달라질 수 있습니다.
    5. 평균보다 튄 지점을 캡처하고, 그 순간의 시스템 지표를 같이 봅니다.

    Linux 환경에서는 sysstat 도구의 sar, iostat, pidstat 조합만으로도 병목 방향을 꽤 좁힐 수 있습니다. 아래 명령은 게임 실행 직후 별도 터미널에서 돌리는 용도입니다.

    # sysstat 설치 예시: 배포판에 따라 패키지 관리자는 다릅니다.
    # Debian/Ubuntu 계열
    sudo apt install sysstat
    
    # Fedora 계열
    sudo dnf install sysstat
    
    # Arch 계열
    sudo pacman -S sysstat
    # 게임 프로세스 PID 확인
    pgrep -af GameExecutable
    
    # CPU 전체 사용률과 iowait 관찰
    sar -u 1 60 | tee ~/game-review-sar.log
    
    # 디스크 지연, 큐, 사용률 관찰
    iostat -xz 1 60 | tee ~/game-review-iostat.log
    
    # 최신 게임 프로세스 하나를 대상으로 CPU, 메모리, 디스크 I/O 추적
    pidstat -rud -p $(pgrep -n GameExecutable) 1 60 | tee ~/game-review-pidstat.log

    iostat -xz에서 await가 프레임타임 스파이크와 같은 타이밍에 튀면 저장 장치 대기를 의심할 수 있습니다. 다만 %util만 보고 단정하면 위험합니다. 빠른 SSD에서도 순간적인 동기 쓰기나 세이브 압축 때문에 체감 끊김이 생길 수 있고, 반대로 %util이 높아 보여도 실제 플레이 영향은 작을 수 있습니다. 저는 반드시 프레임타임 그래프와 이벤트 시점을 같이 맞춰 봅니다.

    프로세스 이름이 자주 바뀌는 게임은 pgrep이 빗나갈 수 있습니다. 이럴 때는 Steam 실행 옵션에 wrapper 스크립트를 넣거나, 게임 실행 직후 ps -eo pid,comm,args –sort=-%cpu | head로 실제 프로세스를 확인하는 편이 안전합니다. 리뷰 글에는 “GameExecutable 기준”이라고 쓰지 말고 실제 프로세스명이나 확인 방법을 적어주세요.

    게임 성능 리뷰 측정 도구 구성 화면

    캡션: MangoHud, sar, iostat, pidstat를 함께 사용하는 리뷰 측정 구성 예시입니다.

    4. 게임 안정성 평가는 이벤트 뷰어와 안정성 모니터를 같이 봅니다

    Windows 환경에서는 오버레이만으로 안정성을 판단하기 어렵습니다. 크래시가 났다면 이벤트 뷰어에서 Application Error를 보고, 안정성 모니터에서 같은 문제가 반복되는지 확인하는 게 빠릅니다. 리뷰에 모든 로그를 붙일 필요는 없지만, 오류 모듈명과 발생 조건은 기록해두는 편이 좋습니다.

    # 최근 24시간 Application Error 이벤트 확인
    Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='Application Error'; StartTime=(Get-Date).AddDays(-1)} |
      Select-Object TimeCreated, Id, ProviderName, Message |
      Format-List
    
    # 게임 이름으로 이벤트 메시지 필터링
    Get-WinEvent -LogName Application -MaxEvents 200 |
      Where-Object {$_.Message -match 'GameExecutable|Faulting application'} |
      Select-Object TimeCreated, Id, Message

    Windows 로그를 읽을 때 흔한 실수는 “Faulting module”만 보고 원인을 확정하는 겁니다. 예를 들어 특정 DLL이 보인다고 해서 그 DLL이 항상 범인은 아닙니다. 게임, 드라이버, 오버레이, 안티치트, 모드 로더가 같은 프로세스 안에서 엮일 수 있습니다. 리뷰에서는 “해당 모듈이 마지막 오류 구간에 반복적으로 등장했다” 정도로 쓰고, 드라이버나 모드와의 인과관계는 재현 테스트로 확인해야 합니다.

    간단한 분리 테스트는 이렇게 갑니다. 오버레이를 끄고 한 번, 모드를 끄고 한 번, 그래픽 프리셋을 낮추고 한 번, 같은 저장 지점에서 다시 실행합니다. 네 가지를 동시에 바꾸면 원인을 잃어버립니다. 서버 장애 분석에서 설정을 한꺼번에 바꾸면 사후 분석이 망가지는 것과 같습니다.

    5. Proton 로그는 마지막 줄보다 반복 패턴을 봐야 합니다

    Linux/Proton 환경에서 크래시를 다룰 때는 Proton 로그가 유용합니다. 다만 로그가 길고 경고가 많습니다. warn이 보인다고 전부 문제는 아닙니다. Wine/Proton 로그에는 정상 실행 중에도 경고가 쌓입니다. 제가 보는 기준은 마지막 줄이 아니라 “같은 구간에서, 같은 문자열 묶음이, 여러 번 반복되는가”입니다.

    # Steam 실행 옵션
    PROTON_LOG=1 %command%
    
    # 홈 디렉터리에 생성된 Steam Proton 로그 확인
    ls -lh ~/steam-*.log
    
    # 오류 후보만 추려 보기
    grep -Ei 'err:|warn:|crash|exception|fault|terminate|access violation' ~/steam-*.log | tail -n 120
    
    # 같은 패턴이 반복되는지 대략 집계
    grep -Eio 'err:[^ ]+|warn:[^ ]+|exception|access violation|fault' ~/steam-*.log | sort | uniq -c | sort -nr | head

    Proton 로그를 리뷰에 쓸 때는 원문을 길게 붙이지 않는 편이 좋습니다. 독자는 로그 전문보다 판단을 원합니다. 예를 들어 “Proton 로그의 마지막 구간에서 같은 access violation 패턴이 반복됐고, 모드를 끈 뒤 같은 저장 지점에서는 재현되지 않았습니다”처럼 조건과 변화만 적어도 충분히 실용적입니다.

    반대로 한 번만 난 오류, 다른 구간에서는 재현되지 않는 오류, 게임 업데이트 직후 사라진 오류는 조심해서 다뤄야 합니다. “제 환경에서는 한 차례 발생했지만 반복 재현은 되지 않았습니다”라고 쓰는 게 낫습니다. 이 문장은 약해 보이지만, 사실 리뷰 신뢰도를 올립니다. 단정하지 않는 것도 기술 글의 실력입니다.

    6. 실제 케이스: 자동 저장 순간 끊김을 GPU 문제로 착각했습니다

    제가 겪은 가장 헷갈리는 패턴은 자동 저장 순간의 끊김이었습니다. 처음에는 그림자 옵션과 반사 옵션을 낮췄습니다. 평균 FPS는 조금 달라졌지만 끊기는 순간은 그대로였습니다. 그때 프레임타임 그래프와 iostat 로그를 나란히 놓고 보니 자동 저장 아이콘이 뜨는 순간 await가 같이 솟았습니다. 그래픽 부하가 아니라 저장 처리와 겹친 이벤트성 병목에 가까웠던 겁니다.

    이런 경우 리뷰 문장은 이렇게 바뀌어야 합니다. “최적화가 나쁘다”가 아니라 “특정 자동 저장 시점에서 프레임타임 스파이크가 반복됐고, 그래픽 옵션 조정만으로는 사라지지 않았습니다. 디스크 활동 증가와 같은 타이밍이라 저장 처리 영향 가능성이 있습니다.” 이 정도면 단정은 피하면서도 독자가 자기 환경을 점검할 단서를 얻습니다.

    • 같은 저장 지점에서 반복되는 끊김이면 게임 이벤트와 연결해서 봅니다.
    • 옵션을 낮춰도 같은 타이밍에 끊기면 GPU 병목만으로 설명하지 않습니다.
    • 첫 실행에서만 심하고 두 번째 실행에서 줄어들면 셰이더 캐시나 스트리밍을 의심합니다.
    • 모드 적용 후에만 생기면 모드 로더, 스크립트, 파일 덮어쓰기 여부를 분리합니다.
    • 온라인 기능과 함께 생기면 네트워크, 인증, 클라우드 저장도 후보에 넣습니다.

    기술 블로거의 장점은 바로 이런 분리입니다. 감상 리뷰는 “끊긴다”에서 끝날 수 있지만, 분석 리뷰는 “언제, 무엇과 함께, 무엇을 바꿨을 때 사라지는가”까지 갑니다.

    7. 모딩 지원 분석은 설치 난이도보다 복구 난이도를 봅니다

    모딩 지원 분석에서 제가 가장 중요하게 보는 것은 모드 설치 성공률이 아닙니다. 원래 상태로 돌아가는 길입니다. 모드가 잘 깔려도 원본 파일을 덮어쓰고, 제거 방법이 불명확하고, 업데이트 후 세이브가 깨진다면 기술 리뷰에서는 높은 점수를 주기 어렵습니다.

    모딩 친화적인 게임은 보통 원본 파일, 사용자 설정, 세이브, 모드 파일을 분리합니다. 반대로 실행 파일 폴더에 DLL과 스크립트를 직접 넣고, 제거하려면 기억에 의존해 파일을 지워야 하는 구조라면 주의가 필요합니다. 특히 기술 블로거라면 “모드가 많다”보다 “모드가 망가졌을 때 회복 가능한가”를 써야 독자에게 도움이 됩니다.

    # 테스트 전 백업 폴더를 한 번만 계산해서 재사용합니다.
    BACKUP_DIR=~/game-review-backup/$(date +%Y%m%d-%H%M%S)
    mkdir -p "$BACKUP_DIR"
    
    # 설정과 세이브 백업 예시: 실제 게임 경로에 맞게 바꿔 쓰세요.
    cp -a ~/.config/GameName "$BACKUP_DIR/config" 2>/dev/null || true
    cp -a ~/.local/share/GameName "$BACKUP_DIR/save" 2>/dev/null || true
    cp -a ~/.steam/steam/steamapps/common/GameName "$BACKUP_DIR/install-snapshot" 2>/dev/null || true
    
    # 모드 적용 전후 변경 파일 확인
    find ~/.config/GameName ~/.local/share/GameName -type f -printf '%TY-%Tm-%Td %TH:%TM %p\n' 2>/dev/null | sort | tail -n 50

    위 예시는 일부러 단순하게 썼습니다. 다만 경로에 공백이 섞일 수 있어 백업 변수는 따옴표로 감쌌습니다. 모든 게임 폴더를 통째로 백업하면 디스크 공간을 많이 먹습니다. 대형 게임에서는 원본 설치 폴더 전체 백업보다 설정, 세이브, 모드 폴더, 변경 파일 목록을 남기는 쪽이 현실적입니다.

    결정 지점 A를 선택할 때 B를 선택할 때 리뷰에 남길 표현
    백업 범위 세이브·설정만 백업: 용량을 아끼고 빠르게 테스트할 때 설치 폴더까지 백업: 원본 파일 덮어쓰기 모드가 있을 때 복구 가능성은 모드 방식에 따라 달랐습니다
    모드 배포 Workshop·공식 로더: 비활성화와 업데이트 추적이 쉬울 때 수동 복사: 버전 고정이나 특수 패치가 필요할 때 설치보다 제거와 충돌 관리가 관건입니다
    테스트 순서 하나씩 적용: 원인 분리가 중요할 때 묶음 적용: 실제 플레이 환경을 빠르게 재현할 때 문제가 생기면 단일 모드 단위로 되돌려 확인했습니다
    업데이트 대응 자동 업데이트 유지: 일반 독자 환경을 반영할 때 버전 고정: 모드 호환성이 핵심일 때 패치 후 모드 유지 비용이 있는 게임입니다

    출처 불명의 실행 파일을 요구하는 모드는 특히 조심해야 합니다. 기술 리뷰에서 “잘 됩니다”만 쓰면 독자는 보안 위험을 놓칠 수 있습니다. 모드 관리자는 공식 배포처, 해시 제공 여부, 소스 공개 여부, 커뮤니티 검증 정도를 함께 봐야 합니다. 모든 독자가 샌드박스나 별도 테스트 PC를 갖고 있지는 않으니까요.

    8. 흔한 실패 모드와 근본 원인 후보

    리뷰를 오래 쓰다 보면 비슷한 실패 패턴이 반복됩니다. 중요한 건 증상 이름을 붙이는 게 아니라 원인 후보를 좁히는 겁니다. 아래 표는 제가 초안 검토할 때 자주 쓰는 체크리스트입니다.

    증상 흔한 근본 원인 확인 방법 추천 판단
    첫 전투나 첫 지역 진입 때만 큰 끊김 셰이더 컴파일, 에셋 스트리밍, 캐시 생성 같은 구간을 두 번째 실행했을 때 줄어드는지 확인 첫 실행 경험과 반복 플레이 경험을 구분해서 씁니다
    해상도를 낮춰도 FPS가 거의 안 오름 CPU 병목, 메인 스레드 병목, 시뮬레이션 부하 GPU 사용률이 낮은지, CPU 일부 코어가 높은지 확인 GPU 업그레이드만으로 해결될 문제처럼 쓰지 않습니다
    저장·로딩 순간 프레임타임 급등 동기식 파일 쓰기, 압축, 클라우드 저장, 느린 저장 장치 저장 아이콘, iostat await, 디스크 쓰기 타이밍 비교 저장 장치와 게임 저장 구조의 영향을 함께 설명합니다
    모드 적용 후 특정 메뉴에서 크래시 UI 스크립트 충돌, 로더 버전 불일치, 의존성 누락 모드를 하나씩 끄고 같은 메뉴 재진입 게임 자체 안정성과 모드 안정성을 분리합니다
    오버레이를 켰을 때만 이상 동작 후킹 충돌, 안티치트, 캡처 도구 충돌 Steam, Discord, GPU 오버레이를 각각 끄고 재현 측정 도구가 결과에 영향을 줬을 가능성을 적습니다

    이 표를 그대로 본문에 다 넣을 필요는 없습니다. 하지만 리뷰 작성자는 이 정도 분기표를 머릿속에 갖고 있어야 합니다. 그래야 “끊김이 있습니다”라는 문장을 “첫 실행의 셰이더 캐시성 끊김에 가깝습니다” 또는 “저장 이벤트와 겹치는 반복 스파이크입니다”로 바꿀 수 있습니다.

    9. 결과 정리: 독자가 바로 판단할 수 있는 형태로 씁니다

    결과 섹션은 문장이 화려할수록 오히려 약해질 때가 많습니다. 저는 리뷰 말미에 항상 같은 형식의 작은 판정표를 둡니다. 독자가 자기 환경과 비교하기 쉽기 때문입니다.

    • 테스트 환경: OS, CPU, GPU, RAM, 저장 장치 종류, 드라이버 또는 런타임 버전을 적었는가
    • 그래픽 조건: 해상도, 화면 모드, 프리셋, 업스케일링, 프레임 제한 여부를 적었는가
    • 측정 구간: 저장 지점, 이동 루트, 전투 구간, 반복 횟수를 설명했는가
    • 성능 해석: 평균 FPS와 프레임타임 스파이크를 구분했는가
    • 안정성 해석: 한 번 발생한 문제와 반복 재현 문제를 나눴는가
    • 모딩 해석: 설치, 비활성화, 충돌, 롤백, 업데이트 후 복구를 확인했는가
    • 한계 고지: 내 환경에서 확인한 범위와 확인하지 못한 범위를 분리했는가

    제가 추천하는 결론 문장 구조는 단순합니다. “이럴 땐 추천, 저럴 땐 보류”가 들어가야 합니다. 예를 들어 프레임타임이 안정적이고 크래시 재현이 없으며 모드 롤백도 쉬운 게임은 기술적으로 추천할 수 있습니다. 평균 FPS는 높지만 특정 이벤트에서 반복 스파이크가 있고 원인을 피하기 어렵다면 고주사율 환경의 독자에게는 보수적으로 권해야 합니다. 모딩은 풍부하지만 원본 파일 덮어쓰기와 수동 복구가 필수라면 초보자에게는 조심하라고 말해야 합니다.

    게임 리뷰 분석 결과 대시보드

    캡션: 성능 로그, 안정성 이벤트, 모딩 체크리스트를 한 화면에서 정리한 결과 대시보드 예시입니다.

    10. 게임 리뷰 분석 추천 기준: 이럴 땐 A, 저럴 땐 B

    리뷰의 마지막 판단은 애매하게 열어두지 않는 편이 좋습니다. 단, 자신이 확인한 범위 안에서만 단호해야 합니다. 제가 쓰는 기준은 아래와 같습니다.

    독자 상황 추천 판단 이유
    짧은 리뷰, 빠른 구매 판단이 목적 MangoHud나 기본 오버레이로 FPS와 프레임타임만 확인 복잡한 시스템 로그보다 체감 끊김 여부가 먼저입니다
    기술 블로그용 심층 리뷰 프레임타임 로그와 sar, iostat, pidstat를 함께 기록 CPU, GPU, 저장 장치 후보를 분리할 수 있습니다
    크래시 제보가 많은 게임 이벤트 로그나 Proton 로그를 켜고 같은 구간을 반복 한 번의 크래시보다 재현성이 판단의 핵심입니다
    모드가 핵심인 RPG·시뮬레이션 설치보다 비활성화, 백업, 업데이트 후 복구를 먼저 확인 오래 플레이할수록 유지보수 비용이 커집니다
    안티치트나 온라인 기능이 강한 게임 오버레이, 모드, 후킹 도구 사용을 보수적으로 접근 측정 도구나 모드가 실행 안정성에 영향을 줄 수 있습니다

    제가 독자에게 직접 권한다면 이렇게 말하겠습니다. “구매 전 성능이 궁금한 독자”에게는 평균 FPS보다 프레임타임 스파이크 유무를 먼저 보라고 하겠습니다. “문제가 생겼을 때 해결하며 플레이할 독자”에게는 로그와 재현 조건이 있는 리뷰를 고르라고 하겠습니다. “모드 플레이가 목적인 독자”에게는 모드 수보다 제거와 롤백이 쉬운 게임을 고르라고 하겠습니다.

    11. 자주 묻는 질문

    Q. 게임 성능 리뷰에서 평균 FPS만 적으면 안 되나요?

    짧은 인상평이라면 가능하지만, 분석 글이라면 부족합니다. 평균 FPS는 전체 요약이고 프레임타임은 체감의 흔들림입니다. 특히 “숫자는 높은데 끊긴다”는 불만은 평균 FPS만으로 설명하기 어렵습니다.

    Q. 1% low나 0.1% low를 꼭 써야 하나요?

    도구가 안정적으로 제공하고 측정 구간을 고정했다면 도움이 됩니다. 다만 짧은 구간에서 얻은 하위 백분위 수치는 튀는 이벤트 하나에 크게 흔들릴 수 있습니다. 저는 수치만 던지기보다 어떤 장면에서 하위 구간이 나빠졌는지 같이 씁니다.

    Q. 크래시 로그를 못 읽겠으면 어떻게 하나요?

    마지막 오류 한 줄을 해석하려고 애쓰기보다 반복 조건을 먼저 보세요. 같은 저장 지점, 같은 옵션, 같은 모드 조합에서 다시 발생한다면 리뷰에 쓸 가치가 있습니다. 반복되지 않으면 “확인됐지만 재현되지 않음”으로 남기는 편이 정직합니다.

    Q. 모딩 지원은 모드 개수로 판단하면 되나요?

    아닙니다. 모드 개수는 생태계의 크기를 보여주지만, 관리 품질을 보장하지는 않습니다. 설치 문서, 비활성화 방식, 원본 파일 보호, 버전 호환성, 세이브 복구 가능성이 더 중요합니다.

    Q. 벤치마크 수치를 넣지 않으면 글이 약해 보이지 않나요?

    수치가 있으면 좋지만, 조건 없는 수치는 오히려 위험합니다. 확인하지 않은 평균값을 꾸미는 것보다 테스트 환경, 측정 구간, 프레임타임 패턴, 재현 조건을 정확히 적는 쪽이 독자에게 더 쓸모 있습니다.

    12. 마무리: 기술 블로거라면 감상보다 원인 분리를 보여주세요

    게임 리뷰 분석은 감상문과 장애 분석의 중간쯤에 있습니다. 재미와 그래픽 이야기도 필요하지만, 기술 블로거의 가치는 원인 분리에 있습니다. 성능은 평균 FPS보다 프레임타임의 안정성을 보고, 안정성은 크래시 횟수보다 재현 조건을 보고, 모딩은 설치 성공보다 복구 가능성을 봐야 합니다.

    짧은 리뷰라면 MangoHud나 기본 오버레이로 FPS와 프레임타임을 잡는 것만으로도 충분합니다. 깊이 있는 리뷰라면 sar, iostat, pidstat 같은 시스템 지표를 같이 남기세요. Windows라면 이벤트 뷰어와 안정성 모니터를 확인하고, Proton 환경이라면 PROTON_LOG를 켜서 반복 패턴을 봅니다. 모딩이 핵심인 게임이라면 백업, 비활성화, 업데이트 후 복구 과정을 반드시 확인하세요.

    제가 내리는 기준은 분명합니다. 프레임타임이 안정적이고, 크래시가 반복 재현되지 않으며, 모드 제거와 롤백이 쉽다면 기술적으로 추천할 만합니다. 평균 성능은 높지만 특정 이벤트에서 반복적으로 끊기거나, 로그상 같은 문제가 재현되거나, 모드 복구가 수동 기억에 의존한다면 독자에게 조건부 추천으로 안내해야 합니다. 좋은 리뷰는 확신을 파는 글이 아니라, 독자가 자기 환경에서 판단할 수 있게 근거를 정리해주는 글입니다.

    게임 성능 안정성 모딩 지원 분석 요약 인포그래픽

    캡션: 게임 리뷰를 작성할 때 성능, 안정성, 모딩 항목별로 확인할 핵심 기준 요약입니다.

  • OpenStack 스토리지 완전 정리 — Cinder·Glance·Swift 뭐가 다른가

    OpenStack을 공부하다 보면 “스토리지가 왜 이렇게 종류가 많지?” 싶어집니다. Cinder, Glance, Swift — 이름은 다르지만 역할이 명확히 갈립니다. 세 가지를 한 번에 정리하면 프라이빗 클라우드의 스토리지 그림이 잡힙니다.

    1. 세 가지 스토리지, 역할이 다르다

    서비스 유형 역할 비유
    Glance 이미지 OS 템플릿 저장소 설치 ISO 창고
    Cinder 블록 인스턴스에 붙이는 디스크 외장 SSD
    Swift 오브젝트 파일·백업 저장(HTTP) 구글 드라이브/S3

    핵심은 “블록 vs 오브젝트”의 차이입니다. 이걸 이해하면 클라우드 스토리지 전체가 보입니다.

    2. 블록 스토리지(Cinder) — 디스크처럼

    Cinder는 인스턴스에 “디스크”를 붙였다 뗐다 하게 해줍니다. OS 입장에선 그냥 /dev/vdb 같은 블록 디바이스라, 포맷하고 마운트해서 씁니다.

    • 인스턴스를 지워도 볼륨은 남길 수 있음(데이터 보존)
    • 스냅샷·복제 가능
    • DB·앱 데이터처럼 빠른 읽기/쓰기가 필요한 곳에

    홈랩 랩에서 Cinder 백엔드는 보통 LVM(간단) 또는 Ceph(분산)로 붙입니다. 학습이면 LVM으로 시작하는 게 쉽습니다.

    3. 오브젝트 스토리지(Swift) — 파일을 HTTP로

    Swift는 디스크가 아니라 “파일 하나하나를 HTTP API로” 저장합니다. AWS S3와 같은 개념이죠.

    • 마운트하는 게 아니라 API로 업로드/다운로드
    • 사진·백업·로그처럼 대량·비정형 데이터에 적합
    • 수평 확장·복제로 내구성이 강함

    즉 “디스크가 필요하면 Cinder, 파일 보관소가 필요하면 Swift”입니다.

    4. 이미지 저장소(Glance) — 인스턴스의 출발점

    Glance는 인스턴스를 만들 때 쓰는 OS 이미지를 보관합니다. 우리가 Proxmox cloud-init 템플릿을 만들어 두는 것과 정확히 같은 개념입니다 — 미리 준비된 이미지에서 인스턴스를 찍어내는 거죠.

    Glance(이미지) → Nova가 가져와 인스턴스 부팅 → 필요시 Cinder 볼륨 붙임

    5. 언제 무엇을

    필요 선택
    인스턴스에 추가 디스크 Cinder(블록)
    사진·백업·대량 파일 보관 Swift(오브젝트)
    OS 템플릿 관리 Glance(이미지)

    6. 정리

    OpenStack 스토리지는 Glance(이미지)로 시작 → Cinder(블록)로 디스크 확장 → Swift(오브젝트)로 파일 보관, 이 세 축이 전부입니다. “블록은 디스크처럼, 오브젝트는 드라이브처럼”만 기억하면 헷갈릴 일이 없습니다. 홈랩 랩에선 Glance + Cinder(LVM)부터 붙여보는 걸 권합니다.

  • Proxmox cloud-init 템플릿으로 VM 즉시 찍어내기 — 설치 0분 배포

    학습용 VM을 매번 ISO로 OS 설치하고, 계정 만들고, SSH 키 넣고, IP 잡고… 이걸 반복하다 지치신 적 있나요? 저는 Proxmox cloud-init 템플릿으로 이 과정을 없앴습니다. 클론 한 번이면 부팅과 동시에 설정까지 끝난 VM이 나옵니다. 실제로 제 홈랩 학습 VM들이 이 방식으로 찍혀 나옵니다.

    1. cloud-init이 뭔가

    cloud-init은 클라우드 이미지가 첫 부팅 때 자기 자신을 설정하게 해주는 표준 도구입니다. 호스트명·사용자·비밀번호·SSH 키·네트워크(IP)를 부팅 시 주입받아 자동 구성합니다. AWS·OpenStack이 인스턴스를 찍어내는 원리와 같은 것을, Proxmox에서도 그대로 씁니다.

    2. 왜 템플릿 + cloud-init인가

    • 속도: OS 설치 0분. 클론 → 부팅이면 끝.
    • 일관성: 항상 같은 베이스에서 시작(학습·실습에 최적).
    • 자동 설정: SSH 키·IP까지 부팅과 동시에.

    실제로 제 홈랩엔 Ubuntu, Rocky 등 cloud-init 템플릿이 준비돼 있어서, 실습이 필요하면 몇 초 만에 새 VM을 뽑습니다.

    3. 템플릿 만들기 (한 번만)

    배포판이 제공하는 cloud 이미지(.img/.qcow2)를 받아서 VM에 붙이고 템플릿으로 변환합니다.

    # 1) cloud 이미지 다운로드 (예: Ubuntu)
    wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
    
    # 2) 빈 VM 생성
    qm create 9000 --name ubuntu-2404-template --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
    
    # 3) 이미지를 디스크로 임포트
    qm importdisk 9000 noble-server-cloudimg-amd64.img local-zfs
    qm set 9000 --scsihw virtio-scsi-single --scsi0 local-zfs:vm-9000-disk-0
    
    # 4) cloud-init 드라이브 + 부팅 설정
    qm set 9000 --ide2 local-zfs:cloudinit
    qm set 9000 --boot c --bootdisk scsi0 --serial0 socket --vga serial0
    
    # 5) 템플릿으로 변환
    qm template 9000

    4. 템플릿에서 VM 즉시 찍어내기

    이제 실습 VM이 필요할 때마다 클론 + cloud-init 값만 지정하면 됩니다.

    # 템플릿(9000)에서 새 VM(201) 풀 클론
    qm clone 9000 201 --name study-node1 --full
    
    # cloud-init: 사용자·SSH키·고정 IP 주입
    qm set 201 --ciuser myuser --sshkeys ~/.ssh/id_rsa.pub
    qm set 201 --ipconfig0 ip=192.168.x.201/24,gw=192.168.x.1
    
    qm start 201

    부팅되면 지정한 사용자·SSH 키·IP가 이미 적용돼 있어 바로 SSH 접속됩니다. OS 설치도, 초기 설정도 없습니다.

    5. 실전 팁

    • DHCP도 가능: 고정 IP 대신 --ipconfig0 ip=dhcp로 간단히.
    • 디스크 리사이즈: cloud 이미지는 작으니 qm resize 201 scsi0 +20G로 늘리면 cloud-init이 파일시스템까지 확장.
    • OpenStack 학습과 연결: 이 “이미지→인스턴스” 흐름이 바로 OpenStack의 Glance·Nova가 하는 일입니다. 원리가 같아요.
    • 골든 이미지: 자주 쓰는 패키지를 미리 넣어 템플릿을 만들면 클론 즉시 실전 투입 가능.

    6. 정리

    cloud-init 템플릿은 홈랩 생산성을 통째로 바꿉니다. 한 번 템플릿을 만들어 두면, 이후엔 클론 한 줄로 설정 완료된 VM이 나옵니다. 학습·실습으로 VM을 자주 만드는 분이라면 무조건 세팅해 두세요. 그리고 이 원리는 그대로 프라이빗 클라우드(OpenStack)의 인스턴스 배포로 이어집니다.

  • [AI] LangChain 오류 디버깅 전략과 실전 트러블슈팅

    [AI] LangChain 오류 디버깅 전략과 실전 트러블슈팅

    LangChain 오류 디버깅 전략과 실전 트러블슈팅

    LangChain 오류를 오래 들여다보면 한 가지 패턴이 보입니다. 실제 장애의 상당수는 “모델이 이상해서”가 아니라, 모델에 도달하기 전후의 계약이 깨져서 납니다. 입력 키가 하나 빠졌거나, 프롬프트 변수명이 바뀌었거나, retriever가 엉뚱한 문서를 가져왔거나, 출력 파서가 모델의 자연어 한 줄을 JSON으로 착각하는 식이죠.

    인프라 엔지니어 관점에서 보면 LangChain은 단순한 체인 문법이 아니라 API 서버, 큐, 환경변수, 네트워크, 관측성까지 이어지는 실행 파이프라인에 가깝습니다. 이 글은 작은 RAG(Retrieval-Augmented Generation, 검색 증강 생성) 앱을 운영하며 정리한 실전 기준입니다. “어제 되던 코드가 오늘 왜 안 되지?” 싶은 순간에 바로 확인할 수 있도록 명령어와 판단 기준을 함께 넣었습니다.

    LangChain 앱에서 입력, 프롬프트, 모델, 파서, 관측 도구가 어떻게 이어지는지 보여주는 개요 이미지입니다.

    1. LangChain 오류는 컴포넌트보다 계약부터 봐야 합니다

    LangChain은 Prompt Template(프롬프트 템플릿), Chat Model(채팅 모델), Retriever(검색기), Output Parser(출력 파서), Tool(도구)을 파이프처럼 이어줍니다. 여기서 중요한 건 각 단계가 서로 “어떤 입력을 받고 어떤 출력을 넘기는지”입니다. 저는 장애를 볼 때 컴포넌트 이름보다 계약 위반 여부를 먼저 봅니다.

    • 입력 계약 위반: 프롬프트가 요구하는 변수와 실제 payload 키가 다릅니다.
    • 타입 계약 위반: 다음 단계는 문자열을 기대하는데 앞 단계가 dict, list, Document 객체를 넘깁니다.
    • 의존성 계약 위반: 예전 예제의 import 경로와 현재 설치된 패키지 구조가 맞지 않습니다.
    • 인증·네트워크 계약 위반: API 키, 조직 설정, 프록시, 방화벽, timeout 설정이 실행 환경과 충돌합니다.
    • 출력 계약 위반: 모델 응답이 JSON, Pydantic 스키마, enum 값 같은 엄격한 형식을 따르지 않습니다.
    • 검색 품질 계약 위반: RAG에서 retriever가 질문과 관련 없는 문서를 가져오고, 모델은 그 문서 안에서만 그럴듯하게 답합니다.

    이 관점이 유용한 이유는 단순합니다. “LLM이 틀렸다”는 말은 범위가 너무 넓거든요. 반면 “prompt 입력 계약이 깨졌다”, “parser 출력 계약이 깨졌다”라고 말하면 바로 재현 코드와 테스트 케이스를 만들 수 있습니다.

    2. 재현 가능한 환경부터 고정하세요

    LangChain 디버깅을 시작할 때 제일 먼저 할 일은 코드 수정이 아니라 환경 스냅샷입니다. 특히 LangChain은 코어 패키지, 통합 패키지, 커뮤니티 패키지가 분리되어 있어 import 경로와 설치 패키지를 같이 봐야 합니다. OpenAI 연동을 쓴다면 공식 문서 기준으로 langchain-openai를 별도로 설치하는 방식이 일반적입니다.

    python -m venv .venv
    source .venv/bin/activate
    python -m pip install -U pip
    python -m pip install -U langchain langchain-core langchain-openai langsmith python-dotenv
    python -m pip check
    python -m pip freeze > requirements.lock.txt
    python -m pip show langchain langchain-core langchain-openai langsmith

    pip check는 의존성 충돌을 빠르게 확인할 때 좋습니다. 여기서 문제가 나오면 LangChain 코드를 보기 전에 패키지 상태부터 정리하는 편이 낫습니다. 운영 서버에서만 깨지는 오류라면 로컬과 서버에서 아래 명령 결과를 나란히 비교하세요.

    python --version
    python -m pip --version
    python -m pip show langchain langchain-core langchain-openai langsmith
    python - <<'PY'
    import importlib.metadata as m
    for name in ['langchain', 'langchain-core', 'langchain-openai', 'langsmith']:
        try:
            print(name, m.version(name))
        except m.PackageNotFoundError:
            print(name, 'NOT INSTALLED')
    PY

    튜토리얼을 따라 했는데 ModuleNotFoundError나 ImportError가 나면, 코드보다 먼저 “그 예제가 어느 시기의 패키지 구조를 기준으로 쓰였는지”를 보세요. 오래된 글에서 langchain.chat_models 계열 import를 복사했다면 현재 프로젝트의 공식 import 경로와 맞지 않을 수 있습니다. 이건 실력 문제가 아니라 패키지 분리의 흔적입니다.

    3. INVALID_PROMPT_INPUT를 일부러 재현해보기

    초보 단계에서 자주 만나는 LangChain 오류는 프롬프트 변수 누락입니다. LangChain의 INVALID_PROMPT_INPUT 오류는 프롬프트 템플릿이 요구하는 입력과 실제 전달한 값이 어긋날 때 발생합니다. 운영에서는 이게 더 교묘합니다. 프론트엔드에서는 question으로 보내는데 백엔드에서는 query로 넘기거나, 큐 메시지에 필드 하나가 빠지는 식이죠.

    from langchain_core.prompts import ChatPromptTemplate
    
    prompt = ChatPromptTemplate.from_messages([
        ('system', 'You are a helpful assistant.'),
        ('human', '{question}에 대해 {tone} 말투로 설명해줘'),
    ])
    
    print('required variables:', prompt.input_variables)
    
    # 일부러 tone을 빼서 오류를 재현합니다.
    messages = prompt.invoke({'question': 'LangChain 오류'})
    print(messages)

    실제로는 모델 호출 전에 payload를 검증해서 “모델 API 비용을 쓰기도 전에 실패”하게 만드는 편이 좋습니다. 오류를 늦게 발견할수록 로그가 지저분해지고 비용도 새기 쉽거든요.

    def validate_prompt_payload(prompt, payload: dict) -> None:
        required = set(prompt.input_variables)
        received = set(payload.keys())
        missing = required - received
        extra = received - required
    
        if missing:
            raise ValueError(f'Missing prompt variables: {sorted(missing)}')
        if extra:
            print(f'[debug] Extra payload keys ignored by prompt: {sorted(extra)}')
    
    payload = {'question': 'LangChain 디버깅', 'tone': '친절한'}
    validate_prompt_payload(prompt, payload)
    messages = prompt.invoke(payload)
    print(messages)

    실무 기준으로는 extra를 무조건 오류로 보지 않습니다. API 요청 전체에는 사용자 ID, trace ID, locale 같은 부가 필드가 들어갈 수 있으니까요. 다만 missing은 즉시 실패시키는 편이 낫습니다. 누락된 값을 빈 문자열로 채우면 당장은 지나가도 모델 응답 품질이 조용히 망가집니다.

    4. LCEL 체인은 예쁘게 쓰되, 디버깅은 끊어서 합니다

    LCEL(LangChain Expression Language)은 prompt | model | parser처럼 읽기 좋은 체인을 만들 수 있어서 생산성이 좋습니다. 문제는 한 줄로 연결한 체인이 실패하면 어느 단계에서 깨졌는지 한눈에 보이지 않는다는 점입니다. 장애 상황에서는 체인을 잠시 해체하세요. 각 연결부에서 실제 값이 무엇인지 보는 게 훨씬 빠릅니다.

    LangChain 디버깅을 위한 LCEL 체인 단계별 구성도

    프롬프트, 모델, 파서 단계를 나누어 어디서 입력과 출력이 바뀌는지 확인하는 구성도입니다.

    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    prompt = ChatPromptTemplate.from_template('{topic}을 초보자에게 3문장으로 설명해줘.')
    model = ChatOpenAI(model='gpt-4o-mini', temperature=0, timeout=30, max_retries=2)
    parser = StrOutputParser()
    
    inputs = {'topic': 'LangChain 트러블슈팅'}
    
    prompt_value = prompt.invoke(inputs)
    print('[prompt type]', type(prompt_value))
    print('[prompt value]', prompt_value)
    
    model_result = model.invoke(prompt_value)
    print('[model type]', type(model_result))
    print('[model content]', model_result.content)
    
    parsed = parser.invoke(model_result)
    print('[parsed type]', type(parsed))
    print('[parsed]', parsed)

    temperature=0은 디버깅할 때 변동성을 줄이려는 선택입니다. 창의적인 답변을 평가하는 단계가 아니라 오류 재현 단계라면, 같은 입력에 최대한 비슷한 응답이 나오는 편이 원인 분석에 유리합니다. timeout과 max_retries는 운영에서 더 중요합니다. timeout이 없으면 요청이 오래 붙잡혀 API 서버 worker를 묶을 수 있고, retry가 과하면 장애 상황에서 외부 API를 더 세게 두드릴 수 있습니다.

    실패 위치 흔한 에러 신호 근본 원인 먼저 할 일 주의할 트레이드오프
    Prompt 필수 변수 누락, 템플릿 포맷 오류 payload 스키마와 템플릿 변수명 불일치 prompt.input_variables와 요청 body 비교 기본값으로 덮으면 조용한 품질 저하가 생길 수 있음
    Model 인증 실패, timeout, rate limit, 모델명 오류 환경변수, 네트워크, provider 설정 문제 API 키 존재 여부와 실제 모델명을 로그로 확인 retry를 늘리면 안정성은 오르지만 지연과 비용이 늘 수 있음
    Parser JSONDecodeError, validation error 자연어 응답을 엄격한 스키마로 해석 raw output을 저장하고 스키마와 비교 구조화 출력은 안정적이지만 provider 지원 여부를 확인해야 함
    Retriever 답변은 자연스럽지만 근거가 엉뚱함 청크, 임베딩, 검색 쿼리, 필터 조건 문제 검색된 문서 제목·점수·본문 일부 출력 top_k를 늘리면 recall은 오르지만 잡음과 토큰 사용량도 늘 수 있음
    Tool 도구 인자 validation 실패 모델이 tool schema와 다른 인자를 생성 tool call 원문과 schema 필수 필드 확인 스키마를 너무 엄격하게 만들면 재시도가 잦아질 수 있음

    5. 환경변수는 .env와 런타임 값을 분리해서 봅니다

    LLM 앱에서 인증 오류는 의외로 단순한 곳에서 납니다. .env에는 키가 있는데 프로세스에는 로드되지 않았거나, Docker 컨테이너에는 다른 값이 들어갔거나, 스테이징 서버가 운영용 LangSmith 프로젝트로 trace를 보내는 식입니다. 로컬 개발에서는 python-dotenv가 편하지만, 운영에서는 플랫폼의 secret 관리 기능을 우선하세요.

    OPENAI_API_KEY=sk-여기에_실제_키를_넣지_말고_로컬에서만_관리
    LANGSMITH_API_KEY=lsv2_여기에_LANGSMITH_키
    LANGSMITH_TRACING=true
    LANGSMITH_PROJECT=langchain-debug-local
    from dotenv import load_dotenv
    import os
    
    load_dotenv()
    
    required_env = ['OPENAI_API_KEY']
    for key in required_env:
        if not os.getenv(key):
            raise RuntimeError(f'Missing required environment variable: {key}')
    
    print('LANGSMITH_TRACING=', os.getenv('LANGSMITH_TRACING', 'false'))
    print('LANGSMITH_PROJECT=', os.getenv('LANGSMITH_PROJECT', 'not-set'))

    운영 관점에서 LANGSMITH_PROJECT는 꼭 나누는 편이 좋습니다. 개발, 스테이징, 운영 trace가 섞이면 장애 분석 때 “어느 요청이 진짜 고객 요청인지”부터 다시 걸러야 합니다. 로그 비용과 보안 정책도 같이 봐야 합니다. 프롬프트나 검색 문서에 개인정보·계약 정보·내부 장애 내용이 들어갈 수 있다면 trace에 무엇을 남길지 팀 기준을 먼저 정해야 합니다.

    6. LangSmith trace는 print의 대체가 아니라 요청 단위 블랙박스입니다

    로컬에서 한 명이 테스트할 때는 print만으로도 충분합니다. 하지만 API 서버에서 동시에 여러 요청이 들어오면 stdout 로그는 금방 섞입니다. LangSmith 같은 관측성 도구를 붙이면 chain run, model call, parser 실행 흐름을 요청 단위 trace로 따라갈 수 있습니다. 이거 붙여두면 장애 회고 때 진짜 편하더라고요.

    • 입력: 사용자가 보낸 원문과 서버가 체인에 넘긴 값이 같은지 확인합니다.
    • 중간 출력: 프롬프트 완성본, 검색된 문서, 모델 raw response를 분리해서 봅니다.
    • 실패 지점: 전체 latency 중 어디에서 오래 걸렸고 어느 단계에서 예외가 났는지 봅니다.
    export OPENAI_API_KEY='여기에_API_키'
    export LANGSMITH_API_KEY='여기에_LANGSMITH_키'
    export LANGSMITH_TRACING='true'
    export LANGSMITH_PROJECT='langchain-debug-lab'
    python app.py

    print 로그를 완전히 버리라는 뜻은 아닙니다. 개발 초반에는 print + 단계별 invoke가 가장 빠릅니다. 다만 팀 단위 운영으로 넘어가면 trace ID를 HTTP 응답 헤더나 애플리케이션 로그에 같이 남기는 쪽이 좋습니다. 고객 문의, 서버 로그, LangSmith trace를 같은 ID로 묶을 수 있으면 원인 추적 속도가 확 달라집니다.

    상황 추천 도구 이유 쓰지 않아도 되는 경우
    개인 실험, 노트북 코드 print, 단계별 invoke 설정이 적고 즉시 확인 가능 동시 요청이 없고 실패 입력이 단순할 때
    FastAPI/Flask API 서버 구조화 로그 + trace ID 요청별 입력과 예외를 묶기 쉬움 일회성 데모라면 과할 수 있음
    팀 운영 서비스 LangSmith tracing 체인 내부 단계와 모델 호출을 시각적으로 추적 민감 데이터 로깅 정책을 정하지 못했다면 먼저 보류
    회귀 방지 pytest 재현 테스트 한 번 고친 오류가 다시 나는지 확인 모델 품질 평가처럼 정답이 유동적인 경우엔 별도 eval이 필요

    7. JSON 파싱 오류는 프롬프트만으로 해결하기 어렵습니다

    AI 프레임워크 오류 중 사람을 가장 오래 붙잡는 게 출력 파싱입니다. 모델은 자연어를 잘하지만, 애플리케이션은 종종 엄격한 JSON을 원합니다. 쉼표 하나, 따옴표 하나, enum 값 하나만 달라도 parser는 실패합니다. 제가 보는 선택지는 세 가지입니다.

    • 문자열 후처리: 빠르지만 취약합니다. 데모나 내부 도구에만 제한적으로 씁니다.
    • Output Parser: 프롬프트에 형식 지시를 넣고 파서로 검증합니다. 실패 시 raw output 저장이 중요합니다.
    • 구조화 출력: 모델 호출 단계에서 스키마를 강제합니다. 지원 모델과 provider를 확인해야 하지만 운영 안정성은 대체로 이쪽이 낫습니다.
    from typing import Literal
    from pydantic import BaseModel, Field
    from langchain_openai import ChatOpenAI
    
    class Ticket(BaseModel):
        title: str = Field(description='장애 제목')
        severity: Literal['low', 'medium', 'high'] = Field(description='장애 심각도')
        summary: str = Field(description='장애 요약')
    
    model = ChatOpenAI(model='gpt-4o-mini', temperature=0, timeout=30)
    structured_model = model.with_structured_output(Ticket)
    
    result = structured_model.invoke('결제 API가 간헐적으로 실패하고 있습니다. 영향 범위가 넓어 보입니다.')
    print(result)
    print(type(result))

    Literal을 쓴 이유는 단순합니다. severity가 critical, urgent, 높음처럼 제멋대로 늘어나면 downstream 시스템에서 다시 분기 오류가 납니다. 스키마를 좁게 잡으면 validation 실패는 늘 수 있지만, 잘못된 값이 조용히 DB에 저장되는 위험은 줄어듭니다. 운영에서는 둘 중 무엇이 더 비싼 장애인지 판단해야 합니다.

    from pydantic import ValidationError
    
    try:
        ticket = structured_model.invoke('DB 연결이 실패해서 로그인 API가 실패합니다.')
    except ValidationError as exc:
        print('[schema validation failed]')
        print(exc)
        # 운영에서는 원문 입력, raw 응답, trace_id를 함께 저장하는 쪽이 좋습니다.
        raise

    반대로 간단한 블로그 요약, 내부 메모 생성처럼 사람이 읽고 끝나는 작업이라면 엄격한 Pydantic 스키마가 오히려 과할 수 있습니다. 이럴 땐 StrOutputParser로 충분합니다. 기준은 “사람이 읽을 텍스트인가, 시스템이 소비할 데이터인가”입니다. 시스템이 소비한다면 구조화 출력이나 검증 레이어를 빼지 않는 게 맞습니다.

    8. RAG 오류는 모델보다 검색 결과부터 의심하세요

    문서 검색 챗봇을 만들 때 가장 많이 속는 부분이 RAG 품질 문제입니다. 답변은 매끄러운데 사실관계가 묘하게 틀릴 때가 있거든요. 처음엔 모델을 바꿔야 하나 싶지만, trace를 보면 retriever가 질문과 관련 없는 문서를 가져오는 경우가 꽤 많습니다. 모델은 받은 근거 안에서 열심히 답했을 뿐입니다.

    RAG에서 “틀린 답변”은 최소 세 종류로 나눠야 합니다. 첫째, 관련 문서를 못 찾은 검색 실패입니다. 둘째, 관련 문서는 찾았지만 청크가 잘려 핵심 문맥이 빠진 경우입니다. 셋째, 문서는 맞는데 프롬프트가 근거 밖 추론을 허용한 경우입니다. 이 셋은 대응이 다릅니다.

    def debug_documents(docs, limit: int = 3) -> None:
        for i, doc in enumerate(docs[:limit], start=1):
            source = doc.metadata.get('source', 'unknown')
            title = doc.metadata.get('title', 'no-title')
            text = doc.page_content.replace('\n', ' ')[:500]
            print(f'\n[doc {i}] source={source} title={title}')
            print(text)
    
    query = 'FastAPI에서 LangChain 체인이 timeout 날 때 어디를 봐야 하나요?'
    docs = retriever.invoke(query)
    debug_documents(docs)

    위 출력에서 질문과 상관없는 문서가 계속 나온다면 모델 파라미터를 만지기 전에 retriever 설정을 봐야 합니다. top_k를 늘리면 관련 문서를 포함할 가능성은 올라가지만, 무관한 문서도 같이 들어와 토큰 사용량과 혼선을 늘릴 수 있습니다. chunk size를 키우면 문맥은 보존되지만 검색 단위가 둔해질 수 있고, 너무 작게 자르면 답변에 필요한 전후 맥락이 사라질 수 있습니다. 그래서 실패 질문을 모아 회귀 테스트처럼 돌리는 게 낫습니다.

    증상 먼저 의심할 곳 확인 방법 추천 대응
    답변이 유창하지만 근거가 틀림 Retriever 검색된 문서 제목·본문 일부를 출력 쿼리 재작성, metadata filter, top_k 조정
    문서는 맞는데 답이 반쪽 Chunking 원문에서 앞뒤 문맥이 잘렸는지 비교 chunk size, overlap, 문서 분할 기준 재검토
    근거 밖 추측이 섞임 Prompt 프롬프트가 모르면 모른다고 하게 만드는지 확인 근거 기반 답변 규칙과 citation 요구 추가
    요청마다 품질 편차가 큼 모델 설정·검색 후보 같은 질문을 여러 번 실행해 raw context 비교 temperature 낮추기, 검색 결과 고정 테스트 작성

    9. 재시도와 timeout은 비용 증폭 장치이기도 합니다

    외부 모델 API를 쓰면 timeout, rate limit, 일시적인 네트워크 오류를 피할 수 없습니다. 그래서 max_retries를 켜는 건 합리적입니다. 다만 재시도는 공짜가 아닙니다. 실패 요청이 많을 때 retry가 겹치면 지연이 늘고, 일부 실패 모드에서는 비용도 커질 수 있습니다.

    환경 timeout retry 이유
    로컬 디버깅 짧게 낮게 빨리 실패해야 원인 확인이 쉬움
    사용자-facing API 제품 SLA에 맞춤 제한적으로 사용자 대기 시간과 성공률 사이 균형 필요
    배치 작업 상대적으로 길게 조금 더 허용 실시간 응답보다 완료율이 중요할 수 있음
    장애 재현 테스트 짧게 0 또는 낮게 재시도가 원래 오류를 가리지 않게 해야 함
    from langchain_openai import ChatOpenAI
    
    # 장애 재현용: 빨리 실패시키고 원인을 숨기지 않습니다.
    debug_model = ChatOpenAI(model='gpt-4o-mini', temperature=0, timeout=10, max_retries=0)
    
    # 일반 API용: 일시적 실패를 약간 흡수합니다.
    api_model = ChatOpenAI(model='gpt-4o-mini', temperature=0, timeout=30, max_retries=2)

    핵심은 모든 환경에 같은 모델 객체를 쓰지 않는 것입니다. 디버깅 모드에서는 오류를 선명하게 보고, 운영 모드에서는 사용자 경험을 보호해야 합니다. 같은 코드베이스라도 목적이 다르면 timeout과 retry 정책도 달라져야 합니다.

    10. 실패 입력은 테스트로 남겨야 다시 안 밟습니다

    LangChain 트러블슈팅을 하다 보면 그 자리에서 고치고 넘어가기 쉽습니다. 하지만 실패 입력을 테스트로 남기지 않으면 프롬프트를 조금 바꾼 날 같은 문제가 다시 돌아옵니다. 최소한 프롬프트 변수 검증, parser validation, retriever 결과 확인은 작은 테스트로 남겨두세요.

    # tests/test_prompt_contract.py
    import pytest
    from langchain_core.prompts import ChatPromptTemplate
    
    prompt = ChatPromptTemplate.from_template('{question}에 대해 {tone} 말투로 답해줘')
    
    def validate_prompt_payload(prompt, payload: dict) -> None:
        missing = set(prompt.input_variables) - set(payload.keys())
        if missing:
            raise ValueError(f'Missing prompt variables: {sorted(missing)}')
    
    def test_prompt_payload_requires_tone():
        with pytest.raises(ValueError):
            validate_prompt_payload(prompt, {'question': 'LangChain 오류'})
    
    def test_prompt_payload_accepts_required_keys():
        validate_prompt_payload(prompt, {'question': 'LangChain 오류', 'tone': '차분한'})
    python -m pytest -q tests/test_prompt_contract.py -s

    모델 호출까지 포함한 테스트는 더 신중해야 합니다. 외부 API 상태, rate limit, 비용, 모델 응답 변동성이 끼어들기 때문입니다. 계약 테스트는 로컬에서 빠르게 돌리고, 실제 모델 품질 평가는 별도 eval이나 스테이징 파이프라인으로 분리하는 편이 관리하기 쉽습니다.

    LangChain 오류 분석을 위한 LangSmith 추적 대시보드 예시

    요청별 trace, 체인 단계, 오류 발생 지점을 한눈에 확인하는 대시보드 예시입니다.

    11. LangChain 트러블슈팅 순서

    아래 순서는 장애를 볼 때 거의 습관처럼 쓰는 흐름입니다. 중요한 건 모델 호출을 맨 앞에 두지 않는 겁니다. 입력과 검색 결과가 깨진 상태에서 모델을 바꿔봐야 문제만 흐려집니다.

    1. 실패 입력을 고정합니다. 사용자 질문, request body, trace ID, 환경 이름을 한 묶음으로 저장합니다.
    2. 패키지와 import를 확인합니다. pip show, pip check, Python 버전을 비교합니다.
    3. 프롬프트 계약을 봅니다. prompt.input_variables와 payload 키를 비교합니다.
    4. LCEL 체인을 끊습니다. prompt.invoke, model.invoke, parser.invoke를 따로 실행합니다.
    5. RAG라면 검색 결과를 출력합니다. 문서 제목, source, 본문 앞부분을 직접 봅니다.
    6. raw output을 저장합니다. parser가 실패했다면 모델 원문 응답 없이는 원인 분석이 어렵습니다.
    7. timeout과 retry를 확인합니다. 재현 테스트에서는 retry를 낮춰 원래 오류를 가리지 않게 합니다.
    8. 고친 뒤 테스트를 남깁니다. 실패 payload를 최소 재현 케이스로 바꿔 회귀를 막습니다.

    이 순서대로 보면 “LangChain이 이상하다”는 막연한 느낌이 “프롬프트 변수 누락”, “retriever 후보 품질 문제”, “JSON schema validation 실패”처럼 처리 가능한 단위로 바뀝니다. 디버깅은 결국 이름 붙이기 싸움입니다.

    12. 자주 묻는 질문: LLM 앱 개발 문제 해결

    Q. LangChain 오류가 나면 가장 먼저 뭘 봐야 하나요?

    A. 에러 메시지 마지막 줄만 보지 말고 실패 단계를 먼저 나누세요. Prompt, Model, Parser, Retriever 중 어디서 깨졌는지 확인하면 해결 속도가 빨라집니다. 제일 빠른 방법은 LCEL 체인을 끊어서 각 단계의 입력·출력 타입을 출력하는 것입니다.

    Q. 버전 문제는 어떻게 줄이나요?

    A. 설치 직후 requirements.lock.txt를 남기고, 로컬·서버에서 python -m pip show langchain langchain-core langchain-openai langsmith 결과를 비교하세요. 오래된 블로그 예제의 import 경로를 그대로 쓰면 현재 패키지 구조와 맞지 않을 수 있습니다.

    Q. JSON 파싱 오류는 프롬프트를 고치면 충분한가요?

    A. 사람이 읽는 데모라면 프롬프트 보강으로도 버틸 수 있습니다. 하지만 시스템이 JSON을 소비한다면 Pydantic 스키마나 구조화 출력을 우선 검토하세요. 프롬프트만으로 형식을 강제하면 모델 응답 변동에 약합니다.

    Q. LangSmith는 언제 붙이는 게 좋나요?

    A. 개인 실험 단계에서는 필수는 아닙니다. API 서버로 여러 요청을 받거나 팀에서 장애를 같이 봐야 한다면 붙이는 편이 좋습니다. 단, trace에 민감 데이터가 남을 수 있으니 로깅 정책을 먼저 정해야 합니다.

    Q. RAG 답변이 이상하면 모델을 바꾸는 게 먼저인가요?

    A. 보통은 아닙니다. 검색된 문서가 질문과 관련 있는지 먼저 확인하세요. retriever가 엉뚱한 문서를 가져오면 좋은 모델도 그 문서 안에서 그럴듯한 오답을 만들 수 있습니다.

    13. 상황별 추천: 이럴 땐 이렇게 가세요

    LangChain 오류를 줄이는 핵심은 전체 체인을 한 번에 믿지 않는 것입니다. 단계별 계약을 확인하고, 실패 입력을 저장하고, 고친 뒤 테스트로 남기면 같은 문제를 반복해서 밟을 가능성이 확 줄어듭니다. 관련 구현 예제가 더 필요하다면 블로그의 RAG 구축 글, FastAPI 배포 글, LLM 관측성 글을 함께 연결해 내부 링크로 안내하면 좋습니다.

    • 처음 개발 중이라면: print + 단계별 invoke로 충분합니다. 복잡한 도구보다 빠른 재현이 우선입니다.
    • 프롬프트 오류가 잦다면: payload 검증 함수를 모델 호출 전에 두세요. 누락 키를 조기에 실패시키는 게 비용과 시간을 아낍니다.
    • API 서버로 운영한다면: timeout, retry, trace ID, LangSmith tracing을 같이 설계하세요. 관측성 없이 운영하면 장애 때 감으로 뒤지게 됩니다.
    • JSON 결과가 중요하다면: 문자열 파싱보다 구조화 출력 또는 Pydantic 검증을 우선하세요. 시스템이 소비하는 데이터는 느슨하게 두면 나중에 더 비싸게 터집니다.
    • RAG 품질이 문제라면: 모델 교체 전에 retriever 결과와 청크 전략을 확인하세요. 검색이 틀리면 생성도 흔들립니다.
    • 장애 재현 중이라면: retry를 낮추고 temperature를 낮춰 오류를 선명하게 보세요. 운영 안정화 설정과 디버깅 설정은 달라도 됩니다.

    기본값은 단순합니다. 개인 개발은 빠르게 재현하고, 팀 운영은 관측 가능하게 만들고, 시스템 연동은 스키마를 엄격하게 가져가세요. LangChain은 체인을 빠르게 만들게 해주지만, 안정적인 LLM 앱은 결국 각 단계의 입력과 출력을 얼마나 차분하게 검증하느냐에 달려 있습니다.

    LangChain 트러블슈팅과 디버깅 전략 요약 인포그래픽

    입력, 체인, 모델, 파서, 관측성 기준으로 디버깅 우선순위를 요약한 이미지입니다.

    참고한 공식 문서

  • KVM·QEMU·libvirt — Proxmox 가상화의 실체 이해하기

    Proxmox에서 VM을 만들면 그 아래에서 실제로는 무엇이 도는지 아시나요? KVM, QEMU, libvirt — 가상화의 3대 축입니다. 이 구조를 이해하면 Proxmox 설정 하나하나가 왜 그런지 보이기 시작합니다. 가상화의 실체를 정리합니다.

    1. 하이퍼바이저 타입 — Type 1 vs Type 2

    Type 1 (베어메탈) Type 2 (호스트형)
    구조 하드웨어 위에 직접 일반 OS 위에 앱처럼
    성능 높음 상대적으로 낮음
    예시 KVM, ESXi, Proxmox VirtualBox, VMware Workstation

    Proxmox는 Type 1입니다. 정확히는 리눅스 커널 자체가 하이퍼바이저가 되는 KVM 방식이죠.

    2. 3대 축 — KVM · QEMU · libvirt

    • KVM (Kernel-based VM): 리눅스 커널 모듈. CPU의 하드웨어 가상화 기능(Intel VT-x/AMD-V)을 써서 VM을 네이티브에 가깝게 돌립니다. “가상화의 엔진”.
    • QEMU: 에뮬레이터·디바이스 모델. 가상 디스크·네트워크카드·칩셋 등 “가상 하드웨어”를 제공합니다. KVM과 결합해 qemu-kvm으로 동작.
    • libvirt: 이들을 다루는 관리 API/데몬. Proxmox·virsh 같은 도구가 libvirt를 통해 VM을 제어합니다.
    관리도구(Proxmox) → libvirt/QEMU → KVM(커널) → CPU 가상화(VT-x/AMD-V)

    즉 KVM은 엔진, QEMU는 차체, libvirt는 운전대라고 보면 됩니다.

    3. 전가상화 vs 반가상화(virtio)

    가상 하드웨어를 “진짜 하드웨어처럼 완벽히 흉내”내면(전가상화) 호환성은 좋지만 느립니다. 그래서 나온 게 virtio(반가상화)입니다.

    • virtio: “이건 가상 환경이야”를 게스트가 알고, 전용 드라이버로 훨씬 빠르게 디스크·네트워크를 처리.
    • Proxmox에서 디스크를 VirtIO SCSI, 네트워크를 virtio로 두는 이유가 성능입니다.
    # Proxmox에서 VM 디스크/네트워크를 virtio로 (성능 최적)
    qm set 100 --scsihw virtio-scsi-single
    qm set 100 --net0 virtio,bridge=vmbr0

    단, 윈도우 게스트는 virtio 드라이버를 따로 설치해줘야 인식합니다(리눅스는 대부분 기본 내장).

    4. CPU 타입과 중첩 가상화

    Proxmox의 VM CPU 타입도 이 맥락입니다.

    • kvm64(기본): 호환성 위주, CPU 기능 일부 감춤
    • host: 물리 CPU 기능을 그대로 노출 → 성능↑, 중첩 가상화 가능

    그래서 OpenStack 컴퓨트 노드처럼 “VM 안에서 또 VM”을 돌려야 하면 반드시 --cpu host를 줍니다. 이제 왜 그런지 이해되시죠?

    5. 정리

    Proxmox의 VM은 KVM(커널 엔진) + QEMU(가상 하드웨어) + libvirt/관리도구의 합작입니다. 성능의 핵심은 virtio, 중첩 가상화의 핵심은 CPU host. 이 구조를 알고 나면 Proxmox의 디스크·네트워크·CPU 옵션이 더 이상 주술이 아니라 논리로 보입니다.