Sidekiq, how to reliability?

2018/07/26

Categories: rails

Thông thường, khi xây dựng một ứng dụng web, quy trình đơn giản sẽ như sau:

Ví dụ trong thực tế, bạn đăng ký user ở một website nào đó, sau khi nhập đầy đủ thông tin, submit -> request sẽ được gửi lên server và người dùng sẽ chờ cho tới khi server xử lý xong và trả về kết quả (thành công hoặc thất bại). Do người dùng phải chờ nên ta gọi đó là synchronous task.

Tuy nhiên trong thực tế, không phải lúc nào web app cũng xử lý nhanh hoặc không phải lúc nào chúng ta cũng cần phản hồi ngay lập tức kết quả cho người dùng. Ví dụ sau khi đăng ký ta có gửi email thông báo tới cho người dùng, nhưng không nhất thiết phải gửi email ngay lập tức cho người dùng, có thể chậm 1-2 phút cũng chấp nhận được hoặc ví dụ các tác vụ resize, optimize ảnh do người dùng upload thường tốn nhiều thời gian, nếu để user chờ ở màn hình upload cũng không phải là cách hay.

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. Giúp tránh bắt người dùng phải chờ đợi lâu mà vẫn đảm bảo là sẽ xử lý tác vụ đó cho người dùng.

Mô hình chung sẽ như sau:

task-queue

image source from https://drivy.engineering/taskqueues-tips

Sidekiq là một background processing framework cho ngôn ngữ Ruby để giải quyết các bài toán tương tự như trên. Xử lý background không phải là bài toán mới, bất kỳ ngôn ngữ lập trình nào cũng có những cách giải quyết tương tự, về job queue cũng có rất nhiều lựa chọn khác nhau ví dụ Gearmand, Celery, rq.

Các xử lý mà sẽ phù hợp để đưa vào chạy nền bao gồm:

1. Sidekiq Architecture

Sidekiq gồm 3 phần chính:

sidekiq-flow image source from https://brandonhilkert.com/blog/sidekiq-as-a-microservice-message-queue

Đứng ở góc độ vận hành hệ thống, mình chỉ quan tâm chuyện gì xảy ra khi bất cứ thành phần trên fail và làm cách nào để đảm bảo hệ thống có khả năng fail-over. Để trả lời câu hỏi đó thì cần phải hiểu cách hoạt động, implement của sidekiq.

2. How to Reliability?

2.1 Sidekiq client

Khi sidekiq client push một job tới redis, giả định rằng network không hoạt động tốt, dẫn tới job không thể lưu trữ vào redis, vậy làm sao đảm bảo độ tin cậy cho sidekiq client? Một hướng tiếp cận đó là implement một local queue để lưu trữ các job nếu gọi network fail và sẽ delivery khi network kết nối thành công. fluentd cũng có một cách tương tự gọi là buffer, logstash cũng có cách giải quyết tương tự là persistent queue. Tuy nhiên nó cũng có một số nhược điểm:

2.2 Redis

Redis được dùng để lưu trữ các job cần xử lý, nếu storage fail ta mất tất cả các job chưa kịp xử lý và người dùng sẽ không nhận được thứ mà họ cần.

Câu hỏi là tại sao lại là redis chứ không phải cái gì khác? Và làm sao đảm bảo dù fail cũng phải đảm bảo tác vụ của người dùng được xử lý.

2.3 Sidekiq server

Câu hỏi đặt ra là:

Về câu hỏi đầu tiên, đây là một điểm khá thú ví về cách thiết kế, nếu ai quen với nginx sẽ thấy một cơ chế tương tự. Phần giải thích này thực chất là phần Signals trong wiki của Sidekiq.

=> 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.

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.

Nếu sau 25 lần retry và job vẫn không thành công thì sidekiq sẽ chuyển job đó và Dead Job queue và phải can thiệp thủ công để chạy lại job đó. Và nếu sau 6 tháng, job đó không được xử lý thì sidekiq sẽ discard job đó.

Về câu hỏi cuối, làm thế nào để đảm bảo tính tin cậy cho một job khi được fetch bởi sidekiq server, và sidekiq server bị crash thì job đó vẫn có thể được xử lý. Đây là một bài toán rất thú vị để tìm hiểu và qua đó ta sẽ thấy sức mạnh của redis.

Trước tiên ta sẽ nói 1 chút xíu về list và queue trong redis. Trong redis thì list là một tập hợp của các phần tử có kiểu dữ liệu là string đã được sắp xếp bằng thuật toán insertion sort. List trong redis được implement bằng cấu trúc dữ liệu là linked list.

