1 điểm bởi GN⁺ 3 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Sau khi chuyển từ MariaDB sang SQLite, lobste.rs đã xử lý ổn định cả đợt tăng vọt lưu lượng vào thứ Hai, đồng thời ghi nhận mức sử dụng CPU và bộ nhớ giảm cùng cảm nhận phản hồi nhanh hơn
  • Khi tắt VPS MariaDB riêng, chi phí VPS giảm một nửa, và nhiệm vụ migration bắt đầu từ năm 2019 cũng được hoàn tất
  • Ở lần triển khai đầu tiên, chỉ riêng lưu lượng đọc cũng đã khiến toàn bộ CPU chạm 100% nên phải rollback, nhưng sau khi sửa 2 lần quét toàn bộ bảng và vấn đề N+1 trên các bảng lớn thì đã triển khai lại thành công
  • Bên cạnh việc triển khai regexp, if, stddev bằng hàm do người dùng định nghĩa của SQLite, nhóm cũng phải xử lý các giới hạn tương thích như không hỗ trợ unsigned bigint và NOCASE chỉ xử lý ASCII
  • Để thay đổi cơ sở dữ liệu nền mà không truy cập vào cơ sở dữ liệu production, cần có dữ liệu kiểm thử với quy mô thực tế, test suite, checklist triển khai/rollback và sự phối hợp chặt chẽ

Từ thảo luận năm 2019 đến việc chuyển sang SQLite

  • Thảo luận về migration bắt đầu từ issue #539 vào năm 2019
    • Khi đó, MySQL được đề xuất như một phương án thay thế để cân nhắc tính tương thích với MariaDB, nhưng quá trình thảo luận chuyển sang PostgreSQL đã diễn ra sẵn
    • Năm 2025, khi Rahul nhắc đến việc K1 mua lại MariaDB, các trao đổi chi tiết về migration sang PostgreSQL tiếp tục được nối lại
    • Đến tháng 2, Rahul hỏi liệu lobsters có thể chạy trên SQLite hay không, và từ tháng 6 năm 2025, việc tham gia vào dự án bắt đầu được đẩy mạnh
  • PR đầu tiên vào tháng 8 năm 2025 đã bị đóng như một PR cũ trên GitHub sau khi công việc bị gián đoạn, nên không thể mở lại
    • PR thứ hai bao gồm kiểm thử hiệu năng, script migration giữa các DB tự viết, và việc debug liên quan đến tính toàn vẹn dữ liệu
    • Không có script chuyển đổi MariaDB/MySQL→SQLite sẵn có nào đáp ứng được yêu cầu, nên nhóm đã viết một script riêng

Thất bại ở lần triển khai đầu và sửa nút thắt hiệu năng

  • Lần triển khai đầu tiên vào ngày 21 tháng 2 được thực hiện sau khi chuẩn bị checklist triển khai, nhưng ngay sau khi triển khai, chỉ riêng lưu lượng trong trạng thái chỉ đọc đã khiến toàn bộ CPU chạm 100%
    • Do không tìm được nguyên nhân nên đã rollback
    • Vì không thể truy cập DB production nên rất khó tái hiện trước các vấn đề hiệu năng thực tế
  • Hai ngày sau thất bại đó, PR thứ ba và cũng là PR cuối cùng được mở để sửa các vấn đề liên quan đến tìm kiếm, đồng thời bổ sung script tạo dữ liệu dung lượng lớn nhằm tạo dữ liệu cục bộ có kích thước bằng một nửa dữ liệu lobsters thực tế
    • Mất một tuần để tạo dữ liệu
    • Nút thắt ở lần triển khai đầu là các lần quét toàn bộ bảng do hai truy vấn gây ra trên các bảng lớn, cùng với một vấn đề N+1 riêng biệt
    • Nhóm đã áp dụng thay đổi 1, thay đổi 2, thay đổi 3 để xử lý chúng
    • Sáng ngày triển khai, nhóm còn thêm slow query log để có thể theo dõi thêm các vấn đề hiệu năng

