13년차의 서버실

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

[태그:] 인프라 트러블슈팅

  • [AI] LLM 추론 최적화: 지연 문제 진단과 병목 해결 전략

    [AI] LLM 추론 최적화: 지연 문제 진단과 병목 해결 전략

    목차

    [인프라] LLM 추론 최적화: 지연 문제 진단과 병목 해결 전략

    LLM 추론 최적화는 GPU를 바꾸기 전에 먼저 해볼 일이 꽤 많습니다. 운영에서 실제로 느려지는 이유를 뜯어보면, 모델 자체보다 앞단 연결, 큐 적체, 토크나이저 CPU 경합, 스트리밍 버퍼링 같은 바깥 요인이 더 자주 문제를 만들더라고요. 저도 처음엔 “모델이 무거워서 느리겠지”라고 봤는데, 막상 들어가 보면 keep-alive 미설정, 요청별 JSON 직렬화 비용, 긴 입력이 몰릴 때 생기는 prefill 지연이 한꺼번에 겹친 경우가 많았습니다. 그래서 이 글은 막연한 튜닝 팁보다 어디서 기다리는지 먼저 분해하고, 그다음 손보는 순서에 집중합니다.

    핵심은 간단합니다. TTFT(Time To First Token, 첫 토큰까지 시간), TPOT(Time Per Output Token, 출력 토큰당 시간), Queue Wait(대기열 대기 시간)를 분리해서 봐야 합니다. total latency 하나만 붙들고 있으면 프록시 문제를 GPU 문제로, CPU 병목을 모델 병목으로 잘못 읽기 쉽거든요. 운영에서 시간을 아끼는 가장 빠른 방법은 최적화 자체보다 틀린 곳을 만지지 않는 것입니다.

    LLM 추론 최적화 관점의 전체 병목 진단 아키텍처 이미지

    LLM 추론 지연 시간 병목을 한눈에 보여주는 전체 아키텍처 개요입니다.

    1. LLM 추론 최적화는 “GPU가 느리다”보다 “어느 단계가 줄을 세우는가”로 봐야 합니다

    추론 요청은 보통 아래 단계를 지납니다. 이걸 한 덩어리로 보면 답이 잘 안 나옵니다.

    • Ingress 또는 API 프록시: 연결 수립, TLS, keep-alive, buffering
    • Queue: 워커가 바쁘거나 동시성 제한에 걸려 대기
    • Tokenization: 입력 전처리, 템플릿 결합, 토큰 계산
    • Prefill: 긴 입력 문맥을 모델이 한 번에 읽는 구간
    • Decode: 토큰을 하나씩 생성하는 구간
    • Post-processing: 스트리밍 직렬화, 로그, 압축, 감사 기록

    여기서 많이 헷갈리는 지점이 있습니다. GPU 사용률이 낮다고 효율적인 건 아닙니다. 오히려 GPU가 놀고 있는데 응답이 느리다면 그때가 더 골치 아픈 경우가 많습니다. 계산 장치가 아니라 요청 공급 경로가 끊기고 있다는 뜻일 수 있어서요. 반대로 GPU가 꽉 찼다고 바로 나쁜 것도 아닙니다. 처리량이 중요한 작업이라면 높은 점유율이 오히려 정상입니다.

    운영에서 먼저 보는 지표

    • TTFT: 사용자가 가장 먼저 체감하는 값입니다. 챗봇, 검색 보조, 문서 질의응답은 대부분 여기서 승부가 납니다.
    • TPOT: 생성이 시작된 뒤 토큰이 끊기지 않고 매끄럽게 이어지는지 보여줍니다.
    • P95/P99 tail latency: 평균만 보면 느린 요청이 숨어버립니다.
    • Queue Wait: 워커 수와 동시성 상한이 맞는지 판단할 때 중요합니다.
    • GPU utilization / memory utilization / memory used: 계산 병목인지, 메모리 압박인지 가늠하는 기본 축입니다.
    • CPU user/system, run queue: 토크나이저, 로깅, 압축, 네트워크 스택 비용을 읽는 데 유용합니다.
    • socket reuse, retransmission, proxy buffering: 스트리밍인데 첫 토큰이 늦을 때 꼭 봐야 합니다.

    2. 지연은 세 갈래로 자르면 판단이 빨라집니다

    실무에서 자주 쓰는 분류는 아래 세 가지입니다. 이렇게 나눠 놓으면 무엇부터 의심할지 훨씬 또렷해집니다.

    구간 관찰되는 증상 근본 원인 후보 먼저 할 일 지금 하지 말 것
    입장 전 연결은 되는데 첫 응답이 늦음 프록시 buffering, keep-alive 미사용, 워커 앞단 큐 적체 프록시와 클라이언트의 연결 재사용, 스트리밍 설정 확인 GPU 교체부터 검토
    모델 전후 GPU는 한가한데 total latency가 큼 토크나이저 CPU 경합, JSON 직렬화, 감사 로그, gzip, 동기식 후처리 요청 단계별 계측 추가, CPU 코어 점유 패턴 확인 배치만 무작정 키우기
    모델 내부 TTFT도 길고 TPOT도 느림 긴 입력으로 인한 prefill 부담, KV cache 압박, 메모리 병목, 배치 과대 입력 길이 정책, 동시성 제한, 배치 전략 재조정 로그만 줄이고 끝내기

    이 표에서 중요한 건 증상과 처방을 1:1로 바로 묶지 않는다는 점입니다. 예를 들어 TTFT만 길고 TPOT은 멀쩡하다면 prefill이 길어졌을 수도 있지만, 프록시가 첫 바이트를 묶고 있을 수도 있습니다. 반대로 첫 토큰은 빨리 나오는데 뒤가 끊긴다면 디코드보다 스트리밍 flush 간격, 네트워크 backpressure, 응답 직렬화 비용이 더 문제일 때도 있습니다.

    3. 실전 진단은 시스템 30%, 애플리케이션 70%입니다

    운영 현장에서 느낀 건 이겁니다. nvidia-smi만 봐서는 절반도 못 찾습니다. 시스템 지표로 병목 위치를 좁히고, 애플리케이션 계측으로 원인을 확정해야 합니다. 아래 명령어들은 Linux에서 많이 쓰는 조합인데, 배포판에 따라 <code>sysstat 패키지 설치가 필요하고 소켓 정보는 권한에 따라 일부만 보일 수 있습니다.

    1단계. CPU, 런큐, 디스크, 네트워크, 소켓을 같이 봅니다

    PID=12345
    PORT=8000
    
    pidstat -dur -h -p "$PID" 1
    mpstat -P ALL 1
    vmstat 1
    iostat -x 1
    sar -n DEV 1
    ss -tinp | grep ":$PORT"
    

    각 명령어를 보는 기준은 꽤 분명합니다.

    • pidstat -dur: 프로세스별 CPU, I/O, minor/major fault를 함께 봅니다. CPU가 높은데 GPU가 비면 모델 밖 병목일 가능성이 큽니다.
    • mpstat -P ALL: 특정 코어만 과열되면 토크나이저 스레드, 로깅 스레드, 이벤트 루프가 한쪽에 몰린 상황을 의심합니다.
    • vmstat: run queue가 길고 context switch가 튄다면 스레드 수를 늘린 게 오히려 독이 된 경우가 많습니다.
    • iostat -x: 디스크 활용률과 await를 같이 봅니다. 모델 로드가 아니라 로그 flush나 swap 때문에 지연될 수도 있습니다.
    • sar -n DEV: 인터페이스 오류, burst 패턴, 원격 스토리지 경유 트래픽을 파악할 때 좋습니다.
    • ss -tinp: ESTAB 연결이 재사용되는지, 요청마다 새 소켓이 생기는지 확인합니다.

    이 단계에서 자주 나오는 실패 모드가 있습니다. API 프로세스 CPU는 높고 GPU는 비어 있는데, 팀은 계속 배치나 양자화만 만집니다. 그 방향은 대개 틀립니다. 토큰을 만들기 전에 이미 시간을 다 써버리고 있는데 모델 쪽만 튜닝하고 있는 셈이거든요.

    2단계. GPU가 진짜 계산 병목인지 구분합니다

    nvidia-smi
    nvidia-smi dmon -s pucvmet -d 1
    watch -n 1 'nvidia-smi --query-gpu=utilization.gpu,utilization.memory,memory.used,memory.total,power.draw --format=csv,noheader'
    

    여기서는 평균보다 패턴을 보는 편이 낫습니다. 참고로 nvidia-smi dmon은 GPU와 드라이버 지원 여부에 따라 표시 가능한 항목이 조금 다를 수 있습니다.

    • 사용률이 톱니형으로 튀고 중간에 비는 경우: 요청 공급이 끊기거나 큐에서 건네주는 속도가 불안정한 경우가 많습니다.
    • 사용률은 높은데 메모리도 꽉 찬 경우: 긴 입력, 큰 배치, 동시성 과다로 prefill 또는 KV cache 압박이 강할 가능성이 큽니다.
    • 메모리는 넉넉한데 TPOT만 느린 경우: 계산보다 스트리밍, 후처리, 네트워크 flush 간격을 함께 봐야 합니다.

    운영 판단에서 중요한 건 이것입니다. GPU utilization 하나로 건강 상태를 판정하지 말 것. 메모리 사용 패턴, 요청 간 공백, TTFT/TPOT과 같이 봐야 의미가 생깁니다.

    3단계. API 레벨에서 연결 시간과 첫 바이트 시간을 분리합니다

    curl -N -sS -o /dev/null \
      -w 'dns=%{time_namelookup}\nconnect=%{time_connect}\nappconnect=%{time_appconnect}\nstarttransfer=%{time_starttransfer}\ntotal=%{time_total}\n' \
      -H 'Content-Type: application/json' \
      -d '{"prompt":"안녕하세요. LLM 지연 진단 테스트입니다."}' \
      http://127.0.0.1:8000/generate
    

    time_starttransfer는 첫 바이트까지의 시간을 보여주므로, 스트리밍 API에서는 TTFT에 가까운 운영 힌트로 쓸 수 있습니다. 다만 엄밀히 말하면 네트워크 경로와 서버 처리 시간을 함께 포함한 값이라, 모델 내부의 첫 토큰 생성 시각과 완전히 같지는 않습니다. 저는 프록시 직통 호출과 프록시 경유 호출을 둘 다 같은 프롬프트로 반복 측정합니다. 여기서 두 값 차이가 크면 모델이 아니라 네트워크 경로와 프록시 설정부터 보는 게 맞습니다.

    4. 애플리케이션 계측이 없으면, 튜닝이 아니라 추측을 하게 됩니다

    시스템 지표만으로는 “느리다”는 사실까지만 알 수 있습니다. 운영에서 실제로 문제를 줄이려면 요청 하나가 어디에서 얼마나 머물렀는지가 로그에 남아 있어야 합니다. 최소 기준으로 두는 건 아래 네 가지입니다.

    • 큐에 들어간 시각과 워커가 잡은 시각
    • 토크나이즈 시작/종료 시각
    • 모델 실행 시작 시각과 첫 토큰 시각
    • 스트리밍 완료 시각과 최종 응답 바이트 수
    import time
    import logging
    from contextvars import ContextVar
    from fastapi import FastAPI, Request
    
    logging.basicConfig(level=logging.INFO)
    log = logging.getLogger("llm_latency")
    request_id_var = ContextVar("request_id", default="-")
    app = FastAPI()
    
    def now_ms() -> float:
        return time.perf_counter() * 1000
    
    @app.middleware("http")
    async def timing_middleware(request: Request, call_next):
        rid = request.headers.get("x-request-id", "-")
        request_id_var.set(rid)
        t0 = now_ms()
        response = await call_next(request)
        total_ms = now_ms() - t0
        log.info("request_id=%s path=%s status=%s total_ms=%.2f",
                 rid, request.url.path, response.status_code, total_ms)
        response.headers["X-Request-Time-Ms"] = f"{total_ms:.2f}"
        return response
    
    def log_stage(stage: str, started_ms: float, **fields):
        elapsed_ms = now_ms() - started_ms
        extra = " ".join(f"{k}={v}" for k, v in fields.items())
        log.info("request_id=%s stage=%s elapsed_ms=%.2f %s",
                 request_id_var.get(), stage, elapsed_ms, extra)
        return now_ms()
    
    # 예시 흐름
    # t = now_ms()
    # t = log_stage("queue_wait", t, queue_depth=queue_depth)
    # t = log_stage("tokenize", t, prompt_chars=len(prompt), prompt_tokens=prompt_tokens)
    # t = log_stage("prefill", t)
    # t = log_stage("first_token", t)
    # t = log_stage("decode", t, output_tokens=output_tokens)
    # t = log_stage("serialize", t, bytes=response_bytes)
    

    이 정도만 있어도 판단이 꽤 달라집니다. 전체 응답 시간이 길어도 tokenize가 길다면 CPU 쪽이고, first_token 전까지 오래 걸리면 큐나 prefill이 의심됩니다. serialize가 길면 모델이 아니라 응답 포맷팅이나 로깅 설계가 발목을 잡고 있을 수 있습니다. 이거 로그 한 번 쪼개 놓으면 생각보다 훨씬 편하더라고요.

    LLM 지연 시간 계측과 병목 구간 분리를 설명하는 이미지

    요청 시간을 단계별로 쪼개 기록하는 계측 흐름 예시입니다.

    5. LLM 추론 최적화에서 우선순위 높게 손볼 포인트

    운영에서 효과가 큰 건 의외로 화려한 알고리즘보다 파이프라인의 낭비를 없애는 일입니다. 아래 항목은 체감 개선이 뚜렷했던 것들입니다.

    프록시와 연결 재사용: 첫 토큰이 늦으면 여기부터 봅니다

    스트리밍 응답을 프록시 뒤에 둘 때는 keep-alive, buffering, HTTP 버전이 맞물립니다. 이 셋 중 하나만 어긋나도 사용자는 “아예 멈춘 것 같다”고 느낍니다.

    upstream llm_backend {
        server 127.0.0.1:8000;
        keepalive 64;
    }
    
    server {
        listen 80;
        server_name _;
    
        location / {
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_buffering off;
            proxy_request_buffering off;
            proxy_read_timeout 300s;
            proxy_send_timeout 300s;
            proxy_pass http://llm_backend;
        }
    }
    

    특히 proxy_buffering off;는 스트리밍 경로에서 중요합니다. 첫 토큰이 서버에서는 나왔는데 프록시가 중간에서 모아두면, 클라이언트는 모델이 느린 줄 압니다. 이런 경우 GPU 로그를 아무리 봐도 원인이 잘 안 나옵니다. 첫 바이트를 누가 붙잡고 있는지를 봐야 합니다. 환경에 따라 애플리케이션에서 X-Accel-Buffering: no 헤더를 함께 쓰는 것도 도움이 됩니다.

    CPU 경합 줄이기: 토크나이저와 로그가 의외로 많이 잡아먹습니다

    GPU 서버에서 CPU를 가볍게 보는 경우가 많지만, 실제론 토크나이저와 직렬화, 로깅, 압축이 CPU를 잡아먹으면서 TTFT를 흔드는 일이 잦습니다. 특히 긴 프롬프트를 템플릿과 합치는 코드가 비효율적이면 모델을 호출하기도 전에 시간이 새기 시작합니다.

    • DEBUG 로그와 요청별 전체 payload 로깅은 스트리밍 경로에서 끄는 편이 낫습니다.
    • gzip은 대역폭이 정말 아쉬운 환경이 아니라면 스트리밍 응답에선 먼저 의심해 볼 만한 비용입니다.
    • 토크나이저 스레드 수와 워커 수를 동시에 키우면 오히려 코어 경합이 심해질 수 있습니다.
    • 한 프로세스에 너무 많은 역할을 몰아넣지 말아야 합니다. 추론, 로깅, 수집 에이전트, 배치 전처리가 같은 코어 그룹을 두드리면 tail latency가 나빠집니다.

    실제로 자주 본 실패 패턴은 이렇습니다. 평균 응답 시간은 그럭저럭인데 사용자가 느끼는 건 “가끔 엄청 느리다”입니다. 이런 경우 평균보다 느린 요청 몇 건의 stage 로그를 보는 편이 훨씬 빠릅니다. tail latency는 대개 경합에서 생기고, 경합은 평균값에 잘 안 드러나거든요.

    배치, 동시성, 컨텍스트 길이: 세 개를 따로 보지 마세요

    이 세 가지는 묶어서 봐야 합니다. 배치만 키우면 throughput은 나아질 수 있지만 TTFT가 손해를 보고, 동시성을 올리면 GPU는 더 바빠지지만 queue wait와 메모리 압박이 같이 커질 수 있습니다. 긴 입력은 prefill을 늘리고, prefill이 늘면 첫 토큰이 늦어집니다. 결국 한 가지 값보다 서비스 목표에 맞는 균형이 더 중요합니다.

    목표 우선 지표 추천 전략 피해야 할 실수
    대화형 챗봇 TTFT, P95 짧은 큐, 과도한 배치 회피, 입력 길이 상한 관리 처리량 욕심으로 배치 과대 설정
    비동기 문서 생성 Throughput, GPU 점유율 배치 확대, 워커 활용도 최적화, 후처리 비동기화 챗봇용 설정을 그대로 재사용
    요청 길이 편차가 큰 서비스 P99, Queue Wait 짧은 요청과 긴 요청 분리, admission control 적용 모든 요청을 같은 큐에 넣기
    GPU 메모리 여유가 적음 안정성, 실패율 동시성 상한과 입력 길이 정책을 먼저 고정 OOM을 배치 재시도로만 덮기

    판단 기준은 단순합니다. 사람이 기다리는 인터랙티브 서비스면 TTFT를, 백그라운드 작업이면 throughput을 우선합니다. 둘 다 잡겠다고 한 설정으로 몰아가면 보통 둘 다 애매해집니다.

    6. 재현 가능한 트러블슈팅 시나리오: “가끔만” 느릴 때가 제일 어렵습니다

    현실적인 시나리오 하나를 보겠습니다. 사내 문서 요약 API가 있고, 프록시 뒤에서 스트리밍으로 응답합니다. 사용자는 “매번 느린 건 아닌데, 어떤 요청은 첫 응답이 한참 뒤에 나온다”고 합니다. 이때 평균만 보면 문제가 잘 안 보입니다. 저는 이런 순서로 갑니다.

    1. 프롬프트 길이가 비슷한 요청과 유난히 긴 요청을 분리합니다. 긴 입력이 섞이면 prefill 지연과 앞단 문제를 구분하기 어려워집니다.
    2. 프록시 직통과 프록시 경유를 각각 curl -w로 여러 번 측정합니다. starttransfer 차이가 크면 프록시부터 봅니다.
    3. ss로 연결이 재사용되는지 확인합니다. 매 요청 새 연결이면 keep-alive가 안 먹고 있을 수 있습니다.
    4. pidstat / mpstat로 느린 순간 CPU 특정 코어가 튀는지 봅니다. 토크나이즈, 로그 flush, 압축이 원인일 때가 많습니다.
    5. nvidia-smi dmon으로 GPU가 중간에 비는지 확인합니다. 비는 구간이 길면 모델이 아니라 공급 경로 문제일 가능성이 큽니다.
    6. 애플리케이션 로그에서 queue_wait, tokenize, first_token 단계를 비교합니다.

    이런 케이스에서 자주 잡히는 원인은 두 가지였습니다. 하나는 프록시가 스트리밍을 묶고 있던 경우, 다른 하나는 입력 전처리와 로그가 같은 CPU 코어를 두드려서 TTFT가 흔들리던 경우입니다. 둘 다 공통점이 있습니다. 평균 total latency만 보면 잘 안 보인다는 점입니다.

    자주 틀리는 해석과 진짜 원인

    • GPU 사용률이 낮다: 좋은 신호가 아닐 수 있습니다. 요청 공급이 끊기거나 CPU가 앞단에서 병목일 수 있습니다.
    • 디스크가 바쁘다: 모델을 계속 읽고 있다고 단정하면 안 됩니다. 로그 적재, 임시 파일, swap 가능성도 같이 봐야 합니다.
    • 로컬 환경이라 네트워크 문제는 아니다: 로컬에서도 프록시 buffering, 소켓 재사용, flush 지연은 충분히 생깁니다.
    • 배치를 키우면 무조건 효율적이다: 처리량은 좋아질 수 있어도 TTFT와 tail latency는 악화될 수 있습니다.
    • 평균값만 줄면 됐다: 사용자는 평균이 아니라 느린 요청을 기억합니다. 운영 품질은 P95/P99에서 갈립니다.

    7. 검증은 “얼마나 빨라졌나”보다 “어느 단계가 줄었나”가 중요합니다

    튜닝 후 검증에서 봐야 할 건 단순한 전후 비교표가 아닙니다. 병목 위치가 실제로 이동했는지를 확인해야 합니다. 예를 들어 total time은 줄었는데 TTFT가 그대로면 사용자는 여전히 답답하다고 느낄 수 있습니다. 반대로 TTFT가 개선됐는데 TPOT이 흔들리면 스트리밍 경험은 여전히 좋지 않을 수 있습니다.

    • TTFT: 첫 체감이 나아졌는지 확인합니다.
    • TPOT: 토큰 생성이 끊기지 않고 안정적인지 봅니다.
    • Queue Wait: 동시성 설정이 맞았는지 판단합니다.
    • GPU/CPU 패턴: 톱니형 유휴 구간, 특정 코어 과열, 불규칙한 burst가 줄었는지 봅니다.
    • P95/P99: 평균이 아닌 꼬리 지연이 얼마나 개선됐는지 확인합니다.

    실무에서는 이 판단이 중요합니다. 최적화가 성공한 것처럼 보여도 사용자 불만은 그대로인 경우가 종종 있습니다. 그럴 때 로그를 뜯어보면 마지막 응답 종료 시점만 짧아졌지 첫 반응은 그대로인 경우가 많았습니다. 대화형 서비스는 특히 초반 1~2초의 인상이 전체 만족도를 크게 좌우합니다.

    LLM 추론 최적화 전후 결과를 보여주는 대시보드 이미지

    최적화 전후를 비교하는 검증 대시보드 예시입니다.

    검증 체크리스트

    1. 같은 프롬프트 길이로 여러 번 호출해 편차를 봅니다.
    2. 짧은 입력과 긴 입력을 분리해 측정합니다.
    3. 동시 요청이 없을 때와 있을 때를 나눠 봅니다.
    4. 프록시 직통 호출과 프록시 경유 호출을 둘 다 측정합니다.
    5. 느린 요청 몇 건의 stage 로그를 직접 확인합니다.
    6. 튜닝 후에도 GPU 유휴 구간이 남는지 확인합니다.

    8. 상황별 추천: 이럴 땐 A, 저럴 땐 B로 가면 됩니다

    운영에서는 결국 선택을 해야 합니다. 아래처럼 우선순위를 고정해 두면 쓸데없는 우회가 줄어듭니다.

    상황 우선순위 추천 접근 보류할 것
    첫 응답이 답답한 챗봇 TTFT 프록시 buffering 해제, 큐 대기 축소, 입력 길이 상한 적용 배치 확대부터 시도
    긴 문서 생성 배치 작업 처리량 배치 전략 최적화, 후처리 비동기화, 로그 비용 절감 챗봇과 동일한 저지연 설정 고집
    GPU는 한가한데 느림 비모델 구간 제거 토크나이저, 로깅, JSON 직렬화, 소켓 재사용부터 점검 하드웨어 증설
    GPU 메모리 여유가 적음 안정성 입력 길이 정책, 동시성 상한, 긴 요청 분리 동시성만 밀어 올리기
    가끔만 매우 느린 tail latency P95/P99 코어 경합, 느린 요청 로그, 긴 입력 분리, 큐 설계 재검토 평균값만 보고 종료

    추천을 한 줄로 줄이면 이렇습니다. TTFT가 길면 프록시와 큐부터, TPOT이 흔들리면 스트리밍과 디코드 주변부터, GPU가 비는데도 느리면 CPU와 전처리부터 보시면 됩니다. 이 순서가 잘 먹히는 이유는 간단합니다. 비용이 적고 효과가 빠른 영역부터 건드리는 편이 증설보다 실패 확률이 훨씬 낮기 때문입니다.

    또 하나, LLM 추론 최적화는 설정값 암기 게임이 아닙니다. 모델이 바뀌면 prefill 특성이 달라지고, 프롬프트 정책이 바뀌면 TTFT 분포가 달라집니다. 그래서 개별 숫자를 외우기보다 관측 → 분리 → 수정 → 재검증 흐름을 팀 습관으로 만드는 쪽이 더 오래 갑니다. 비용도 줄고, 장애 대응 시간도 확실히 짧아집니다.

    관련 글이 있다면 프롬프트 길이 관리, RAG 캐시 전략, 스트리밍 API 운영 체크리스트와 내부 링크로 묶어 두는 것도 좋습니다. 검색 유입을 넓히는 데도 꽤 도움이 됩니다.

    LLM 병목 현상 진단 순서와 최적화 선택 기준 요약 이미지

    병목 진단 순서와 최적화 선택 기준을 한 장으로 요약한 인포그래픽입니다.

    FAQ

    Q1. GPU만 더 좋은 걸로 바꾸면 해결되나요?

    항상 그렇진 않습니다. GPU 사용률이 낮은데 지연이 길다면 모델 밖에서 시간을 쓰고 있을 가능성이 큽니다. 프록시, 큐, CPU, 전처리, 응답 직렬화부터 먼저 확인하는 편이 맞습니다.

    Q2. 어떤 로그부터 남기면 가장 실무에 도움이 되나요?

    요청 시작 시각, 큐 진입/탈출, 토크나이즈 시작/종료, 첫 토큰 시각, 응답 종료 시각, 입력 토큰 수, 출력 토큰 수 정도는 꼭 남기는 걸 권합니다. 이 정도만 있어도 TTFT와 total latency를 분리해서 볼 수 있습니다.

    Q3. 배치를 키우면 무조건 효율이 좋아지지 않나요?

    처리량 관점에선 좋아질 수 있지만, 대화형 서비스에선 TTFT와 tail latency가 나빠질 수 있습니다. 사람이 기다리는 서비스인지, 백그라운드 작업인지부터 먼저 정하는 게 좋습니다.

    Q4. 프록시 문제와 모델 문제는 가장 빠르게 어떻게 구분하나요?

    같은 요청을 프록시 직통과 프록시 경유로 각각 curl -w 측정해 보시면 됩니다. time_starttransfer 차이가 크면 모델보다 경로 문제일 가능성이 큽니다.

    마무리

    여러 번 겪고 나니 패턴이 꽤 분명했습니다. 느린 LLM 서비스의 원인은 생각보다 자주 모델 바깥에 있습니다. 그래서 저는 장비보다 먼저 첫 토큰 전까지 어디서 멈추는지, 토큰 생성 중 무엇이 끊는지, GPU가 왜 놀고 있는지부터 확인합니다. 이 순서로 가면 엉뚱한 튜닝을 줄일 수 있고, 증설 없이 해결되는 문제도 꽤 많습니다.

    실무 기준으로 추천을 한 줄로 압축하면 이렇습니다. 챗봇이면 TTFT 우선, 배치 작업이면 throughput 우선, GPU가 비면 CPU와 프록시부터, tail latency가 튀면 평균이 아니라 느린 요청 로그부터 보시면 됩니다. 이 기준만 잡아도 LLM 추론 최적화의 시행착오를 크게 줄일 수 있습니다.

  • [OpenStack] 오픈스택 업그레이드 실패 사례 분석: Horizon 대시보드 접근 불가 문제 해결

    [OpenStack] 오픈스택 업그레이드 실패 사례 분석: Horizon 대시보드 접근 불가 문제 해결

    [OpenStack] 오픈스택 업그레이드 실패 사례 분석: Horizon 대시보드 접근 불가 문제 해결

    오픈스택 업그레이드 실패를 한 번이라도 겪어보신 분이라면 공감하실 겁니다. 업그레이드 작업 자체는 끝났는데, 막상 Horizon(호라이즌, 웹 기반 대시보드)이 안 열리면 정말 답답하더라고요. API는 살아 있는 것 같은데 웹 화면만 500 에러가 뜨거나, 로그인 루프가 걸리거나, 정적 파일이 깨져서 CSS 없이 하얀 화면만 나오는 경우가 꽤 많습니다. 저도 홈랩과 테스트 환경에서 이런 업그레이드 후 장애를 몇 번 겪었는데, 처음엔 이게 네트워크 문제인지 애플리케이션 문제인지 감이 안 오더라고요.

    이번 글은 제가 실제로 자주 밟았던 흐름을 기준으로, 오픈스택 업그레이드 실패 이후 발생한 Horizon 대시보드 오류를 어떻게 좁혀가고, 어떤 순서로 복구하면 좋은지 정리한 글입니다. 특정 배포판이나 특정 릴리스에만 묶이지 않도록, 검증된 일반 원칙과 현장에서 바로 써먹을 수 있는 점검 순서 중심으로 풀어보겠습니다.

    오픈스택 업그레이드 실패와 Horizon 대시보드 접근 구조를 설명하는 아키텍처 이미지

    Horizon, 웹 서버, Keystone, Memcached, 정적 파일 경로의 관계를 한눈에 보여주는 아키텍처 개요 이미지입니다.

    왜 Horizon만 죽는 걸까요? 오픈스택 업그레이드 실패의 전형적인 패턴

    쉽게 말해 Horizon은 혼자 동작하는 화면이 아닙니다. Apache(아파치, 웹 서버)나 Nginx(엔진엑스, 웹 서버) 뒤에서 돌아가고, 내부적으로는 Django(장고, 파이썬 웹 프레임워크) 기반 설정을 읽고, 로그인은 보통 Keystone(키스톤, 인증 서비스)과 연동되고, 세션은 Memcached(메모리 캐시)를 쓰는 경우가 많습니다. 여기에 정적 파일(static files), 정책 파일(policy files), WSGI(웹 서버 게이트웨이 인터페이스) 경로까지 얽혀 있죠.

    그래서 업그레이드 직후 Horizon 접근이 안 된다고 해서 원인이 꼭 Horizon 패키지 하나에만 있지는 않습니다. 실제로는 아래처럼 엮여 있는 경우가 많더라고요.

    • 웹 서버 설정은 살아 있지만 WSGI 경로가 이전 버전을 가리키는 경우
    • 패키지 업그레이드 후 정적 파일이 재배포되지 않아 화면이 깨지는 경우
    • local_settings.py 같은 설정 파일이 유지되면서 새 릴리스와 충돌하는 경우
    • Memcached 세션 문제로 로그인만 무한 반복되는 경우
    • Keystone 엔드포인트(endpoint, 서비스 접속 주소)나 도메인 설정이 달라져 인증만 실패하는 경우

    여기서 중요한 포인트가 있습니다. Horizon 대시보드 오류는 증상이 비슷해 보여도 원인은 꽤 다르거든요. 그래서 무작정 재시작부터 하면 시간만 더 씁니다. 저도 처음엔 서비스 재시작만 반복했었는데, 로그를 순서대로 본 날부터 복구 시간이 확 줄었습니다.

    증상별로 원인을 좁히는 방법

    제가 직접 해보니 가장 빨랐던 방법은 증상 기준으로 분류하는 거였습니다. 아래 표처럼 보면 훨씬 덜 헤맵니다.

    증상 가능성 높은 원인 우선 확인할 곳
    브라우저에서 500 Internal Server Error Django 설정 충돌, WSGI 오류, 패키지 의존성 문제 웹 서버 에러 로그, Horizon 애플리케이션 로그
    로그인 후 다시 로그인 화면으로 돌아감 세션 저장 실패, Memcached 문제, 쿠키/호스트 설정 문제 memcached 상태, local_settings.py, 브라우저 쿠키
    화면은 열리는데 CSS/JS가 깨짐 정적 파일 누락, collectstatic 미실행, 웹 서버 alias 불일치 정적 파일 경로, 웹 서버 설정
    특정 메뉴만 403 또는 비정상 정책 파일, RBAC(Role-Based Access Control, 역할 기반 접근 제어) 반영 문제 policy 파일, 서비스 연동 상태
    대시보드가 매우 느리거나 간헐 실패 Keystone 연동 지연, 캐시 문제, DNS 또는 백엔드 네트워크 이슈 API 응답, DNS, 캐시 상태

    이 표를 기준으로 보면, 막연한 OpenStack 문제 해결이 아니라 실제 점검 순서를 잡을 수 있습니다. 장애 대응에서 이 차이가 꽤 크더라고요.

    실전 점검 1단계: 웹 서버와 Horizon 프로세스부터 확인

    저는 항상 가장 바깥쪽부터 봅니다. 사용자는 웹으로 접속하니까, 먼저 웹 서버가 정상 응답하는지 확인해야 하거든요.

    1. Horizon 가상호스트(vhost, 가상 호스트) 설정이 로드되는지 확인합니다.
    2. 웹 서버 프로세스가 살아 있는지 봅니다.
    3. 에러 로그에서 Python traceback(트레이스백, 예외 호출 기록)이 있는지 찾습니다.
    # Debian/Ubuntu 계열 예시
    systemctl status apache2
    journalctl -u apache2 -n 100 --no-pager
    
    # RHEL 계열 예시
    systemctl status httpd
    journalctl -u httpd -n 100 --no-pager
    
    # Horizon 관련 설정 파일 위치 예시 확인
    ls -al /etc/openstack-dashboard/
    ls -al /usr/share/openstack-dashboard/
    

    여기서 ModuleNotFoundError, ImportError, TemplateDoesNotExist 같은 에러가 보이면 방향이 꽤 명확해집니다. 대개 패키지 업그레이드 이후 Python 모듈 경로나 템플릿, 또는 설정 파일이 새 구조와 안 맞는 경우가 많거든요.

    반대로 웹 서버는 멀쩡하고 정적 파일만 404가 난다면, 애플리케이션 자체보다 배포 경로나 alias 설정 쪽이 더 의심스럽습니다.

    오픈스택 업그레이드 실패 시 Horizon 대시보드 오류 진단 순서를 보여주는 이미지

    웹 서버 로그 확인, WSGI 점검, Keystone 인증 확인, 정적 파일 점검 순서를 정리한 트러블슈팅 플로우차트입니다.

    실전 점검 2단계: 설정 파일과 WSGI 경로를 비교합니다

    업그레이드 후 장애에서 정말 자주 나오는 게 이전 설정 파일이 남아 있는 상태입니다. 특히 local_settings.py는 환경마다 많이 손보는 파일이라, 예전 옵션이 새 코드와 충돌하기 쉽습니다. 저도 처음엔 설정을 많이 남겨두는 게 안전하다고 생각했는데, 실제로 써보니까 최소 설정만 남기고 차이를 다시 보는 쪽이 훨씬 낫더라고요.

    # 설정 파일 백업 후 비교
    cp /etc/openstack-dashboard/local_settings.py /root/local_settings.py.bak
    
    # 배포판에 따라 샘플 파일 위치는 다를 수 있으므로 실제 경로를 확인해서 비교
    find /usr/share/openstack-dashboard -name "*local_settings*" -o -name "settings.py"
    

    이 단계에서 제가 중점적으로 보는 항목은 아래입니다.

    • OPENSTACK_HOST: Keystone 또는 컨트롤러 접근 대상
    • ALLOWED_HOSTS: 웹 접근 호스트 허용 목록
    • CACHES: Memcached 백엔드 주소와 포트
    • SESSION_ENGINE: 세션 저장 방식
    • WEBROOT: 프록시 뒤 경로가 바뀐 경우 중요
    • 압축, 보안 헤더, SSL 종료 위치와 관련된 프록시 옵션

    WSGI 설정도 꼭 같이 봐야 합니다. 웹 서버가 여전히 예전 Python 경로나 예전 Horizon 설치 디렉터리를 바라보면, 패키지는 업그레이드됐는데 실행은 이전 구조를 참조하는 애매한 상태가 생기거든요.

    # 웹 서버 설정에서 dashboard, wsgi, static 경로 확인
    grep -R "wsgi\|static\|dashboard" /etc/apache2 /etc/httpd 2>/dev/null
    

    혹시 이런 경험 있으신가요? 서비스는 살아 있는데 브라우저에선 계속 500만 보이는 상황이요. 그런 경우 로그 안에 실제 원인이 거의 다 들어 있습니다. 눈에 잘 안 띄어서 그럴 뿐이죠.

    예시: 점검 포인트를 정리한 설정 스니펫

    # local_settings.py 예시 점검 포인트
    OPENSTACK_HOST = "controller"
    ALLOWED_HOSTS = ['*']
    WEBROOT = '/'
    
    CACHES = {
        'default': {
            'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
            'LOCATION': '127.0.0.1:11211',
        }
    }
    
    SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
    

    위 값 자체가 정답이라는 뜻은 아닙니다. 환경마다 다르거든요. 중요한 건 업그레이드 전후에 같은 의도를 유지하고 있는지, 그리고 새 버전에서 더 이상 쓰지 않는 옵션이 없는지를 보는 겁니다.

    실전 점검 3단계: Keystone 인증과 세션 문제를 분리해서 봅니다

    Horizon이 안 열릴 때 많은 분들이 웹 서버만 보는데, 로그인 단계에서 튕긴다면 사실상 Keystone 연동과 세션 저장을 같이 봐야 합니다. 특히 업그레이드 후 장애에서 많이 나오는 게 로그인 성공처럼 보이는데 다시 로그인 화면으로 돌아오는 케이스입니다. 이건 체감상 정말 답답합니다 ㅎㅎ

    1. CLI로 Keystone 인증이 정상인지 확인합니다.
    2. 서비스 엔드포인트가 올바른지 확인합니다.
    3. Memcached가 정상인지 확인합니다.
    # OpenStack CLI 인증 확인 예시
    openstack token issue
    openstack endpoint list
    openstack service list
    
    # memcached 상태 확인 예시
    systemctl status memcached
    ss -lntp | grep 11211
    

    CLI 인증이 되는데 Horizon 로그인만 실패하면, 저는 거의 항상 세션이나 쿠키 설정을 의심합니다. 반대로 CLI 인증부터 안 되면 Horizon 복구 전에 Keystone 쪽부터 정상화해야 하죠. 이 순서를 뒤집으면 시간을 많이 버립니다.

    Horizon 대시보드 오류 원인인 설정 파일과 Memcached 연동을 설명하는 이미지

    Horizon 설정, Keystone 인증 흐름, Memcached 세션 저장 위치를 연결해서 보여주는 구성 다이어그램입니다.

    실전 복구: 제가 주로 쓰는 복구 절차

    여기부터는 제가 직접 해보니 성공 확률이 높았던 순서입니다. 핵심은 한 번에 많이 바꾸지 않는 것입니다. 급하다고 이것저것 동시에 손대면, 나중에 뭐가 원인이었는지 또 모르게 되거든요.

    1. 웹 서버 에러 로그에서 첫 번째 traceback을 확보합니다.
    2. local_settings.py의 커스텀 값을 최소화하고, 필수 값만 남겨 재시작합니다.
    3. 정적 파일 경로와 권한을 확인합니다.
    4. 세션 캐시를 점검하고 필요 시 캐시를 비웁니다.
    5. 웹 서버를 재시작한 뒤 브라우저 캐시를 비우고 다시 접속합니다.
    # 정적 파일 경로 및 권한 예시 확인
    find /usr/share/openstack-dashboard -maxdepth 3 -type d | grep static
    find /var/lib/openstack-dashboard -maxdepth 3 -type d 2>/dev/null
    
    # 웹 서버 재시작 예시
    systemctl restart apache2 || systemctl restart httpd
    
    # 재시작 후 즉시 로그 확인
    journalctl -u apache2 -n 50 --no-pager || journalctl -u httpd -n 50 --no-pager
    

    정적 파일이 의심될 때는 CSS, JS, 폰트 요청이 200인지 404인지 브라우저 개발자 도구에서도 꼭 봅니다. 화면이 아예 안 열리는 것과, 사실은 HTML은 뜨는데 리소스만 깨지는 건 대응 방식이 다르니까요.

    정적 파일 문제가 의심될 때

    패키지 업그레이드 이후 정적 파일 alias 경로가 달라졌거나, 수집된 파일이 맞지 않으면 화면이 하얗게 깨집니다. 이때는 웹 서버 설정의 Alias 또는 정적 파일 루트를 먼저 확인합니다. 배포판마다 관리 방식이 다르니, 임의 명령을 바로 넣기보다 현재 패키징 구조를 확인하는 게 더 안전합니다.

    세션 문제가 의심될 때

    로그인 루프는 세션 저장 실패일 가능성이 높습니다. Memcached 주소가 바뀌었거나, 로컬호스트/호스트명 해석이 꼬였거나, 여러 컨트롤러 노드에서 캐시 설정이 일치하지 않으면 이런 증상이 나옵니다. HA(High Availability, 고가용성) 환경이면 더 자주 겪습니다.

    ⚠️ 실제로 많이 겪는 함정들

    여긴 진짜 중요합니다. 저도 삽질을 좀 했습니다 ㅎㅎ 아래 항목들은 문서만 보고는 놓치기 쉬운데, 현장에서는 자주 만납니다.

    • 브라우저 캐시 때문에 복구가 안 된 것처럼 보이는 경우
      정적 파일이 바뀐 뒤에도 예전 JS/CSS를 잡고 있으면 여전히 깨져 보입니다.
    • 로드밸런서 뒤에서 WEBROOT 또는 호스트 헤더가 어긋나는 경우
      리버스 프록시(reverse proxy, 역방향 프록시)를 쓰면 경로와 스킴 전달이 중요합니다.
    • 정책 파일만 옛것을 유지해 메뉴가 사라지는 경우
      대시보드가 안 뜨는 문제와는 다르지만, 사용자는 같은 장애로 인식합니다.
    • 패키지 업그레이드는 됐는데 서비스 재기동 순서가 꼬인 경우
      특히 캐시와 웹 서버가 엇갈리면 증상이 애매합니다.
    • 컨트롤러가 여러 대인데 노드마다 설정이 다른 경우
      한 번은 되고 한 번은 안 되는 증상은 이 패턴이 많습니다.

    저는 이런 함정을 막으려고, 업그레이드 전에 꼭 아래 체크리스트를 남겨둡니다.

    # 업그레이드 전 백업/기록 체크 예시
    cp -a /etc/openstack-dashboard /root/backup-openstack-dashboard-$(date +%F)
    cp -a /etc/apache2 /root/backup-apache2-$(date +%F) 2>/dev/null || true
    cp -a /etc/httpd /root/backup-httpd-$(date +%F) 2>/dev/null || true
    
    openstack endpoint list > /root/openstack-endpoints-before.txt
    openstack service list > /root/openstack-services-before.txt
    

    이런 기록이 있으면 나중에 비교가 정말 빨라집니다. 문서화가 귀찮아도, 장애 한 번 줄이면 바로 본전 뽑습니다.

    검증: 복구가 끝났다면 어디까지 확인해야 할까

    대시보드 첫 화면만 뜬다고 끝이 아닙니다. 저는 최소한 아래까지 확인해야 진짜 복구라고 봅니다.

    1. 로그인 성공 후 프로젝트 목록이 정상 표시되는지 확인
    2. 인스턴스(Instance, 가상 머신) 목록 페이지가 열리는지 확인
    3. 이미지(Image), 네트워크(Network), 볼륨(Volume) 메뉴 접근 확인
    4. 브라우저 개발자 도구에서 정적 파일 404/500이 없는지 확인
    5. 웹 서버 로그에 신규 에러가 없는지 확인
    # API 자체는 정상인지 교차 검증
    openstack server list
    openstack network list
    openstack volume list
    

    CLI 결과가 정상이고 Horizon 화면까지 문제없이 뜬다면, 그제야 드디어 됐다! 싶은 순간이 옵니다. 저는 이때 꼭 운영 노트에 원인과 조치 순서를 적어둡니다. 다음 업그레이드 때 똑같은 실수를 안 하려고요.

    오픈스택 업그레이드 실패 복구 후 Horizon 대시보드 정상 검증을 보여주는 이미지

    로그인 성공, 프로젝트 목록, 인스턴스/네트워크/볼륨 메뉴 확인이 완료된 상태를 보여주는 검증 이미지입니다.

    정리: 오픈스택 업그레이드 실패를 줄이려면

    이번 사례를 한 줄로 정리하면 이렇습니다. 오픈스택 업그레이드 실패처럼 보이는 현상도, Horizon만 놓고 보면 웹 서버, 설정 파일, 인증, 세션, 정적 파일 중 하나로 꽤 잘 분해됩니다. 전체를 한 번에 보지 말고, 바깥에서 안쪽으로 좁혀가는 게 핵심입니다.

    점검 영역 핵심 질문 복구 힌트
    웹 서버 500 에러가 나는가? journalctl, error log, WSGI 경로 확인
    설정 파일 기존 커스텀 설정이 남아 있는가? local_settings.py 최소화 후 비교
    인증 CLI 인증은 되는가? openstack token issue, endpoint 점검
    세션/캐시 로그인 루프가 있는가? Memcached 상태와 주소 확인
    정적 파일 CSS/JS가 깨지는가? static 경로, alias, 브라우저 네트워크 탭 확인

    다음 글에서는 업그레이드 전에 미리 확인해야 할 체크리스트와 롤백 전략도 따로 다뤄볼 예정입니다. 이전 글에서 다뤘던 컨트롤러 노드 점검 루틴과 같이 보시면 더 흐름이 잘 잡히실 겁니다.

    오픈스택 업그레이드 실패와 Horizon 대시보드 오류 점검 우선순위를 요약한 이미지

    웹 서버, 설정 파일, 인증, 캐시, 정적 파일 순서로 점검하는 우선순위를 요약한 인포그래픽입니다.

    자주 묻는 질문

    Q1. Horizon만 안 되고 OpenStack CLI는 되면 어디부터 봐야 하나요?

    웹 서버 로그와 local_settings.py를 먼저 보시면 됩니다. 이 경우는 대개 Horizon 애플리케이션 계층 문제거나 세션/정적 파일 문제인 경우가 많습니다.

    Q2. 로그인만 반복되면 Keystone 장애라고 봐야 하나요?

    반드시 그렇진 않습니다. Keystone 자체보다 세션 저장이나 쿠키 처리 문제일 때도 많습니다. 그래서 CLI 인증과 웹 로그인 문제를 꼭 분리해서 확인하셔야 합니다.

    Q3. 업그레이드 후 장애를 줄이려면 가장 중요한 건 뭔가요?

    제가 느낀 1순위는 설정 백업과 차이 비교입니다. 그다음이 서비스별 검증 순서 고정입니다. 즉흥적으로 대응하면 같은 장애를 반복하게 되더라고요.

    혹시 지금 비슷한 Horizon 대시보드 오류를 겪고 계시다면, 위 순서대로만 점검해도 원인 범위를 꽤 빠르게 좁히실 수 있을 겁니다. 완벽한 정답보다, 재현 가능한 점검 루틴을 갖는 게 훨씬 강합니다.

  • [Kubernetes] Karpenter 프로비저닝 실패 디버깅 실제 사례 분석

    [Kubernetes] Karpenter 프로비저닝 실패 디버깅 실제 사례 분석

    [Kubernetes] Karpenter 프로비저닝 실패 디버깅 실제 사례 분석

    Karpenter 프로비저닝 실패 때문에 새 파드(Pod, 쿠버네티스에서 배포되는 최소 실행 단위)는 계속 Pending 상태인데, 오토스케일링(auto scaling, 부하에 따라 자동으로 늘고 줄어드는 기능)은 움직이지 않고, 로그만 한참 들여다보셨던 적 있으신가요? 저는 홈랩이든 업무 환경이든 이런 상황을 몇 번 겪었는데요. 처음엔 “분명 노드가 부족한데 왜 안 뜨지?” 싶었고, 실제로는 Karpenter 설정 자체보다 IAM(Role, 권한 역할), 서브넷(Subnet, 네트워크 구간) 태그, 인스턴스 제약 조건 같은 바깥 조건에서 막히는 경우가 많더라고요. 이번 글에서는 제가 실제로 디버깅할 때 밟는 순서대로, Karpenter 프로비저닝 실패를 어떻게 좁혀가는지 정리해보겠습니다.

    특히 Kubernetes 노드 문제 해결이나 Karpenter 디버깅이 익숙하지 않은 분들은, 무작정 로그부터 보는 것보다 “스케줄링 실패 → 프로비저닝 판단 → 클라우드 리소스 생성 → 노드 조인(join, 클러스터 참여)” 이 흐름으로 보면 훨씬 덜 헷갈리실 거예요. 저도 처음엔 이걸 한 덩어리로 봐서 삽질 좀 했습니다 ㅎㅎ

    Karpenter 프로비저닝 실패 흐름을 보여주는 Kubernetes 아키텍처 다이어그램

    Karpenter 프로비저닝 실패가 어느 단계에서 발생하는지 한눈에 보여주는 개요 이미지입니다.

    Karpenter 프로비저닝 실패를 볼 때 먼저 이해해야 할 흐름

    쉽게 말해 Karpenter는 “스케줄되지 못한 워크로드가 있네? 그럼 조건에 맞는 노드를 하나 띄워보자” 하고 판단하는 컴포넌트입니다. 여기서 중요한 포인트는, 단순히 노드를 많이 만드는 도구가 아니라 스케줄링 제약 조건을 해석해서 그에 맞는 노드를 계산한다는 점입니다.

    문제가 나는 지점은 보통 4군데입니다

    1. 파드 스케줄링 조건이 너무 빡빡한 경우
      예: nodeSelector, affinity, toleration, resource request가 충돌합니다.
    2. Karpenter가 적절한 노드 후보를 계산하지 못하는 경우
      예: 인스턴스 타입 제약, capacity type, 아키텍처 제약이 과합니다.
    3. 클라우드 리소스 생성 단계에서 실패하는 경우
      예: IAM 권한, 보안 그룹(Security Group), 서브넷 태그, 용량 부족 등이 걸립니다.
    4. 노드는 생성됐지만 클러스터에 조인하지 못하는 경우
      예: 부트스트랩(user data), 인증, 네트워크 경로 문제가 있습니다.

    이 네 구간만 분리해서 봐도 디버깅 난이도가 확 내려갑니다. 실제로 써보니까 “Karpenter가 고장났다”기보다는, 주변 조건 하나가 꼬여서 연쇄적으로 실패하는 경우가 더 많았습니다.

    Kubernetes 노드 문제 해결을 위한 기본 점검 순서

    제가 현장에서 가장 먼저 하는 건 거창한 분석이 아니라 증상 분리입니다. 파드가 문제인지, Karpenter 컨트롤러(controller, 제어 루프) 판단이 문제인지, 아니면 클라우드 API 호출이 막히는지부터 확인해야 하거든요.

    1. Pending 파드부터 확인

    kubectl get pods -A
    kubectl describe pod <pod-name> -n <namespace>

    여기서 꼭 보는 건 Events입니다. 예를 들어 다음 같은 메시지가 보이면 방향이 잡힙니다.

    • 0/3 nodes are available: 기존 노드에 스케줄이 안 되는 상황입니다.
    • Insufficient cpu 또는 Insufficient memory: 리소스 부족입니다.
    • node(s) had untolerated taint: 톨러레이션(toleration, 테인트 허용 설정) 문제입니다.
    • node affinity/selector mismatch: 파드 조건이 너무 좁습니다.

    혹시 여기서 이미 affinity나 nodeSelector 충돌이 보이면, Karpenter 이전에 워크로드 정의부터 손봐야 합니다. 이걸 놓치면 Karpenter 로그만 하루 종일 보게 되거든요.

    2. Karpenter 로그 확인

    kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=200
    kubectl get events -A --sort-by=.lastTimestamp

    로그에서는 이런 식의 단서를 찾습니다.

    • 프로비저닝 후보를 계산했는지
    • 요구사항(requirements) 때문에 제외된 인스턴스 타입이 있는지
    • 클라우드 제공자(provider) 호출에서 에러가 나는지
    • 생성 이후 노드 등록(register)이 안 되는지

    제가 직접 해보니, 로그를 길게 읽는 것보다 에러 키워드를 먼저 잡는 게 훨씬 빨랐습니다. 예를 들면 access denied, no subnets found, insufficient capacity, launch template 관련 문구 같은 것들이요.

    3. 노드 생성 여부와 조인 여부 분리

    kubectl get nodes
    kubectl get nodeclaims
    kubectl get nodepools
    kubectl describe nodeclaim <name>

    환경에 따라 리소스 이름은 다를 수 있지만, 핵심은 같습니다. 클라우드 인스턴스는 떴는데 노드가 안 보이는지, 아니면 인스턴스 자체가 생성되지 않았는지를 나눠서 봐야 합니다. 이 차이가 엄청 큽니다.

    Karpenter 디버깅을 위한 파드 이벤트와 로그 비교 이미지

    파드 이벤트와 Karpenter 로그를 같이 놓고 보면 어디서 막히는지 훨씬 빨리 찾을 수 있습니다.

    실전 사례: Karpenter 디버깅을 이렇게 풀었습니다

    이건 제가 자주 보는 전형적인 패턴입니다. 특정 워크로드가 배포된 뒤 Pending 상태가 길게 이어졌고, 클러스터 오토스케일링 오류 분석이 필요했던 상황이었습니다. 기존 노드는 여유가 없었고, Karpenter가 새 노드를 올려줘야 정상인데 아무 변화가 없었죠.

    증상

    • 파드는 Pending 상태 유지
    • Karpenter는 동작 중
    • 기존 노드는 리소스 부족
    • 새 노드는 추가되지 않음

    제가 먼저 확인한 파드 스펙

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sample-workload
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: sample-workload
      template:
        metadata:
          labels:
            app: sample-workload
        spec:
          nodeSelector:
            kubernetes.io/arch: amd64
          tolerations:
            - key: "workload-type"
              operator: "Equal"
              value: "batch"
              effect: "NoSchedule"
          containers:
            - name: app
              image: nginx:stable
              resources:
                requests:
                  cpu: "1"
                  memory: "1Gi"

    겉으로 보면 큰 문제 없어 보이죠. 저도 처음엔 워크로드 쪽은 괜찮다고 생각했었습니다. 근데 여기서 Karpenter 쪽 제약과 같이 봐야 했습니다.

    확인한 Karpenter 측 제약 예시

    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata:
      name: general
    spec:
      template:
        spec:
          requirements:
            - key: kubernetes.io/arch
              operator: In
              values:
                - amd64
            - key: kubernetes.io/os
              operator: In
              values:
                - linux
            - key: karpenter.sh/capacity-type
              operator: In
              values:
                - spot

    여기서 문제는 단순 설정 문법이 아니었습니다. 워크로드는 특정 톨러레이션을 요구하고, NodePool은 spot만 허용하며, 실제 가용 영역이나 계정 조건상 적절한 인스턴스 풀이 너무 좁아진 상태였던 거죠. 게다가 서브넷 태그도 일부 누락돼 있었습니다. 결국 Karpenter 입장에서는 만들 수 있는 노드 후보가 사실상 거의 없었던 셈입니다.

    정리하면 원인은 두 개였습니다

    구분 문제 내용 영향
    네트워크 일부 서브넷 태그 누락 적절한 서브넷 탐색 실패
    용량 제약 spot만 허용하고 요구사항이 좁음 프로비저닝 후보 부족

    이런 경우 진짜 헷갈립니다. 로그에는 하나의 에러만 크게 보일 때도 있지만, 실제론 조건이 두세 개 겹쳐서 실패하는 경우가 많거든요.

    제가 적용했던 점검 명령어와 확인 포인트

    아래 명령어들은 특정 클라우드에 종속되지 않게, 쿠버네티스 관점에서 먼저 확인할 수 있는 것들입니다. 이 순서대로 보면 꽤 안정적으로 원인을 줄여갈 수 있었습니다.

    1. 파드 이벤트 확인
    2. Karpenter 로그에서 프로비저닝 시도 여부 확인
    3. NodePool/NodeClaim 조건 확인
    4. 생성된 노드 존재 여부 확인
    5. 클라우드 콘솔 또는 API에서 인스턴스 생성 실패 원인 확인
    kubectl describe pod <pod-name> -n <namespace>
    kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --since=30m
    kubectl get nodepools -o yaml
    kubectl get nodeclaims -o yaml
    kubectl get nodes -o wide

    여기서 중요한 포인트! Karpenter 디버깅은 “쿠버네티스 안쪽 정보”와 “클라우드 바깥 정보”를 같이 봐야 끝이 납니다. 쿠버네티스 쪽만 보면 “왜 안 되지?”에서 끝나고, 클라우드 쪽만 보면 “인스턴스가 왜 안 뜨지?”로 끝나더라고요.

    Karpenter 프로비저닝 실패 원인인 NodePool 제약 조건 다이어그램

    NodePool 제약 조건이 겹칠수록 프로비저닝 가능한 후보가 급격히 줄어드는 상황을 설명하는 이미지입니다.

    ⚠️ 실제로 많이 만나는 Karpenter 프로비저닝 실패 원인

    이 섹션은 제가 여러 번 겪으면서 “아, 이건 체크리스트로 빼야겠다” 싶었던 것들입니다. 운영 중인 분들이라면 특히 공감하실 거예요.

    1. 파드 요구사항이 너무 강한 경우

    • nodeSelector가 특정 아키텍처나 라벨만 강제
    • podAntiAffinity가 과도하게 설정됨
    • requests가 커서 맞는 노드가 매우 제한적
    • toleration이 없어서 tainted node에 못 올라감

    처음엔 Karpenter가 노드를 안 만드는 줄 알았는데, 실제론 만들어도 스케줄이 안 맞는 상태라 계산상 제외되는 경우가 있었습니다.

    2. NodePool 요구사항이 좁은 경우

    • 특정 인스턴스 계열만 허용
    • spot만 허용
    • 특정 zone만 허용
    • 아키텍처와 운영체제 조건이 불필요하게 제한됨

    이건 비용 최적화하려다 자주 생깁니다. 저도 spot 위주로 예쁘게 짜보려다가, 정작 트래픽이 몰릴 때 후보가 없어서 자동 확장이 안 된 적이 있었거든요. 그래서 운영 환경에서는 제약은 최소화하고, 꼭 필요한 정책만 남기는 쪽이 더 안전했습니다.

    3. 클라우드 리소스 검색 실패

    • 서브넷 태그 누락
    • 보안 그룹 태그 누락
    • IAM 권한 부족

    이 부분은 쿠버네티스 YAML만 아무리 봐도 안 나옵니다. 특히 태그 기반 디스커버리(discovery, 자동 탐색)를 쓰는 경우라면, 네트워크 리소스가 올바르게 식별되는지 꼭 확인하셔야 합니다.

    4. 노드 생성 후 조인 실패

    • 부트스트랩 스크립트 문제
    • 클러스터 API 엔드포인트 접근 문제
    • 노드 인증 관련 설정 누락

    이 경우는 인스턴스는 생성되는데 kubectl get nodes에 안 보입니다. 그래서 “Karpenter가 아무것도 안 했다”고 오해하기 쉬워요. 근데 사실 한 단계는 진행된 상태인 거죠.

    문제를 줄이기 위한 운영 팁

    실제로 써보니까 아래 네 가지가 제일 효과 있었습니다.

    • NodePool 제약을 처음부터 너무 촘촘하게 만들지 않기
    • 파드 requests/limits를 현실적으로 설정하기
    • 서브넷, 보안 그룹, IAM 같은 외부 조건을 체크리스트화하기
    • 이벤트와 로그를 함께 보기

    특히 Kubernetes 노드 문제 해결이 필요한 상황에서 오토스케일링 오류 분석은 재현이 어려운 경우가 많아서, 문제가 생겼을 때 바로 볼 수 있는 로그 수집 체계를 미리 만들어두는 게 좋습니다. 이전 글에서 다뤘던 클러스터 이벤트 정리 방식이 있다면 같이 묶어보셔도 좋고요. 이 부분은 다음 글에서 더 깊게 다뤄볼 예정입니다.

    검증: 수정 후 무엇을 확인했나

    저는 설정을 손본 뒤 항상 세 가지를 확인합니다. 단순히 노드가 떠도 끝이 아니거든요.

    1. Pending 파드가 실제로 Running으로 전환되는지
    2. 새 노드가 기대한 라벨과 용량으로 조인되는지
    3. 불필요하게 과한 스케일아웃이 일어나지 않는지
    kubectl get pods -A -w
    kubectl get nodes --show-labels
    kubectl top nodes

    문제가 해결되면 보통 흐름이 이렇게 바뀝니다.

    • Pending 파드 발생
    • Karpenter가 새 노드 계산 및 생성
    • 노드가 클러스터에 조인
    • 파드가 새 노드에 스케줄됨

    드디어 됐다! 하는 순간이 여기입니다. 특히 오래 Pending이던 워크로드가 새 노드에 바로 올라가는 걸 보면, 이거 진짜 편하더라고요. 다만 성공했다고 끝내지 말고, 왜 실패했는지 기록해두는 게 다음 장애 때 훨씬 큰 도움이 됩니다.

    Karpenter 프로비저닝 실패 해결 후 Pending 파드가 Running으로 전환된 결과 이미지

    수정 전후를 비교해 Pending 파드가 Running으로 바뀌는 결과 확인 이미지입니다.

    정리: Karpenter 디버깅은 순서가 전부입니다

    Karpenter 프로비저닝 실패를 잡을 때 가장 중요한 건, 증상을 한 번에 해석하려고 하지 않는 겁니다. 저도 처음엔 로그 한 줄에서 답을 찾으려 했는데, 실제론 파드 조건, NodePool 제약, 클라우드 리소스 검색, 노드 조인 이 네 단계를 분리해서 봐야 풀리더라고요.

    혹시 지금도 Kubernetes 노드 문제 해결 때문에 막혀 계시다면, 아래 체크리스트부터 차근차근 보시면 좋겠습니다.

    • 파드 Events에 스케줄링 실패 원인이 보이는가
    • Karpenter가 실제로 프로비저닝을 시도했는가
    • NodePool 제약이 과하지 않은가
    • 서브넷, 보안 그룹, IAM 조건이 맞는가
    • 생성된 노드가 클러스터에 정상 조인하는가

    이 순서만 익혀도 Karpenter 디버깅 속도가 꽤 빨라집니다. 저도 처음엔 헷갈렸는데, 몇 번 정리해두고 나니 장애 대응 시간이 확 줄었습니다. 마무리 전에 요약 하나 더 남겨볼게요.

    Karpenter 프로비저닝 실패 디버깅 체크리스트 인포그래픽

    Karpenter 프로비저닝 실패 원인과 대응 순서를 한 장으로 정리한 요약 이미지입니다.

    FAQ: 자주 헷갈리는 포인트

    Q1. 파드가 Pending이면 무조건 Karpenter 문제인가요?

    아닙니다. 파드 스펙의 affinity, toleration, resource request가 원인인 경우도 많습니다.

    Q2. 노드가 생성되지 않으면 어디부터 봐야 하나요?

    파드 이벤트, Karpenter 로그, NodePool 조건을 먼저 보고, 그 다음 클라우드 리소스 조건을 확인하는 게 좋습니다.

    Q3. spot만 허용해도 괜찮을까요?

    가능은 하지만, 워크로드 특성과 가용성 요구사항을 고려해야 합니다. 운영에선 제약을 너무 좁히면 Karpenter 프로비저닝 실패 가능성이 올라가더라고요.

    마무리

    이번 글은 특정 버전 문법을 깊게 파기보다는, 문제가 났을 때 어떤 순서로 생각해야 하는지에 집중해봤습니다. 사실 운영에서는 정답 YAML 하나보다, 실패를 분해해서 보는 관점이 더 오래 가거든요. 다음 글에서는 Karpenter와 Cluster Autoscaler를 어떤 기준으로 나눠서 볼지, 그리고 워크로드별로 어떤 제약을 어디까지 허용할지 이어서 정리해보겠습니다.

  • [보안] Nmap 오류 해결: 스캔 실패 원인부터 디버깅 팁까지

    [보안] Nmap 오류 해결: 스캔 실패 원인부터 디버깅 팁까지

    Nmap 오류 해결: 스캔 실패 원인부터 디버깅 팁까지

    Nmap 오류 해결 때문에 검색창을 열어보신 분들, 아마 저랑 비슷한 상황이었을 겁니다. 분명 명령어는 간단한데 결과가 안 나오거나, Host seems down, Failed to resolve, Operation not permitted 같은 메시지가 뜨면 순간 멈추게 되거든요. 저도 홈랩에서 VLAN(브이랜, 가상 LAN) 나누고 방화벽 규칙을 만지다가 Nmap 스캔 실패를 여러 번 겪었습니다. 처음엔 대상 서버가 죽은 줄 알았는데, 실제로는 DNS(도메인 이름 해석), 권한, ICMP(인터넷 제어 메시지), 라우팅 중 하나가 문제인 경우가 많더라고요.

    이번 글에서는 제가 실무와 홈랩에서 자주 부딪혔던 네트워크 스캔 문제를 기준으로, Nmap이 왜 실패하는지, 어디서부터 확인해야 하는지, 그리고 실제로 어떤 옵션을 붙이면 디버깅이 쉬워지는지 차근차근 정리해보겠습니다. 단순히 명령어만 던지는 글이 아니라, 왜 그런 결과가 나오는지까지 같이 보실 수 있게 구성했습니다.

    Nmap 오류 해결 흐름을 보여주는 홈랩 네트워크 개요 다이어그램

    홈랩 네트워크에서 Nmap 스캔 오류 원인을 DNS, 라우팅, 방화벽, 권한 순서로 추적하는 전체 흐름입니다.

    Nmap 스캔 실패, 쉽게 말해 어디에서 막히는 걸까요?

    쉽게 말해 Nmap은 대상에게 여러 방식으로 말을 걸어보고, 그 반응을 분석해서 포트 상태나 호스트 존재 여부를 판단하는 도구죠. 여기서 중요한 건 내가 보낸 패킷(packet, 네트워크 데이터 조각)이 제대로 나갔는지, 상대가 응답할 수 있는 환경인지, 그리고 그 응답을 내 시스템이 읽을 권한이 있는지입니다.

    예를 들어 이런 식입니다.

    • 이름 자체를 못 찾으면 DNS 문제예요.
    • 패킷은 갔는데 응답이 막히면 방화벽(Firewall, 트래픽 제어 장치) 문제일 수 있거든요.
    • 응답은 오는데 내가 못 읽으면 권한 또는 로컬 보안 정책 문제일 수 있어요.
    • 상대가 ICMP를 막아두면 살아 있어도 죽은 것처럼 보일 수 있어요.

    저도 처음엔 Nmap 결과만 보고 서버가 꺼졌다고 단정했었는데, 실제로 써보니까 스캔 방식 차이 때문에 오해하는 경우가 꽤 많았어요. 특히 클라우드 환경이나 사내망처럼 중간 장비가 많은 곳에서는 더 그렇더라고요.

    Nmap 오류 해결 전에 먼저 구분해야 할 증상

    증상을 먼저 구분하면 삽질 시간을 꽤 줄일 수 있어요. 아래 표는 제가 자주 보는 패턴만 추린 겁니다.

    증상 의심 원인 먼저 볼 것
    Failed to resolve DNS 또는 오타 호스트명, /etc/hosts, nslookup 결과
    Host seems down ICMP 차단, 라우팅 문제, 대상 응답 제한 -Pn 사용, ping 결과, 게이트웨이 경로
    Operation not permitted 권한 부족 sudo 사용 여부, 스캔 방식
    All ports filtered 방화벽 또는 ACL 보안 장비 규칙, 대상 서버 정책
    너무 느리게 진행됨 패킷 손실, 속도 제한, 과도한 재시도 -T 옵션, 재시도, 대상 네트워크 품질
    예상과 다른 포트만 열림 NAT, 프록시, 로드밸런서 실제 종단점, 포트포워딩, 보안그룹

    여기서 중요한 포인트! Nmap 오류 해결은 명령어 암기보다 증상 분류가 먼저거든요. 이 순서만 잡혀도 디버깅이 훨씬 빨라집니다.

    Nmap 디버깅 시작: 가장 먼저 확인하는 기본 명령어

    저는 스캔이 안 될 때 바로 복잡한 NSE(Nmap Scripting Engine, 엔맵 스크립트 엔진) 스크립트부터 돌리지 않아요. 먼저 가장 단순한 확인부터 가거든요. 아래 순서를 추천드립니다.

    1. 호스트명이 맞는지 확인합니다.
    2. 대상 IP로 라우팅이 되는지 확인합니다.
    3. 권한이 필요한 스캔인지 확인합니다.
    4. 호스트 발견(Host Discovery, 생존 확인) 단계와 포트 스캔 단계를 분리해서 봅니다.

    1. 이름 해석부터 확인

    nslookup example.local
    getent hosts example.local
    ping -c 1 example.local

    여기서 이름이 안 풀리면 Nmap 이전에 DNS 쪽을 봐야 해요. 사내 테스트망이나 홈랩에서는 /etc/hosts에 임시로 등록해둔 값을 잊는 경우도 많거든요. 저도 VM(가상머신) 이름 바꿔놓고 예전 이름으로 계속 쏘다가 한참 헤맨 적 있습니다 ㅎㅎ

    2. 가장 단순한 포트 확인

    nmap 192.168.0.10
    nmap -p 22,80,443 192.168.0.10

    이 단계에서는 결과가 아주 정교할 필요는 없어요. 우선 대상이 보이는지, 일부 포트라도 반응하는지 보는 용도니까요.

    3. 권한 이슈 분리

    sudo nmap -sS 192.168.0.10
    nmap -sT 192.168.0.10

    -sS는 SYN Scan(신 스캔, 절반만 연결 시도하는 방식)이고 보통 raw packet 접근이 필요해서 권한 문제가 걸릴 수 있어요. 반면 -sT는 TCP Connect Scan(TCP 연결 스캔)이라 일반 사용자 환경에서도 비교적 시도하기 쉽더라고요. 같은 대상인데 -sS만 실패하면 권한 쪽을 의심해볼 수 있어요.

    4. 호스트 발견을 건너뛰고 직접 확인

    nmap -Pn 192.168.0.10
    nmap -Pn -p 443 192.168.0.10

    이 옵션은 정말 자주 써요. 대상이 ICMP 응답을 막아두면 살아 있어도 죽은 것처럼 보일 수 있거든요. 처음엔 이게 뭔가 싶었는데, 방화벽이 잘 짜인 서버일수록 오히려 ping이 안 되는 경우가 많더라고요.

    Nmap 오류 해결을 위한 기본 점검 명령어와 권한 차이 화면

    Nmap 기본 점검 순서와 root 권한이 필요한 스캔 방식 차이를 터미널 흐름으로 보여주는 이미지입니다.

    실전에서 바로 쓰는 Nmap 디버깅 옵션

    기본 확인으로 원인이 안 보이면 그다음은 Nmap 디버깅 옵션을 활용해야 해요. 저는 아래 조합을 가장 많이 써요.

    nmap -Pn -p 22,80,443 -vv 192.168.0.10
    nmap -Pn --reason 192.168.0.10
    nmap -Pn --packet-trace 192.168.0.10
    nmap -d 192.168.0.10
    • -vv: 상세 출력(Verbose, 자세한 로그)을 늘려요.
    • --reason: 왜 open, closed, filtered로 판단했는지 이유를 보여줘요.
    • --packet-trace: 패킷 송수신 흐름을 추적해요.
    • -d: 디버그(Debug, 내부 동작 로그) 레벨 출력을 켜요.

    개인적으로는 --reason이 아주 유용했어요. 결과만 보면 막막한데, 판단 근거가 보이기 시작하면 문제 지점이 훨씬 선명해지거든요. 특히 filtered가 뜰 때 이게 진짜 대상 서버 방화벽인지, 중간 장비인지 추정하는 데 정말 도움이 돼요.

    속도 문제를 분리하는 방법

    nmap -T4 192.168.0.10
    nmap --max-retries 2 192.168.0.10
    nmap --host-timeout 30s 192.168.0.10

    스캔이 지나치게 느릴 때는 네트워크 품질이 안 좋거나, 필터링 장비가 응답을 늦추고 있을 가능성도 있어요. 다만 무작정 공격적으로 올리면 오탐(false positive, 잘못된 탐지)이나 누락이 생길 수 있거든요. 그래서 저는 처음엔 기본값으로 보고, 답답할 때만 범위를 좁혀서 조정해요.

    ⚠️ 흔히 겪는 Nmap 오류 해결 사례

    이제부터는 제가 실제로 자주 봤던 패턴이에요. 여기서 많이 갈리더라고요.

    사례 1. “Host seems down” 이 뜨는데 서버는 멀쩡한 경우

    이건 정말 흔해요. 대상 서버가 ICMP를 차단하거나, 보안 장비가 호스트 발견 패킷에 반응하지 않으면 이런 메시지가 떠요.

    nmap 192.168.0.20
    nmap -Pn 192.168.0.20
    traceroute 192.168.0.20

    저는 이런 경우 -Pn으로 다시 보고, 그래도 안 되면 경로 추적과 방화벽 정책을 같이 확인해요. 특히 다른 VLAN 사이를 넘을 때 ACL(Access Control List, 접근 제어 목록)이 숨어 있는 경우가 많았거든요.

    사례 2. “Failed to resolve” 는 사실 Nmap 문제가 아닌 경우

    호스트명 오타, 사설 DNS 누락, VPN(가상사설망) 미접속 상태에서 자주 나와요. 이건 Nmap 자체의 스캔 실패라기보다 입력 또는 이름 해석 문제에 가까워요.

    nslookup lab-web01
    cat /etc/hosts

    처음엔 스캐너가 이상한 줄 알았는데, 실제로 써보니까 이름 하나 틀린 경우가 생각보다 많더라고요. 웃긴데, 이런 게 제일 오래 걸려요.

    사례 3. “Operation not permitted” 또는 비정상 종료

    Linux 계열에서는 raw socket 접근이 필요한 기능이 권한 문제에 걸릴 수 있어요. macOS나 보안이 강화된 환경에서도 비슷한 제약을 볼 수 있어요.

    sudo nmap -sS 192.168.0.30
    nmap -sT 192.168.0.30

    만약 sudo 환경에서는 되고 일반 사용자에서는 안 된다면 방향이 꽤 명확해요. 이 경우 Nmap 자체를 의심하기보다 실행 권한과 스캔 타입을 분리해서 봐야 하는 거죠.

    사례 4. 포트가 전부 filtered로 보이는 경우

    이건 보통 대상 서버 앞단 어딘가에서 걸러지고 있다는 뜻이에요. 서버 로컬 방화벽일 수도 있고, 클라우드 보안그룹(Security Group, 가상 방화벽)일 수도 있고, 중간 IPS/IDS(침입 방지/탐지 장비)일 수도 있어요.

    nmap -Pn --reason -p 1-1024 192.168.0.40

    여기서 중요한 건 서버 한 대만 보지 말고 경로 전체를 봐야 한다는 거예요. 저도 처음엔 대상 서버의 ufw나 firewalld만 뒤졌는데, 정작 문제는 상위 스위치 ACL이었어요. 삽질 좀 했습니다 ㅎㅎ

    사례 5. 스캔 결과가 들쭉날쭉한 경우

    같은 명령인데 어떤 때는 열려 있고 어떤 때는 안 보이면, 로드밸런서(Load Balancer, 부하 분산 장비), Rate Limit(요청 제한), 패킷 손실을 의심해볼 수 있어요.

    nmap -Pn -p 443 --reason --packet-trace 192.168.0.50

    이럴 때는 여러 번 반복해서 비교하고, 가능하면 대상 서비스를 직접 curl 같은 도구로도 확인해보는 편이 좋아요. 스캔 도구 하나만 믿고 결론 내리면 헷갈릴 수 있거든요.

    Nmap 스캔 실패 원인인 방화벽과 ACL, 라우팅 문제 다이어그램

    방화벽과 ACL, 라우팅 누락 때문에 Nmap 스캔 실패가 발생하는 대표적인 네트워크 경로 예시입니다.

    단계별 점검 체크리스트

    현장에서 빨리 판단해야 할 때는 아래 순서가 꽤 쓸 만해요. 저는 메모장에 거의 템플릿처럼 적어두고 써요.

    1. 대상 식별: IP, 호스트명, 포트 범위가 맞는지 확인합니다.
    2. 이름 해석: DNS 또는 /etc/hosts가 정상인지 봅니다.
    3. 기본 연결성: ping, traceroute로 대략적인 경로를 확인합니다.
    4. 스캔 타입 분리: -sT, -sS, -Pn을 나눠봅니다.
    5. 상세 로그 확보: -vv, --reason, --packet-trace를 붙입니다.
    6. 중간 장비 확인: 방화벽, ACL, NAT, 보안그룹을 확인합니다.
    7. 대상 서비스 검증: ssh, curl, nc 같은 도구로 실제 서비스 반응을 교차 검증합니다.

    이 체크리스트를 따라가면 Nmap 오류 해결 과정이 훨씬 체계적이 돼요. 특히 여러 사람이 같이 문제를 볼 때, 어디까지 확인했는지 공유하기도 좋아요.

    검증: 결과를 어떻게 확인해야 믿을 수 있을까요?

    스캔이 한 번 성공했다고 바로 끝내면 아쉬워요. 저는 최소한 아래 세 가지는 같이 봐요.

    • Nmap 결과가 반복 실행에서도 비슷하게 나오는지
    • 실제 서비스 접속 결과와 일치하는지
    • 방화벽 정책과 스캔 결과가 논리적으로 맞는지
    nmap -Pn -p 22,80,443 192.168.0.10
    nc -vz 192.168.0.10 22
    curl -I http://192.168.0.10

    예를 들어 80번 포트가 open으로 보였는데 curl이 완전히 다른 응답을 주면, 프록시나 로드밸런서가 앞단에 있을 수 있어요. 반대로 Nmap에서는 안 보이는데 애플리케이션 접속은 된다면 스캔 방식이나 필터링 규칙을 다시 봐야겠죠. 결국 교차 검증에서 확정되는 순간이 정말 시원해요.

    Nmap 오류 해결 후 스캔 결과와 실제 서비스 검증을 비교하는 화면

    Nmap 포트 결과와 nc, curl 검증 결과를 나란히 비교해 신뢰도를 확인하는 장면입니다.

    자주 묻는 질문

    Q1. ping은 되는데 Nmap만 실패합니다. 왜 그럴까요?

    가능성은 여러 가지예요. ping은 ICMP 기반이고, Nmap 포트 스캔은 TCP 또는 UDP 기반이기 때문에 방화벽 정책이 다를 수 있거든요. 즉, 살아 있는 건 맞지만 포트 접근은 차단된 상황일 수 있어요.

    Q2. Nmap 스캔 실패가 나면 무조건 대상 서버 문제인가요?

    아니에요. 로컬 권한, DNS, VPN 연결 상태, 중간 방화벽, 라우팅, NAT까지 전부 후보거든요. 저도 처음엔 서버부터 의심했는데, 의외로 클라이언트 쪽 원인이 자주 나왔어요.

    Q3. UDP 스캔은 왜 더 헷갈리나요?

    UDP는 TCP보다 응답이 제한적이라 결과 해석이 더 어려워요. open|filtered처럼 애매한 상태가 자주 보일 수 있고, 시간도 오래 걸리는 편이에요. 그래서 처음부터 넓게 보기보다 필요한 포트 중심으로 좁혀서 확인하는 편이 낫거든요.

    마무리: Nmap 오류 해결의 핵심은 “도구”보다 “순서”입니다

    오늘 정리한 내용을 한 줄로 줄이면 이거예요. Nmap 오류 해결은 옵션 암기 싸움이 아니라, 이름 해석, 연결성, 권한, 방화벽, 실제 서비스 검증을 순서대로 좁혀가는 작업이거든요. 저도 처음엔 결과 한 줄에 흔들렸는데, 실제로 여러 번 부딪혀보니까 결국 답은 기본기 쪽에 있더라고요.

    혹시 지금도 Nmap 디버깅 때문에 막혀 계신다면, 우선 -Pn, --reason, --packet-trace 조합부터 써보세요. 그리고 결과를 서비스 접속 테스트와 꼭 같이 비교해보시고요. 이 루틴만 익숙해져도 네트워크 스캔 문제를 보는 눈이 꽤 달라질 거예요.

    다음 글에서는 Nmap 스캔 실패 이후에 tcpdump(티씨피덤프, 패킷 캡처 도구)로 패킷을 직접 보면서 원인을 좁히는 방법을 다룰 예정이에요. 이전 글에서 다룬 방화벽 기초 점검 내용과 함께 보시면 더 이해가 잘 될 거예요.

    Nmap 오류 해결 체크리스트와 디버깅 흐름 요약 인포그래픽

    Nmap 오류 해결 체크리스트를 한 장으로 정리한 요약 인포그래픽입니다.