30 điểm bởi darjeeling 12 ngày trước | 4 bình luận | Chia sẻ qua WhatsApp

ultrathink.art là một cửa hàng thương mại điện tử do các AI agent tự vận hành. Từ thiết kế sản phẩm, xử lý đơn hàng cho đến viết blog, mọi thứ đều do AI đảm nhiệm. Bài viết này chia sẻ trải nghiệm vận hành cửa hàng đó bằng SQLite trong môi trường production, bao gồm cả việc xử lý thanh toán Stripe thực tế.


Cấu hình: 4 tệp, 1 volume

Trong môi trường production, hệ thống chạy tổng cộng 4 cơ sở dữ liệu SQLite: primary (đơn hàng, sản phẩm, người dùng), cache (bộ nhớ đệm Rails), queue (background job), cable (Action Cable), và tất cả đều được lưu trong một Docker volume duy nhất.

Rails 8 đã biến SQLite thành lựa chọn hạng nhất, và trên thực tế họ được hưởng các lợi ích như đơn giản hóa triển khai, không cần quản lý connection pool, không cần máy chủ DB riêng.


Cơ chế WAL mode giúp tạo ra khả năng đồng thời

Chế độ journal mặc định của SQLite sẽ khóa toàn bộ DB khi ghi, nên không phù hợp với web app có nhiều yêu cầu đồng thời. Trong WAL (Write-Ahead Logging) mode, thao tác ghi được thêm vào tệp -wal riêng, còn thao tác đọc vẫn tiếp tục dùng tệp chính, nên có thể xử lý đồng thời nhiều lượt đọc và một lượt ghi. Rails 8 bật WAL mode mặc định cho SQLite.


Sự cố: 2 đơn hàng đã biến mất

Ngày 4 tháng 2, trong vòng 2 giờ đã có 11 lần commit được push lên main. Với mỗi lần push, quy trình blue-green deployment của Kamal được chạy, tạo ra một khoảng chồng lấn khi container cũ và container mới cùng lúc mở chung một tệp WAL. Khi 11 lần triển khai chồng lên nhau, đã xảy ra tình huống container A đang draining thì B khởi động, và trước khi B sẵn sàng hoàn toàn thì deployment của C lại bắt đầu.

Đơn hàng số 16 và 17 đã được Stripe thanh toán thành công và tiền cũng đã bị trừ khỏi tài khoản khách hàng, nhưng trong DB không còn bản ghi nào. Kiểm tra bằng sqlite_sequence cho thấy bộ đếm tự tăng đang trỏ tới 17, trong khi số hàng thực tế chỉ có 15.


Giải pháp: hãy làm chậm tốc độ triển khai

Giải pháp không mang tính kỹ thuật mà mang tính quy trình. Họ gộp các thay đổi liên quan để triển khai cùng nhau, và ghi rõ trong tệp governance (CLAUDE.md) mà các AI agent phải tuân theo một quy tắc là tránh push liên tiếp trong thời gian ngắn.

Đây không phải vấn đề của SQLite mà là vấn đề của deployment pipeline. PostgreSQL kết nối qua TCP socket, nên container mới cũng sẽ kết nối tới cùng một DB server và thứ tự ghi được DB engine quản lý. SQLite thì phụ thuộc vào filesystem lock của Docker volume dùng chung, và khi các container chồng lấn lên nhau thì điều đó bị phá vỡ.


sqlite_sequence: tận dụng như một công cụ pháp chứng

Bảng sqlite_sequence là một trong những công cụ debug bị đánh giá thấp nhất của SQLite. Nó nhớ giá trị tự tăng lớn nhất từng được cấp phát trong quá khứ, kể cả với những hàng sau này đã bị xóa. Nếu số hàng hiện tại và giá trị sequence cách xa nhau một cách bất thường, đó là tín hiệu cho thấy đã có thứ gì đó xóa nhầm hàng.


Những cái bẫy không ai nói cho bạn biết