Kết quả triển khai lại và giảm chi phí vận hành

  • Ở lần triển khai thứ hai vào ngày 11 tháng 7, checklist triển khai/rollback tiếp tục được chuẩn bị lại, và sau khi chuyển đổi, website vẫn vận hành bình thường, mức sử dụng CPU và bộ nhớ cũng được duy trì ổn định
    • Trong khi theo dõi các chỉ số của website và IRC, nhóm đã xử lý ngay các vấn đề được báo cáo bằng bản sửa 1, bản sửa 2
    • Ngay cả trong đợt tăng vọt lưu lượng vào thứ Hai cũng không phát sinh vấn đề, đồng thời xác nhận mức sử dụng CPU/bộ nhớ giảm và cảm nhận phản hồi nhanh hơn
    • Khi dừng VPS MariaDB, chi phí VPS giảm một nửa, và issue migration cũng được đóng lại

Những giới hạn của SQLite khác với MariaDB

  • Tận dụng hỗ trợ hàm do người dùng định nghĩa (UDF) của gem SQLite, nhóm đã tự triển khai regexp, if, stddev vốn không có sẵn trong SQLite
    • Nhờ đó không cần thêm quá nhiều cách обход cho SQL migration
  • SQLite không hỗ trợ unsigned bigint vốn được dùng cho một số ID trong MariaDB, nên đã đổi sang bigint
  • Quy tắc sắp xếp cũng bị giới hạn hơn MariaDB
    • MariaDB dùng utf8mb4_general_ci, còn trên SQLite áp dụng NOCASE
    • NOCASE không hỗ trợ chuyển đổi hoa/thường đầy đủ cho toàn bộ UTF mà chỉ xử lý ký tự ASCII
  • Với bảng tìm kiếm toàn văn của SQLite, cách phù hợp là Contentless-Delete Tables, vốn là phương pháp được khuyến nghị thay vì cấu hình mặc định
  • Thiết lập PRAGMA mặc định của Rails hoạt động bình thường trên lobsters
    • Vì migration cơ sở dữ liệu khác nhau theo từng DB, các migration cũ được chuyển sang thư mục old_migrations để db:migrate tiếp tục hoạt động

Bài học rút ra về kiểm thử và phối hợp

  • Trong codebase của lobsters, có search parserheinous_inline_partials, một kiểu hack để tăng tốc độ render
    • Nhờ test suite, có thể xác minh tính chính xác của việc chuyển sang SQLite mà không cần lượng lớn kiểm thử thủ công
  • Giao tiếp trơn tru giữa những người tham gia là yếu tố quyết định thành công của migration
    • Việc thay thế cơ sở dữ liệu nền trong khi không thể truy cập DB production là cực kỳ khó
    • Nếu phải làm lại công việc này, cần chuẩn bị trước quy mô dữ liệu thực tế
  • Nếu trong quá trình test có thể cấu hình để thất bại ngay khi xuất hiện quét toàn bộ bảng, thì đã có thể phát hiện sớm vấn đề hiệu năng của lần triển khai đầu
  • Cần có công cụ để dễ dàng tạo bộ dữ liệu tương tự production mà không phải tự viết thủ công hoặc chờ cả một tuần

