SQLite trên VPS có phù hợp cho production không?
Biết khi nào SQLite phù hợp trên một VPS, cách bật WAL, busy_timeout, backup bằng Litestream và các giới hạn khiến bạn phải chuyển sang database server.
Khi SQLite là cơ sở dữ liệu production phù hợp trên VPS
Chạy SQLite trong production trên VPS là lựa chọn phù hợp cho hầu hết ứng dụng nhỏ. Lý do rất đơn giản: một process trên một máy ghi vào một file không cần database server. Bạn không cần giám sát daemon, mở firewall cho một port, xoay vòng password hoặc duy trì một máy thứ hai. Query là một lần gọi hàm thay vì một network round trip. Vì vậy, một trang chạy 40 query sẽ tốn 40 lần gọi hàm.
Giới hạn này rõ ràng và có thật. SQLite chỉ cho phép một writer tại một thời điểm trên toàn bộ database file. File cũng không thể được chia sẻ giữa hai máy. Cả hai giới hạn đều không gây vấn đề khi một VPS chạy một ứng dụng duy nhất. Nhưng chúng trở thành giới hạn nghiêm trọng ngay khi mô hình đó không còn phù hợp. Hướng dẫn này trình bày các thiết lập giúp SQLite an toàn trên server, cách backup liên tục bằng Litestream và thời điểm bạn nên dừng sử dụng SQLite.
Trước tiên, hãy cài command line tool. Tất cả các lệnh bên dưới được chạy trên Ubuntu 24.04.
sudo apt update
sudo apt install -y sqlite3
sqlite3 --versionLệnh này in ra một version bắt đầu bằng 3., theo sau là ngày build và source hash. Tính đến tháng 7 năm 2026, Ubuntu 24.04 cung cấp SQLite 3.45.1. Ứng dụng của bạn có thể không dùng binary này: hầu hết language runtime đóng gói bản SQLite library riêng, thường là bản mới hơn. Vì vậy, hãy kiểm tra version mà database driver báo cáo trước khi dựa vào một feature mới.
Vì sao WAL mode là thứ đầu tiên bạn thay đổi
Theo mặc định, SQLite sử dụng rollback journal. Trước khi thay đổi một page, nó sao chép page gốc vào file -journal, rồi chỉnh sửa database tại chỗ. Để thực hiện an toàn, SQLite khóa độc quyền toàn bộ file. Vì vậy, mọi reader phải chờ trong khi có bất kỳ thao tác ghi nào đang chạy. Trên laptop, bạn thường không nhận thấy điều này. Trên web server, một thao tác ghi chậm sẽ làm chậm mọi request truy cập database.
WAL (write-ahead log) mode đảo ngược thứ tự này. Writer nối các page mới vào một file -wal riêng và không thay đổi database chính. Reader tiếp tục đọc file chính tại snapshot mà nó đã bắt đầu, nên reader không chặn writer và writer không chặn reader. Sau đó, một checkpoint sao chép các page WAL đã tích lũy trở lại database chính. Thay đổi này là yếu tố chính giúp SQLite hoạt động tốt phía sau một web application.
Bật chế độ WAL và xác nhận chế độ đã được lưu
mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"Lệnh in ra wal. Kết quả này không phải thông tin thừa. PRAGMA journal_mode trả về chế độ thực tế của database, nên nếu nhận được delete thì thay đổi đã thất bại và database vẫn đang dùng rollback journal.
Chế độ WAL được lưu liên tục. Đây là một flag trong database header, không phải thiết lập của connection. Vì vậy, bạn chỉ cần chạy lệnh này một lần cho mỗi database file. Mọi connection sau đó sẽ tự kế thừa chế độ này, kể cả sau khi reboot. Hãy xác nhận điều đó bằng một connection mới.
sqlite3 ~/app/app.db "PRAGMA journal_mode;"Bây giờ hãy tạo một table và kiểm tra các file xuất hiện trên disk.
sqlite3 ~/app/app.db <<'SQL'
CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
INSERT INTO notes (body) VALUES ('first row');
SQL
ls -l ~/app/Bây giờ có 3 file: app.db, app.db-wal và app.db-shm. File -wal chứa các page đã commit nhưng chưa được checkpoint. File -shm là shared memory index được mọi connection map vào để chúng cùng biết WAL đang chứa gì. Cả hai file đều thuộc về database và không phải scratch file. Nếu chỉ copy app.db trong khi application đang chạy, bạn sẽ nhận được một file bị thiếu mọi commit gần đây. Nếu xóa app.db nhưng giữ nguyên 2 file còn lại, SQLite sẽ áp dụng các WAL page cũ đó vào bất kỳ file mới nào xuất hiện với tên đó. Đây là cách người dùng làm hỏng một database mới khi cố reset database cũ.
Các thiết lập kết nối mà mọi ứng dụng production cần
Chỉ journal_mode được lưu trong database. Mọi thiết lập khác bên dưới áp dụng cho từng connection. Điều đó có nghĩa là ứng dụng phải chạy thiết lập đó trên mỗi connection mà ứng dụng mở, bao gồm cả mọi connection do pool tạo ở chế độ nền.
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;busy_timeout = 5000 yêu cầu SQLite tiếp tục thử khi database bị khóa trong tối đa 5000 milliseconds trước khi trả về database is locked. Giá trị mặc định là 0. Vì vậy, theo mặc định, SQLite thất bại ngay lần đầu tiên 2 writer hoạt động chồng lấn. Chỉ cần đặt giá trị này là đã loại bỏ phần lớn lỗi lock thường bị đổ lỗi cho chính SQLite.
synchronous = NORMAL là thiết lập phù hợp trong WAL mode, nhưng cần hiểu rõ sự đánh đổi. Với FULL, SQLite gọi fsync trên WAL sau mỗi commit. Với NORMAL, SQLite chỉ sync tại các checkpoint. Tài liệu SQLite nói rõ điều bạn phải đánh đổi: transaction không còn durable sau khi mất điện hoặc hard reset. Database không bị corrupt do mất điện; bạn chỉ mất các commit cuối cùng chưa được ghi xuống disk. Trên VPS, đây thường là lựa chọn phù hợp vì nó loại bỏ một lần fsync khỏi đường xử lý của từng write.
foreign_keys = ON mặc định bị tắt để tương thích ngược và áp dụng cho từng connection. Một schema chứa đầy các mệnh đề REFERENCES sẽ không thực thi được gì cho đến khi từng connection bật thiết lập này.
Một thiết lập khác chỉ quan trọng ở giai đoạn sau. SQLite tự động checkpoint khi WAL lớn hơn 1000 page. Connection nào vừa hoàn tất transaction tại thời điểm đó sẽ thực hiện công việc này. Bản thân điều đó không có vấn đề. Nhưng khi Litestream đang chạy, đây trở thành một vấn đề vì Litestream cần kiểm soát thời điểm checkpoint diễn ra.
Vì sao database is locked vẫn xảy ra sau khi bạn đặt busy_timeout
Đây là lỗi khiến mọi người quay lại dùng Postgres. Lỗi này có một nguyên nhân cụ thể.
busy timeout cài đặt một busy handler, nhưng SQLite không đảm bảo sẽ gọi handler này.
Nếu SQLite xác định rằng việc gọi busy handler có thể gây deadlock, nó sẽ trả về SQLITE_BUSY cho ứng dụng thay vì gọi busy handler.
Deadlock mà SQLite tránh xảy ra khi transaction được nâng cấp. Một BEGIN đơn lẻ trong SQLite có nghĩa là BEGIN DEFERRED. Nếu câu lệnh đầu tiên sau đó là SELECT, bạn đang ở trong read transaction. Khi một UPDATE tiếp theo trong cùng transaction cần chuyển thành write transaction, và một connection khác đã ghi dữ liệu kể từ khi thao tác đọc của bạn bắt đầu, SQLite không thể buộc bạn chờ. Snapshot của bạn đã cũ và việc chờ chỉ khiến hai connection deadlock lẫn nhau. Tài liệu nêu rõ kết quả:
Các câu lệnh ghi tiếp theo sẽ nâng cấp transaction thành write transaction nếu có thể, hoặc trả về SQLITE_BUSY.
Timeout 5000 millisecond của bạn không bao giờ được kiểm tra. Lỗi xuất hiện ngay lập tức, nên có vẻ như thiết lập không có tác dụng.
Cách sửa chỉ cần một từ.
BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;BEGIN IMMEDIATE giữ write lock ngay từ đầu, trước khi đọc bất kỳ dữ liệu nào. Không có thao tác nâng cấp, nên không có deadlock cần tránh. Vì vậy busy handler có tác dụng và connection sẽ chờ đến lượt thay vì thất bại. Hãy giữ các read-only transaction ở chế độ deferred. Mọi transaction có thao tác ghi phải dùng chế độ immediate.
Nguyên nhân thứ hai gây lỗi lock khó phát hiện hơn: giữ write transaction trong thời gian dài khi thực hiện công việc chậm. SQLite tuần tự hóa các writer. Vì vậy, một transaction mở, gọi external API qua network rồi commit sẽ chặn mọi writer khác trong suốt thời gian gọi đó. Hãy đọc dữ liệu cần thiết, đóng transaction, thực hiện công việc chậm, sau đó mở một write transaction ngắn để lưu kết quả.
Sao lưu liên tục với Litestream
Bản sao chạy hằng đêm có thể làm mất tối đa một ngày dữ liệu ghi mới. Chạy cp trên cơ sở dữ liệu SQLite đang hoạt động cũng có thể tạo ra bản sao không thể mở. Có 2 cách an toàn. sqlite3 app.db ".backup /path/to/backup.db" sử dụng giao diện sao lưu trực tuyến của SQLite nên hoạt động được với cơ sở dữ liệu đang được sử dụng. Litestream đi xa hơn: công cụ này theo dõi WAL và liên tục tải các thay đổi lên object storage. Nhờ đó, mức mất dữ liệu tối đa giảm từ 1 ngày xuống còn khoảng 1 giây.
Litestream là 1 binary Go chạy cùng với ứng dụng của bạn. Công cụ này không nằm giữa ứng dụng và cơ sở dữ liệu. Ứng dụng vẫn ghi vào SQLite như trước. Litestream đọc WAL và tải lên những phần đã thay đổi.
cd /tmp
curl -fsSL -O https://github.com/benbjohnson/litestream/releases/download/v0.5.14/litestream-0.5.14-linux-x86_64.deb
sudo dpkg -i litestream-0.5.14-linux-x86_64.deb
litestream versionv0.5.14 là release được trang cài đặt Linux chính thức ghi nhận vào tháng 7 năm 2026. v0.5.15 được phát hành sau đó vào ngày 21 tháng 7 năm 2026. Thay đổi version trong cả 2 dòng để khớp với tag hiện tại trên trang releases. Nếu VPS của bạn dùng arm64, hãy dùng package arm64 tương ứng.
File cấu hình nằm tại /etc/litestream.yml. Hãy bắt đầu với replica cục bộ. Cách này kiểm tra toàn bộ quy trình mà không cần cloud credentials.
dbs:
- path: /home/appuser/app/app.db
replica:
type: file
path: /var/backups/litestream/appLưu ý rằng trường này là replica, ở dạng số ít. Litestream 0.5 đã thay mảng replicas của dòng 0.3 bằng 1 replica block duy nhất. Vì vậy, config có 2 entry sẽ lỗi khi khởi động. Nhiều hướng dẫn của bên thứ 3 vẫn hiển thị mảng cũ. Hãy dùng đúng cấu trúc ở trên thay vì sao chép ví dụ đầu tiên tìm thấy trong kết quả tìm kiếm. Dòng 0.5 cũng đổi tên subcommand litestream wal thành litestream ltx vì format backup trên disk đã thay đổi.
Kiểm tra config có thể được parse trước khi bật bất kỳ tính năng nào.
sudo litestream databases -config /etc/litestream.ymlSau đó, kiểm tra toàn bộ quy trình theo cách thủ công. Cú pháp này bỏ qua file cấu hình và replicate 1 cơ sở dữ liệu vào 1 path.
mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/appLệnh này chạy ở foreground và tiếp tục chạy. Trong shell thứ 2, hãy ghi 1 row rồi restore replica vào 1 file mới.
sqlite3 ~/app/app.db "INSERT INTO notes (body) VALUES ('written after replication started');"
litestream restore -o /tmp/restored.db file:///tmp/replica/app
sqlite3 /tmp/restored.db "SELECT count(*) FROM notes;"Count bao gồm row mới. Nếu không có, thay đổi vẫn chưa được sync: Litestream thực hiện push theo sync-interval, mặc định là 1 giây. Hãy chờ rồi restore lại. 1 giây đó cũng là recovery point của bạn. Nếu xảy ra crash, tối đa các bản ghi trong khoảng sync gần nhất sẽ bị mất. Không có cấu hình nào có thể giảm mức này xuống 0.
Để dùng storage thực tế, thay replica block bằng URL S3. Cách này hoạt động với Amazon S3 và object storage tương thích S3 của các nhà cung cấp khác.
dbs:
- path: /home/appuser/app/app.db
replica:
url: s3://your-bucket-name/app
region: us-east-1
snapshot:
interval: 24h
retention: 24hKhông lưu credentials trong file đó. Litestream đọc LITESTREAM_ACCESS_KEY_ID và LITESTREAM_SECRET_ACCESS_KEY từ environment. Vì vậy, hãy đặt chúng trong systemd drop-in do root sở hữu và đặt mode là 600.
Các giá trị snapshot ở trên là giá trị mặc định. Giá trị retention thường gây bất ngờ. Retention là khoảng thời gian Litestream giữ các snapshot và những file liên quan. Vì vậy, đây cũng là khoảng thời gian tối đa bạn có thể restore về trước. 24 giờ nghĩa là một migration lỗi mà bạn phát hiện vào sáng thứ 4 đã không thể khôi phục từ trạng thái của thứ 2. Đặt retention: 168h thành 1 tuần và chấp nhận chi phí storage bổ sung.
Kiểm tra quá trình khôi phục trước khi cần đến nó
litestream restore -o /tmp/check.db /home/appuser/app/app.db
sqlite3 /tmp/check.db "PRAGMA integrity_check;"
sqlite3 /tmp/check.db "SELECT count(*) FROM notes;"Với đường dẫn cơ sở dữ liệu, litestream restore tìm replica tương ứng trong /etc/litestream.yml rồi tải replica đó xuống. PRAGMA integrity_check in ok nếu file còn tốt. Bất kỳ đầu ra nào khác đều có nghĩa là bản sao đã khôi phục không thể sử dụng. Hãy chạy lệnh này theo lịch bằng một systemd service và timer, rồi đọc đầu ra. Cho đến khi khôi phục backup một lần, bạn chưa thể biết backup có hoạt động hay không.
Chạy Litestream bằng systemd
Gói Debian cài một unit litestream đọc /etc/litestream.yml.
sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -fOutput bình thường liệt kê từng database trong config, sau đó không ghi thêm gì ngoài các dòng đồng bộ định kỳ. Lỗi no such file or directory đối với đường dẫn database cho biết đường dẫn trong config không đúng hoặc process không thể đọc đường dẫn đó. Theo mặc định, unit chạy dưới quyền root, cao hơn mức cần thiết cho công việc này. Litestream phải có quyền đọc và ghi cả database lẫn thư mục chứa database, vì nó làm việc với các file -wal và -shm nằm cạnh database. Do đó, hãy cấp quyền cho account mà ứng dụng của bạn đang sử dụng.
# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuserÁp dụng bằng sudo systemctl daemon-reload và sudo systemctl restart litestream. Thiết lập một service account riêng với quyền tối thiểu chỉ mất vài phút. Đây là khác biệt giữa một backup agent và một process thứ hai chạy dưới quyền root trên máy.
Một chi tiết về thứ tự rất quan trọng nếu bạn phải dựng lại máy từ đầu. Bạn cần restore database trước khi ứng dụng khởi động. litestream restore chấp nhận -if-db-not-exists; tùy chọn này thoát với mã 0 khi file đã tồn tại, nên có thể chạy an toàn trong mỗi lần boot. Thêm lệnh này vào dòng ExecStartPre trong unit của ứng dụng. Khi đó, một VPS mới sẽ tải database xuống, còn VPS đã có database sẽ không làm gì. litestream replicate có flag -restore-if-db-not-exists tương ứng nếu bạn muốn đặt cấu hình này tại một nơi duy nhất.
SQLite gặp giới hạn ở đâu trên VPS
Hệ thống tệp qua mạng. Đây là giới hạn không thể khắc phục bằng cấu hình. Chế độ WAL yêu cầu mọi process sử dụng database phải chia sẻ một vùng bộ nhớ nhỏ. File -shm cung cấp vùng bộ nhớ đó. Tài liệu SQLite nêu rõ quy tắc này:
Mọi process sử dụng database phải chạy trên cùng một máy chủ; WAL không hoạt động qua hệ thống tệp qua mạng.
Vì vậy, database trên NFS được mount (network file system) hoặc share SMB có thể bị hỏng. Không có pragma nào ngăn được việc này. Có một điểm khác biệt thường bị bỏ qua. Thiết bị block qua mạng, là loại mà hầu hết nhà cung cấp VPS gắn thêm để làm storage, xuất hiện trong Linux như một disk thông thường có filesystem thông thường. Trường hợp này không có vấn đề. File share được mount thì khác.
Một application server thứ hai. Không có setting nào giúp mô hình này hoạt động. Khi cần 2 máy cùng phục vụ một bộ dữ liệu, bạn cần một database giao tiếp qua network. Hãy quyết định chuyển đổi khi vẫn còn thời gian để lập kế hoạch.
Workload có nhiều thao tác ghi. Mỗi lần chỉ một writer được ghi là đặc tính của định dạng file, không phải tùy chọn có thể điều chỉnh. Các thao tác ghi ngắn có chi phí thấp vì mỗi commit được append vào WAL. Do đó, throughput phụ thuộc vào độ trễ ghi nhỏ của disk nhiều hơn CPU. Xem NVMe so với storage SSD SATA trên VPS để biết khác biệt này thể hiện như thế nào. Vấn đề thực sự là các transaction dài, vì chúng khiến mọi writer khác phải xếp hàng phía sau.
Truy vấn phân tích. SQLite là row store được xây dựng cho transaction. Một dashboard quét 100 triệu row là một công việc khác và cần một tool khác. Bài DuckDB so với SQLite khi dùng cho server giải thích ranh giới này.
VACUUM trong quá trình replication. Một VACUUM đầy đủ sẽ ghi lại toàn bộ database file. Vì vậy, Litestream phải upload lại toàn bộ file. Tài liệu Litestream khuyến cáo không chạy thao tác này tại chỗ khi replication đang hoạt động. Hãy stop replicator, chạy vacuum, start lại replicator và chờ một full snapshot mới.
2 replicator trên cùng một database. Không bao giờ chạy 2 process Litestream trên cùng một database hoặc cùng một replica destination. Tài liệu nêu rõ bạn phải tự ngăn tình trạng này. Nếu không, replica sẽ không thể restore.
Litestream không bảo vệ những gì
Litestream chỉ bảo vệ tệp cơ sở dữ liệu. Không bảo vệ phần nào khác. Bạn vẫn phải tự xử lý các tệp đã tải lên, cấu hình ứng dụng, chứng chỉ TLS (bảo mật lớp truyền tải) và các tệp unit. Hãy kết hợp Litestream với bản sao lưu được mã hóa bên ngoài máy bằng restic theo lịch để bao phủ cả hai phần. Nếu máy còn mới, 10 phút đầu tiên trên một VPS mới hướng dẫn thiết lập tài khoản người dùng và firewall mà hướng dẫn này giả định đã hoàn tất.
FAQ
SQLite có đủ tốt cho ứng dụng production không?
Có, nếu bạn chạy một ứng dụng trên một server, bật chế độ WAL, đặt busy timeout và sao lưu liên tục. Các giới hạn quan trọng mang tính cấu trúc: mỗi lần chỉ có một writer và chỉ có một máy host. Ứng dụng nằm trong các giới hạn này sẽ có database không cần network hop và không cần process riêng để monitor. Nếu ứng dụng vượt quá các giới hạn đó, bạn cần database client-server; không mức tuning nào có thể thay đổi điều này.
Tại sao tôi vẫn nhận được database is locked sau khi đặt busy_timeout?
Vì SQLite bỏ qua busy handler khi việc chờ có thể gây deadlock. Transaction bắt đầu bằng BEGIN không có tùy chọn là transaction deferred: SELECT mở đầu sẽ đưa transaction vào trạng thái read transaction, sau đó một thao tác write phải nâng cấp transaction. Nếu một connection khác đã write trong thời gian đó, SQLite trả về SQLITE_BUSY ngay lập tức thay vì gọi busy handler của bạn, vì read snapshot của bạn đã cũ. Hãy bắt đầu mọi transaction sẽ thực hiện write bằng BEGIN IMMEDIATE để lấy write lock ngay từ đầu và áp dụng timeout.
Tôi có thể lưu database SQLite trên network storage không?
Không, nếu đó là network filesystem như NFS hoặc SMB. Chế độ WAL cần mọi process chia sẻ memory thông qua file -shm, và tài liệu SQLite nêu rõ mọi process sử dụng database phải chạy trên cùng một host computer. Network block device do nhà cung cấp gắn vào là trường hợp khác: Linux nhìn thấy một disk bình thường với filesystem bình thường, và SQLite hoạt động trên đó.
Tôi có cần Litestream nếu đã chạy backup hằng đêm không?
Điều này phụ thuộc vào lượng dữ liệu bạn có thể chấp nhận mất. Job chạy hằng đêm có nghĩa là bạn có thể mất tối đa 24 giờ dữ liệu write. Litestream sync khoảng một lần mỗi giây, nên một sự cố thường chỉ làm mất khoảng giây dữ liệu cuối cùng. Litestream cũng an toàn hơn việc sao chép database file bằng cp, vì cách đó có thể ghi lại database đúng lúc đang write. Litestream chỉ bảo vệ database, vì vậy hãy chạy thêm backup file tổng quát bên cạnh nó.