← 목록
기타 2026-07-30 8KB 읽기 6분

T65 — 대기티켓찾기 증분갱신/캐시 설계안

개요

/Users/ef/crowny-deacon/한선씨/미션장부.한선대기티켓찾기(모델)이 폴링마다 티켓목록()(=_PSV파일유니크최신(_티켓파일))을 호출해 티켓.psv 전체를 재파싱한다. 장부가 append-only로 계속 커지므로(T수 증가), 폴링 주기가 짧을수록 O(전체행수) 재스캔이 누적 CPU/토큰 비용이 된다. 코드 수정 없이 설계만 제시한다.

현재 경로 (실코드 기준)

  • 대기티켓찾기티켓목록()_PSV파일유니크최신(_티켓파일)_PSV읽기
읽기(파일) 전체 로드 + 분리(내용,"\n") 전체 파싱 + 맵 기반 last-wins 재계산(O(R)).
  • 매 폴링마다: 디스크 read 전체 + 파싱 O(R) + last-wins O(R) + 대기/모델 필터 O(R).
  • R(총 행수, append-only이므로 unique 티켓수보다 항상 큼: 상태전환마다 새 행 추가)이
계속 증가 → 폴링 비용이 시간에 따라 우상향.

함정: 한선씨 장수서버 배열힙 무회수 OOM

  • 메모리 인용(feedback_모니터판_OOM_디스크폭주.md, feedback_crownyc_힙144MB_장기구동_누적OOM.md):
장수 구동 crownyc 프로세스는 힙 캡(144MB)이 있고, 배열/버퍼를 append만 하고 회수하지 않으면 장기 폴링 누적으로 OOM에 도달한다("프레시 실행은 정상, 장기구동만 실패").
  • 함의: 캐시 설계에서 "폴링마다 조금씩 growing하는 전역 배열/맵"을 무제한 유지하면
안 된다 — 캐시 자체가 새로운 누적 OOM 벡터가 된다. 캐시는 크기 상한 + 교체 정책이 반드시 있어야 하고, 장기구동 데몬(deacon 서버)에는 별도 재기동 정책(KeepAlive 등)이 이미 있으므로 캐시는 "재기동 시 자연 소멸 + 재구축"을 전제로 설계한다.

참조 패턴: 인메모리캐시 부팅1회

  • 메모리 인용 "라이브 서비스군C" 등에서 반복되는 패턴: 부팅 시 1회 전체 로드 →
이후엔 요청마다 파일 변경분만 반영(변경 없으면 캐시 그대로 반환).
  • 이 패턴을 미션장부에 이식하되, 장부는 append-only이므로 "변경분"은 항상
파일 끝에 붙은 새 바이트/새 행으로 한정할 수 있다(과거 행 수정 없음 — 스펙 보장).

설계안 A — 파일 오프셋 기반 증분 파싱 (권장, 최소 침습)

자료구조 (모듈 전역, 함수 스코프 밖에 1세트만 유지)

_캐시_오프셋       // 마지막으로 읽은 파일 바이트 오프셋 (숫자)
_캐시_행들         // 지금까지 파싱된 전체 행 배열 (append-only 그대로 growing)
_캐시_최신맵       // id → 최신행 맵 (_PSV파일유니크최신의 결과, 이미 O(R) 스캔 완료본)
_캐시_대기인덱스    // 모델 → [대기 상태인 티켓ID 배열] 맵 (대기티켓찾기 전용 파생인덱스)

갱신 절차 (매 폴링 시)

  1. 파일 크기 확인 (stat 또는 읽기글자수 — VM에 파일 크기만 얻는 API 없으면
전체 읽기가 불가피하므로, 최소한 "새 오프셋부터 끝까지"만 다시 분리하도록 읽기 자체를 오프셋 파라미터화하는 것이 이상적이나 현재 읽기()가 전체파일만 지원하면 대안 B로 폴백).
  1. 새로 추가된 행만 파싱해 _캐시_행들에 append, _캐시_최신맵을 해당 id만 갱신
(append-only 스펙 덕에 과거 id 재확인 불필요 — 최신행은 반드시 새로 추가된 쪽).
  1. 상태전환 행(같은 티켓ID가 새 상태로 재기록)이 오면 _캐시_대기인덱스에서
해당 티켓ID를 이전 상태 리스트에서 제거하고 새 상태가 "대기"면 추가.
  1. 대기티켓찾기(모델)_캐시_대기인덱스[모델]에서 번호 최소값만 선형탐색