1 bình luận

 
Ý kiến trên Lobste.rs
  • @thomas0 đã cẩn thận phụ trách đợt di chuyển quy mô lớn, @355E3B hỗ trợ lập kế hoạch và vận hành, và nhờ những người đóng góp liên quan đến #539 cùng #lobsters nên cả khoảng thời gian chỉ đọc cũng trôi qua khá vui vẻ
    Ban đầu ở #539 họ dự định dùng PostgreSQL, nhưng vì người tình nguyện trực tiếp hỗ trợ công việc này đã dùng SQLite và họ muốn tránh một giải pháp lớn, phức tạp hơn nhu cầu thực tế nên đã chọn SQLite. PostgreSQL thường là lựa chọn mặc định của họ, nhưng nó kéo theo gánh nặng phải vận hành, điều phối và bảo trì một dịch vụ riêng
    Đáng ngạc nhiên là sau khi di chuyển, mức sử dụng CPU và RAM đều giảm, và biểu đồ chi tiết nằm ở phần cuối của #539

  • Mỗi lần nhìn vào cấu hình mặc định của SQLite đều thấy bất ngờ. Sẽ rất tốt nếu có một chế độ cho dự án mới với WAL, khóa ngoại, synchronous=NORMAL v.v. được bật sẵn theo mặc định, và có lẽ vẫn còn thêm vài giá trị mặc định khác nên được cải thiện

    • Tôi nhớ mang máng rằng cách sắp xếp hoặc ghi trước thay đổi trên đĩa có lợi cho HDD nhưng khá không phù hợp với NVMe
  • Tôi tò mò về chiến lược sao lưu cho cơ sở dữ liệu SQLite. Không rõ họ có dùng công cụ như litestream hay áp dụng cách khác không

  • Tôi muốn biết lobste.rs xử lý các tác vụ cập nhật như thế nào

    • Họ dùng câu lệnh update của SQLite. Nếu bạn đang nói đến loại cập nhật khác thì hãy nói rõ hơn
  • SQLite đã có sẵn hàm iif, nhưng bản thân việc vận hành hàm do người dùng định nghĩa trong cùng khu vực đó cũng khá thú vị

  • Trước đây tôi nghĩ SQLite không hỗ trợ đồng thời ở mức như PostgreSQL hay MySQL, nhưng đọc tài liệu tổng quan thì có vẻ giờ nó có thể được dùng từ nhiều tiến trình; vậy có đúng là lobste.rs vẫn phải chạy trên một máy chủ duy nhất không
    Tôi cũng muốn biết liệu các tiến trình có đồng bộ với nhau bên trong SQLite bằng bộ nhớ chia sẻ hay mmap hay cách nào đó, để chỉ có một tiến trình được ghi tại một thời điểm còn nhiều tiến trình được đọc đồng thời, và các tác vụ ghi đang chờ có xếp hàng lần lượt hay không

    • SQLite FAQ có thể giải đáp một phần. Có thể thực hiện nhiều tác vụ đọc qua nhiều tiến trình, nhưng ghi thì chỉ một tác vụ tại một thời điểm, và SQLite đảm bảo điều này bằng khóa của hệ thống tệp
      Với các tác vụ ghi phải chờ, có vẻ nó không dùng hàng đợi bộ nhớ chia sẻ tường minh mà là đợi một lúc rồi thử lại. Tôi không chắc việc dùng SQLite trên nhiều máy là hoàn toàn bất khả thi, nhưng vì cách đơn giản là chia sẻ tệp qua NFS bị khuyến cáo rõ ràng là không nên, nên xem đây là cấu hình một máy chủ duy nhất là một cách gần đúng hợp lý; mà hiện nay một máy cũng có thể xử lý quy mô khá lớn
    • Nó có cho các tác vụ ghi chờ ở mức nào đó, nhưng cấu hình mặc định không tốt nên cần tinh chỉnh. Thiết lập hiện tại như sau, và cũng cần bật chế độ giao dịch tức thì để busy_timeout hoạt động
      "PRAGMA foreign_keys=ON;"  
      "PRAGMA journal_mode = WAL;"  
      "PRAGMA synchronous = NORMAL;"  
      "PRAGMA busy_timeout = 5000;"  
      "PRAGMA temp_store = MEMORY;"  
      "PRAGMA mmap_size = 134217728;"  
      "PRAGMA journal_size_limit = 67108864;"  
      "PRAGMA cache_size = 2000;"  
      
      Tài liệu tham khảo: https://fractaledmind.github.io/2024/04/… , https://kerkour.com/sqlite-for-servershttps://www.sqlite.org/lang_transaction.html#immediate ; có thể xem chi tiết ở phần SQLITE_BUSY của liên kết thứ hai và liên kết thứ ba
  • Đây là một bản nâng cấp ấn tượng, với các chi tiết được ghi chép cực kỳ tốt trong PR, issue và Gist. Tôi tò mò liệu thay đổi này có tạo ra khác biệt về tính năng như tìm kiếm trên trang hay không, và hiện tại tệp SQLite trên đĩa có kích thước bao nhiêu

    • Kết quả tìm kiếm dùng tính năng xếp hạng của SQLite, nên thứ tự có thể khác so với MariaDB. Có test tìm kiếm nhưng phần kiểm tra xếp hạng còn ít, nên có khả năng tồn tại thay đổi về mặt chức năng, dù hiện chưa có bằng chứng rõ ràng
      Tôi nhớ là đã có chỗ nói tệp SQLite có kích thước 3.8GB