11MB가 2.76GB가 되다
이날은 LiveSync 데이터베이스가 250배 부풀어 있는 걸 발견하고 원인을 추적한 날이다.
실제 콘텐츠는 11MB인데 데이터베이스가 2.76GB였다.
원인은 청크 단위 리비전 누적이었다.
그리고 이걸 자동으로 정리하는 설정은 소스코드에 존재하지 않았다.
옵시디언 볼트에 “데이터베이스 용량 경고” 팝업이 떴다.
실제 콘텐츠는 1,342개 파일에 11MB다.
근데 CouchDB가 2.76GB를 차지하고 있었다.
250배다.
원인 추적
원인을 단계별로 추적했다.
첫째, LiveSync는 파일을 청크 단위로 쪼갠다.
rabin-karp 알고리즘으로 분할한다.
하나의 마크다운 파일이 수십에서 수백 개 조각이 된다.
둘째, 한 글자만 수정해도 변경된 부분의 새 청크가 생성된다.
“저장할 때마다 동기화”가 켜져 있으니까, 매 저장마다 청크가 늘어난다.
셋째, 과거 청크를 영구 보존한다.
버전 이력이랑 충돌 복구를 위해서다.
그러니까 11MB는 “현재”이고, 2.76GB는 “누적”이다.
실측해봤다.
174,831개 문서 중에 청크가 170,446개였다.
파일당 평균 127개 조각이다.
대부분이 과거 편집 버전이었다.
자동 삭제는 없다
“자동으로 오래된 리비전을 삭제하는 설정이 있겠지”라고 생각했다.
소스코드를 전수조사했다.
없었다.
자동으로 삭제된 파일의 메타데이터를 정리하는 설정은 있었다.
근데 기존 파일의 편집 이력을 정리하는 설정은 없었다.
retention policy 개념 자체가 소스코드에 없었다.
LiveSync는 모든 편집 버전을 청크 단위로 영구 보존하는 게 기본 설계였다.
시간이 지나면 당연히 부풀어간다.
설정으로 막을 수 없다.
재구축 절차
해결은 데이터베이스를 재구축하는 거였다.
근데 순서가 중요했다.
첫째, 백업한다.
복제로 전체 백업을 받는다.
둘째, 서버를 먼저 재구축한다.
클라이언트를 먼저 건드리면 안 된다.
원격이 변하지 않은 상태에서 클라이언트만 재구축하면, 원본이 그대로니까 경고가 안 사라진다.
셋째, 서버에서 compaction을 돌린다.
디스크 빈 공간을 회수한다.
넷째, 다른 기기에서 다시 가져온다.
로컬 데이터를 버리고 서버에서 다시 받는다.
핵심은 “서버 먼저”다.
서버가 정리되어야 클라이언트가 정리된다.
폴백 체인이 비어 있었다
틈틈이 Hermes의 폴백 설정도 수정했다.
폴백 프로바이더를 문자열로 적어놨었다.
근데 문자열은 조용히 무시된다.
딕셔너리 형태로 적어야 한다.
문자열로 적으면 폴백 체인이 빈 상태가 된다.
속도 제한이나 과부하가 나면 어디로도 떨어지지 못한다.
그냥 실패한다.
이것도 “조용히 실패” 패턴이다.
에러가 안 난다.
경고도 안 나온다.
설정이 무시될 뿐이다.
“설정했으니까 되겠지”라고 믿고 있었는데, 실제로는 빈 체인이었다.
MCP 로딩 아키텍처 정정
pi의 MCP 로딩 구조도 정리했다.
처음에 oh-my-openagent나 mcporter가 MCP 호스트라고 가정했다.
근데 둘 다
틀렸다.
전역 grep으로 MCP 코드를 찾았는데, 특정 위치에서 찾았다고 그게 런타임 동작을 증명하지 않는다.
실제로 pi 바이너리 자체에는 MCP 코드가 0줄이었다.
MCP는 별도 패키지가 담당했다.
“전역 grep은 런타임 동작을 증명하지 않는다.” 이건 중요한 교훈이다.
어디에 코드가 있는지 찾는 건 그 코드가 런타임에 실행되는지와 다르다.
마무리
이날의 핵심은 “250배 부풀어 있는 원인을 소스코드까지 추적한 것”이었다.
LiveSync는 모든 편집 버전을 영구 보존한다.
자동 삭제 설정은 없다.
이걸 “설정이 있겠지”라고 가정하지 않고 소스코드를 전수조사해서 확인했다.
없으면 없는 거다.
그리고 재구축 순서가 중요했다.
서버 먼저, 클라이언트는 나중에. 순서를 바꾸면 경고가 안 사라진다.
마지막으로 “조용히 실패” 패턴이 또 나왔다.
폴백 체인이 문자열로 적혀 있어서 빈 상태였다.
설정이 무시되는데 에러도 경고도 없었다.
“설정했으니까 되겠지”라는 믿음이 틀릴 수 있다.
설정이 실제로 동작하는지 확인해야 한다.