← 목록
기타 2026-07-18 6KB 읽기 5분

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%)도 동반 관찰 — 동일 계열의 "정리 없는 누적" 패턴.

적용한 구조

  1. 엔티티상태 스냅샷 컴팩션 (_엔티티컴팩션검사, 서버.한선):
- 엔티티상태.psv는 latest-wins 스냅샷 성격 → 파일이 임계치(8000자)를 넘으면 id당 최신 1행만 남기고 쓰기()로 통째 재작성(append 아님). - 트리거를 write-time(엔티티설정/엔티티설정품질 호출마다) + tick-time(/api/tick, 60초마다) 이중으로 배치. tick만으로는 짧은 시간에 몰린 대량 요청(스트레스 테스트로 실측)에서 파일이 임계치를 넘어도 다음 tick까지 계속 자라 O(n²) 파싱비용이 누적, 응답이 6~10초까지 느려지는 현상을 실측 → write-time 체크를 추가해 근본 해결.
  1. 로그북 append-log 회전 (_로그북기록/_로그북현재인덱스/_로그북파일경로):
- 기존 수신프레임_N.psv와 동일한 idx 파일 회전 패턴 적용(3800자 근접 시 다음 idx로 회전). 조회(/api/logbook)는 항상 "현재 idx 파일만" 읽음. - 로그북에 쓰던 7개 호출부(_엔티티설정·_엔티티설정품질·_엔티티신규등록·_고지서납부·_대여반납·_처리손님코드·에너지 힌트)를 전부 _로그북기록() 경유로 교체.
  1. 요청 핫패스 메모리 마커/복원 (메인 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컴팩션회전" 서버.한선 학습 완료.