ILIKE, thứ mà các lập trình viên PostgreSQL dùng theo thói quen, sẽ gây lỗi cú pháp trong SQLite. Thay vào đó phải dùng LOWER(name) LIKE. json_extract sẽ trả về số nguyên nếu giá trị được lưu dưới dạng số, và khi so sánh chuỗi thì nó sẽ âm thầm thất bại. kamal app exec tạo một container mới mỗi lần chạy; nếu chạy đồng thời hai lần trên server 2GB RAM thì OOM killer sẽ giết tiến trình web.


Nếu chọn lại, có còn dùng SQLite không?

Có. Với tải ghi ở mức vừa phải trên một máy chủ đơn, SQLite loại bỏ toàn bộ sự phức tạp của hạ tầng. Sao lưu cũng chỉ cần một lệnh sqlite3 .backup là đủ (xử lý an toàn cả WAL mode và ghi đồng thời). Đến ngày cần scale theo chiều ngang hoặc cần khả năng đồng thời multi-writer thực sự, khi đó có thể migrate sang PostgreSQL. Rails giúp việc chuyển đổi đó trở nên đơn giản.


Nguồn gốc: ultrathink.art Blog, 2026.04.03

4 bình luận

 

Dù có vấn đề về độ phức tạp hạ tầng đến đâu đi nữa, thì với những nơi cần rất nhiều ghi đồng thời như sàn thương mại điện tử, chẳng phải ngay từ đầu không dùng sqlite sẽ là lựa chọn tốt hơn sao?
Thậm chí nếu được cấu hình bằng Docker thì độ phức tạp hạ tầng của việc dùng postgresql cũng đâu tăng lên quá nhiều.
Mình có cảm giác là vì làm bằng rails nên hệ sinh thái đã được định hình theo hướng dùng sqlite, rồi từ đó bị dẫn dắt đến kết luận rằng nó là lựa chọn tốt.
Đã xảy ra lỗi nghiêm trọng như bỏ sót đơn hàng, và ngay cả khi có những vấn đề về ghi đồng thời như với pg, thì với một hệ thống như shopping mall nơi nhu cầu ghi đồng thời rất lớn, chẳng phải ngay từ đầu không dùng sqlite sẽ là lựa chọn tốt hơn sao?
Mình cũng nghĩ là vì được làm bằng rails nên hệ sinh thái đã đóng khung theo hướng dùng sqlite, rồi bị dẫn dắt đến suy nghĩ rằng như vậy là tốt.
Đã phát sinh lỗi nghiêm trọng như bỏ sót đơn hàng, và cách giải quyết tận gốc chuyện này là dùng một cơ sở dữ liệu hỗ trợ ghi đồng thời như pg.
Vì về mặt kỹ thuật thích sqlite nên vẫn cố chấp nói sẽ tiếp tục dùng nó, với mình nghe như một phát ngôn làm giảm độ tin cậy của họ với tư cách kỹ sư.
Nó cho mình cảm giác như phiên bản ngược lại của kiểu phát triển driven by CV: dựng k8s khi không cần thiết, cấu hình HPA với replica = 1, rồi biến một monolith đang chạy tốt thành MSA.

 

Vấn đề là do thiết lập volume sai khi chạy bằng container, chứ không phải do không thể ghi đồng thời nên mới phát sinh vấn đề. Nếu đọc lại bài viết, bạn sẽ thấy trong đó nói rằng việc không thể ghi đồng thời đã được xử lý đủ bằng busy timeout. Thiết lập volume thì
có thể giải quyết bằng ipc=host.

Cuối cùng thì postgres vẫn cần phải được vận hành, còn sqlite thì chỉ cần phân phối binary của ứng dụng là được ở bất cứ đâu.

Chính vì vô số kinh nghiệm vận hành và những lần thất bại đã tích lũy lại nên sqlite mới bắt đầu trở nên phổ biến, và
việc ghi đồng thời cũng được nêu rõ trong bài là hoàn toàn không phải vấn đề.

 
darjeeling 10 ngày trước

Thực ra chỉ cần chỉnh vài thiết lập là được, nhưng rốt cuộc vẫn cần có kinh nghiệm thôi, hì hì. Dù sao thì tôi khá thích những bài viết như thế này.

 
darjeeling 11 ngày trước

Đúng vậy, vì thế càng mong thỉnh thoảng sẽ có những bài viết như thế này được đăng lên.