crowny-space 서버(:9607) OOM 컴팩션 수리
개요
라이브 crowny-space 서버가 47분 uptime에서 ARRAY힙 OOM으로 크래시(KeepAlive 자활)한 사고를 조사·수리. append-log 재파싱 구조가 근본 원인이었고, 스냅샷 컴팩션 + 로그북 회전 + 요청 핫패스 메모리 마커/복원 3중 방어로 개선.
OOM 원인 진단 (근거: /tmp/crowny-space.err)
[ARRAY] OOM! base=143750456 mc=143754553 MAX=144000000 heap=143758477
- MAX=144,000,000 셀 상한에 도달. crownyc VM의 배열/맵 힙은 GC 없는 단조증가(bump) 할당자이며, 배열 1개는 원소수 무관 고정 ~4097셀(가드레일 실측치)을 소비한다.
- 매 요청마다 PSV append-log를 통째로 재파싱(
_PSV읽기→_PSV파싱)하고, 이 파싱 배열이 재기동 전까지 절대 회수되지 않는 구조 → 요청 누적에 비례해 배열힙이 단조증가.
- 추가로
_PSV파싱이 글자(s,i) 문자 인덱스 루프를 쓰는데 글자()는 매 호출 O(pos)라 전체 스캔이 O(n²) — 파일이 커질수록 파싱 자체도 느려져(체감상 응답이 멈춘 것처럼 보임), append-log가 무한 성장하는 구조에서 시간이 지날수록 이중으로 악화.
- STR 조기경고(문자열 핸들 432000/480000, 90%)도 동반 관찰 — 동일 계열의 "정리 없는 누적" 패턴.
적용한 구조
- 엔티티상태 스냅샷 컴팩션 (
_엔티티컴팩션검사, 서버.한선):
-
엔티티상태.psv는 latest-wins 스냅샷 성격 → 파일이 임계치(8000자)를 넘으면 id당 최신 1행만 남기고
쓰기()로 통째 재작성(append 아님).
- 트리거를
write-time(엔티티설정/엔티티설정품질 호출마다) +
tick-time(
/api/tick, 60초마다) 이중으로 배치. tick만으로는 짧은 시간에 몰린 대량 요청(스트레스 테스트로 실측)에서 파일이 임계치를 넘어도 다음 tick까지 계속 자라 O(n²) 파싱비용이 누적, 응답이 6~10초까지 느려지는 현상을 실측 → write-time 체크를 추가해 근본 해결.
- 로그북 append-log 회전 (
_로그북기록/_로그북현재인덱스/_로그북파일경로):
- 기존
수신프레임_N.psv와 동일한 idx 파일 회전 패턴 적용(3800자 근접 시 다음 idx로 회전). 조회(
/api/logbook)는 항상 "현재 idx 파일만" 읽음.
- 로그북에 쓰던 7개 호출부(
_엔티티설정·
_엔티티설정품질·
_엔티티신규등록·
_고지서납부·
_대여반납·
_처리손님코드·에너지 힌트)를 전부
_로그북기록() 경유로 교체.
- 요청 핫패스 메모리 마커/복원 (메인 TCP 루프):
- 매 연결 처리(TCP수락→TCP읽기→요청처리→TCP닫기) 전후로
메모리마커()/
맵힙마커() 캡처 후
메모리복원()/
맵힙복원()으로 되돌림(opcode 730/731/734/735).
- 응답을 소켓에 다 쓴 뒤 시점이라 안전(가드레일 문서화된 관용구,
libs/타원곡선.한선 패턴과 동일).
- 명령큐·허브맵·자동화발화는 지시대로 손대지 않음(이미 소형·안전).
스트레스 실측 (풀 300 커맨드 + 100 SC1 프레임 ingest)
- 최초 배포 직후 반복 toggle 15회 연속에서 응답 없음(타임아웃) 재현 → 원인이 "무한루프"가 아니라 엔티티상태.psv가 tick 사이에 계속 자라며 O(n²) 파싱비용이 누적되는 것으로 진단(격리된 비교테스트로 확인: 동일 코드, 작은 데이터에선 재현 안 됨 / 라이브 데이터(26KB, 305행) 복사본에선 재현, 실제로는 6.7초 후 정상 응답 — 무한이 아니라 초과지연).
- write-time 컴팩션 추가 후: 최초 1회(레거시 26KB 파일 컴팩션, ~9.7초, 1회성 카타치업) 이후 300 커맨드는 191초, 100 ingest는 49초로 정상 완주. 이후 매 요청 응답시간 200ms대로 안정.
- 로그북 회전: idx 1→8까지 회전 발생(각 파일 1.4~4.6KB로 상한 내). 수신프레임: idx 1→2 회전 발생.
- 엔티티상태.psv: 400회 상태변경 후에도 10.7KB로 유지(컴팩션 없었다면 수십~수백KB로 성장했을 것).
- RSS 3회 샘플(ps): 476960→426128→63792→(최종 유휴)10736 KB — 상승 발산 없음, PID 불변(97166), 재기동/크래시 0.
- 회전 후 재기동 → 상태 복원 무손상 확인(light.study, 카트, 허브맵 전부 재기동 전후 동일).
회귀 12종 결과 (전부 PASS)
health / spaces / entities / command→logbook / tick / energy / bridge.js / ingest(SC1 프레임, 체크섬 수식 재현으로 유효 프레임 생성) / commands(허브 큐) / hub·register / scenes / bills
관련 파일
/Users/ef/crowny-space/서버.한선 (수리 대상, 1877줄)
- 백업:
서버.한선.bak-컴팩션전-1784292606, 서버.toau.bak-컴팩션전-1784294247, 서버.toau.bak-wt컴팩션전-1784338230
- 라이브 바이너리:
서버.toau (재컴파일 배포 완료, CROWNY_STRICT=1 경고 0)
- plist:
~/Library/LaunchAgents/org.crowny.space.plist(KeepAlive), org.crowny.space.tick.plist(60초 /api/tick)
잔여 이슈
감사.psv는 append-only로 계속 성장하지만 서버가 재파싱하지 않아 OOM 리스크는 없음(디스크 hygiene 문제만, 후속 과제).
- 로그북 회전 후 "과거 로그 전체 조회"는 현재 idx 파일 범위로 제한(지시된 설계,
/api/logbook?limit=이 큰 값이어도 현재 파일 안의 행만 반환) — 과거 로그북 파일 통합조회는 후속 과제.
- 요청당 전체 PSV 재파싱 구조 자체(엔티티/자동화/장바구니/대여 등)는 유지 — 각 파일이 작게 유지되는 한 문제 없으나, 향후 자동화 규칙 수가 크게 늘면 동일 패턴(write-time 컴팩션 또는 인메모리 캐시)이 필요할 수 있음.
~/.claude/scripts/crownycode-brain.sh learn "PSV컴팩션회전" 서버.한선 학습 완료.