- Rails 8 đã loại bỏ sự phụ thuộc vào Redis khỏi stack mặc định, và chuyển sang xử lý mọi tác vụ trên cơ sở dữ liệu quan hệ (RDB) thông qua SolidQueue·SolidCache·SolidCable
- Redis nhanh và ổn định, nhưng kéo theo độ phức tạp vận hành như cấu hình, bảo mật, quản lý cluster, sao lưu
- SolidQueue tận dụng tính năng
FOR UPDATE SKIP LOCKED của PostgreSQL để triển khai xử lý tác vụ song song không tranh chấp
- Cung cấp miễn phí các tính năng vốn là tính năng trả phí của Redis+Sidekiq như tác vụ định kỳ, kiểm soát đồng thời, dashboard giám sát (Mission Control)
- Phần lớn ứng dụng Rails chỉ cần SolidQueue là đủ; chỉ một số trường hợp cần xử lý thời gian thực siêu nhanh mới cần tiếp tục dùng Redis
Chi phí ẩn của Redis
- Ngoài chi phí hosting đơn thuần, Redis còn tạo ra gánh nặng quản trị liên tục như cài đặt, bảo trì, cấu hình bảo mật, quản lý cluster HA
- Cần có kết nối mạng và cấu hình tường lửa giữa Rails và Redis, xác thực client, cùng điều phối tiến trình Sidekiq
- Khi sự cố xảy ra, phải debug đồng thời cả Redis và RDBMS, đồng thời cũng cần chiến lược sao lưu kép
- Ngược lại, với stack Rails không có Redis, chỉ cần quản lý một PostgreSQL duy nhất, nên có thể đơn giản hóa đáng kể
Cách SolidQueue hoạt động
- Sử dụng tính năng
FOR UPDATE SKIP LOCKED của PostgreSQL để nhiều worker có thể lấy tác vụ cùng lúc mà không bị tranh chấp khóa (lock contention)
- Cấu trúc bảng chính
solid_queue_jobs: lưu metadata của tác vụ
solid_queue_scheduled_executions: chờ các tác vụ đã lên lịch
solid_queue_ready_executions: hàng đợi các tác vụ sẵn sàng thực thi
- Các tiến trình worker, dispatcher, scheduler, supervisor phối hợp với nhau bằng cách polling định kỳ các bảng khác nhau
- Nhờ thiết kế MVCC và autovacuum của PostgreSQL, hệ thống vẫn xử lý ổn định cả khi có khối lượng lớn thao tác insert và delete
Lập lịch tác vụ lặp lại
- SolidQueue cung cấp sẵn tác vụ lặp theo kiểu cron, được cấu hình trong file
config/recurring.yml
- Scheduler sẽ đưa các tác vụ đến thời điểm chạy vào queue, đồng thời tự động lên lịch cho lần chạy tiếp theo
- Dùng thư viện Fugit để phân tích lịch bằng ngôn ngữ tự nhiên, và tạo thread bằng Concurrent::ScheduledTask
- Áp dụng cách lập lịch mang tính xác định (deterministic) của GoodJob để vẫn giữ đúng lịch ngay cả khi tiến trình khởi động lại
Tính năng kiểm soát đồng thời
- SolidQueue dùng mẫu semaphore POSIX để hỗ trợ giới hạn số tác vụ chạy đồng thời theo từng đơn vị công việc
- Ví dụ: khi cấu hình
limits_concurrency to: 1, key: ->(user) { user.id }, mỗi người dùng chỉ có 1 tác vụ được chạy cùng lúc
- Có thể đặt thời hạn hết hiệu lực của semaphore (
duration) để ngăn xung đột tác vụ và deadlock
- Các bảng liên quan
solid_queue_semaphores: theo dõi giới hạn đồng thời
solid_queue_blocked_executions: lưu các tác vụ đang chờ
Giám sát bằng Mission Control
- Mission Control Jobs là dashboard mã nguồn mở miễn phí cho Rails 8, có thể mount đơn giản tại đường dẫn
/jobs
- Tính năng chính
- Trạng thái queue theo thời gian thực, theo dõi tác vụ thất bại, điều khiển retry/discard
- Trực quan hóa timeline của tác vụ đã lên lịch và tác vụ lặp
- Biểu đồ throughput và metric theo từng queue
- Hỗ trợ truy vấn dựa trên SQL, nên có thể phân tích trực tiếp trong cơ sở dữ liệu mà không cần công cụ bổ sung
Migration từ Sidekiq sang SolidQueue
- Bước 1: đặt
config.active_job.queue_adapter = :solid_queue
- Bước 2: chạy
bundle add solid_queue, sau đó rails solid_queue:install và db:migrate
- Bước 3: chuyển lịch cron trong
sidekiq.yml sang recurring.yml
- Bước 4: thêm
jobs: bundle exec rake solid_queue:start vào Procfile
- Bước 5: xóa các gem liên quan đến Redis và Sidekiq
- Mã ActiveJob hiện có vẫn hoạt động nguyên vẹn mà không cần chỉnh sửa
Khi nào vẫn cần Redis
- Xử lý liên tục hàng nghìn tác vụ mỗi giây trở lên
- Hệ thống thời gian thực bắt buộc có độ trễ dưới 1ms
- Cần cấu trúc pub/sub phức tạp hoặc rate limiting và phép đếm tinh vi
- Ví dụ, Shopify vận hành 833 request mỗi giây và 1.172 tiến trình worker, sử dụng hạ tầng Redis
Hướng dẫn triển khai thực tế
- Khi tạo app Rails 8 mới, SolidQueue·SolidCache·SolidCable sẽ được cấu hình tự động
- Khuyến nghị cấu hình kết nối cơ sở dữ liệu queue riêng trong
config/database.yml
- Thêm xác thực cho Mission Control và mount route
/jobs
- Thêm
jobs: bundle exec rake solid_queue:start vào Procfile.dev, rồi chạy toàn bộ hệ thống bằng bin/dev
- Sau khi tạo tác vụ thử nghiệm, có thể kiểm tra trạng thái trong Mission Control
Các vấn đề thường gặp và cách xử lý
- Vẫn có thể dùng cấu hình một cơ sở dữ liệu duy nhất, nhưng tính linh hoạt khi vận hành sẽ giảm
- Trong môi trường production, bắt buộc phải thêm xác thực cho Mission Control
- Khoảng polling mặc định là 1 giây cho tác vụ đã lên lịch, 0,2 giây cho tác vụ tức thời, phù hợp với phần lớn ứng dụng
- Khi dùng ActionCable/Turbo Streams, cần cấu hình
SolidCable với kết nối DB riêng
Khả năng mở rộng và hiệu năng
- SolidQueue có thể mở rộng đủ tốt cho phần lớn ứng dụng Rails
- Dựa trên PostgreSQL, hệ thống có thể xử lý 200–300 tác vụ mỗi giây, và 37signals xử lý 20 triệu tác vụ mỗi ngày mà không cần Redis
- Bảng so sánh
| Hạng mục |
Redis + Sidekiq |
SolidQueue |
| Độ phức tạp cấu hình |
Cần dịch vụ riêng |
Dùng DB tích hợp |
| Ngôn ngữ truy vấn |
Lệnh Redis |
SQL |
| Giám sát |
Dashboard riêng |
Mission Control |
| Kịch bản sự cố |
Từ 6 trở lên |
2 |
| Thông lượng |
Hàng nghìn tác vụ/giây |
200–300 tác vụ/giây |
| Đối tượng phù hợp |
99,9% ứng dụng |
95% ứng dụng |
Kết luận
- Redis và Sidekiq là các công nghệ rất xuất sắc, nhưng với phần lớn ứng dụng Rails, chúng gây ra độ phức tạp và chi phí vượt mức cần thiết
- SolidQueue hiện thực hóa đơn giản hóa vận hành, cắt giảm chi phí và tăng hiệu quả bảo trì trên nền tảng một cơ sở dữ liệu duy nhất
- Trong kỷ nguyên Rails 8, chuyển sang SolidQueue được khuyến nghị như lựa chọn mặc định
2 bình luận
Redis thì tốt đấy.
Ý kiến trên Hacker News
Tôi nghĩ mọi tác giả mã nguồn mở đều có quyền kiểm soát phạm vi dự án của mình
Nhưng nhóm chúng tôi đang hối hận vì đã chuyển từ good_job sang SolidQueue
Basecamp lấy MySQL làm trung tâm nên không chấp nhận các truy vấn tối ưu riêng cho từng engine RDBMS. Chỉ cần nhìn vào GitHub issue là thấy họ chỉ tập trung vào hiệu năng của MySQL
Ngoài ra hiện vẫn chưa có hỗ trợ batch job (PR liên quan)
Trong các JOIN phức tạp, MySQL khá hay lập sai query plan, nên tôi dùng STRAIGHT_JOIN để ép thứ tự. Coi như để phòng tương lai
Tôi đang so sánh hai lựa chọn này để chuyển từ resque. GoodJob không tương thích với chế độ transaction của pgbouncer do dùng tính năng riêng của pg
Việc phải duy trì session liên tục thì khá phiền, nhưng mức cải thiện hiệu năng ở phần lớn quy mô lại không quá có ý nghĩa
Dù vậy, mô hình phát triển và độ dễ đọc của mã trong GoodJob tạo cảm giác đáng tin cậy hơn hẳn
Nếu có thể làm đơn giản môi trường production thì lúc nào cũng là điều tốt
Tôi nghĩ tình huống lý tưởng trong Rails là có một cấu trúc có thể chuyển sang Redis dễ dàng
Sẽ rất tốt nếu có thể bắt đầu với SolidQueue rồi khi chạm trần khả năng mở rộng thì chuyển sang Redis
Phần lớn ứng dụng Rails không có lưu lượng lớn, nên việc duy trì cả hai hệ thống lại còn phức tạp hơn
Tất nhiên cũng có ứng dụng phụ thuộc vào một hiện thực hàng đợi cụ thể, nhưng trong trường hợp phổ biến thì chỉ cần đổi cấu hình
Nó có dùng thêm snapshot để tránh log phình quá lớn không, và liệu điều này có hoạt động trong chế độ phân tán hay không
Đặc biệt khi việc tạo job diễn ra cùng với các thay đổi DB khác, vấn đề là sẽ mất đi sự bảo đảm đó
Redis trước đây có lợi thế ở điểm này vì nó là một kho trạng thái nhẹ và độc lập
Có vẻ SolidQueue không làm rõ ranh giới tách biệt này (riverqueue.com)
Tôi đã thử nghiệm SolidQueue trong dự án phụ của mình
Kết luận là, nếu Sidekiq không có vấn đề gì thì không có lý do phải đổi
Chỉ đáng cân nhắc khi bạn muốn bỏ hạ tầng Redis
Nếu là dự án mới thì GoodJob có vẻ trưởng thành hơn và cộng đồng cũng tốt hơn
Tôi thấy UI của SolidQueue quá đơn giản nên khá bất tiện. Do không tối ưu index, khi dữ liệu nhiều thì trang bị treo
Cũng cần tính đến việc dùng RDBMS sẽ phát sinh thêm chi phí quản lý connection pool
Với những người lo về khả năng mở rộng, xem benchmark của Oban trong Elixir thì
một node đơn có thể xử lý một triệu job mỗi phút. Hầu hết ứng dụng có tải thấp hơn rất nhiều
Nó nhét 5000 job theo từng lô một lần, nên TPS thực tế chỉ khoảng 200
Nếu đẩy từng job riêng lẻ không theo lô thì tải transaction SQL sẽ cao hơn nhiều
Chúng tôi đã lưu job trong DB từ trước cả khi có SolidQueue
Ưu điểm là có thể snapshot nguyên trạng thái production sang môi trường phát triển
Tuy nhiên rate limiter thì vẫn để ở Redis. Làm vậy để tránh tăng tải cho DB
Giới hạn của hàng đợi dựa trên DB là payload lớn
Nếu nhét JSON lớn vào hàng đợi thì sẽ kém hiệu quả vì overhead ghi DB
Redis (Sidekiq) nhanh hơn nhiều trong trường hợp này
SolidQueue+SQLite vẫn ổn nếu chỉ dùng để truyền primary key
Nhưng nếu nhiều worker cùng polling một DB thì sẽ nhanh chóng thành nút thắt cổ chai
Tôi nghĩ dữ liệu lớn nên để ở storage bên ngoài như S3 rồi chỉ truyền tham chiếu
Tôi khá tò mò không biết có tài liệu nào tổng hợp kết quả benchmark không
SolidQueue có nhắc đến SKIP LOCKED, nhưng giữ transaction cho một job kéo dài 15 phút là rất rủi ro
Transaction mở quá lâu sẽ phá hiệu năng DB và cũng dễ tổn thương trước sự cố mất kết nối mạng
Kiểu kiến trúc này có thể dẫn đến phản mẫu. Sau đó tôi mới thấy có vẻ nó dùng cơ chế lease
Tôi đồng cảm với triết lý Postgres for everything
Tôi nghĩ đơn giản hóa bằng cách hợp nhất vào một PostgreSQL duy nhất là điều tốt
Tôi không biết nên phản biện ẩn dụ này thế nào
Tôi tự hỏi có còn lý do gì để dùng Redis nếu phải đánh đổi bằng việc tăng độ phức tạp
“Doanh nghiệp mà độ trễ dưới 1ms là quan trọng”, ý là chạy HFT bằng Rails sao?
Postgres sẽ nuốt chửng thế giới