Stan 기술블로그

디스크가 곧 쿼터일 때 — 리플레이 보존 정책

사이징 편에서 숫자를 정했다. 다음은 정책이다. self-hosted Sentry에는 쿼터가 없다. SaaS는 한도를 넘으면 버리지만, self-hosted는 SDK가 보낸 걸 전부 저장한다. 그래서 정책의 중심은 “무엇을 지울까”가 아니라 “무엇을 받을까”였다. 정책은 네 층으로 나눴고, 중간에 넣으려던 SDK 중복 제거는 키를 네 번 바꾼 끝에 넣지 않기로 했다. 팀 확정 전 초안이고, 보관 기간은 아직 논의 중이다.

(사내 인프라 특성상 서비스·프로젝트 이름은 일반화했다.)


리플레이는 영상이 아니다

Session Replay는 화면을 녹화한 영상이 아니다. DOM 스냅샷과 마우스·입력·스크롤 이벤트를 텍스트로 기록해 두었다가, 재생할 때 화면을 다시 그린다(rrweb 방식). 그래서 하나가 200KB 남짓으로 작고, 입력값을 가리는 마스킹도 구조 단위로 된다.

작다고 안심할 일은 아니다. 사이징 편에서 봤듯이 세션이 많으면 쌓이는 양은 금방 수십 GB가 된다.

정책 네 층

받는 것을 고른다 — 유일한 진짜 밸브

replaysSessionSampleRate: 0     # 아무 세션이나 무작위 녹화 — 끈다
replaysOnErrorSampleRate: 1.0   # 에러가 난 세션만 녹화

진짜 위험은 에러 리플레이가 아니라 샘플링 설정 실수다.

하루 세션 설정 90일 누적
10,000 에러 세션만 (1.0) 약 9GB
10,000 무작위 녹화 100% (실수) 약 160GB — 디스크가 터진다

무작위 녹화를 켜는 순간 하루 2GB 가까이 쌓인다. 그래서 정책의 첫 줄은 replaysSessionSampleRate: 0이다. 실측한 에러율이 높게 나오면 onError를 0.5로 내린다.

지우는 것 — 이미 돌고 있었다

자동 삭제는 새로 만들 필요가 없었다. 테스트 서버에서 확인한 것:

  • 보관 기간은 /opt/sentry/.env의 SENTRY_EVENT_RETENTION_DAYS(기본 90) 하나로 정한다. sentry.conf.py가 이 값을 읽어 Sentry 옵션에 넣는다. config.yml에는 보관 기간 설정이 없다.
  • cleanup 컨테이너(sentry-self-hosted-sentry-cleanup-1)가 매일 자정에 돈다. 로그에 Removing ReplayRecordingSegment for days=90이 찍혀 리플레이도 삭제 대상임을 확인했다. 한 번 도는 데 3초 남짓이라 부하는 신경 쓰지 않아도 된다.
  • 리플레이 말고도 첨부 파일 같은 것들이 같은 정리 대상이라, 데이터 볼륨 전체가 자동 정리 안에 있다.

지켜보는 것 — 따로 만들지 않기로 했다

처음 계획은 “하루 한 번 볼륨을 재서 기준을 넘으면 Slack 알림”이었다. 월 1회 손으로 보는 걸로는 샘플링 실수로 며칠 만에 디스크가 가득 차는 상황을 못 잡는다. 기준은 사이징 편에서 정한 남은 공간 30GB 경고 / 15GB 위험이다.

9월 30일, 별도 알림은 만들지 않기로 했다. Sentry에는 자기가 도는 서버의 디스크를 지켜보는 알림 기능이 없다(알림은 감시하는 앱 기준이다). 그리고 사내 모니터링 콘솔에 “전체 서버 디스크 85% 초과” 규칙이 이미 운영 중이다. 98GB 디스크에서 85%면 남은 공간이 약 15GB라 “위험” 기준과 거의 같다. 다만 Sentry가 도는 서버가 그 콘솔의 감시 대상인지는 아직 확인하지 못했다. 등록돼 있지 않다면 디스크 알림이 하나도 없는 상태다.

비상 대응 — retention 단축은 되돌릴 수 없다

디스크가 급할 때 쓸 수 있는 손은 둘이다. 리플레이를 하나씩 지우는 API(DELETE /api/0/projects/{org}/{project}/replays/{replay_id}/), 또는 .env에서 보관 기간을 줄이고 재시작하는 것.

두 번째는 조심해야 한다.

  • 보관 기간은 리플레이만이 아니라 에러 이벤트에도 같이 적용된다. 줄이면 에러 기록도 함께 사라진다.
  • cleanup이 한 번 돌면 그 구간은 영구히 지워진다. “잠깐만 줄였다가 되돌리자”가 안 된다.

보관 기간 — 45일로 시작하자

90일을 그대로 둘지는 에러율에 달려 있다. 사이징 편의 최악(성수기 두 번 겹침)을 98GB 디스크에 넣으면, 에러율 5%일 때 약 68%, 10%일 때 약 97%를 쓴다. 10%면 반드시 줄여야 한다. 그런데 에러율은 아직 실측하지 못했다.

