16년 묵은 SQLite WAL-reset bug, Tailscale의 19번 장애가 찾아낸 경쟁 조건

Tailscale control plane의 SQLite database는 6개월 동안 19번 손상됐다. 최근 배포와 특정 shard, 고객, 시간대, 부하 사이에는 공통점이 없었고 synthetic test로도 재현되지 않았다. 이미 commit된 write가 다음 transaction에서 사라지는 현상과 WAL보다 더 많은 page를 checkpoint했다고 보고하는 지표가 남은 단서였다. 평범해 보이는 database corruption이 SQLite 내부의 16년 된 경쟁 조건으로 이어진 과정이다.

구조 자체는 단순했다. 각 control-plane shard를 하나의 Go process가 전담하고, SQLite를 single writer로 사용한다. 다만 빠르고 일관된 backup을 만들기 위해 SQLite의 자동 동작에 맡기지 않고 checkpoint를 자주 직접 실행했다. 문서화되고 지원되는 방식이지만 흔한 운영 경로는 아니었다. 드문 timing bug를 반복해서 만날 확률이 이 환경에서 커졌다.

결정적인 도구는 SQLite 개발자가 만든 tmstmpvfs shim이었다. virtual filesystem 계층을 감싸 page write와 checkpoint 변화를 더 자세히 기록했고, 다음 손상 직후 write transaction과 WAL reset이 충돌하는 data race를 포착했다. 특정 순간에 write가 들어오면 checkpoint가 일부 page를 main database로 옮겼다고 잘못 판단한다. 실제로 기록되지 않은 page가 사라지고, 이를 참조하는 index 등만 남으면서 file이 손상된다.

SQLite 팀은 checkpoint 함수에 WAL이 다른 thread에서 reset됐는지 확인하는 검사를 추가했다. 최초 수정판 3.52.0은 text-to-floating-point 변환 변화로 stale expression index를 corruption처럼 보고하는 별도 문제를 일으켜 철회됐다. WAL-reset 수정만 담은 3.51.3이 다시 배포됐고, Tailscale은 timestamp 정밀도를 정수 초로 낮춰 표현식 index 문제도 피했다.

수정 뒤 사고가 없다는 사실만으로는 충분하지 않았다. Tailscale은 write와 WAL reset이 겹치면 경고하도록 driver를 고쳤고, 두 달 뒤 정확한 충돌 조건이 발생했지만 database는 손상되지 않았다. 이후 글 작성 시점까지 네 달 동안 추가 사고가 없었다. 원인을 설명하는 이 긍정적 신호가 장기간의 조용함보다 강한 검증이 됐다.

토론에서는 회사가 SQLite support contract와 새 VFS debugging tool 개발 비용을 지원한 점도 주목받았다. Tailscale의 상세 회고는 운영 복구와 조사 과정을 공개하고, Hacker News 토론은 multiple connection과 checkpoint 구조를 더 깊게 파고든다. 검증된 기술도 비표준 운용 경로에서는 별도의 관측 장치와 실패 복구 설계가 필요하다는 사례다.