(대기 티켓 수는 전체 R보다 훨씬 작음 — O(대기중인 티켓수)로 축소).

무회수 OOM 방지책

  • _캐시_행들은 원본 파일과 1:1 growing이라 회수 대상이 아님(파일 자체가 append-only
이므로 캐시도 append-only인 게 정합적) — 대신 주기적 장부 압축(회전)이 근본 해법 (메모리 인용 "장부 압축·회전 필수" 패턴 재사용): 완료/만료된 오래된 미션의 티켓 행을 별도 아카이브 파일로 이관하고 _티켓파일을 재기록하면 _캐시_오프셋도 0으로 리셋.
  • _캐시_대기인덱스는 파생 인덱스이므로 상시 작게 유지됨(대기 상태 티켓 수만큼) —
이 자체가 무제한 growing 위험이 없는 부분.
  • 캐시 전체를 감싸는 크기 가드: _캐시_행들 길이가 임계치(예: 5000행) 초과 시
경고 로그 + 압축 트리거 신호(별도 배치가 처리) — 폴링 경로 자체는 판단만 하고 압축은 비동기/별도 실행으로 분리(폴링 핫패스에 회전 로직을 넣지 않는다 — 지연 유발).

설계안 B — 읽기() 전체 로드 유지 + "변경없음 조기반환"만 캐시 (현재 VM 제약 하 현실적 대안)

설계안 A는 읽기()가 오프셋 기반 부분읽기를 지원해야 진짜 증분이 되는데, VM 함정 목록상 파일 API가 전체 로드 위주로 보인다(파일존재()도 always -1 반환하는 등 제약이 많음). 이런 VM 제약 하에서는:
  1. 매 폴링마다 파일을 여전히 전체 읽되, 읽은 내용의 글자수(또는 마지막 N바이트
해시)를 이전 폴링과 비교.
  1. 동일하면(장부에 새 append 없음) _캐시_대기인덱스를 그대로 재사용 — 파싱/맵갱신
스킵. 이 경우도 디스크 read는 매번 발생하지만(글자수 확인 위해 최소 필요), 분리()·맵넣어()·last-wins 재계산이라는 진짜 비싼 부분을 스킵할 수 있어 폴링이 잦고(초 단위) 티켓 생성/전환이 드문(분 단위) 워크로드에서 큰 절감.
  1. 변경 있으면 설계안 A의 "새 오프셋부터"를 부분(내용, 이전글자수, 새글자수)
문자열 슬라이스해 시뮬레이션 가능 — 부분()이 O(n) 스캔이라도 신규분만 스캔하면 전체 재파싱보다 저렴.

권장 결론

  • 1단계(즉시 적용 가능, 저위험): 설계안 B — 글자수 비교 기반 "변경없음 스킵".
코드 변경 최소, VM 제약과 무관, 기존 _필드에서 핫패스 최적화 기조와 일관.
  • 2단계(성능이 실제 병목으로 실측되면): 설계안 A — 부분(내용, 이전오프셋, 끝)
슬라이스 기반 신규행만 파싱 + _캐시_대기인덱스 파생 인덱스 유지.
  • 두 단계 모두 캐시는 append-only 성격에 맞춰 growing 허용하되, 대기인덱스처럼
작게 유지되는 파생 구조로 실제 조회 비용을 낮추고, 원본 growing 방지는 캐시가 아닌 장부 압축/회전 배치로 별도 처리한다(캐시 안에 회수 로직을 넣지 않는다 — 무회수 OOM 함정은 "캐시가 스스로 비대해지는 것"이 아니라 "폴링 핫패스에서 회수/압축 같은 무거운 로직을 안 하던 걸 새로 넣어 오히려 지연·메모리를 더 쓰는 것"도 포함하므로, 회전은 반드시 저빈도 배치로 분리).

잔여 이슈

  • 읽기()가 오프셋/부분읽기를 지원하는지 VM 레벨 확인 필요(현재 문서상 전체로드만
확인됨) — 지원 안 하면 설계안 A는 "문자열 슬라이스"로 흉내내는 수준에 그침.
  • 장부 압축/회전 배치의 트리거 조건(행수 임계치·주기)은 실측(현재 R 증가 속도) 후 확정.
  • 다중 프로세스(여러 deacon 세션)가 동시에 캐시를 갖는 경우 캐시 일관성은 각자
자기 프로세스 내 폴링 오프셋만 추적하면 되므로 별도 락 불필요(읽기 전용 캐시).