테일스케일, 16년 묵은 SQLite 데이터베이스 버그 잡았다...6개월간 19차례 데이터 손상 겪은 끝에 'WAL 리셋' 경쟁 조건 규명, SQLite 개발진과 수정판 배포
테일스케일이 반년간 서비스를 괴롭힌 SQLite 버그의 원인을 밝혀냈다.
[한국정보기술신문] 기업용 가상사설망(VPN) 서비스 업체 테일스케일(Tailscale)이 지난해 하반기부터 반년 가까이 자사 서비스를 불안정하게 만든 원인을 널리 쓰이는 데이터베이스 소프트웨어 SQLite(에스큐라이트)의 오랜 버그에서 찾아냈다. 테일스케일은 8월 12일(현지시간) 자사 블로그에 이 과정을 정리한 글을 올려, 문제의 버그가 SQLite에 최소 16년 동안 숨어 있던 것이며 SQLite 개발진과 함께 원인을 규명하고 수정했다고 밝혔다. 글은 테일스케일 소속 알렉스 챈(Alex Chan)이 작성했다.
VPN은 여러 기기를 하나의 사설 네트워크로 안전하게 묶어 주는 기술이다. 테일스케일은 기기들끼리 직접 암호화된 연결을 맺도록 돕는 서비스로, 어느 기기가 어디에 있고 누구와 연결될 수 있는지 같은 설정 정보를 관리하는 중앙 시스템을 갖추고 있다. 회사는 이 시스템을 '컨트롤 플레인(control plane)'이라고 부른다. 이번에 문제가 생긴 곳이 바로 이 컨트롤 플레인의 데이터베이스였다.

