13년차의 서버실

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

[태그:] 메모리 누수

  • [HomeLabs] UDM Pro 문제 해결: WAN 불안정과 메모리 누수 디버깅

    [HomeLabs] UDM Pro 문제 해결: WAN 불안정과 메모리 누수 디버깅

    UDM Pro 문제 해결: WAN 불안정과 메모리 누수 디버깅

    홈랩을 오래 굴리다 보면 제일 사람을 지치게 하는 순간이 있습니다. 분명 인터넷은 들어오는데 체감이 끊기는 것 같고, 화상회의는 순간적으로 튀고, 게임 핑은 갑자기 치솟고, 로그를 보면 또 멀쩡해 보이는 상황이요. 저도 UniFi Dream Machine Pro(UDM Pro)를 메인 라우터로 쓰면서 이런 일을 꽤 겪었습니다. 특히 UDM Pro 문제 해결이 필요한 순간은 보통 한 번에 오지 않더라고요. WAN 불안정과 리소스 누적이 겹치면서, 겉으로는 회선 문제처럼 보이는데 실제로는 장비 내부 상태가 원인인 경우가 있었습니다.

    처음엔 ISP 회선부터 의심했습니다. 저도 늘 그랬거든요. 근데 며칠 단위로 패턴을 기록해 보니까, 회선 자체보다는 UDM Pro 내부 메모리 사용량이 점점 올라가고 특정 시점부터 UI 반응과 WAN 품질이 같이 나빠지는 흐름이 보였습니다. 여기서 중요한 포인트는 WAN 불안정과 메모리 누수를 따로 봐서는 안 된다는 거예요. 오늘 글은 제가 실제로 홈랩 라우터를 디버깅할 때 쓰는 순서대로 정리해보겠습니다.

    UDM Pro 문제 해결을 위한 홈랩 네트워크 전체 구성도

    UDM Pro를 중심으로 WAN, 스위치, 서버, 클라이언트가 연결된 홈랩 구조를 한눈에 보여주는 개요 이미지입니다.

    1. 왜 UDM Pro 문제 해결이 까다로운가

    쉽게 말해 라우터 문제는 증상이 거짓말을 많이 합니다. 인터넷이 순간적으로 느려지면 다들 WAN부터 의심하지만, 실제로는 NAT(Network Address Translation, 주소 변환), IDS/IPS(Intrusion Detection/Prevention System, 침입 탐지/차단), 로그 적재, 컨트롤러 프로세스 같은 내부 요소가 먼저 흔들리는 경우가 있습니다.

    특히 UniFi UDM Pro는 라우터, 컨트롤러, 보안 게이트웨이 역할이 한 장비에 묶여 있거든요. 이 구조는 편합니다. 정말 편해요. 근데 한쪽 리소스가 밀리면 다른 기능에도 영향을 줄 수 있어요. 제가 직접 해보니 이런 식으로 연결되더라고요.

    • 메모리 사용량이 계속 올라감
    • 관리 UI 반응 속도가 둔해짐
    • 세션 처리나 DPI(Deep Packet Inspection, 패킷 심층 분석) 쪽이 무거워짐
    • WAN 지연시간과 패킷 손실이 간헐적으로 튐
    • 사용자는 그냥 인터넷이 끊긴다고 느끼게 됨

    그래서 네트워크 디버깅은 증상보다 흐름을 봐야 합니다. 한 번의 속도 측정으로 끝내면 안 되고, 시간 축으로 기록해야 하죠.

    2. WAN 불안정과 메모리 누수를 어떻게 구분할까

    저도 처음엔 헷갈렸는데, 기준을 세워두면 생각보다 빨리 갈립니다. WAN 쪽 문제인지, 장비 내부 문제인지 대략 아래처럼 분류할 수 있어요.

    증상 WAN 회선 이슈 가능성 장비 내부 리소스 이슈 가능성
    특정 시간대만 느림 높음 중간
    재부팅 직후 멀쩡함 낮음 높음
    관리 UI도 같이 느려짐 낮음 매우 높음
    외부 핑은 튀는데 내부 통신은 정상 높음 중간
    며칠 지나면 반복적으로 악화 중간 높음

    재부팅 후 한동안 멀쩡하다가 다시 나빠진다면, 저는 거의 무조건 메모리와 프로세스 상태부터 봅니다. 반대로 장비는 멀쩡한데 ISP 게이트웨이까지 핑이 튄다면 회선 쪽일 확률이 높더라고요.

    3. 먼저 확인할 최소 체크리스트

    본격적으로 SSH 붙기 전에, 저는 아래 순서부터 확인합니다. 괜히 깊이 들어가기 전에 큰 원인을 먼저 쳐내는 거죠.

    1. 인터넷 회선 장애 공지가 있는지 확인합니다.
    2. 광모뎀 또는 상위 모뎀의 링크 상태가 변한 적 있는지 봅니다.
    3. UDM Pro의 WAN 포트 협상 속도와 케이블 상태를 확인합니다.
    4. 최근 설정 변경 사항, 특히 IDS/IPS, Smart Queue, Traffic Identification 관련 변경이 있었는지 봅니다.
    5. 문제가 생기는 시간대가 백업, 카메라 업로드, 대용량 동기화 시간과 겹치는지 체크합니다.

    여기서 중요한 포인트! 설정 변경 이력을 꼭 보셔야 합니다. 홈랩은 특히 제가 직접 건드린 게 원인인 경우가 많았거든요. 삽질 좀 했습니다 ㅎㅎ

    4. 실전 1단계: UDM Pro에서 기본 상태 확인

    이제 SSH로 들어가서 최소한의 상태를 봅니다. 특정 버전 의존적인 명령보다는 범용 Linux/BusyBox 계열 명령 위주로 접근하는 게 안전하더라고요.

    ssh admin@udm-pro-ip
    uptime
    free -m
    top
    df -h
    dmesg | tail -n 50
    ping -c 20 1.1.1.1
    ping -c 20 8.8.8.8

    제가 실제로 써보니까 여기서 바로 감이 오는 경우가 많았습니다.

    • uptime: 장비가 얼마나 오래 켜져 있었는지 확인해요.
    • free -m: 메모리 사용량과 여유 메모리를 봅니다.
    • top: 어떤 프로세스가 CPU/메모리를 계속 먹는지 봐요.
    • dmesg: NIC(Network Interface Card, 네트워크 인터페이스) 오류나 커널 메시지를 확인합니다.
    • ping: 외부 목적지까지 손실과 지연 변동이 있는지 빠르게 확인해요.

    만약 이 시점에 관리 UI도 느리고, 메모리 여유가 비정상적으로 줄어 있다면 회선보다 내부 상태를 더 의심해볼 만합니다.

    UniFi UDM Pro의 WAN 불안정과 리소스 점검 장면

    SSH 터미널에서 uptime, free, top, ping 결과를 확인하며 UDM Pro 문제의 원인을 좁혀가는 장면을 보여주는 이미지입니다.

    5. 실전 2단계: 외부 모니터링으로 WAN 불안정을 잡아내기

    라우터 안에서만 보면 놓치는 게 있습니다. 그래서 저는 항상 별도 리눅스 장비나 NAS, 미니 PC 하나에서 외부 모니터링을 같이 돌립니다. 이게 진짜 중요합니다. 라우터가 힘들어하는 순간에도 바깥에서 본 기록이 남거든요.

    아래처럼 간단한 bash 스크립트를 하나 돌려두면 packet loss와 latency 변화를 시간대별로 남길 수 있습니다.

    #!/usr/bin/env bash
    TARGETS=("1.1.1.1" "8.8.8.8")
    LOGFILE="/var/log/wan-check.log"
    
    while true; do
      TS=$(date "+%Y-%m-%d %H:%M:%S")
      for target in "${TARGETS[@]}"; do
        RESULT=$(ping -c 5 -W 2 "$target" | tail -n 2 | tr '\n' ' ')
        echo "$TS target=$target $RESULT" >> "$LOGFILE"
      done
      sleep 60
    done

    이 로그를 보면 재미있는 패턴이 보여요. 문제가 생긴 시간에 모든 대상이 동시에 튀면 WAN 또는 라우터 공통 구간 문제일 가능성이 높고, 특정 대상만 흔들리면 외부 경로 문제일 수도 있습니다.

    좀 더 정리하면 이런 기준으로 보면 됩니다.

    1. 모든 외부 대상 핑이 동시에 튄다: UDM Pro 또는 회선 공통 구간 의심
    2. 관리 UI가 동시에 느려진다: 내부 리소스 문제 가능성 상승
    3. 재부팅 후 그래프가 초기화되듯 좋아진다: 메모리 누적 문제 가능성 상승
    4. 특정 시간에만 튄다: 스케줄 작업이나 트래픽 폭증 의심

    6. 실전 3단계: 메모리 누수처럼 보일 때 제가 확인한 포인트

    이 부분은 조심해서 봐야 해요. 엄밀히 말하면 모든 메모리 증가가 곧바로 leak(누수)인 건 아니거든요. Linux 계열 시스템은 캐시를 적극적으로 쓰기 때문에, 숫자만 보고 결론 내리면 안 됩니다. 저도 처음엔 숫자 보고 깜짝 놀랐는데, 실제로는 캐시(cache)와 프로세스 실제 점유 메모리(resident memory)를 같이 봐야 하더라고요.

    제가 주로 보는 패턴은 이렇습니다.

    • 특정 프로세스의 메모리 점유가 시간에 따라 계속 증가하는가
    • 관리 기능이 느려지는 시점과 메모리 증가 시점이 겹치는가
    • 재부팅 또는 관련 기능 비활성화 후 증상이 사라지는가
    • DPI, IDS/IPS, 트래픽 통계 같은 부가 기능과 상관관계가 있는가

    여기서 너무 공격적으로 설정을 다 꺼버리면 원인 파악이 안 돼요. 저는 보통 한 번에 하나씩만 바꿉니다. 예를 들면 이런 순서죠.

    1. 트래픽이 많은 시간대를 기록합니다.
    2. 그 시간대 직전과 직후의 메모리 상태를 비교합니다.
    3. 부가 기능을 하나만 조정합니다.
    4. 24시간에서 72시간 정도 추세를 다시 봅니다.

    이 방식이 느려 보여도 제일 덜 헤맵니다. 한꺼번에 다 바꾸면 드디어 됐다 싶다가도 뭐가 원인이었는지 모르게 끝나거든요.

    기능별 점검 포인트

    항목 왜 확인하나 제가 보는 신호
    IDS/IPS 패킷 분석 부하 증가 가능성 CPU 상승, 지연 증가
    DPI/트래픽 통계 세션 및 분석 정보 누적 장기 사용 시 UI 반응 저하
    Smart Queue 대역폭 제어에 따른 처리 부담 고부하 시간대 지연 증가
    로그 적재 문제 시점 추적용 이상 메시지 반복 여부

    7. ⚠️ 제가 실제로 겪었던 흔한 함정

    이 섹션은 꼭 넣고 싶었어요. 문서만 보면 안 보이는 부분이 있거든요.

    첫 번째 함정은 케이블과 링크 협상입니다. WAN 불안정이라고 해서 무조건 소프트웨어 문제는 아닙니다. 애매하게 손상된 케이블이나 포트 접점 문제는 정말 사람 미치게 합니다. 핑이 항상 나쁜 게 아니라 간헐적으로만 튀니까요. 저는 예전에 라우터 설정만 계속 들여다보다가, 결국 상위 모뎀과 UDM Pro 사이 케이블 교체로 증상이 크게 줄어든 적도 있었습니다.

    두 번째는 재부팅 효과를 과대평가하는 것입니다. 재부팅 후 괜찮아졌다고 해서 원인이 사라진 건 아니에요. 단지 증상이 초기화된 걸 수 있거든요. 특히 UDM Pro 문제 해결에서 이 패턴이 자주 보입니다.

    세 번째는 로그 없이 기억에 의존하는 것입니다. 사람 기억은 생각보다 부정확합니다. 저는 이제 무조건 시간 기록부터 남깁니다. 끊긴 시간, 어떤 서비스가 영향받았는지, 그때 내부 UI가 느렸는지까지 같이 적어두면 나중에 원인 분리가 쉬워져요.

    네 번째는 기능을 너무 많이 켜두는 것입니다. 홈랩 라우터는 만능처럼 보여도 결국 자원은 유한합니다. 기능을 켜는 건 쉽지만, 디버깅은 어려워집니다.

    UDM Pro 문제 해결을 위한 WAN 지연과 메모리 사용량 대시보드

    메모리 사용량 증가와 외부 핑 지연 상승이 같은 시점에 나타나는 대시보드 형태의 결과 시각화 이미지입니다.

    8. 검증: 문제를 해결했다고 판단하는 기준

    해결은 느낌으로 하면 안 됩니다. 이 부분은 인프라 쪽에서 특히 중요하죠. 저는 아래 기준을 만족해야 해결로 봐요.

    1. 24시간 이상 외부 대상 핑 손실이 안정적일 것
    2. 관리 UI 반응 속도가 이전보다 일관될 것
    3. 메모리 사용량 추세가 비정상적으로 계속 상승하지 않을 것
    4. 문제 시간대에도 WAN 지연이 급격히 튀지 않을 것
    5. 사용자 체감 이슈가 재현되지 않을 것

    가능하면 before/after를 직접 남겨보세요. 예를 들어 설정 조정 전후로 다음 항목을 비교하면 좋습니다.

    date
    uptime
    free -m
    ping -c 20 1.1.1.1
    ping -c 20 8.8.8.8
    dmesg | tail -n 20

    그리고 간단한 점검표도 추천드립니다.

    검증 항목 변경 전 변경 후
    UI 반응 속도 느림/보통/빠름 느림/보통/빠름
    외부 핑 안정성 불안정/보통/안정 불안정/보통/안정
    메모리 증가 추세 가파름/완만/안정 가파름/완만/안정
    재현 여부 있음 없음

    이렇게 남겨두면 다음에 비슷한 문제 생겨도 훨씬 빨라져요. 저는 이 기록 덕분에 두 번째부터는 훨씬 덜 헤맸습니다.

    9. 정리: UniFi UDM Pro 디버깅은 순서가 전부입니다

    정리하면, UniFi UDM Pro에서 보이는 WAN 불안정은 회선 문제처럼 보여도 장비 내부 리소스 이슈와 연결되는 경우가 꽤 있어요. 특히 장시간 운영 후 느려지고, 재부팅 후 잠시 좋아지고, 관리 화면까지 버벅인다면 메모리와 프로세스 상태를 꼭 같이 보셔야 합니다. 제가 직접 해보니 핵심은 복잡한 기술보다도 순서 있게 분리해서 보는 습관이더라고요.

    • 회선과 내부 리소스를 분리해서 확인하기
    • 외부 모니터링 로그 남기기
    • 기능은 한 번에 하나씩만 조정하기
    • 재부팅 전후를 반드시 기록하기

    혹시 이런 경험 있으신가요? 멀쩡하던 홈랩 라우터가 며칠 지나면 이상하게 답답해지는 그 느낌이요. 저도 처음엔 이게 뭔가 싶었는데, 결국 기록과 비교가 답이었어요. 다음 글에서는 UniFi 환경에서 VLAN(Virtual LAN, 가상 랜) 분리와 모니터링 기준을 어떻게 잡는지 이어서 다뤄볼 예정입니다. 이전 글에서 다뤘던 홈랩 기본 세그먼트 설계 내용과 함께 보시면 더 이해가 잘 되실 거예요.

    UDM Pro 문제 해결 단계와 체크리스트 요약 인포그래픽

    회선 점검, SSH 확인, 외부 모니터링, 검증 단계까지 한 장으로 정리한 요약 인포그래픽 이미지입니다.

    자주 묻는 질문

    Q1. 메모리 사용량이 높으면 무조건 메모리 누수인가요?

    아닙니다. 캐시 사용 때문일 수 있습니다. 중요한 건 시간에 따라 특정 프로세스가 계속 증가하는지, 그리고 그 증가가 실제 장애 증상과 연결되는지예요.

    Q2. WAN 불안정이면 바로 ISP에 문의해야 하나요?

    바로 문의해도 되지만, 가능하면 먼저 외부 핑 기록과 라우터 내부 상태를 같이 확보해두세요. 문의 품질이 달라집니다.

    Q3. 홈랩 라우터에서 기능을 많이 켜면 무조건 안 좋은가요?

    무조건은 아닙니다. 다만 IDS/IPS, DPI, 큐잉 같은 기능은 자원 사용량과 체감 성능 사이 균형을 봐야 합니다. 목적 없는 활성화는 디버깅 난이도만 올릴 수 있어요.

  • [Cloud] Sentry 메모리 누수 해결: 실전 디버깅 사례 연구

    [Cloud] Sentry 메모리 누수 해결: 실전 디버깅 사례 연구

    Sentry 메모리 누수 해결: 실전 디버깅 사례 연구

    Sentry 메모리 누수 문제는 생각보다 늦게 티가 납니다. 처음에는 응답이 조금씩 느려지는 정도라서 그냥 트래픽이 늘었나 싶거든요. 그런데 어느 순간부터 컨테이너가 재시작되고, OOMKilled(메모리 부족으로 인한 강제 종료)가 찍히고, 에러는 많은데 원인은 안 보이는 상황이 옵니다. 저도 프로덕션 환경에서 딱 그 구간을 겪어봤습니다. 처음엔 애플리케이션 코드보다 인프라 쪽 문제라고 의심했었는데, 실제로 파고 들어가 보니 요청별로 쌓이는 객체 참조가 해제되지 않는 전형적인 메모리 누수였습니다. 이번 글은 Sentry 메모리 누수를 어떻게 추적했고, 어떤 식으로 좁혀 갔는지, 그리고 최종적으로 어떤 기준으로 해결 여부를 검증했는지 정리해 봤습니다.

    특히 Sentry 디버깅, 클라우드 에러 모니터링, 메모리 누수 해결, 성능 최적화가 한 번에 엮여 있는 사례라서, 운영 중인 Python API 서버나 컨테이너 환경을 다루는 분들께 꽤 현실적인 참고가 될 거 같습니다.

    Sentry 메모리 누수 추적을 위한 프로덕션 아키텍처 다이어그램

    프로덕션 API 서버, Sentry, 컨테이너 메모리 사용량 흐름을 한눈에 보여주는 개요 이미지입니다.

    Sentry 메모리 누수, 왜 잡기 어려운가

    쉽게 말해 메모리 누수(memory leak, 더 이상 필요 없는 메모리가 해제되지 않고 계속 남는 현상)는 에러 한 번으로 끝나지 않습니다. 요청은 성공하는데 프로세스 메모리만 조금씩 올라갈 수도 있고, 특정 조건에서만 객체가 쌓일 수도 있죠. 그래서 로그(log, 텍스트 기록)만 봐서는 감이 잘 안 옵니다.

    여기서 주목할 포인트가 있습니다. Sentry는 기본적으로 에러 모니터링(error monitoring)과 이벤트 추적(event tracking)에 강점이 있고, 메모리 누수는 증상과 맥락을 묶어 보는 용도로 정말 유용했습니다. 저는 아래 네 가지를 먼저 봤어요.

    • 언제 메모리 사용량이 급격히 올라가는지
    • 어떤 릴리스(release, 배포 버전) 이후부터 재현되는지
    • 어떤 엔드포인트(endpoint, API 경로)에서 현상이 두드러지는지
    • 재시작 직전 남기는 경고 이벤트와 예외 스택이 무엇인지

    혹시 이런 경험 있으신가요? CPU는 멀쩡한데 메모리만 톱니처럼 올라가고, 재배포하면 잠깐 괜찮아졌다가 다시 터지는 상황이요. 이럴 때는 감으로 고치면 거의 다시 터집니다. 저도 처음엔 캐시(cache) 설정만 바꾸면 되겠지 했는데, 삽질 좀 했습니다 ㅎㅎ

    Sentry 디버깅 관점에서 본 핵심 개념

    제가 실제로 써보니, 메모리 누수는 도구를 나눠서 보는 게 훨씬 편했어요. Sentry는 현상의 타이밍과 요청 맥락을 모으고, Python 내장 도구는 어떤 객체가 쌓이는지를 확인하는 식이었습니다.

    도구 역할 이번 사례에서 한 일
    Sentry 이벤트 추적, 릴리스 비교, 환경 분리 특정 배포 이후 경고 이벤트 증가와 엔드포인트 상관관계 확인
    tracemalloc 메모리 할당 추적 어느 코드 경로에서 객체가 계속 쌓이는지 비교
    gc 가비지 컬렉션 상태 확인 참조가 남아 해제되지 않는 객체 패턴 점검
    ps/top/kubectl 운영 레벨 리소스 확인 프로세스 RSS 증가, 재시작 시점, OOM 패턴 확인

    쉽게 말해, Sentry 메모리 누수 대응은 Sentry 하나만으로 끝내는 작업이 아니라, 관측(observability, 시스템 상태를 관찰하는 능력)의 중심축으로 Sentry를 두고 주변 도구를 붙이는 작업입니다.

    제가 잡았던 실제 패턴

    문제의 원인은 요청 처리 중 생성한 큰 응답 데이터를 전역 리스트(global list)에 디버깅 목적으로 붙여 놓은 코드였습니다. 개발 단계에서는 편했는데, 프로덕션에서 요청이 누적되면서 리스트가 비워지지 않았죠. 더 골치 아픈 건 예외가 바로 나지 않았다는 점입니다. 그래서 클라우드 에러 모니터링만 보고 있으면 늦게 알아차리기 쉬워요.

    실전 구현 1: Sentry에 운영 맥락 심기

    처음 한 일은 Sentry 이벤트에 운영 맥락을 더 많이 넣는 것이었습니다. 그냥 SDK만 붙여 두면 예외는 잘 모이는데, Sentry 메모리 누수처럼 느린 장애는 맥락이 부족할 때가 많거든요.

    1. 릴리스(release)와 환경(environment)을 분리합니다.
    2. 의심되는 엔드포인트에 breadcrumb(브레드크럼, 이벤트 전 단계 기록)를 남깁니다.
    3. 메모리 사용량이 임계치에 가까워지면 warning 이벤트를 직접 보냅니다.
    4. 컨테이너 재시작 전후 로그와 Sentry 타임라인을 맞춰 봅니다.
    import os
    import time
    import resource
    import sentry_sdk
    from sentry_sdk import capture_message, set_tag, add_breadcrumb
    from sentry_sdk.integrations.logging import LoggingIntegration
    
    logging_integration = LoggingIntegration(
        level=None,
        event_level=None,
    )
    
    sentry_sdk.init(
        dsn=os.environ.get("SENTRY_DSN"),
        environment=os.environ.get("APP_ENV", "production"),
        release=os.environ.get("APP_RELEASE", "unknown"),
        integrations=[logging_integration],
        traces_sample_rate=0.1,
    )
    
    MEMORY_WARN_MB = 700
    
    def get_rss_mb():
        rss_kb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
        if os.uname().sysname == "Darwin":
            return rss_kb / 1024 / 1024
        return rss_kb / 1024
    
    def observe_request(path: str, request_id: str):
        set_tag("endpoint", path)
        set_tag("request_id", request_id)
        add_breadcrumb(
            category="request",
            message=f"handling {path}",
            level="info",
            data={"request_id": request_id, "ts": int(time.time())},
        )
    
        rss_mb = get_rss_mb()
        if rss_mb >= MEMORY_WARN_MB:
            capture_message(
                f"high memory usage detected: {rss_mb:.1f}MB",
                level="warning",
            )

    여기서 중요한 포인트! Sentry에 경고 이벤트를 직접 남기면, 예외가 나기 전 단계의 흐름이 보입니다. 저는 이걸 넣고 나서야 특정 API 요청이 몰린 뒤 10~20분 정도 지나면 경고가 늘어난다는 걸 확인했어요.

    Sentry 메모리 누수 분석에 필요한 release와 environment 태그 구성 이미지

    Sentry에서 release, environment, endpoint 태그를 어떻게 나눠서 보는지 설명하는 구성 이미지입니다.

    실전 구현 2: 재현 스크립트와 누수 코드 분리

    운영에서만 보이는 문제라도, 결국 로컬이나 스테이징(staging, 사전 검증 환경)에서 비슷하게 재현할 수 있어야 고칠 수 있죠. 저는 홈랩에서도 비슷하게 많이 해보는데, 재현이 되는 순간부터 속도가 확 올라갑니다.

    문제를 단순화한 예시는 이런 식이었습니다.

    from fastapi import FastAPI
    
    app = FastAPI()
    DEBUG_CACHE = []
    
    @app.get("/items")
    def items():
        payload = {"rows": ["x" * 10000 for _ in range(500)]}
    
        # 문제 코드: 요청마다 큰 객체를 전역 리스트에 보관
        DEBUG_CACHE.append(payload)
    
        return {"count": len(payload["rows"]), "cache_size": len(DEBUG_CACHE)}

    이런 코드는 테스트 몇 번 할 때는 티가 안 나요. 근데 프로덕션에서 요청이 계속 들어오면 얘기가 달라집니다. 그래서 부하를 살짝 줘서 반복 요청을 만들었습니다.

    for i in $(seq 1 300); do
      curl -s http://127.0.0.1:8000/items > /dev/null
    done
    
    ps -o pid,rss,command -p $(pgrep -f uvicorn)

    이후 tracemalloc으로 전후 차이를 찍었습니다.

    import tracemalloc
    import linecache
    
    tracemalloc.start(25)
    
    # 부하 실행 전 snapshot
    before = tracemalloc.take_snapshot()
    
    # 여기서 반복 호출 수행
    
    after = tracemalloc.take_snapshot()
    stats = after.compare_to(before, "lineno")
    
    for stat in stats[:10]:
        frame = stat.traceback[0]
        print(f"{frame.filename}:{frame.lineno} -> {stat.size_diff / 1024:.1f} KiB")
        print(linecache.getline(frame.filename, frame.lineno).strip())

    실제로 써보니까 이 방식이 좋았던 이유는 명확합니다. Sentry에서 어느 요청 흐름이 이상한지를 찾고, tracemalloc으로 어느 코드 줄이 증가하는지를 고정할 수 있었거든요. 두 도구가 따로 노는 게 아니라 딱 이어졌습니다.

    실전 구현 3: 수정 방향과 배포 전략

    수정 자체는 의외로 단순했습니다. 전역에 보관하던 디버그 데이터를 없애고, 꼭 필요하면 크기를 제한한 ring buffer(링 버퍼, 고정 길이 순환 버퍼)로 바꿨어요. 사실 운영 서버에서는 요청 본문이나 큰 응답 객체를 오래 들고 있는 패턴 자체를 피하는 게 맞습니다.

    from collections import deque
    from fastapi import FastAPI
    
    app = FastAPI()
    DEBUG_CACHE = deque(maxlen=20)
    
    @app.get("/items")
    def items():
        payload = {"rows": ["x" * 10000 for _ in range(500)]}
    
        # 필요한 메타데이터만 제한적으로 저장
        DEBUG_CACHE.append({
            "row_count": len(payload["rows"]),
        })
    
        return {"count": len(payload["rows"]), "cache_size": len(DEBUG_CACHE)}

    배포할 때도 한 번에 전체 트래픽을 넘기지 않았습니다. 제가 운영할 때는 이런 문제는 꼭 점진 배포(rolling deployment, 순차 배포)로 갑니다. 왜냐하면 메모리 누수는 수정했다고 끝이 아니라, 정말 증가 추세가 꺾였는지를 봐야 하거든요.

    1. 수정 버전을 새 릴리스로 배포합니다.
    2. Sentry에서 이전 릴리스와 새 릴리스를 분리해 봅니다.
    3. 동일 엔드포인트 기준 warning 이벤트 발생 빈도를 비교합니다.
    4. 컨테이너 재시작 횟수와 RSS 증가 그래프를 같이 봅니다.

    재현 스크립트, 문제 코드, 수정 후 구조를 비교해서 보여주는 디버깅 흐름 이미지입니다.

    ⚠️ 주의사항: 제가 실제로 겪은 삽질 포인트

    여기서부터가 진짜 중요합니다. 메모리 누수는 원인을 하나만 보면 놓치는 경우가 많아요.

    1. ru_maxrss를 실시간 메모리로 착각

    ru_maxrss는 최대 RSS 값이라서 순간 메모리 스냅샷처럼 쓰면 해석이 꼬일 수 있습니다. 저도 처음엔 현재 메모리라고 생각하고 봤다가 그래프 해석을 잘못했었어요. 그래서 운영에서는 ps, 컨테이너 메트릭, 노드 메모리 지표를 같이 봐야 합니다.

    2. 예외가 없으니 애플리케이션 문제를 늦게 의심

    OOMKilled는 쿠버네티스(Kubernetes)나 런타임 차원의 이벤트로 보이는 경우가 많아서, 애플리케이션 버그와 분리해서 생각하기 쉬워요. 근데 여기서 중요한 건, 예외가 없다고 누수가 없는 게 아니다라는 점입니다.

    3. 디버그 로그가 문제를 키움

    디버깅하려고 요청 본문, 응답 객체, ORM 결과를 통째로 메모리에 들고 있으면 오히려 누수를 악화시킬 수 있습니다. 특히 전역 변수, 캐시, 클로저(closure, 외부 변수를 참조하는 함수), 콜백 등록 구조는 한 번 더 의심해 보셔야 합니다.

    4. Sentry를 만능 분석기로 기대

    Sentry는 정말 강력한 도구지만, heap dump 분석기나 프로파일러(profiler)를 완전히 대체하지는 않습니다. 저는 Sentry를 상황판으로 쓰고, 세부 원인 분석은 tracemalloc과 코드 리뷰로 마무리했어요. 이 조합이 제일 현실적이었습니다.

    • ✅ Sentry: 릴리스, 환경, 요청 흐름, 경고 이벤트 연계
    • ✅ 시스템 도구: RSS, 재시작, 컨테이너 상태 확인
    • ✅ 언어 도구: 객체 증가 경로 추적
    • ⚠️ 단일 지표만 보고 결론 내리기 금지

    검증: 해결됐다고 판단한 기준

    드디어 됐다! 라고 말하기 전에, 저는 꼭 기준을 숫자가 아니라 패턴으로 봅니다. 구체적인 절대 수치는 환경마다 다르니까요. 대신 아래 기준은 어느 환경에서도 꽤 유효합니다.

    1. 동일 트래픽 조건에서 프로세스 메모리가 계속 우상향하지 않는지
    2. 문제 엔드포인트 호출 후에도 일정 시간 뒤 메모리 증가 추세가 평탄해지는지
    3. Sentry warning 이벤트 빈도가 줄었는지
    4. OOM 재시작이 사라졌는지
    5. 새 릴리스 이후 동일 이슈 그룹이 다시 생기지 않는지

    저는 수정 전후를 이런 식으로 운영 점검했습니다.

    kubectl get pods -n prod
    kubectl top pods -n prod
    kubectl describe pod api-server-xxxxx -n prod
    
    # 애플리케이션 로그와 Sentry 이벤트 시점을 함께 대조
    kubectl logs deployment/api-server -n prod --since=30m

    결과적으로 수정 후에는 특정 API 호출이 몰려도 메모리 그래프가 계속 계단식으로 올라가지 않았고, 재시작도 멈췄습니다. Sentry에서도 문제 릴리스 기준으로 쌓이던 경고 이벤트가 눈에 띄게 줄었어요. 이런 순간이 있죠. 아, 이건 증상만 눌러 놓은 게 아니라 원인을 제대로 건드렸구나 하는 느낌이요. 이거 진짜 편하더라고요.

    Sentry 메모리 누수 해결 전후 결과를 보여주는 대시보드 이미지

    수정 전후 메모리 사용량 추세와 Sentry 이벤트 변화를 비교하는 결과 대시보드 이미지입니다.

    정리: Sentry 메모리 누수 대응 체크리스트

    마지막으로, 제가 다시 같은 상황을 만나면 이 순서대로 갑니다. 독자분들도 북마크해 두시면 꽤 쓸 만하실 거 같습니다.

    단계 체크 포인트 목적
    1 Sentry에 release, environment, endpoint 태그 확인 문제 범위 좁히기
    2 경고 이벤트 수동 전송 추가 예외 전 단계 포착
    3 부하 재현과 tracemalloc 비교 코드 라인 단위 확인
    4 전역 객체, 캐시, 콜백 참조 점검 누수 패턴 제거
    5 점진 배포 후 Sentry와 메트릭 비교 해결 여부 검증

    정리해 보면, Sentry 메모리 누수 대응의 핵심은 Sentry를 단순 에러 수집기로 쓰지 않는 데 있습니다. 릴리스 비교, 경고 이벤트, 요청 태그를 잘 심어 두면 메모리 누수처럼 느리고 애매한 장애도 꽤 빨리 좁혀 갈 수 있거든요. 저도 처음엔 헷갈렸는데, 결국 답은 비슷했습니다. 증상을 구조화해서 보고, 재현하고, 작은 단서부터 묶어 가는 것. 운영은 늘 그렇게 풀리더라고요.

    다음 글에서는 OpenTelemetry(오픈텔레메트리, 분산 추적 표준)와 Sentry를 함께 붙여서 요청 지연과 에러를 한 번에 보는 흐름도 다뤄볼 예정입니다. 이전 글에서 다뤘던 로그 집계 파이프라인과 같이 보시면 더 이해가 잘 되실 거 같습니다.

    자주 묻는 질문

    Sentry만으로 메모리 누수를 찾을 수 있나요?

    완전히 단독으로 찾기보다는, 어느 흐름에서 문제가 커지는지를 좁히는 데 강합니다. 실제 누수 지점은 tracemalloc, gc, 코드 리뷰 같은 보조 수단이 필요할 때가 많아요.

    메모리 누수 해결 후 무엇을 가장 먼저 봐야 하나요?

    같은 트래픽에서 메모리 사용량이 계속 증가하는지, 컨테이너 재시작이 멈췄는지, 그리고 Sentry 디버깅 기준으로 동일 릴리스 이슈가 줄었는지를 먼저 보시면 됩니다.

    클라우드 에러 모니터링만 잘 되어 있으면 충분한가요?

    충분하지 않을 때가 많습니다. 메모리 누수는 인프라 이벤트처럼 보이지만 실제 원인은 코드 참조 구조인 경우가 많아서, 애플리케이션 계층과 시스템 계층을 같이 보셔야 해요.

    Sentry 메모리 누수 대응 체크리스트 인포그래픽

    Sentry, tracemalloc, 시스템 메트릭을 조합한 대응 순서를 한 장으로 요약한 이미지입니다.