VPN 구축·네트워크 연결 서비스인 Tailscale이, 자사의 서비스가 불안정했던 이유에 대해 수개월에 걸쳐 조사한 결과, 데이터베이스인 SQLite에 16년간 잠재되어 있던 버그가 원인であることが 밝혀졌습니다. Tailscale은 “무슨 일이 있었던가?”, “어떻게 대응했는가?”, “SQLite의 불具合 발견에 어떻게 기여했는가”에 대한 설명을 자사의 블로그에 공개했습니다. How Tailscale helped find the SQLite WAL-Reset bug https://tailscale.com/blog/sqlite-wal-reset-bug Tailscale은 2022년부터 SQLite를 주요 데이터베이스로 활용하기 시작했습니다. 사용자가 Tailscale의 엔드포인트에 접근하면 “컨트롤 플레인”에 연결되며, 컨트롤 플레인은 여러 개의 “샤드”로 구성됩니다. 안전한 프라이빗 네트워크 “테일넷”은 샤드 중 하나 위에 존재하지만, 상황에 따라 샤드 간을 매끄럽게 전환할 수 있는 메커니즘이 적용되어 있습니다. 각 샤드는 자신이 관리하는 테일넷에 대한 모든 정보를 저장하는 SQLite 데이터베이스를 보유합니다. 단일한 Go 프로세스가 SQLite 데이터베이스에 독점적으로 접근하여 테일넷의 컨트롤 플레인을 처리합니다.
백업 메커니즘은 수분마다 데이터베이스의 완전한 스냅샷을 획득하고 SQLite 파일 전체를 Amazon S3 버킷에 업로드합니다. 일련의 구성은 2023년 초부터 아무런 문제 없이 작동해 왔지만, 2025년 8월에 S3 백업으로 인해 데이터베이스의 손상이 보고된 것을 계기로 문제가 발생했습니다. 데이터베이스 손상은 6개월 동안 19건 발생했으나, 영향은 컨트롤 플레인 설정 데이터에만 국한되었습니다. 그러나 손상이 발생할 때마다 컨트롤 플레인 프로세스를 중단하고 데이터베이스를 복구 또는 복원해야 했기 때문에 대상 샤드를 포함하는 컨트롤 플레인이 사용할 수 없게 되는 다운타임이 발생했습니다.
문제 해결을 위해 먼저 최근 변경 사항을 확인했지만, 문제와 관련된 변경 사항은 발견되지 않았습니다. 특히 SQLite와 연동하는 저수준 코드는 모두 수년 전에 작성되었으며, 문제 발생이 없었기에 코드 수정은 이루어지지 않았습니다. 염려하여 꼼꼼하게 코드의 정사를 수행한 결과 데이터 손상을 일으킬 만한 문구가 발견되지 않았습니다. 또한 문제 발생을 유발하는 트리거가 불명확했기에 버그의 재현을 수행할 수 없었고, 라이브 환경에서의 포렌식 텔레메트리에 의존할 수밖에 없었습니다. 더욱 심각한 점은, 사고가 수시간마다 발생하기도 하고 몇 주 동안 아무 일도 일어나지 않기도 한다는 것입니다. 쉽게 수정할 수 있는 문제가 아니라고 판단한 Tailscale은 SQLite 개발자와 프로페셔널 지원 계약을 체결하고 협력하여 문제의 검증을 수행하기로 했습니다. 근본적인 원인의 조사 중에도 플랫폼은 가동 중이었고, 복구를 자동화하거나 다운타임을 단축하기 위해 필요한 다음 조치를 취했습니다. ·데이터베이스가 손상되면 즉시 하드 스톱하도록 샤드를 구성하고 ·자동 백업을 도입하고 ·운영 절차서와 온콜 훈련을 개선했습니다. 위의 노력으로 응답 시간을 1시간 미만으로 단축할 수 있었고, 더불어 예기치 않은 문제의 단서 발견의 계기가 되었습니다. 데이터 손실이나 위험 없이 서비스를 복구하는 방법을 모색하던 Tailscale은 트랜잭션 로그 파이프라인을 구축하고, 데이터베이스를 변경하는 모든 SQL 문을 로그 파일에 스트리밍하는 것을 목표로 했습니다. 최신 정상적인 백업에 대해 이러한 트랜잭션을 재현하면 데이터베이스가 최신 상태로 복원되고, 데이터베이스 손상이 안전하게 회피될 수 있다는 목표였습니다.
트랜잭션 로그 파이프라인 도입은 기대했던 대로 원활하게 작동할 뿐만 아니라, 문제 발생의 단서도 얻을 수 있었습니다. 2건의 인시던트에서 트랜잭션 로그 재현에 실패했으며, 자세히 조사한 결과, 특정 트랜잭션에 의해 기록되고 커밋된 데이터가 이후 트랜잭션에는 표시되지 않는다는 사실이 밝혀졌습니다. 명확해지는 상황을 바탕으로 SQLite의 체크포인트 프로세스 어딘가에 문제가 있을 가능성이 제기되었습니다. SQLite 데이터베이스는 일련의 “페이지”, 즉 정보의 작은 블록으로 구성되어 있습니다. 데이터베이스를 업데이트할 때는 페이지 일부를 수정된 정보가 포함된 것으로 대체해야 합니다. 성능과 동시성을 향상시키기 위해 SQLite는 “Write-Ahead Logging(WAL)”을 활성화하여 실행됩니다. 즉, 새로운 페이지는 데이터베이스 파일에 직접 쓰여지지 않고, 일단 WAL에 먼저 기록됩니다.
새로운 페이지를 WAL에 무기한으로 기록할 수 없기 때문에, 어느 시점에서 메인 데이터베이스 파일에 복사하여 되돌려야 합니다. 이 프로세스가 “체크포인트”입니다.
일반적으로 엔드 사용자 또는 개발자가 명시적으로 체크포인트 프로세스를 실행할 필요 없이 SQLite가 자동으로 체크포인트를 실행하도록 구성되어 있습니다. 그러나 컨트롤 플레인에서는 고속 및 일관성 있는 백업을 위해 체크포인트 프로세스를 수동으로 제어하는 비표준적인 접근 방식을 취하고 있습니다. 하나의 단서로, 데이터베이스 손상 시 SQLite가 실제로 사용 가능한 페이지보다 더 많은 페이지를 WAL(Write-Ahead Log)에서 복사한다는 지표가 나타났습니다. SQLite는 여러 레이어로 구성되어 있습니다. 최상위 레이어는 파서 및 코드 생성기로, SQL 문을 SQLite의 내부 데이터 구조로 변환합니다. 이러한 데이터 구조는 페이지어에 전달되어 페이지어에 의해 디스크에 기록되는 개별 페이지로 분할됩니다. 실제 디스크에 대한 쓰기는 OS 인터페이스, 즉 “가상 파일 시스템”에 의해 처리됩니다. 이 접근 방식 덕분에 다양한 레이어를 다양한 구현으로 대체하거나 기존 레이어를 래핑하여 더 많은 정보를 얻을 수 있습니다.
문제 진단을 지원하기 위해 SQLite 개발자는 추가적인 추적 정보와 데이터베이스 변경에 대한 로그를 기록하는 가상 파일 시스템 래퍼를 만들었습니다. 이 래퍼에는 SQLite의 가상 파일 시스템(VFS) 계층을 확장 및 래핑하는 디버깅/진단용 VFS 시뮬레이션 “tmstmpvfs shim”이 포함되어 있습니다.
래퍼를 본번 환경에 배포한 직후에 발생한 데이터베이스 손상으로 tmstmpvfs shim이 남긴 추가 로그로 인해, 마침내 SQLite 개발팀은 버그를 발견하고 수정할 수 있었습니다. 버그의 원인은 체크포인트와 쓰기 트랜잭션 사이에서 드물게 발생하던 데이터 경쟁이었으며, 체크포인트 처리 중에 쓰기 트랜잭션이 발생할 경우, 체크포인트 프로세스가 혼란을 겪고 데이터가 손실되는 것이었습니다. SQLite 개발자는 발견된 버그를 “WAL-Reset 버그”라고 명명하고, 최소 16년 동안 SQLite에 잠재해 있었다고 추정했습니다. 드물게 발생하는 현상임에도 불구하고 Tailscale이 잦은 버그 발생에 직면했던 이유는 Tailscale이 체크포인트 생성 프로세스를 수동으로 제어하고 있었으며, 또한 매우 적극적으로 체크포인트를 생성했기 때문이었습니다. SQLite 개발자는 WAL-Reset 버그를 수정하여 SQLite 3.52.0을 출시했지만, 또 다른 문제가 발생하여 3.52.0을 철회하고 WAL-Reset 버그 수정만 포함하는 3.51.3을 재공개했으며, 최종적으로 모든 문제가 해결된 SQLite 3.53.0이 공개되었습니다. 수정의 적용 후, 마지막 작업으로 버그의 요인이었던 “쓰기 트랜잭션과 WAL 리셋의 충돌”이 발생해도 데이터베이스가 손상되지 않는지 확인해야 했습니다. 2개월이 경과한 시점, 마침내 “기다왔던” 경고가 발생하고, 드디어 데이터베이스 손상이 발생하지 않게 된 것을 확인했습니다.
최종적으로 Tailscale은 WAL-Reset 버그 수정이 유효함을 확인한 후 4개월 동안 데이터베이스 인시던트 없이 운영을 지속하고 있다고 합니다. WAL-Reset 버그와 관련된 경험은 “평범한 기술”을 비표준적인 방식으로 실행할 위험을 부각시키고, 표준적인 구성과 경로의 신뢰성을 재인식하게 했습니다. 인시던트 해결은 Tailscale과 SQLite 개발자 양측 모두에게 대규모의 공동 작업이었으며, SQLite의 오랜 버그 수정과 Tailscale의 백업 및 복구 프로세스 개선으로 이어졌다고 결론짓고 있습니다.
원문 보기 | 출처: Gigazine