Tailscale이 19번의 DB 손상 끝에 찾아낸 SQLite의 16년 된 버그

Tailscale은 6개월 동안 SQLite 데이터베이스 손상을 19번 겪었어요. 재현 조건조차 잡히지 않던 문제를 운영 환경의 트랜잭션 로그와 VFS 추적으로 좁혔고, 끝내 SQLite의 16년 된 WAL-Reset 버그를 찾아냈어요. 1
잘 알려진 데이터베이스를 단일 작성자 구조로 썼는데도 장애가 반복됐다는 점이 눈에 띄어요. 원인은 SQLite 자체의 희귀한 경쟁 조건과 Tailscale의 공격적인 수동 체크포인트 운용이 맞물린 데 있었어요. 2
핵심 요약
| 구분 | 내용 |
| 발생 규모 | 6개월 동안 서로 다른 데이터베이스 손상 사고가 19번 발생했어요. |
| 직접 원인 | 체크포인트와 쓰기 트랜잭션이 겹칠 때 WAL 페이지가 복사된 것으로 잘못 처리되는 경쟁 조건이었어요. |
| 조사 방법 | SQL 트랜잭션 로그와 `tmstmpvfs` VFS 추적을 운영 환경에 배포해 사라진 쓰기를 포착했어요. |
| 수정 상태 | WAL-Reset 수정은 SQLite 3.51.3에 반영됐고, 3.53.0에는 오래된 표현식 인덱스의 자체 복구 기능이 추가됐어요. |
| 운영 교훈 | 문서화된 기능도 흔하지 않은 방식으로 조합하면 일반 설정보다 실제 검증 범위가 좁을 수 있어요. |
1. 재현되지 않던 손상을 운영 로그로 붙잡았어요
Tailscale의 제어 평면은 여러 샤드로 나뉘고, 각 샤드는 하나의 Go 프로세스가 SQLite 데이터베이스에 접근해요. 데이터베이스에는 장치와 네트워크 구성 메타데이터가 들어 있어요. 비공개 암호화 키나 네트워크 트래픽은 저장하지 않아요. 2
첫 손상은 S3 백업을 읽는 데이터 파이프라인에서 발견됐어요. 이후 6개월 동안 사고가 19번 반복됐지만 특정 고객, 샤드, 시간대, 부하와의 공통점을 찾지 못했어요. 발생 간격도 몇 시간부터 몇 주까지 들쭉날쭉했어요. 합성 환경에서 안정적으로 재현할 조건이 없었어요.
Tailscale은 원인을 찾는 동안 복구 절차부터 줄였어요. 백업마다 `PRAGMA integrity_check`를 실행했고, 손상을 감지하면 해당 제어 평면 샤드를 멈추도록 바꿨어요. 모든 쓰기 SQL을 별도 로그로 기록해 마지막 정상 백업 위에 재생하는 복구 경로도 만들었어요.
이 트랜잭션 로그에서 결정적인 흔적이 나왔어요. 한 트랜잭션이 오류 없이 커밋한 데이터를 다음 트랜잭션이 읽지 못했어요. SQLite가 성공으로 응답했는데 쓰기가 사라진 셈이에요. 단순한 파일 손상보다 더 낮은 계층을 의심할 근거가 생겼어요.
SQLite 개발진은 파일 입출력 계층을 감싸는 `tmstmpvfs` 추적 도구를 만들었어요. Tailscale은 이를 운영 환경에 배포해 다음 사고의 VFS 기록을 확보했어요. 로그에는 체크포인트와 쓰기 트랜잭션 사이의 희귀한 데이터 경쟁이 남아 있었어요. 3
SQLite의 WAL 모드에서는 새 페이지를 주 데이터베이스에 바로 쓰지 않고 WAL 파일에 먼저 기록해요. 체크포인트는 이 페이지를 주 데이터베이스로 옮겨요. 문제가 된 경쟁 조건에서는 체크포인트가 실제로 옮기지 않은 페이지까지 처리했다고 판단했어요. WAL이 재설정되면 해당 쓰기는 영구히 사라지고, 그 페이지를 참조하는 인덱스와 데이터가 어긋나면서 파일이 손상됐어요.
SQLite 개발진은 이 결함을 WAL-Reset 버그라고 이름 붙였어요. 해당 코드 경로에는 최소 16년 동안 문제가 숨어 있었던 것으로 봤어요. Tailscale은 빠르고 일관된 백업을 위해 체크포인트 시점을 직접 제어했고, 일반적인 배포보다 체크포인트를 자주 실행했어요. 그 결과 아주 좁은 경쟁 조건을 만날 빈도가 높아졌어요.
수정 과정도 한 번에 끝나지 않았어요. WAL-Reset 패치가 들어간 SQLite 3.52.0을 배포하자 13개 데이터베이스에서 무결성 오류가 나타났어요. 실제 신규 손상은 아니었어요. 부동소수점 변환 방식이 바뀌면서 오래된 표현식 인덱스와 계산 결과가 달라진 거짓 경고였어요. SQLite 개발진은 3.52.0을 철회하고 WAL-Reset 수정만 담은 3.51.3을 내놓았어요. 3.53.0에는 오래된 표현식 인덱스를 스스로 복구하는 기능도 넣었어요. 2
Tailscale은 패치 뒤에도 사고가 없다는 사실만으로 해결을 선언하지 않았어요. 쓰기와 WAL 재설정이 겹치면 경고를 남기도록 드라이버를 고쳤어요. 두 달 뒤 운영 환경에서 그 경고가 실제로 발생했지만 데이터베이스는 손상되지 않았어요. 이후 글 작성 시점까지 4개월 동안 같은 손상 사고가 다시 나오지 않았어요.
왜 중요한가요
이 사례는 재현이 어려운 장애를 다룰 때 어떤 증거를 더 남겨야 하는지 구체적으로 보여줘요. 오류 메시지와 무결성 검사만으로는 사라진 쓰기의 시점을 알 수 없었어요. Tailscale은 SQL 수준의 트랜잭션 기록과 VFS 수준의 파일 작업을 함께 남겨 애플리케이션 동작과 저장 계층의 불일치를 연결했어요. 2
백업이 존재하는 것과 복구할 수 있는 것은 달라요. Tailscale은 백업마다 무결성 검사를 실행하고, 마지막 정상 백업에 트랜잭션을 재생하는 절차를 실제 사고에서 반복해 봤어요. 운영팀이라면 백업 성공 여부뿐 아니라 복원된 데이터의 무결성과 손실 가능한 쓰기 범위까지 확인할 필요가 있어요.
검증된 기술도 운용 방식에 따라 위험이 달라져요. 수동 체크포인트는 SQLite가 지원하는 기능이지만, 높은 빈도로 직접 제어하는 조합은 일반 설정만큼 폭넓게 시험되지 않았어요. 기본 동작을 벗어나는 최적화를 선택했다면 그 경로에 맞춘 관측 지표와 장애 주입, 복구 연습을 함께 준비해야 해요.
오픈소스 유지보수자와의 유료 지원 계약도 실질적인 역할을 했어요. Tailscale은 SQLite 개발진과 사고 자료를 함께 분석했고, SQLite 개발진은 전용 VFS 추적 도구와 수정안을 만들었어요. 제품 팀이 핵심 오픈소스의 드문 결함을 만났을 때 무료 이슈 제보만 기다리지 않고 전문 지원을 구매하는 선택이 문제 해결 시간을 줄일 수 있어요.
참고 자료
- Tailscale이 데이터베이스 손상 원인을 16년 된 SQLite WAL-Reset 버그로 추적한 과정 — GeekNews
- How Tailscale helped find the SQLite WAL-Reset bug — Tailscale
- The WAL-Reset Bug — SQLite
'IT & AI' 카테고리의 다른 글
| AI 코딩을 멈춘 20년 차 개발자, 속도 뒤에서 잃은 것들 (0) | 2026.08.13 |
|---|---|
| 2.4조 파라미터 Qwen3.8 공개, 자체 호스팅의 문턱은 얼마나 높을까요 (0) | 2026.08.13 |
| AI 코딩이 빨라질수록 코드 리뷰가 병목이 되는 이유 (0) | 2026.08.13 |
| Unsloth Desktop, 로컬 AI 실행부터 학습까지 한 앱에 담았어요 (0) | 2026.08.13 |
| AI 코드 리뷰, 결정은 빨라져도 품질은 따라오지 않았어요 (0) | 2026.08.13 |