그래서 권고는 45일로 시작해서, 실측해 보고 늘린다이다. 늘리는 건 언제든 할 수 있지만, 줄여서 지워진 건 되돌릴 수 없다.

계산하다 하나 배운 게 있다. 보관 기간을 줄여도 점유가 기간에 비례해 줄지 않는다. 90일을 30일로, 3분의 1로 줄여도 점유는 65% 수준까지만 내려간다. 창이 짧을수록 “그 창이 통째로 성수기”일 가능성이 커져서, 평시가 섞여 평균을 낮춰 주던 효과가 사라지기 때문이다.

SDK에서 중복 리플레이 거르기 — 키를 네 번 바꾸고 넣지 않기로

“같은 에러는 하루에 리플레이 하나만” 남기면 좋겠다는 생각은 자연스럽다. 브라우저 SDK에는 리플레이를 보내기 직전에 불리는 beforeErrorSampling(event) 훅이 있고, 여기서 false를 돌려주면 그 리플레이는 안 만들어진다. 에러 시그니처를 localStorage에 적어 두면 “오늘 이미 본 에러”를 거를 수 있다. 서버 설정은 손댈 필요가 없다.

전부 키를 무엇으로 잡느냐에 달려 있었다.

[1차] event.fingerprint — 전부 하나로 뭉쳤다

처음 샘플 코드는 event.fingerprint를 키로 썼다. 그런데 이슈를 묶는 그룹핑은 서버가 계산한다. 브라우저에서 보는 fingerprint는 거의 항상 비어 있어서, 모든 에러가 "[]"라는 키 하나로 뭉친다. 그러면 하루 첫 에러 하나만 리플레이가 남고 나머지는 전부 사라진다. 리뷰에서 잡은 실제 버그였다.

[2차] 에러 종류 + 메시지 — 조용히 안 먹혔다

type + message로 바꿨다. 이번엔 메시지에 ID·숫자가 섞인 에러(“User 12345 not found” 같은)가 매번 다른 키가 됐다. 중복 제거가 조용히 안 먹힌다. 리플레이는 매번 저장되니 안전하긴 한데, 넣은 의미가 없다.

반대로 type만 쓰면 서로 다른 에러가 TypeError 하나로 뭉쳐 다른 에러의 리플레이를 잃는다. 안 먹히는 쪽은 저장이 넘치고(fail-open), 너무 먹히는 쪽은 기록을 잃는다(fail-closed).

[3차] 스택 맨 위 프레임 + 정리한 메시지

절충안은 에러 종류 + 스택 맨 위 프레임(함수@파일, 줄·열 번호는 뺌) + 숫자와 따옴표 안 값을 지운 메시지였다. 스택 프레임이 메시지보다 안정적이라 서버 그룹핑에 그나마 가깝다. 그래도 스택이 없는 에러에는 무력하고, 압축된(minified) 함수 이름은 빌드마다 바뀐다.

[4차] 일반 중복 제거는 넣지 않는다

결국 이건 서버의 그룹핑을 브라우저에서 흉내 내는 일이라 완벽할 수 없다. 키는 브라우저마다 따로라, 사용자 둘이 같은 에러를 만나면 리플레이는 어차피 두 개 온다. 게다가 이 훅은 에러 세션 녹화 경로에서만 돌고, 무작위 녹화에는 걸리지도 않는다.

결론은 일반적인 중복 제거는 넣지 않는다. 특정 에러 하나가 실제로 리플레이를 도배하면, 그 에러만 골라 막는다. 중복 저장(fail-open)이 기록 유실(fail-closed)보다 안전하다.

아직 열려 있는 것

  • 보관 기간 확정 — 45일 시작 권고, 팀 논의 대기
  • 에러율 실측 — React 앱 프로젝트의 세션 데이터가 8월 21일 이후 끊겨 분모가 없다. 같은 프로젝트의 에러와 리플레이는 계속 들어오고 다른 프로젝트의 세션은 정상이라, 서버가 아니라 프런트 SDK 설정이나 release 값 쪽을 의심하고 있다
  • Sentry 서버가 사내 모니터링 콘솔의 감시 대상인지 — 아니라면 디스크 알림이 없다

마치며

진짜로 배운 것

  1. 쿼터가 없는 시스템의 진짜 밸브는 받는 쪽에 있다. 지우는 장치(cleanup)는 이미 있었고, 정책이 할 일은 무엇을 받을지 정하는 것이었다.
  2. 되돌릴 수 없는 손잡이는 작은 쪽에서 시작한다. 보관 기간은 늘리기는 쉽고 줄인 건 복구할 수 없다. 그래서 45일에서 시작한다.
  3. 실패의 방향을 고른다. 중복 제거가 완벽할 수 없다면, 기록을 잃는 쪽(fail-closed)보다 더 저장되는 쪽(fail-open)이 낫다. “완벽하지 않은 장치를 넣는 것”보다 “넣지 않고 문제가 생길 때 표적으로 막는 것”이 나을 때가 있다.
  4. 새 알림을 만들기 전에 이미 있는 감시를 찾는다. 사내 콘솔의 85% 규칙이 거의 같은 기준이었다. 다만 그 감시가 정말 이 서버를 보고 있는지는 확인해야 한다.

self-hosted Sentry 카테고리의 글