livesync-bridge가 push만 하고 pull은 죽어 있을 때 — 에러 한 줄 없이 한쪽 방향만 멎는다
환경: livesync-bridge(Deno) 2대(라즈베리파이·미니PC) · CouchDB 3.3(미니PC 로컬) · tailscale · Obsidian Self-hosted LiveSync
TL;DR / 빠른 해결
브리지가 한쪽 방향만 죽을 수 있다. 올리는 건 되는데 내려받는 게 안 되면, 로그에서 방향별 이벤트 수를 세는 게 제일 빠르다.
# <couchdb-peer>, <storage-peer> = dat/config.json 의 peers[].name
docker logs <bridge-container> 2>&1 \
| grep -cE "\[<couchdb-peer>\] -->|\[<storage-peer>\] <--"피어 이름은 각자 설정한 값이다. 우리 설정으로는 이렇게 된다.
docker logs obsidian-livesync-bridge 2>&1 | grep -cE "\[obsidiandb\] -->|\[pi-vault\] <--"컨테이너가 충분히 오래 떠 있었는데 이 값이 0이면 pull이 죽은 것이다(컨테이너를 새로 만든 직후라 로그가 비었거나, 그동안 원격 변경이 아예 없었다면 당연히 0이다). 로그에 에러가 없어도 죽어 있을 수 있다. 대괄호까지 그대로 넣어야 한다 — 이걸 빼면 살아 있는 브리지에서도 0이 나와서 정반대로 진단하게 된다.
- 복구는 컨테이너 재시작이면 된다. 다만 재시작 전에 양쪽 파일을 정합시켜야 안전하다.
- 진짜 피해는 "동기화가 안 된다"가 아니라 오래된 파일이 최신인 척 올라가는 것이다. pull이 죽은 기기에서 파일을 고치면, 그 기기가 들고 있던 옛 내용이 push로 올라가 다른 기기의 편집을 덮는다.
- 원인은 확정하지 못했다. 재시작으로 살아나는 것까지가 이 글의 관측이다.
증상
이 글은 지금 다시 실행해 재현한 게 아니라, 그때 남긴 운영기록을 바탕으로 내가 다시 정리한 것이다. 시리즈 전체의 배경은 노션 대신 Obsidian 자가호스팅에 정리해뒀다.
동기화 허브를 라즈베리파이에서 미니PC로 옮긴 뒤였다. CouchDB는 미니PC에서 돌고, 브리지는 양쪽에 하나씩 있다.
증상은 두 갈래로 보였다.
하나 — 폰이나 데스크탑에서 고친 게 파이에 안 왔다. 파이에서 볼트 파일을 열어보면 며칠 전 내용 그대로였다.
둘 — 파이에서 고친 건 잘 올라갔다. 그래서 "동기화가 죽었다"고 말하기도 애매했다. 절반은 멀쩡했으니까.
여기서 진짜 문제가 터졌다. 이 글의 진단과 복구도 파이에서 한 것이고, 옛 파일을 밀어 올리고 있던 것도 나였다. 그 편집이 어쩌다 한 번도 아니었다 — 그 기기에서 도는 세션은 일하면서 노트를 계속 쓴다. 파이에서 볼트 파일을 건드릴 때마다 내가 들고 있던 오래된 내용이 CouchDB로 올라가 다른 기기의 편집을 덮었다. 폰에서 지운 문구가 시간이 지나면 되살아나는 식이었다. 사용자 입장에서는 "지웠는데 왜 자꾸 살아나지"로 보인다.
오진 배제: 권한 문제로 보였다
파이 볼트에는 실제로 권한 문제가 하나 있었다. 봇이 만든 영상 노트 폴더 44개가 root:root 소유라 브리지(uid 1000)가 접근하지 못하고 있었다.
그래서 처음엔 이걸 원인으로 봤다. 소유권을 파이 사용자로 바꾸자 권한 에러는 0건이 됐고, 그 폴더들도 정상 동기화됐다.
그런데 pull은 여전히 죽어 있었다. 권한은 실재하는 별개 문제였을 뿐 이 증상의 원인이 아니었다. 고쳐서 증상이 안 사라지면 그 가설은 거기서 접어야 하는데, 이걸 놓기까지 필요 이상으로 오래 걸렸다.
진단: 방향별로 로그를 세보면 pull이 0이다
브리지 로그는 어느 피어에서 어느 피어로 흘렀는지를 남긴다. 우리 설정 기준으로 이렇게 갈린다.
- push(파일시스템 → CouchDB):
[pi-vault] --> …뒤에[obsidiandb] <-- saved - pull(CouchDB → 파일시스템):
[obsidiandb] --> …뒤에[pi-vault] <-- saved
이걸 세보니 pull 이벤트가 0건이었다. 에러도 경고도 없이 그 방향만 조용히 멎어 있었다.
같은 시점에 미니PC 쪽 브리지는 양방향 다 정상이었다. 미니PC는 CouchDB와 같은 호스트라 네트워크를 타지 않는다. 파이만 뒤처져 있었으니 미니PC 볼트가 사실상 정본인 상태였다.
원인이 무엇이냐는 여기서 멈춘다. tailscale HTTPS를 타고 흐르는 _changes 피드가 어느 시점에 끊기고 다시 붙지 못한 것으로 추정했지만, 그때 이걸 확인할 근거를 남겨두지 못했다. 확정된 건 재시작하면 pull이 되살아난다는 것까지다.
해결: 정합부터 하고, 그다음에 재시작
여기서 바로 재시작하면 안 된다. 파이가 들고 있는 옛 파일들이 다시 push로 올라갈 수 있기 때문이다. 순서가 중요하다.
1단계 — 차이부터 본다(dry-run). 파이와 미니PC의 마크다운 파일만 비교한다.
rsync -rtcn --delete \
--include='*/' --include='*.md' --exclude='*' \
mini:~/Obsidian/HoradricCube/ ~/Obsidian/HoradricCube/mini는 ssh 설정에 정의해둔 정본 기기 별칭이다. -n이 dry-run, -c는 타임스탬프 말고 체크섬으로 비교하라는 뜻이다. 시계가 어긋나 있어도 내용 기준으로 갈린다. 필터도 봐야 한다 — 이 명령은 .md만 본다. 첨부파일이나 .obsidian/ 아래는 대상이 아니니, 볼트에 그런 게 있으면 따로 맞춰야 한다.
2단계 — 브리지를 멈춘다.
docker stop obsidian-livesync-bridge정합 중에 브리지가 파일 변경을 감지해 올려버리면 하려던 일이 뒤집힌다.
3단계 — 정본(미니PC) → 파이 방향으로만 맞춘다. 위 명령에서 -n과 --delete를 뺀다.
rsync -rtc \
--include='*/' --include='*.md' --exclude='*' \
mini:~/Obsidian/HoradricCube/ ~/Obsidian/HoradricCube/--delete를 뺀 게 핵심이다. 생성과 갱신만 하고 삭제는 안 한다. 그래서 이 단계에서 잃는 파일이 없다. dry-run에서 삭제 대상으로 잡혔던 건 빈 디렉토리뿐이었다.
4단계 — 브리지를 다시 켠다.
docker start obsidian-livesync-bridge재시작이 pull을 되살렸다. 그리고 브리지는 켜질 때 오프라인 변경을 스캔하는데, 3단계에서 이미 내용을 맞춰놨으니 전부 Skipped (Same)로 지나갔다. 즉 이 재시작에서 남의 편집을 덮어쓴 게 없다는 걸 로그로 확인할 수 있었다.
5단계 — 라운드트립 검증. 한 방향만 보면 안 된다.
- 미니PC에서 파일 한 줄을 고치고 → 파이에 반영되는지(pull)
- 파이에서 고치고 → 미니PC에 반영되는지(push)
둘 다 통과했다. 최종적으로 48개 파일이 정합됐고, 이 정합 절차에서 잃은 파일은 0이었다.
이 사건에서 제일 위험했던 것
동기화가 멈추는 것보다 한쪽 방향만 멈추는 게 더 위험하다.
양방향이 다 죽으면 금방 눈치챈다. 아무것도 안 넘어가니까. 그런데 push만 살아 있으면 시스템은 계속 "동작"한다. 뒤처진 기기가 자기가 뒤처졌다는 걸 모른 채, 자기가 들고 있는 옛 내용을 최신인 것처럼 계속 올린다.
이 스택에서는 그게 더 나빴다. 파이에서 돌던 세션이 노트를 자동으로 고치고 있었기 때문이다. 자동화가 성실할수록 옛 내용이 더 자주 올라간다.
이렇게 덮인 편집은 CouchDB의 리비전 히스토리가 남아 있는 동안에는 꺼내볼 여지가 있다. 다만 CouchDB는 자동 compaction이 기본으로 돌고, compaction이 지나가면 옛 리비전의 본문은 사라진다. 그리고 나는 어떤 편집이 덮였는지 목록을 만들어보지도, 리비전에서 되살려보지도 않았다 — 그래서 리비전에서 되살리는 건 이 사건에서 시도된 적이 없다.
교훈 / 체크리스트
- 양방향 동기화는 방향별로 죽는다. "동기화 되나요"가 아니라 "올라가나요"와 "내려오나요"를 따로 확인한다. 로그에서 방향별 이벤트 수를 세는 게 제일 싸다.
- 에러가 없다고 살아 있는 게 아니다. 이 사건에서 pull은 예외 하나 없이 그냥 멎어 있었다. 살아 있음의 증거는 "에러 없음"이 아니라 최근 이벤트가 있음이다.
- 정합 없이 재시작하지 않는다. 뒤처진 쪽을 그냥 켜면 그 쪽 옛 파일이 최신을 덮는다. 내용 기준(
rsync -c)으로 먼저 맞추고, 그 단계에서는 삭제 옵션을 빼서 손실 가능성을 없앤다. - 복구 검증은 양방향으로 한다. 한쪽만 확인하면 방금 고친 그 방향만 보고 끝내게 된다.
- 이 증상이 다시 나면 자동으로 잡아야 한다. 사람이 로그를 세고 있을 수는 없기 때문인데, 그건 이 시리즈의 다음 글에서 다룬다.