집사회(deacon:9772) 성능 3종 근본수리 — 증분파싱·캡정확도·핸들헤드룸
전날 무응답 장애 수리(2026-07-31-집사회-무응답장애-수리.md)에서 남긴 잔여 3건을
순서대로 처리했다. 서명 캐시가 "읽기 폭주"만 막았다면, 이번엔 재계산·캡·핸들 자체를 손봤다.
① T44 — 재계산 O(n^1.5) 근본수리
계측으로 범인 특정
원시함수를 격리 벤치(가져오기 "문자열.한선" + 명시 주() 호출)한 결과:| 대상 | 결과 |
|---|---|
분리(내용, "\n") 8,597자/42행 | 0.85 s |
| 〃 17,606자/90행 | 1.99 s |
| 〃 35,220자/187행 | 7.38 s (≈O(n^1.5)) |
부분() 187회(증가오프셋) | 0.06 s |
찾기뒤() 187회(증가오프셋) | 1.30 s |
추가() 374회 | 0.09 s |
_유니크최신)나 집계가 아니라 분리()가 파일 전체를 재분할하는 것.
줄분리()는 분리()의 얇은 래퍼라 대안이 못 된다(libs/문자열.한선:288).⚠ 함정 2건: hanseonc_high는 주()를 자동 호출하지 않는다(명시 주() 필요) —
이걸 모르면 임포트 top-level 비용을 함수 비용으로 오독한다. 그리고 분리는 VM 내장이
아니라 문자열.한선 라이브러리 함수다.
수리 A — 꼬리 증분 파싱
장부는 append-only이고 기록이 항상행 + "\n"(덧쓰기 지점 단일)이라 내용은 늘 개행으로
끝난다 = 캐시한 글자수는 항상 행 경계다. _티켓PSV읽기()가 커진 경우 이전 길이 이후의
꼬리만 파싱해 캐시 배열에 이어붙이도록 변경(_티켓캐시글자수 추가). 줄었거나 같으면
전량 재파싱(압축·동일크기 다른내용 대비 안전 기본값).동치성 검증 = 한선씨/증분파싱검증.한선(실장부 216행, 4/4 PASS):
접두사 일치 · 개행 종료 · 행수 일치 · 전 행 문자열 일치.
수리 B — _티켓JSON 행당 13회 재분할 제거
필드마다 티켓_XXX(행)을 부르면 그때마다 분리(행,"|")가 돌아 한 행에 13번 재분할했다
(45행 미션 tree = 585회). 기존 _필드에서 헬퍼 용도대로 행당 1회 분할 + 인덱스 접근으로 교체,
이스케이프 필드(6/8/9/12)만 장부복원 적용.tree(M8) 16.07 s → 0.37 s, 응답 md5 완전 동일(22,414 B, 9876f2f9… 전후 일치).
② 장부 헤드룸
중요한 정정 — 캡은 바이트가 아니라 글자수
74,183 B 장부를읽기()가 절단 없이 45,507자로 전량 반환(마지막 행 온전) → 캡은
65,535자 기준이다. wc -m 이 VM 글자수와 정확히 일치함을 확인.
기존 경고는 바이트(60,000 B)로 재고 있어 한글 장부(1.6 B/자)에서는 실여유의 60% 지점에서
상시 오경보 → 진짜 신호가 묻혔다(장애 인지 지연의 직접 원인)._장부캡점검을LC_ALL=ko_KR.UTF-8 wc -m기준으로 교체, 임계_장부캡경고글자 = 55000(실캡의 85%).- 배포 후 오경보 0건(현재 36,765자 = 56%).
증가 억제
완료 티켓 분리(아카이브 샤딩)는 의도적으로 보류했다 — 세션 25개가 활발히 claim/보고 중이라 장부 스키마 변경은 위험이 크고, 단일 아카이브 파일은 캡 폭탄을 옮길 뿐이다(진짜 해법은 월별 샤딩). 대신 검증된 원자 연산인 압축의 주기를 늘렸다:org.crowny.deacon-recycle.plist → StartCalendarInterval 배열화 4회/일(04:44·10:44·16:44·22:44).
plutil -lint OK, 10:44 실행 실측 확인(미션.psv 24→23행).③ 문자열 핸들
문자열보호()+200요청마다 문자열GC()는 T184에서 이미 배선돼 있었다. 문제는 순서였다.
.err 최종수정 시각이 프로세스 시작 시각과 동일 → 90% 경고는 부팅 1회(누적 아님).
임포트 top-level 장부 리플레이와 최초 전량 파싱이 만든 임시 문자열이 살아 있는 상태에서
문자열보호()를 부르니 부팅 쓰레기까지 영구 보호돼 런타임 여유가 4만 핸들뿐이었다.
문자열GC()→문자열보호()순서로 교정 → 부팅 STR 경고 0건(이전 매 부팅 발생)- GC 주기 200 → 50(혼합부하 소크에서 200으로는 90% 재발, exit43은 하드 크래시라 여유가 싸다)
최종 실측
| 라우트 | 장애 시 | 전날 수리 후 | 이번 수리 후 |
|---|---|---|---|
| stats | 무응답(000) | 0.024 s | 0.043 s |
| missions | 무응답 | 0.04 s | 0.042 s |
| tree?mission=M8 | 무응답 | 16.07 s | 0.37 s |
| RSS(유휴) | 3.36 GB | — | 5.6 MB |
| 부팅 STR 경고 | 매 부팅 | 매 부팅 | 0건 |
| 핸들 상한 초과 | 발생(exit43) | — | 0건 |
배포.sh(STRICT 0 · 장부검증 회귀 12/12 · 재기동 스모크 [티/정상] · 실패시 자동원복).관련 파일
한선씨/미션장부.한선— 증분 파싱(_티켓캐시글자수), 캡점검 글자수화, 집계 캐시한선씨/증분파싱검증.한선— 신규, 동치성 4/4서버.한선—_티켓JSON1회분할, GC/보호 순서, GC 주기 50~/Library/LaunchAgents/org.crowny.deacon-recycle.plist— 4회/일 (백업: scratchpad/recycle.plist.bak)
잔여 이슈
- 배열힙 무회수 — RSS 유휴 5.6 MB → 요청 버스트 후 1.6~2.7 GB.
문자열GC()는 문자열만
- 혼합부하에서 STR 90% 경고 여전히 산발(2건/200요청, 상한 초과 0). tree가 미션 전 티켓
- 월별 아카이브 샤딩 미착수 — 유니크 티켓이 ~345건에 이르면 압축해도 캡에 닿는다.
집사회.sh --max-time 5는 정상화된 지금 충분하나 rc=28 문구가 "지연"과 "사망"을
후속 (2026-08-02) — 잔여 3건 마감 + 배포도구 오판 수리
앞 문서의 "잔여 이슈" 1·2를 마감했다. 부하가 큰 시간대(load 9~79, 세션 26개)에 재측정하니 지연이 여전히 8초대로 튀어 원인을 더 파고든 결과, 세 개의 독립 병목이 더 나왔다.
④ _이스케이프 per-char 접합 — STR 핸들 폭증의 정체
결과 = 결과 + c // 글자마다 새 문자열 할당
문서화된 O(n²) 버퍼 함정이자, 글자당 문자열 핸들 1개를 만든다. tree 한 번에 45행 × 13필드를
전량 이스케이프하니 수천 핸들이 튀었다 — STR 90% 경고의 직접 원인.처방 = 특수문자 선판정 빠른 경로(찾기뒤(값,…) < 0이면 원본 그대로 반환). 대다수 값은
이스케이프가 불필요하다. 포함()은 인덱스를 반환해 선두매칭 0이 거짓으로 새므로 쓰지 않았다.
효과: 소크 STR 경고 0건 · RSS 1.6~2.7GB → 11.9MB. 이스케이프가 실제 필요한 값(M26 목표문의
\n)은 느린 경로로 정상 처리됨을 라이브 출력에서 확인.
⑤ _파일서명 요청당 6회 = 셸 12개
_파일서명 1회 = 체계() 프로세스 2개(stat + rm). 그런데 stats 한 번에
통계문자열(2) + 티켓목록(1) + _티켓PSV읽기(1) + 미션목록→_미션PSV읽기(1) = 6회 호출됐다.
장부가 안 바뀌어 캐시가 전부 히트인데도 load 9.4에서 stats 8.03초 — 전부 프로세스 스폰 대기였다.
처방 = 장부서명갱신()으로 요청 진입 시 파일당 1회만 계산하고 내부 캐시 검사는 그 값을 재사용.
쓰기(덧쓰기) 직후에도 갱신해 같은 요청 내 후속 읽기가 갱신분을 본다.
갱신을 안 부르면 서명이 ""이라 캐시가 항상 미스 = 느리지만 안전(자가시험 하네스 대비).
효과: load 10.4에서 stats 8.03s → 0.027~0.60s.
⑥ tree 응답 미션별 캐시
tree는 미션 전 티켓을 순회하며 행마다 분리 + 13필드 JSON 조립 + 문자열 접합을 하는 최중량 라우트다(지속 버스트 평균 2.05초, 배열·문자열 핸들 대량 생성 = RSS 급등 주범). 장부 서명이 같으면 응답 바이트가 불변이므로 통째로 재사용. 서명이 바뀌면 맵을 통째로 새로 만들어(부분 무효화 아님) 낡은 항목이 남지 않게 했다.
효과: tree 2.0~14.4s → 0.034~0.09s(히트). M8·M1 응답 md5 전후 완전 동일.
⑦ 배포.sh 정상 기동을 실패로 오판 — 3회 연속 자동원복
⑥ 배포가 3번 연속 원복됐다. 격리 포트로 새 빌드를 직접 띄워보니 health 200/0.02s로 멀쩡해 코드 결함이 아님이 확인됐고, 진짜 원인은 검증 루프였다:
for _i in 1..10; do sleep 3; curl -m 15 …; done
연결거부는 curl이 즉시 반환하므로 실질 대기는 sleep 합계 = 30초뿐이다. 장부가 커지며
부팅(임포트 top-level 장부 리플레이 + 최초 전량 파싱)이 30초를 넘기자 정상 기동이 실패로 판정됐다.
→ 재시도 창 30초 → 120초(seq 1 40)로 확장. 이후 배포 성공(부팅 실측 30~45초).⚠ 교훈: 재시도 루프의 "최대 N초"는 실패가 빠를 때의 실질 대기로 따져야 한다. 타임아웃 값(-m 15)이 아니라 sleep 합계가 창이다.
최종 실측 (load 24.6)
| 장애 시 | 07-31 | 08-01 | 08-02 | |
|---|---|---|---|---|
| stats | 무응답 | 0.024s | 8.03s(부하시) | 0.028~0.059s |
| tree?mission=M8 | 무응답 | 16.07s | 2.05s(평균) | 0.034~0.09s |
| 300요청 혼합 소크 | — | — | — | 18초 · 평균 0.060s |
| STR 경고 | 다발 | 부팅1회 | 산발 | 0건 |
| 핸들 상한 초과 | exit43 | 0 | 0 | 0건 |
| RSS(버스트 후) | 3.36GB | 2.7GB | 2.25GB | 225MB |
남은 잔여
- 부팅 30~45초. 임포트 top-level
_최대ID번호가 장부를 전량 파싱하고(캐시 미경유),
awk)로 뽑거나 캐시 리더 경유로 바꾸면 절반 이상 준다.
리사이클 4회/일마다 이만큼 다운타임이 생기므로 다음 우선순위.
- 배열힙 무회수 여전 — 버스트 후 225MB(10× 개선했으나 유휴 8MB 대비 여전히 큼). VM 과제.
- 월별 아카이브 샤딩 미착수(유니크 ~230건, 캡 여유 있음).
집사회.shrc=28 문구가 "지연"과 "사망"을 구분 못 함.