13년차의 서버실

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

[태그:] Sentry

  • [Cloud] Sentry vs Elastic APM: 클라우드 환경 에러 및 성능 모니터링 솔루션 비교 분석

    [Cloud] Sentry vs Elastic APM: 클라우드 환경 에러 및 성능 모니터링 솔루션 비교 분석

    [인프라] Sentry vs Elastic APM 비교 분석

    Sentry Elastic APM 비교를 고민하시는 분들은 보통 비슷한 시점에 막히더라고요. 서비스는 클라우드에 올렸는데, 장애는 간헐적으로 터지고, CPU나 응답 시간도 오락가락하고, 로그만 봐서는 도대체 어디서부터 추적해야 할지 감이 안 오는 순간이 있습니다. 저도 홈랩이랑 실서비스 비슷한 테스트 환경에서 이것저것 붙여보면서 꽤 삽질했었는데요. 결론부터 말씀드리면 Sentry와 Elastic APM은 겹치는 영역이 있지만 출발점이 꽤 다릅니다. 하나는 에러 트래킹(Error Tracking, 예외 중심 추적)에 강하고, 다른 하나는 APM(Application Performance Monitoring, 애플리케이션 성능 모니터링)과 관측성(Observability, 시스템 상태를 추론하는 능력) 전체 흐름에 더 가깝습니다.

    그래서 오늘은 마케팅 문구 말고, 실제 운영자 시점에서 어떤 팀에 뭐가 더 맞는지 차근차근 정리해보겠습니다. 클라우드 모니터링을 처음 붙이는 분도 이해할 수 있게 풀어볼게요.

    Sentry Elastic APM 비교를 위한 클라우드 모니터링 아키텍처 다이어그램

    애플리케이션, SDK/Agent, 수집 서버, 대시보드까지 이어지는 전체 흐름을 한눈에 보여주는 비교용 다이어그램입니다.

    Sentry와 Elastic APM, 쉽게 말해 뭐가 다른가요?

    쉽게 말해 Sentry는 에러가 났을 때 개발자가 바로 달려가게 만드는 도구에 가깝고, Elastic APM은 서비스가 느려지거나 병목이 생길 때 시스템 전체 문맥을 보게 해주는 도구에 가깝습니다.

    • Sentry: 예외(Exception, 런타임 오류), 스택 트레이스(Stack Trace, 오류 호출 경로), 릴리스 추적(Release Tracking, 배포 버전별 상태) 쪽이 직관적입니다.
    • Elastic APM: 트랜잭션(Transaction, 요청 단위 처리 흐름), 스팬(Span, 세부 작업 구간), 인프라 메트릭(Metrics, CPU/메모리 같은 수치), 로그 연계가 자연스럽습니다.

    처음엔 저도 둘 다 비슷해 보였거든요. 근데 실제로 써보니까 질문 자체가 달라집니다.

    1. Sentry를 볼 때는 “왜 이 에러가 났지?”를 먼저 묻게 됩니다.
    2. Elastic APM을 볼 때는 “왜 이 요청이 느리지?” 또는 “어느 서비스 구간이 병목이지?”를 먼저 보게 됩니다.

    여기서 중요한 포인트! 장애 대응의 시작점이 예외냐, 성능 저하냐에 따라 체감 가치가 꽤 달라집니다.

    클라우드 모니터링 관점에서 보는 핵심 차이

    항목 Sentry Elastic APM
    주력 영역 에러 트래킹, 이슈 그룹화, 릴리스 단위 추적 성능 모니터링, 분산 추적, 로그/메트릭 연계
    초기 진입 난이도 상대적으로 빠름 구성 요소를 이해해야 해서 더 복합적일 수 있음
    개발자 체감 예외 발생 시 원인 파악이 빠름 느린 쿼리, 외부 호출, 서비스 병목 분석에 강함
    운영자 체감 애플리케이션 오류 집중 애플리케이션과 인프라를 함께 보기 좋음
    적합한 팀 작은 팀, 앱/웹 중심 팀, 빠른 에러 대응이 필요한 팀 Elastic Stack을 이미 쓰는 팀, 관측성 통합이 중요한 팀
    확장성 관점 이슈 기반 운영에 편함 로그, 메트릭, 트레이스 통합 설계에 유리

    제가 직접 해보니, 팀에 로그 플랫폼이 이미 있느냐가 생각보다 큽니다. 이미 Elasticsearch(엘라스틱서치, 검색/분석 엔진)와 Kibana(키바나, 시각화 대시보드)를 쓰고 있다면 Elastic APM 쪽이 훨씬 자연스럽게 이어집니다. 반대로 그런 기반이 없고 일단 에러부터 빨리 잡아야 한다면 Sentry가 체감 속도가 빠르더라고요.

    Sentry가 잘 맞는 상황

    Sentry는 특히 이런 상황에서 강합니다.

    • 배포 직후 특정 버전에서 에러가 급증하는지 빠르게 보고 싶을 때
    • 프론트엔드와 백엔드 예외를 한 번에 추적하고 싶을 때
    • 개발자가 스택 트레이스와 컨텍스트만 보고 바로 수정 포인트를 잡아야 할 때
    • 소규모 팀에서 운영 복잡도를 크게 늘리지 않고 에러 트래킹을 붙이고 싶을 때

    실제로 써보니까 Sentry는 “이 에러가 몇 명에게 영향을 줬는가”, “어느 릴리스 이후 시작됐는가” 같은 질문에 답하기 좋았습니다. 특히 이슈 그룹화(Issue Grouping, 같은 유형 오류 묶기)가 잘 되면 알림 피로도도 꽤 줄어듭니다.

    Elastic APM이 빛나는 상황

    Elastic APM은 다음처럼 전체 흐름을 보려는 팀에 잘 맞습니다.

    • 마이크로서비스(Microservices, 기능별로 쪼갠 서비스 구조) 환경에서 요청 경로를 따라가야 할 때
    • 애플리케이션 성능 문제와 인프라 메트릭을 같이 보고 싶을 때
    • 로그, 메트릭, 트레이스를 한 플랫폼에서 묶어 보고 싶을 때
    • 운영팀과 개발팀이 같은 관측성 도구를 공유해야 할 때

    이쪽은 처음엔 좀 묵직합니다. 저도 처음엔 “이걸 다 연결해야 하나?” 싶었는데, 한 번 구조가 잡히면 장점이 확실합니다. 예를 들어 API 응답이 느릴 때, 단순히 느리다는 사실만 보는 게 아니라 DB 쿼리, 외부 HTTP 호출, 특정 서비스 구간까지 내려가서 볼 수 있거든요. 이거 진짜 편하더라고요.

    실전 구현: Python 앱에 둘 다 붙여보기

    비교는 결국 손에 붙여봐야 감이 옵니다. 여기서는 가장 단순한 예제로 Flask(플라스크, Python 웹 프레임워크) 기준으로 보여드릴게요. 실제 서비스에서는 환경 변수로 키를 분리하고, 운영/스테이징 환경을 나눠서 쓰시는 걸 권장합니다.

    1. Sentry SDK 연결

    pip install flask sentry-sdk
    import os
    from flask import Flask
    import sentry_sdk
    from sentry_sdk.integrations.flask import FlaskIntegration
    
    sentry_sdk.init(
        dsn=os.environ.get("SENTRY_DSN"),
        integrations=[FlaskIntegration()],
        traces_sample_rate=0.1,
        send_default_pii=False,
    )
    
    app = Flask(__name__)
    
    @app.route("/")
    def index():
        return {"status": "ok"}
    
    @app.route("/error")
    def error():
        raise RuntimeError("sentry test error")

    여기서 dsn은 Sentry 프로젝트에서 발급받는 연결 정보입니다. traces_sample_rate는 성능 이벤트 샘플링 비율인데요. 무조건 높게 두기보다 트래픽 규모를 보고 잡는 게 좋습니다.

    2. Elastic APM Agent 연결

    pip install flask elastic-apm
    import os
    from flask import Flask
    from elasticapm.contrib.flask import ElasticAPM
    
    app = Flask(__name__)
    app.config["ELASTIC_APM"] = {
        "SERVICE_NAME": "demo-flask-app",
        "SECRET_TOKEN": os.environ.get("ELASTIC_APM_SECRET_TOKEN"),
        "SERVER_URL": os.environ.get("ELASTIC_APM_SERVER_URL"),
        "ENVIRONMENT": os.environ.get("APP_ENV", "production"),
    }
    
    apm = ElasticAPM(app)
    
    @app.route("/")
    def index():
        return {"status": "ok"}
    
    @app.route("/slow")
    def slow():
        import time
        time.sleep(1.2)
        return {"status": "slow"}

    Elastic APM은 보통 APM Server 또는 Elastic Cloud 쪽 엔드포인트로 데이터를 보냅니다. 서비스 이름을 어떻게 나누느냐가 꽤 중요하더라고요. 나중에 대시보드에서 서비스별로 분리해서 보게 되기 때문입니다.

    3. 컨테이너 환경 변수 분리

    services:
      app:
        image: my-flask-app:latest
        environment:
          SENTRY_DSN: "${SENTRY_DSN}"
          ELASTIC_APM_SERVER_URL: "${ELASTIC_APM_SERVER_URL}"
          ELASTIC_APM_SECRET_TOKEN: "${ELASTIC_APM_SECRET_TOKEN}"
          APP_ENV: "production"
        ports:
          - "8000:8000"

    클라우드 환경에서는 Kubernetes(쿠버네티스, 컨테이너 오케스트레이션)나 ECS 같은 플랫폼에서도 같은 원칙입니다. 민감한 값은 Secret으로 빼고, 서비스명과 환경명은 일관되게 가져가세요.

    Sentry Elastic APM 비교용 Flask 연동 구성 다이어그램

    실전 구현 단계에서 앱 코드, 환경 변수, 외부 모니터링 서비스가 어떻게 연결되는지 보여주는 구성 이미지입니다.

    ⚠️ 실제로 자주 겪는 문제와 트러블슈팅

    여기서부터가 진짜 운영 팁입니다. 문서만 보면 금방 붙을 것 같았는데, 저는 여기서 삽질 좀 했습니다 ㅎㅎ

    에러는 들어오는데 성능 데이터가 비어 있는 경우

    • Sentry에서는 트레이싱 샘플링 설정이 너무 낮거나 꺼져 있는지 확인합니다.
    • Elastic APM에서는 Agent 설정은 되었는데 서버 URL 또는 인증 토큰이 맞지 않는 경우가 많습니다.
    • 프록시(Proxy, 중간 전달 장비)나 보안 그룹 때문에 수집 엔드포인트로 아웃바운드가 막힌 경우도 자주 있습니다.

    서비스 이름이 제각각 찍히는 경우

    이건 운영하면서 은근 치명적입니다. 배포마다 이름이 달라지면 대시보드가 찢어집니다. service name 규칙을 미리 정해두세요. 예를 들면 team-service-env 같은 형태로요.

    알림이 너무 많이 오는 경우

    Sentry는 같은 유형의 예외를 얼마나 잘 묶어주느냐가 중요하고, Elastic APM은 임계값(Threshold, 경고 기준치) 설계를 잘해야 합니다. 처음부터 모든 알림을 켜면 금방 무뎌집니다. 저도 초반엔 메신저가 울리기만 하고 아무도 안 보는 상태가 됐었거든요.

    개인정보와 민감정보 유출 위험

    가장 조심해야 할 부분입니다. 에러 이벤트나 트레이스에 요청 본문, 헤더, 사용자 식별값이 섞여 들어갈 수 있습니다. 그래서 기본값을 그대로 믿지 말고 다음 원칙을 꼭 적용하세요.

    1. 민감한 필드는 수집 전에 마스킹(Masking, 값 가리기)합니다.
    2. PII(개인식별정보) 전송 여부를 명시적으로 검토합니다.
    3. 운영 환경과 개발 환경의 수집 범위를 분리합니다.

    검증: 붙인 뒤 무엇을 확인해야 하나요?

    연동이 끝났다고 바로 끝은 아닙니다. 최소한 아래 시나리오는 확인해보셔야 합니다.

    1. Sentry 검증: 테스트 예외를 한 번 발생시켜 이슈가 생성되는지 확인합니다.
    2. Elastic APM 검증: 응답이 느린 엔드포인트를 호출해 트랜잭션과 스팬이 보이는지 확인합니다.
    3. 배포 검증: 새 릴리스 이후 에러 추세가 구분되는지 확인합니다.
    4. 알림 검증: 실제로 필요한 채널로 경고가 가는지 확인합니다.
    curl http://localhost:8000/error
    curl http://localhost:8000/slow

    제가 실제로 써보니까, 검증할 때는 일부러 실패를 만들어보는 게 제일 확실합니다. 정상 상태만 보면 연동이 된 것처럼 보이는데, 막상 장애가 터지면 필요한 필드가 빠져 있는 경우가 있거든요. 드디어 됐다! 싶어서 넘겼다가 나중에 다시 뜯어보는 경우, 정말 많습니다.

    Sentry Elastic APM 비교 결과를 보여주는 에러 및 성능 대시보드 이미지

    하나는 에러 중심, 다른 하나는 성능 흐름 중심이라는 차이를 직관적으로 보여주는 결과 화면 예시입니다.

    어떤 팀이 무엇을 선택하면 좋을까?

    상황 추천 방향
    스타트업/소규모 서비스, 장애 원인 파악이 급함 Sentry 우선
    Elastic Stack을 이미 운영 중 Elastic APM 우선
    프론트엔드 예외와 백엔드 예외를 빠르게 모으고 싶음 Sentry 적합
    로그, 메트릭, 트레이스를 한곳에서 보고 싶음 Elastic APM 적합
    멀티서비스 병목 분석이 중요함 Elastic APM 쪽이 유리
    배포 후 에러 급증 여부를 빠르게 추적해야 함 Sentry 체감이 좋음

    사실 둘 중 하나만 절대 정답이라고 보긴 어렵습니다. 팀의 현재 성숙도와 운영 방식이 더 중요합니다. 로그 플랫폼이 아직 없고 개발자 중심으로 빠르게 대응해야 한다면 Sentry가 훨씬 덜 부담스럽습니다. 반대로 이미 Elastic 기반 운영 체계가 있다면 Elastic APM은 확장성이 좋습니다.

    자주 묻는 질문 정리

    Q. 둘 다 같이 써도 되나요?

    네, 가능합니다. 실제로 에러 트래킹은 Sentry, 통합 관측성은 Elastic APM으로 나눠 가져가는 팀도 있습니다. 다만 데이터 중복과 운영 포인트 증가를 감안하셔야 합니다.

    Q. 클라우드 모니터링 입문자는 무엇부터 시작할까요?

    개인적으로는 장애를 빨리 잡는 경험을 먼저 만드는 걸 추천합니다. 그래서 입문자는 Sentry 같은 에러 중심 도구부터 시작하고, 이후에 성능 모니터링과 분산 추적으로 넓히는 흐름이 덜 헷갈립니다.

    Q. Elastic APM은 언제 더 빛나나요?

    서비스가 여러 개로 나뉘고, 느려지는 구간이 앱 코드인지 DB인지 외부 API인지 구분이 안 될 때요. 그때부터는 트레이스 기반 분석 가치가 확 올라갑니다.

    Sentry Elastic APM 비교와 선택 기준을 정리한 인포그래픽

    팀 규모, 운영 방식, 분석 목적에 따라 어떤 도구가 더 맞는지 빠르게 판단할 수 있도록 요약한 비교 인포그래픽입니다.

    마무리: 결국 질문은 도구가 아니라 운영 방식입니다

    Sentry Elastic APM 비교를 한 줄로 줄이면 이렇습니다. Sentry는 에러 대응의 속도를 높여주고, Elastic APM은 성능과 구조를 읽는 시야를 넓혀줍니다.

    저도 처음엔 “둘 중 뭐가 더 좋지?”만 붙잡고 있었는데, 실제로 써보니까 질문을 바꿔야 하더라고요. 우리 팀은 지금 무엇이 더 아픈가? 에러 원인 파악이 급한지, 아니면 서비스 전체 병목과 클라우드 모니터링 체계를 정리해야 하는지가 먼저입니다.

    혹시 지금 막 APM 솔루션 도입을 검토 중이시라면, 제 추천은 이렇습니다. 작게 붙이고, 일부러 장애를 내보고, 대시보드와 알림이 실제 운영에 도움이 되는지 먼저 확인하세요. 그 다음에 범위를 넓히는 게 훨씬 덜 고생합니다.

    다음 글에서는 OpenTelemetry(OpenTelemetry, 관측성 데이터 수집 표준) 기준으로 Sentry와 Elastic 계열 도구를 어떻게 같이 엮을 수 있는지도 다뤄볼 예정입니다. 이전 글에서 로그 수집 파이프라인 정리한 내용과도 이어보시면 도움이 될 겁니다. 🎉

  • [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, 시스템 메트릭을 조합한 대응 순서를 한 장으로 요약한 이미지입니다.