설정 정보 저장에 SQLite 사용...한 서버가 단독으로 접근하는 구조
테일스케일의 컨트롤 플레인은 이용자에게는 하나의 창구로 보이지만, 내부적으로는 '샤드(shard)'라 불리는 여러 대의 조정 서버로 나뉘어 있다. 샤드는 부하를 나누기 위해 데이터베이스와 서버를 여러 덩어리로 쪼갠 것을 말한다. 각 샤드는 자체 SQLite 데이터베이스를 하나씩 두고, 그 데이터베이스에는 한 개의 프로그램만 단독으로 접근한다. 이는 SQLite가 권장하는 이른바 '단일 기록자' 방식으로, 여러 프로그램이 동시에 데이터를 고치지 못하게 해 충돌을 막는 설계다.
SQLite는 별도의 서버 없이 파일 하나로 작동하는 가볍고 안정적인 데이터베이스다. 테일스케일은 2022년부터 이를 주 데이터베이스로 써 왔다. 회사는 SQLite를 '검증된 지루한 기술'이라고 표현했다. 오랜 기간 널리 쓰여 믿을 수 있고, 큰 규모에서도 문제없이 돌아가는 성숙한 기술이라는 뜻이다.
백업은 몇 분마다 데이터베이스 전체를 통째로 복사해 아마존의 클라우드 저장소인 S3에 올리는 방식으로 이뤄졌다. 2023년 초부터 이 구조를 별 탈 없이 운영해 왔다는 것이 회사 설명이다.
6개월간 19차례 데이터 손상...재현조차 안 되는 유령 버그
이상 징후는 지난해 8월 처음 나타났다. 백업을 읽어 들이던 프로그램이 한 데이터베이스에서 오류를 보고했고, 무결성 검사를 돌려 보니 실제로 데이터가 손상돼 있었다. 무결성 검사는 데이터베이스 내부가 앞뒤가 맞게 저장돼 있는지 점검하는 기능이다. 데이터 손상은 정상적인 운영에서는 좀처럼 겪기 어려운 일이다. 회사는 손상된 데이터베이스를 복구하고 원인을 조사했지만 이유를 찾지 못했다.
같은 일은 그 뒤로 되풀이됐다. 테일스케일은 근본 원인을 잡기까지 6개월 동안 모두 19차례의 데이터 손상을 겪었다고 밝혔다. 다만 손상된 데이터는 이용자의 암호화 키나 실제 통신 내용이 아니라, 기기와 네트워크 구성에 관한 설정 정보뿐이었다. 초기 사고에서는 새로 추가한 기기 몇 대나 일부 설정이 저장되지 않아 다시 입력해야 하는 정도의 피해가 났다.
문제는 손상이 생길 때마다 해당 샤드의 컨트롤 플레인을 멈추고 데이터베이스를 고쳐야 했다는 점이다. 그 시간 동안 해당 샤드에 속한 이용자들은 새 기기를 연결하거나 네트워크 변경 사항을 받아 볼 수 없었다. 초기에는 이 복구에 한 시간 넘게 걸리기도 했다. 대다수 샤드는 아무 영향을 받지 않았지만, 회사가 사고를 상태 페이지에 공개하면서 무관한 이용자들까지 장애를 접하게 됐고 이는 신뢰 저하로 이어졌다.
버그를 잡기는 쉽지 않았다. 최근 고친 코드 가운데 원인이 될 만한 것이 없었고, 사고들 사이에 공통점도 찾을 수 없었다. 특정 샤드나 고객, 기능, 시간대, 부하와도 무관했다. 발생 조건을 알 수 없으니 개발 환경에서 인위적으로 재현하는 것도 불가능했다. 결국 실제 서비스 환경에 진단 장치를 심어 두고 손상이 일어나는 순간을 붙잡는 수밖에 없었다. 사고 간격도 일정하지 않아, 몇 시간 만에 다시 터지는가 하면 몇 주씩 잠잠하기도 했다. 10월부터 12월까지 6주간 사고가 없어 안심하던 차에 다시 손상이 발생하기도 했다.
테일스케일은 SQLite 개발진과 유료 기술 지원 계약을 맺고 함께 원인을 좁혀 나갔다. 양측은 여러 가설을 세우고 사고가 날 때마다 진단 자료를 모아 하나씩 배제했다.
트랜잭션 기록에서 잡힌 단서...흔적 없이 사라진 쓰기 작업
원인 조사와 별개로 회사는 복구 시간을 한 시간 아래로 줄이는 자동화 조치를 취했다. 손상이 감지되면 서버를 즉시 멈추게 하고, 백업을 상시 점검하는 감시 장치를 두는 방식이었다.
이 과정에서 회사는 트랜잭션 기록 장치를 새로 만들었다. 트랜잭션은 데이터베이스를 바꾸는 하나의 작업 단위를 말한다. 회사는 데이터베이스를 변경하는 모든 명령을 별도 파일에 차례로 기록해 두었다. SQLite는 한 번에 한 곳만 데이터를 고치는 단일 기록자 구조라, 이 기록을 순서대로 다시 실행하면 마지막으로 정상이던 백업을 최신 상태로 되돌릴 수 있었다.
그런데 이 장치가 뜻밖의 단서를 내놓았다. 두 건의 사고에서 기록을 다시 실행하는 작업이 매끄럽게 되지 않았던 것이다. 자세히 들여다보니, 한 트랜잭션이 분명히 쓰고 확정한 데이터가 뒤이은 트랜잭션에서는 보이지 않았다. 오류 하나 없이 쓰기 작업이 흔적도 없이 사라진 셈이었다. 원래대로라면 있을 수 없는 일이었다.
미리 쓰기 로그와 체크포인트, 그리고 '데이터 경쟁'
SQLite 개발진은 문제가 '체크포인트' 과정에 있다고 의심하고 있었다. 이를 이해하려면 SQLite가 데이터를 저장하는 방식을 알아야 한다.
SQLite 데이터베이스는 '페이지'라 불리는 작은 조각들의 묶음이다. 데이터를 고치면 관련 페이지를 새 내용으로 바꿔 써야 한다. 테일스케일은 성능을 높이기 위해 '미리 쓰기 로그(WAL·Write-Ahead Logging)' 방식을 쓰고 있었다. 바뀐 페이지를 본체 파일에 곧바로 쓰지 않고, WAL 파일이라는 별도 임시 파일에 먼저 적어 두는 방식이다. 이렇게 쌓인 페이지를 나중에 본체 데이터베이스 파일로 옮겨 적는 작업을 '체크포인트'라고 부른다.
보통은 SQLite가 알아서 체크포인트를 수행하지만, 테일스케일은 빠르고 일관된 백업을 위해 이 과정을 직접 제어하고 있었다. 게다가 매우 자주 실행했다. 이 비표준 방식이 조사가 진행될수록 유력한 의심 지점으로 떠올랐다. 실제로 손상이 생길 때면 WAL 파일에 있는 것보다 더 많은 페이지를 옮겨 적었다는 통계가 잡히기도 했다. 있지도 않은 페이지를 복사했다는 것은 분명한 이상 신호였다.
원인을 눈으로 확인하기 위해, SQLite 개발진은 데이터가 실제로 디스크에 쓰이는 가장 아래 단계를 감싸 추가 기록을 남기는 진단 도구를 새로 만들었다. 이 도구는 SQLite 공개 저장소에도 올라갔다. 테일스케일이 이를 서비스 환경에 심고 다음 손상을 기다리자, 오래지 않아 사고가 터졌다.
16년 숨어 있던 'WAL 리셋 버그'
새 진단 도구가 남긴 기록 덕분에 SQLite 개발진은 마침내 버그를 찾아 고쳤다. 원인은 체크포인트와 쓰기 작업 사이에서 드물게 일어나는 '데이터 경쟁(data race)'이었다. 데이터 경쟁은 두 작업이 같은 자료를 거의 동시에 건드릴 때 순서가 꼬여 예상치 못한 결과가 나오는 현상을 말한다.
구체적으로는, 체크포인트가 진행되는 특정 순간에 쓰기 작업이 끼어들면 체크포인트가 혼란에 빠졌다. WAL 파일의 일부 페이지를 본체로 옮겼다고 착각하지만 실제로는 옮기지 않아, 그 데이터가 영영 본체에 기록되지 못하고 사라졌다. 그러면서 그 사라진 페이지를 참조하는 다른 페이지는 정상적으로 기록돼, 앞뒤가 맞지 않는 손상 상태가 만들어졌다.
SQLite 개발진은 이를 'WAL 리셋 버그'라 이름 붙였다. 이 버그는 SQLite에 최소 16년간 존재해 온 것으로 추정된다. 워낙 드물게 발생해, 개발진이 시험 환경에서 일부러 이 상황을 만들어 내는 코드를 넣어야 할 정도였다. 수정판은 체크포인트 과정에 검사 절차를 하나 더 넣어, WAL이 다른 작업에 의해 초기화됐는지를 확인하도록 했다.
개발진은 이 버그가 테일스케일이 겪은 모든 이상 현상을 설명한다고 확인했다. 데이터 손상은 물론, 기록이 매끄럽게 다시 실행되지 않은 점, 페이지 통계가 어긋난 점이 모두 여기서 비롯됐다. 테일스케일이 다른 이용자보다 이 버그를 더 자주 만난 이유도 드러났다. 체크포인트를 직접, 그것도 매우 자주 실행한 탓에 아무리 드문 조건이라도 언젠가는 걸릴 수밖에 없었다는 것이다. 수정은 SQLite 3.52.0 버전으로 공개됐다.
수정판 배포 뒤 벌어진 또 한 번의 소동
테일스케일은 수정판을 일부 서버에 먼저 적용해 문제가 없는지 확인한 뒤 전체로 확대했다. 그런데 백업 감시 장치가 갑자기 13개 데이터베이스에서 손상을 보고했다. 확인해 보니 이는 진짜 손상이 아니라 수정판에 들어간 또 다른 문제 때문이었다.
원인은 '오래된 계산식 인덱스'였다. 인덱스는 데이터를 빨리 찾기 위한 색인이다. 계산으로 얻은 값에 인덱스를 걸어 두었는데 그 계산 방식이 바뀌면, 색인에 남은 값과 실제 값이 어긋나 손상으로 잘못 보고된다. 테일스케일은 정밀한 시각 정보를 문자로 저장한 뒤 소수 형태의 숫자로 바꿔 쓰고 있었는데, 3.52.0에 함께 들어간 최적화가 이 변환 과정의 반올림 방식을 미세하게 바꾼 것이 화근이었다. 먼저 적용한 서버들에는 해당 조건에 걸리는 시각 값이 없어 사전 점검에서 걸러지지 않았다.
이 때문에 SQLite 개발진은 3.52.0을 거둬들이고, WAL 리셋 버그 수정만 담은 3.51.3을 새로 내놓았다. 테일스케일은 시각 정보의 정밀도를 정수 단위로 낮춰 문제를 피했다. 개발진은 이어 인덱스가 스스로 어긋남을 바로잡는 기능을 3.53.0에 새로 넣었다.
'파티 모드' 경고로 확인한 실제 발생
수정판을 모든 서버에 적용한 뒤에도 테일스케일은 승리를 선언하지 않았다. 앞서 6주간 조용했다가 다시 손상이 터진 경험이 있었기 때문이다. 회사는 데이터 경쟁이 실제로 서비스에서 일어나고 있었는지를 눈으로 확인하고 싶었다.
이를 위해 회사는 쓰기 작업과 WAL 초기화가 겹치는 순간에 경고를 남기도록 자사 SQLite 구동부를 고쳤다. 경고가 뜨는데도 데이터가 멀쩡하다면, 수정 덕분에 손상을 막았다는 증거가 되는 셈이다. 경고를 심어 두고 기다렸지만 두 달 가까이 아무 반응이 없어, 회사는 경고가 고장 났는지 혹은 가설이 틀렸는지 의심하기 시작했다.
그러다 마침내 한 서버에서 기다리던 경고가 울렸다. 손상을 시도했으나 시스템이 막아 냈다는 내용이었다. 이 경고는 WAL 리셋 버그의 발생 조건이 실제 서비스 환경에서 일어난다는 사실을 입증했고, 반년간 이어진 불안정의 범인이 이 버그였음을 뒷받침했다. 회사는 이 경고가 울린 뒤 기사 작성 시점까지 넉 달 동안 데이터베이스 사고를 한 건도 겪지 않았다고 밝혔다.
검증된 기술도 비표준 사용은 위험
테일스케일은 이번 일에서 얻은 교훈으로, 검증된 기술이라도 표준에서 벗어난 방식으로 쓰면 위험이 따른다는 점을 꼽았다. 대다수 이용자는 SQLite를 표준 설정으로 쓰기 때문에 이런 문제를 겪지 않는다. 테일스케일이 쓴 방식도 모두 공식적으로 지원되는 구성이었지만, 체크포인트를 직접 제어하며 자체 속도로 매우 자주 실행하면서 잘 다져진 길에서 벗어났다는 것이다.
회사는 반년에 걸친 이번 조사가 고객과 직원 모두에게 큰 부담이었다며, 반복된 장애로 신뢰가 흔들린 데 대해 사과했다. 다만 오래 묵은 SQLite 버그를 바로잡았고, 원인을 찾는 과정에서 발견한 여러 부수적 문제도 함께 고쳤으며, 진단에 쓰인 공개용 도구 개발을 후원해 앞으로 비슷한 버그를 잡는 데 도움이 되도록 했다는 점에서 이전보다 나은 위치에 서게 됐다고 덧붙였다.
한국정보기술신문 정보기술분과 양한결 기자 news@kitpa.org











