Sidekiq Architecture, Tại sao nó lại tin cậy ?
Giới thiệu
Sidekiq là một backgroud processing framework cho Ruby, nhằm đưa các request yêu cầu từ client vào trong các job/asynchronous để xử lý, nhằm tránh việc người dùng phải chờ đợi lâu. Nguyên nhân vì sao phải sử dụng nó thì chúng ta có ví dụ dưới đây khi tạo web application:
-
Người dùng gửi yêu cầu, request tới web application.
-
Web application nhận request, xử lý (hoặc giao tiếp với service khác như database để xử lý).
-
Cuối cùng trả về kết quả cho user. Trên thực tế, nếu xử lý request quá lâu, người dùng phải chờ ở màn hình cho đến khi xử lý xong thì thật bất tiện và gây khó chịu người dùng, ví dụ như gửi mail cho nhiều user 1 lúc .... Do đó, khi sử dụng sidekiq thì những tác vụ như vậy ta có thể đưa vào chạy nền hay còn gọi là backgroud job/asynchronous job. Mô hình chung:
-
Broker là một queue ví dụ như RabbitMQ, redis ...
-
App sẽ enqueue các tác vụ cần xử lý background phụ thuộc vào từng request
-
CRON sẽ enqueue các bg job có schedule cụ thể (lập lịch chạy một chức năng nào đó)
-
Worker sẽ dequeue và xử lý các tác vụ đó. Các xử lý mà sẽ phù hợp để đưa vào chạy nền bao gồm:
-
Các xử lý nặng về CPU
-
Các xử lý nặng về I/O ví dụ load data tính toán report, ETL.
-
Batch job ví dụ như update/processing report về đêm.
Kiến trúc của sidekiq (Sidekiq Architecture)
Kiến trúc của sidekiq chúng ta có thể tham khảo ảnh dưới đây:
- Sidekiq client chạy trên bất kỳ Ruby process nào đó ví dụ như puma hoặc passsenger cho phép tạo job để xử lý sau.
- Các job sidekiq sẽ sử dụng Redis để lưu trữ (data storage)
- Các job từ queue trong Redis sẽ được xử lý một cách độc lập
Vậy với kiến trúc như trên, thì đối với những job fail vì một nguyên nhân nào đó đối với từng giai đoạn, thì liệu sử dụng kiến trúc như trên có tin cậy hay không. Chúng ta sẽ tìm hiểu từng phần dưới đây
Sidekiq client
Giả sử khi sidekiq client tạo một job lưu trữ trong redis, nhưng vấn đề là network hoặc do một lý do nao đó mà job ko thể lưu trữ vào đây, vậy bài toán đặt ra là xử lý job này thế nào ? Bước đầu tiên là sẽ implement một local queue để lưu trữ các job bị fail. Sau đó sẽ tiến hành delivery nó khi network hoạc động trở lại. Tuy nhiên nó cũng có một số nhược điểm:
- Local queue per-process và in-memory, nếu client process bị restart thì job vẫn bị mất.
- Chỉ có 1000 job cuối cùng được sidekiq lưu trữ, để tránh lưu quá nhiều dẫn tới full mem.
Redis
Vì sidekiq sử dụng redis làm data storage lưu trữ các job cần xử lý, vậy nếu redis fail đương nhiên chúng ta sẽ mất tất cả các job chưa xử lý và không trả về kết quả cho người dùng.
Vậy làm sao để đảm bảo các job này vẫn được xử lý, cũng như tại sao phải sử dụng redis.
- Redis có tốc độ read/write operation rất tốt so với các storage khác như MySQL nên dùng redis lợi hơn về mặt tốc độ (Gearman có hỗ trợ persistent vào MySQL và memcached)
- Redis hỗ trợ nhiều kiểu dữ liệu hơn và vẫn có cơ chế persistent dữ liệu xuống đĩa cứng định kỳ điều này giúp giảm thiểu rủi ro khi redis fail thì dữ liệu bị mất sẽ nhỏ hơn.
- Redis chết hẳn thì đương nhiên job sẽ không lưu trữ được dẫn đến stuck. Lúc này người ta sẽ nghĩ đến việc sử dụng redis sentiel (master-slave) vừa đảm bảo việc persistent job vừa đảm bảo failover nếu 1 server chết.
- Khi full mem, redis sẽ evict data theo policy mà ta cấu hình (ví dụ LRU -> mất các job cũ chưa kịp xử lý nên để đảm bảo không evict nhầm nên cấu hình redis maxmemory-policy noeviction)
- Redis thường dùng để cache dữ liệu, tuy nhiên bản chất cache và storage job là khác nhau (cache có thể invalidate nhưng job thì không được mất) => nên tách rời 2 server redis cho việc cache và lưu job.
Sidekiq server
Các vấn đề được đặt ra như sau:
- Khi có một job đang được sidekiq xử lý, thì làm sao có thể restart sidekiq?
- Cơ chế để retry khi job gặp lỗi là gì?
- Làm thế nào để đảm bảo job vẫn xử lý được nếu sidekiq server đang xử lý một job, nhưng bị segfault hoặc crash hoặc force kill (kill -9) ?
Đối với câu hỏi đầu tiên chúng ta có thể giải thích như sau (Phần giải thích này thực chất là phần Signals trong wiki của Sidekiq).
- Khi ta gửi TSTP signal tới sidekiq (kill -TSTP [sidekiq_pid]), Sidekiq sẽ hiểu là nó sẽ bị shutdown trong tương lai gần, sidekiq sẽ chuyển sang trạng thái "quiet" nghĩa là nó sẽ dừng việc fetch new job từ redis nhưng vẫn tiếp tục xử lý các job mà nó đang giữ, khi tất cả các current job được xử lý xong thì sidekiq sẽ shutdown.
- TERM signal nghĩa là Sidekiq nên shutdown sau khoảng thời thời gian -t timeout. Tương tự như TSTP sidekiq cũng sẽ không fetch job mới nhưng vẫn tiếp tục xử lý các job cũ cho xong. Điểm khác biệt là sau timeout nếu job không được hoàn thành thì sẽ bị force terminated và message đó sẽ được push back lại vào redis, để lần sau khi sidekiq được start job đó sẽ được fetch lại và được xử lý. => Best practice là ta sẽ gửi signal TSTP lúc bắt đầu deploy và TERM lúc kết thúc deploy. Và lúc này có thể thoải mái restart mà không sợ bị miss bất cứ job nào.
Với câu hỏi thứ 2:
-
Sidekiq có built-in một chơ chế để retry, sẽ catch các exception và tự động retry thường xuyên dựa trên công thức (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1)) (tương đương 15, 16, 31, 96, 271, ... giây + một lượng random time), với giá trị default retry là 25 nghĩa là để thực hiện 25 lần retry sẽ vào khoảng 21 ngày, trong 21 ngày này ta có thể fix bug, deploy và job sẽ được xử lý thành công ở lần retry tiếp theo.
-
Sidekiq có cơ chế retry theo công thức (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1)) khi gặp exception. Giả sử bạn để giá trị default là 25 nghĩa là sẽ thực hiện 25 lần retry vào khoảng 21 ngày, trong 21 ngày chúng ta có thể fix bug, ... để xử lý thành công cho các lần retry tiếp. Sau 25 retry sẽ tự động cho job đó vào Dead Job queue => nếu muốn tiếp tục xử lý nó thì sẽ phải xử lý thủ công. Sau 6 tháng thì sẽ discard job đó.
Câu hỏi cuối:
sidekiq dùng lệnh BRPOP để fetch một job từ trong queue redis ra và xử lý, không có gì phải bàn cãi về việc tại sao lại dùng BRPOP nữa, tuy nhiên việc dùng BRPOP hay RPOP bị một vấn đề là sau khi sidekiq fetch job thì job đó không còn tồn tại trong redis. Và bây giờ, nếu sidekiq crash, job vừa được fetch ra, chưa kịp xử lý sẽ biến mất hoàn toàn.
Tóm lại, nếu sidekiq có shutdow thì vẫn có thể push-back job lại về redis, chứ đã crash giữa job thì không cách nào cứu chưa
Do đó khi fetch job thì đừng xóa nó ra khỏi queue của redis. Tuy nhiên nó dẫn tới một vấn đề khác đó là một sidekiq server khác có thể nhìn thấy job đó, fetch ra và xử lý job đó. Hệ quả là job được xử lý 2 lần, nếu bạn gửi mail thì có nghĩa là người dùng sẽ nhận được 2 email có cùng nội dung. Một cách khác là thay vì giữ job đó trong queue thì ta vẫn cứ fetch job đó ra nhưng sau đó sẽ push job đó vào một queue khác gọi là queue_đang_xử_lý. Đây chính xác là cách lệnh RPOPLPUSH trong redis hoạt động, reliable queue pattern. Lệnh này:
- Fetch và xóa last-item (tail) từ queue cũ và cùng thời điểm đó thêm item đó đầu một queue khác -> gọi là processing_queue (head).
- Atomically return nghĩa là hoạt động như transaction trong database, 1 là thành công thì commit (xóa từ queue cũ và thêm vào queue mới), 2 là fail thì rollback, trả job về lại queue cũ.
- Dùng lệnh LREM theo thứ tự để xóa item trong processing_queue một khi item đó đã được xử lý xong
127.0.0.1:6379> LPUSH sidekiq a b c
(integer) 3
127.0.0.1:6379> RPOPLPUSH sidekiq sidekiq_current_processing
"a"
127.0.0.1:6379> LRANGE sidekiq 0 -1
1) "c"
2) "b"
127.0.0.1:6379> LRANGE sidekiq_current_processing 0 -1
1) "a"
Để đảm bảo tính tin cậy trong Sidekiq Pro, thì thay vì dùng fetch ta có hàm super_fetch sử dụng RPOPLPUSH như cách ở trên.
Một số lưu ý
- Cần có thông bảo network fail không thể connect tới queue khi xử lý ở client.
- Luôn đảm bảo redis availability
- Đối với Worker cần phải monitor worker sống hay chết, và phải monitor thêm về thời gian xử lý các job, từ đây sẽ biết cách để tăng số worker hay tối ưu lại xử lý.
Tài liệu tham khảo
https://github.com/mperham/sidekiq/wiki
All rights reserved