feat(ai-file): add per-read memory probe to diagnose RSS ratchet under load
부하테스트에서 file 파드 RSS 가 5분간 서서히 올라 1G(cgroup limit)를 찍고 OOMKill 되는 현상을 진단하기 위한 계측. 파드가 20개라 특정 파드에 디버그 엔드포인트를 찔러볼 수 없어, 잡이 끝날 때마다 파드가 스스로 남기게 한다.
/proc·cgroup 조회로 이미 확인된 것: anon 989MB / file 10MB / Private_Dirty 989MB, memory.events 전부 0. → 페이지 캐시가 아니라 진짜 프로세스 메모리이고, 커널이 눌러서 만든 플래토가 아니라 프로세스가 스스로 하이워터마크에 도달한 상태.
남은 갈래는 "살아있는 파이썬 객체" vs "해제됐지만 OS 로 안 돌아간 할당자 보유분" 하나뿐이며, 이건 /proc 으로는 안 갈린다.
- memprobe.py: perform_read 종료마다 RSS·sys.getallocatedblocks()·cgroup anon/file 을 1줄 로깅. blocks 가 평평한데 RSS 만 오르면 파이썬 누수가 아니라 할당자 미반환. host= 로 파드를 구분한다.
- 회수 프로브(reclaim_every>0): gc → malloc_trim → MuPDF store 비우기를 단계별로 돌려 회수분 출처를 나눈다. PyMuPDF 1.27 의 TOOLS.store_size() 는 미구현(None)이라 store 크기를 읽는 대신 비운 뒤 RSS 차이로 역산한다. 실제로 메모리를 되돌려주므로 관찰 런과 검증 런을 나눠 돌려야 한다.
- perform_read 한 곳에만 걸어 HTTP·큐 컨슈머 양쪽을 함께 덮는다.
- env-or-config 이중 스위치(read_cache_bypass 와 동일 패턴). dev 는 yml 로 상시 ON, 나머지 프로파일은 모델 기본값 False. reclaim_every 는 정수라 truthy-OR 로 "env 로 0 을 줘서 config 값 끄기"가 안 되므로 env 지정 여부를 None 으로 구분해 override 한다.
- 샘플링은 어떤 예외도 밖으로 내보내지 않아 요청 경로를 깨지 않는다.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com