Tailscale Truy Ra Lỗi SQLite 16 Năm Tuổi Gây Hỏng Database

Cuối năm ngoái, uptime của Tailscale tụt dốc. Thủ phạm không nằm ở hạ tầng, mà là một lỗi 16 năm tuổi ẩn sâu trong SQLite — thứ mà đội ngũ chỉ truy ra được sau 6 tháng điều tra và 19 lần database hỏng.
Ngày 12/8/2026, Tailscale công bố bài postmortem chi tiết mang tên "How we tracked down a 16-year-old SQLite bug". Đây không phải câu chuyện về một startup chạy SQLite sai cách. Ngược lại: Tailscale dùng SQLite đúng theo mô hình single-writer mà tài liệu khuyến nghị, vậy mà vẫn vấp phải một lỗi hiếm đến mức chính các tác giả SQLite phải thừa nhận nó đã tồn tại ít nhất 16 năm.
Kiến Trúc Database Của Tailscale
Control plane của Tailscale được chia thành nhiều coordination server, gọi là "shard". Mỗi tailnet nằm trên một shard tại một thời điểm, và mỗi shard có một database SQLite riêng, do đúng một tiến trình Go truy cập độc quyền. Đây chính xác là cách SQLite được thiết kế để chạy: một writer duy nhất.
Tailscale chọn SQLite từ năm 2022 vì nó "nhàm chán" theo nghĩa tích cực — ổn định, phổ biến, được kiểm chứng. Pipeline backup của họ chụp snapshot toàn bộ file database mỗi vài phút rồi đẩy lên S3, và hệ thống này chạy trơn tru từ đầu 2023 cho đến tháng 8 năm ngoái.
Tháng 8/2025, một pipeline đọc backup báo lỗi. Lệnh PRAGMA integrity_check xác nhận database đã hỏng. Họ sửa, điều tra, không tìm ra nguyên nhân — rồi chuyện đó lặp lại. Tổng cộng 19 lần database hỏng trong vòng 6 tháng.
Điểm đáng chú ý: phần dữ liệu hỏng chỉ là metadata về tailnet và thiết bị, không bao giờ chứa private key hay lưu lượng mạng. Nhưng mỗi lần hỏng, toàn bộ control plane của shard đó phải dừng để sửa, nghĩa là tailnet trên shard bị mất kết nối cho thiết bị mới và mất quyền truy cập admin console. Thời gian downtime ban đầu hơn một giờ.
Cuộc Truy Lùng Không Hồi Kết
Điều khiến lỗi này khó bắt là nó không có pattern. Không liên quan tới một shard, một khách hàng, một tính năng, hay một mức tải cụ thể. Thỉnh thoảng các sự cố cách nhau vài giờ, thỉnh thoảng vài tuần — thậm chí có một khoảng lặng 6 tuần từ tháng 10 đến tháng 12 năm 2025.
Không thể tái hiện lỗi trong môi trường test, Tailscale buộc phải gắn telemetry "pháp y" ngay trên production để bắt quả tang. Họ cũng mua hợp đồng hỗ trợ chuyên nghiệp từ chính các tác giả SQLite — một quyết định mà về sau họ đánh giá là rất đáng giá.
Hai bên cùng liệt kê các giả thuyết: POSIX lock bị hủy khi close(), quản lý bộ nhớ sai, hay dùng SQLite đa luồng khi tắt thread safety. Từng giả thuyết bị loại dần qua mỗi sự cố.
Manh Mối: Giao Dịch Biến Mất Không Dấu Vết
Trong lúc truy lỗi, Tailscale vẫn phải giữ dịch vụ sống. Họ xây một pipeline ghi lại mọi câu lệnh SQL thay đổi dữ liệu vào file log riêng. Vì SQLite là single-writer với transaction tuần tự, lịch sử giao dịch hoàn toàn tuyến tính và có thể replay để khôi phục database từ backup tốt nhất.
Pipeline này hoạt động — nhưng lại lộ ra manh mối. Trong hai sự cố, transaction log không replay sạch: dữ liệu được ghi và commit bởi một transaction lại "vô hình" với transaction sau đó. Một write biến mất mà không hề báo lỗi. Về lý thuyết, điều đó là bất khả thi.
Nghi ngờ dồn về cơ chế checkpoint. Khi dùng WAL (Write-Ahead Logging), trang dữ liệu mới không ghi thẳng vào file database mà ghi vào file WAL trước, rồi mới được chép ngược về file chính trong quá trình gọi là checkpoint. Tailscale chủ động điều khiển checkpoint để backup nhanh và nhất quán — một cách dùng không chuẩn so với đa số người dùng.
Một chỉ số bất thường lộ ra trong lúc hỏng: SQLite báo đã chép nhiều trang từ WAL hơn số trang thực sự tồn tại. Nếu WAL có 10 trang mà 20 trang được chép, thì rõ ràng có gì đó sai.
Lỗi WAL-Reset
Các tác giả SQLite viết một công cụ debug mới — tmstmpvfs, một shim bọc quanh lớp virtual filesystem để ghi thêm trace. Tailscale tài trợ công cụ này, triển khai lên production và chờ. Sự cố tiếp theo đến sớm hơn mong đợi.
Trace từ tmstmpvfs giúp xác định nguyên nhân: một data race hiếm gặp giữa checkpoint và một write transaction. Cụ thể, nếu một write xảy ra đúng thời điểm trong lúc checkpoint, tiến trình checkpoint "tưởng" một số trang đã được chép về file chính — nhưng thực ra chưa. Những trang đó không bao giờ được ghi, dữ liệu mất vĩnh viễn, và database hỏng vì các trang khác (như index) vẫn tham chiếu đến chúng.
Các tác giả SQLite đặt tên cho nó là "WAL-Reset bug", ước tính đã tồn tại ít nhất 16 năm — từ khi WAL ra đời năm 2010. Nó sống lâu vậy vì quá hiếm: để test được, nhóm SQLite còn phải thêm code cố tình kích hoạt điều kiện gây lỗi. Bản vá bổ sung một phép kiểm tra trong hàm checkpoint để phát hiện WAL bị reset bởi luồng khác.
Tailscale dính lỗi này nặng hơn người khác đúng vì cách họ chạy SQLite: chủ động điều khiển checkpoint và checkpoint rất quyết liệt, khiến điều kiện hiếm gặp va phải họ sớm muộn gì cũng xảy ra.
Bản Vá Và Một Cú Báo Động Giả
Bản vá được phát hành trong SQLite 3.52.0. Tailscale triển khai từ từ — vài shard canary trước — rồi mở rộng toàn bộ. Monitor backup lập tức đỏ rực, báo hỏng ở 13 database. Nhưng đây là báo động giả: các database không hỏng thật, mà vướng một lỗi thứ hai liên quan đến expression index bị stale.
Nguyên nhân nằm ở chỗ Tailscale lưu timestamp độ chính xác cao dưới dạng text, rồi chuyển thành số thực trong một cột VIRTUAL generated. Bản 3.52.0 vừa sửa data race vừa đổi cách làm tròn phép chuyển text sang số thực, khiến index chứa giá trị lệch và bị integrity_check báo hỏng. Các shard canary không có timestamp dính phải kiểu làm tròn đó, nên lỗi lọt qua đợt rollout.
SQLite rút bản 3.52.0, thay bằng 3.51.3 chỉ chứa đúng bản vá WAL-Reset. Về sau, 3.53.0 bổ sung tính năng tự phục hồi index để chấm dứt kiểu lỗi expression index này.
Tailscale vẫn chưa dám tuyên bố thắng lợi. Một khoảng lặng sự cố không đồng nghĩa đã hết — họ từng có 6 tuần yên ắng giả tạo. Họ patch driver để ghi cảnh báo mỗi khi checkpoint và WAL-reset chồng lấn nhau, rồi chờ. Hai tháng sau, cảnh báo đầu tiên vang lên — bằng chứng điều kiện gây lỗi thực sự tồn tại trong production, và bản vá đã cứu họ khỏi thêm một lần hỏng. Tính đến lúc viết, Tailscale chạy thêm 4 tháng sạch bóng sự cố database.
Điều Cần Biết
- Lỗi là thật, và nó có thể đang nằm trong code của bạn. WAL-Reset bug tồn tại trong SQLite ít nhất 16 năm; nếu bạn dùng WAL kèm checkpoint thủ công, hãy nâng cấp lên ít nhất 3.51.3.
- "Công nghệ nhàm chán" chỉ an toàn khi bạn đi đúng lối mòn. Tailscale dùng SQLite đúng chuẩn, trừ một điểm: chủ động checkpoint. Chính sự lệch khỏi cấu hình mặc định đã đẩy họ vào vùng chưa ai test kỹ.
- Replay log là công cụ debug mạnh hơn cả việc sửa lỗi. Pipeline ghi transaction của Tailscale được xây để phục hồi dữ liệu, nhưng chính nó mới lộ ra manh mối quyết định.
- Trả tiền cho chuyên gia là đầu tư đúng. Hợp đồng hỗ trợ với nhóm SQLite, cộng việc tài trợ công cụ
tmstmpvfs, rút ngắn đáng kể thời gian điều tra một lỗi thuộc về lõi của hệ quản trị.
Bài học lớn nhất ở đây không phải về SQLite, mà về việc vận hành hệ thống. Mọi công nghệ "ổn định" đều có góc chết, và góc chết chỉ lộ ra khi có ai đó đủ tải và đủ quy mô để va phải nó. Tailscale trả giá bằng 6 tháng uptime chao đảo — nhưng đổi lại, cả cộng đồng SQLite được lợi từ một lỗi đã ngủ yên suốt 16 năm.
Bài viết được hỗ trợ bởi AI (Amy 🌸). Nội dung đã được kiểm duyệt bởi tác giả.
Related Posts
DuckDB 2.0 preview: từ thư viện nhúng thành server thực thụ
Bản preview DuckDB 2.0 lần đầu biến DuckDB thành server, kèm kiểu VARIANT, trigger và truy vấn đệ quy nhanh gấp 40 lần.
GitHub Actions Sập 6 Giờ: Sự Cố Lớn Thứ Hai Và Hồi Chuông Cảnh Tỉnh
Ngày 6/8/2026, GitHub Actions trải qua sự cố downtime lớn thứ hai trong lịch sử. Webhook chỉ hoạt động 15%, 65% job thất bại — và đây là hồi chuông cảnh tỉnh.
Zoom-in: Connection Pool
Ứng dụng đột ngột chậm hoặc sập khi traffic tăng cao, log xuất hiện lỗi 'Too many connections'. Tại sao việc tạo kết nối mới liên tục lại nguy hại đến vậy?