Redis hỗ trợ một số lệnh trên list như LPUSH, RPUSH, LPOP, RPOP, LLEN, LINSERT, LINDEX, với tập lệnh này ta có thể dùng list trong redis như queue (FIFO với tập lệnh LPUSH, RPOP, phần tử thêm vào đầu tiên sẽ được lấy ra đầu tiên) hoặc stack (LIFO với tập lệnh RPUSH, LPOP, phần tử thêm vào đầu tiên sẽ được lấy ra sau cùng).

Bằng cách sử dụng list như queue ta có thể implement producer để đẩy dữ liệu và consumer để lấy dữ liệu từ trong queue ra với 2 step đơn giản:

Vấn đề là không phải lúc nào list của chúng ta cũng có phần tử, và khi list không có phần tử thì sẽ consumer sẽ không có gì để xử lý cả, lệnh RPOP trả về nil.

127.0.0.1:6379> LPUSH sidekiq a b c
(integer) 3
127.0.0.1:6379> LLEN sidekiq
(integer) 3
127.0.0.1:6379> RPOP sidekiq
"a"
127.0.0.1:6379> RPOP sidekiq
"b"
127.0.0.1:6379> RPOP sidekiq
"c"
127.0.0.1:6379> RPOP sidekiq
(nil)

Để giải quyết vấn đề trên, ta có thể bắt consumer đợi trong một khoảng thời gian nào đó và sau đó sẽ gọi lại LPOP để lấy dữ liệu, kỹ thuật này gọi là polling. Tuy nhiên kỹ thuật này vẫn có nhược điểm:

Do đó redis implement một số lệnh gọi là blocking operation đó là BLPOP và BRPOP. Nghĩa là nếu list rỗng, thay vì trả về nil thì consumer sẽ block connection và chờ với một khoảng thời gian timeout (ta có thể chờ vô tận bằng cách set timeout = 0), khi có một item mới được thêm vào list thì POP ra và xử lý. Điểm khác biệt là không cần phải định kỳ quay trở lại kiểm tra -> tránh thực hiện các lệnh vô nghĩa.

Quay trở lại vấn đề của sidekiq server, 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, đen thôi, đỏ quên đi, nếu quay trở lại vấn đề restart, sidekiq có shutdown thì vẫn có thể push-back job lại về redis chứ đã crash giữa đường job sẽ không có cách nào cứu chữa.

Vậy thử nghĩ xem có cách nào để đảm bảo tính tin cậy khi xử lý job, theo cách suy nghĩ thông thường thì ĐƠN GIẢN là khi fetch job thì đừng XÓA job đó 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 tiếp cận 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:

Với Sidekiq Pro, để đảm bảo tính tin cậy thì thay vì dùng fetch ta có hàm super_fetch sử dụng RPOPLPUSH như cách ở trên.

2.4 Others

Mặc định, sidekiq sử dụng duy nhất một queue là default trong redis. Nếu muốn sử dụng nhiều queue thì chỉ việc định nghĩa thêm queue, và vấn đề tiếp theo là làm thế nào để biết queue nào được ưu tiên xử lý.

Ví dụ ta có 2 queue sau:

sidekiq -q critical,2 -q default

Chỉ số phía sau queue gọi là weight của queue, queue critical sẽ được check/fetch 2 lần so với queue default có weight là 1.

Hoặc nếu muốn xử lý job theo thứ tự khai báo, đơn giản là định nghĩa queue không có trọng số

sidekiq -q critical -q default -q low

=> Nghĩa là job trong default queue chỉ được xử lý khi critical queue rỗng.

Một số vấn đề khác về bao nhiêu queue là đủ, concurrent của sidekiq bao nhiêu là hợp lý có thể tìm thấy trong wiki sidekiq

3. Chốt

Nhưng tui đâu có biết Ruby đâu, vậy tại sao tui phải đọc bài này làm chi? Thực ra mục đích ban đầu của mình là tìm hiểu một số vấn đề của sidekiq như bao nhiêu queue là đủ, bao nhiêu sidekiq process hoặc tại sao lại timeout nhưng sau khi tìm hiểu xong học thêm đc khá nhiều thứ hay ho về cách thiết kế, implement và một số khái niệm mới:

Ngoài ra, sau khi đọc bài này bạn cũng sẽ hiểu một số metric cần thiết cho việc monitor nhằm đảm bảo tính tin cậy cho xử lý backgroud job, ví dụ một số metric cần lưu ý như:

4. Ref