- vừa được xem lúc

Nâng tầm kỹ năng backend với kỹ thuật sử dụng task queue

0 0 19

Người đăng: Thao Hoang

Theo Medium

Bạn đang trên con đường tu luyện để trở thành một backend developer siêu đẳng? Bạn đang cố gắng học hỏi, trau dồi thêm nhiều kiến thức, kỹ năng mới để làm giàu skill set của mình? Nếu cảm thấy việc thực hiện các thao tác CRUD đã quá quen thuộc và muốn học hỏi những kỹ thuật được áp dụng để xử lý cho những vấn đề phức tạp hơn thì task queue rất có thể là thứ tiếp theo bạn muốn tìm hiểu.

Đặt vấn đề

Chúng ta sẽ bắt đầu bằng việc xem xét ví dụ sau đây, giả sử bạn là một kỹ sư phần mềm tiềm năng trong công ty, phòng truyền thông muốn phát triển một công cụ để hỗ trợ các chiến dịch marketing qua email. Bạn được yêu cầu thiết kế một chức năng cho phép lọc những người dùng thoả mãn một số yêu cầu nhất định và thực hiện gửi email đến họ với nội dung được cung cấp trước. Với yêu cầu như vậy chúng ta có thể đưa ra ngay một giải pháp đơn giản như sau:

Như vậy, mỗi khi ai đó trong team marketing dùng phần mềm của bạn để gửi email, bạn sẽ xử lý từng email một cho đến hết rồi trả về kết quả. Thử hình dung nếu việc gửi mỗi email mất 0.1 giây và bạn cần gửi cho 10.000 khách hàng, khi đó sẽ phải mất 1000 giây, tương đương 16 phút để thực thi xong và kết quả mới được trả về. Chưa kể tới 16 phút là một thời gian quá dài, kết nối giữa client và server sẽ bị timeout. Một giải pháp cải tiến ở đây là ta sẽ gửi email theo batch, giả sử mỗi batch có thể gửi 1000 email cùng lúc và cũng vẫn tốn 0.1 giây cho mỗi lần thực thi, như vậy ta cần gửi 10 batch thời gian phản hồi giảm xuống còn 1 giây. Vậy nếu input đầu vào lớn hơn thay vì gửi tới 10.000 khách hàng mà gửi tới 100.000, 200.000 khách hàng thì sao? Dễ dàng tính được thời gian xử lý sẽ lên đến 10 giây, 20 giây. Với thời gian xử lý lâu như vậy cho một request thì UX của ứng dụng sẽ tương đối tệ. Từ khi “bấm nút” người dùng phải chờ hàng chục giây nhìn màn hình loading trong lúc server xử lý, chẳng may trong lúc đấy người dùng có reload hay tắt đi rồi bật lại thì mọi thứ trở nên thật rắc rối.

Tình hình còn tệ hơn vào lúc cao điểm, khi mà rất nhiều user sử dụng tính năng này cùng lúc, việc server bị quá tải vì phải xử lý đồng thời nhiều tác vụ nặng là điều dễ dàng xảy ra. Đó chính là vấn đề với hướng xử lý đồng bộ hay còn gọi là synchronous, khi mà phải chờ tác vụ thực thi xong thì response mới được trả về. Có một hướng tiếp cận khác để giải quyết vấn đề trên hiệu quả hơn đó chính là xử lý bất đồng bộ — asynchronous.

Giới thiệu giải pháp

Hãy thử hình dung một vấn đề tương tự ngoài đời sống, giả sử hôm nay là một ngày đặc biệt, bạn muốn đặt một chiếc bánh thật đẹp và ngon cho người ấy. Việc bạn làm là gì? Bạn tới cửa hàng, chọn hương vị, kiểu dáng và chốt với cửa hàng, nhân viên sẽ cho bạn một phiếu hẹn và bạn tiếp tục công việc của mình, đi làm, đi chơi, ngủ, game… và tới giờ thì bạn đến lấy bánh. Cửa hàng sẽ sắp xếp và làm bánh xong cho bạn trước thời điểm đã hẹn. Bạn không phải chờ ở cửa hàng cả ngày tới khi bánh được chuẩn bị xong.

Ý tưởng giải quyết vấn đề ở đây cũng tương tự như vậy. Thay vì xử lý công việc được yêu cầu ngay lập tức, hãy bỏ nó vào queue và sắp xếp xử lý sau, trả về một reference ví dụ như id của task, người dùng có thể sử dụng id này để theo dõi tình hình, tiến độ, trạng thái của task. Bạn cũng có thể chủ động gửi notification mỗi khi task chuyển trạng thái để đưa tới một trải nghiệm người dùng tốt hơn.

So sánh xử lý đồng bộ và bất đồng bộ

Với bài toán gửi mail, sau khi người dùng submit task, hệ thống sẽ không thực hiện việc gửi mail ngay mà trả về thông báo rằng task đã được tiếp nhận và sẽ được xử lý sớm, trong thời gian này người dùng có thể tiếp tục làm những công việc khác, mỗi khi có thay đổi về trạng thái cũng như khi task được xử lý xong người dùng sẽ nhận được thông báo. Quá trình thực sự gửi mail sẽ được thực hiện phía sau. Task queue chính là một kỹ thuật xử lý bất đồng bộ như thế. Các thành phần, cơ chế hoạt động của một hệ thống task queue như thế nào, chúng ta sẽ cùng tìm hiểu trong bài viết hôm nay nhé.

Định nghĩa

Task queue là một hệ thống giúp cho một ứng dụng có thể thực thi các tác vụ (tasks) một cách bất đồng bộ bên ngoài phạm vi thời gian của một user request thông thường. Đối với các hệ thống lớn, đôi khi việc nhận và xử lý các yêu cầu từ phía người dùng hoặc các tác vụ cần nhiều thời gian, do đó mô hình đồng bộ (synchronous) trong cơ chế request-response không còn phù hợp, thay vào đó là mô hình xử lý bất đồng bộ (asynchronous). Task queue thường được dùng trong các bài toán như: thực thi tác vụ tốn nhiều thời gian (long-running task), các tác vụ cần delay thời gian xử lý (delayed task) hay xử lý tác vụ cần tính toán phức tạp (compute-intensive task). Để có cái nhìn rõ ràng hơn, chúng ta hãy cùng nhau tìm hiểu về kiến trúc và cơ chế hoạt động của hệ thống task queue ngay sau đây.

Kiến trúc và cơ chế hoạt động

Về cơ bản, một hệ thống bất đồng bộ sử dụng task queue sẽ có những thành phần như sau:

  • Producer: Service gửi các tác vụ đến queue.
  • Message broker: Đây chính là nơi chứa queue mà chúng ta nhắc đến xuyên suốt bài viết này. Nhiệm vụ của broker là nhận tác vụ từ producer, lưu chúng vào queue và gửi đến consumer tương ứng, giữ vai trò trung gian quản lý các tác vụ, và làm cầu nối giữa producer-consumer. Một số message broker được sử dụng phổ biến hiện nay có thể kể đến RabbitMQ, Redis hay Kafka.
  • Consumer: Service nhận và xử lý tác vụ từ queue.
Các thành phần và cơ chế hoạt động của hệ thống task queue

Thông thường, producer và consumer là hai thành phần tách biệt như ví dụ sẽ mô tả dưới đây. Tuy nhiên cũng có thể implement để một service vừa là producer, vừa là consumer bằng cách task được gửi từ producer đến broker, rồi broker gửi ngược lại về service đang chạy producer và tác vụ sẽ được xử lý tại đây. Rất linh hoạt đúng không?

Ngoài ra, để producer và consumer giao tiếp được với nhau, thông thường chúng sẽ thống nhất sử dụng chung định dạng message, đó có thể là JSON, pickle hoặc một định dạng khác. Khi đó producer sẽ serialize task dưới định dạng chung và gửi đến broker, broker lưu các task dưới dạng này vào queue, rồi sau đó consumer deserialize task và tiến hành thực thi.

Áp dụng

Xác định các thành phần của hệ thống task queue với bài toán đã nêu

Như chúng ta đã tìm hiểu, trong một hệ thống task queue sẽ có ba thành phần chính: producer, message broker và consumer. Bây giờ ta sẽ quay lại bài toán ban đầu và xác định từng thành phần để có hình dung rõ hơn.

  • Producer — Với nhiệm vụ chính là đẩy task vào queue, trong bài toán gửi mail ta có thể dễ dàng xác định sau khi nhận được request từ người dùng, hệ thống sẽ không xử lý ngay mà tạo một task và đẩy task này vào queue. Như vậy server xử lý request từ client chính là producer.
  • Message broker — Như đã giới thiệu ở phần trước, một số message broker phổ biến hiện nay là RabbitMQ, Kafka hay Redis. Trong ví dụ với Celery dưới đây, để đơn giản chúng ta sẽ sử dụng Redis.
  • Consumer — Thành phần thực sự xử lý tác vụ, ở đây là việc gửi mail. Chúng ta sẽ cài đặt và chạy Celery worker để đảm nhiệm vai trò consumer.

Ví dụ với Celery

Celery là một hệ thống distributed task queue được sử dụng rất phổ biến trong cộng đồng Python. Hệ thống này tuy đơn giản nhưng rất linh hoạt và có tính ổn định cao. Celery tập trung vào việc xử lý các tác vụ real-time. Chi tiết thêm về Celery có thể tham khảo tại http://celeryproject.org/.

Bây giờ ta sẽ tiến hành cài đặt các thành phần. Lưu ý các cài đặt dưới đây chỉ mang ý nghĩa minh hoạ để bạn đọc hình dung tổng quan cách một hệ thống task queue hoạt động nên những phần không cần thiết sẽ được lược bỏ.

Consumer

Trước tiên ta cần khởi tạo Celery, message broker được sử dụng ở đây là Redis. Logic xử lý việc gửi marketing email được cài đặt trong hàm send_marketing_email_by_batches. Ta sẽ cần chia người nhận thành từng batch và xử lý từng batch với mỗi batch có kích thước 1.000 items.

Để có thể nhận và xử lý task ta sẽ cần chạy worker, lệnh tham khảo như sau:

Trong đó tasks là tên Celery app, tham số loglevel=info cho ta thấy được các thông tin khi task được nhận và xử lý.

Producer

Giả sử ta sử dụng Flask để nhận và xử lý request từ phía người dùng. Trong đoạn code dưới đây, ta sẽ khai báo một endpoint để phục vụ việc gửi email của team marketing.

Với cách cài đặt như trên, việc gửi email đã được tách riêng với việc xử lý HTTP request. Tác vụ này sẽ được thực hiện bất đồng bộ bởi Celery worker và không hề ảnh hưởng tới các tác vụ khác của người dùng.

Các trường hợp nên sử dụng task queue và ví dụ thực tế

Qua phần bên trên chắc hẳn các bạn đã phần nào hình dung được cách thức hoạt động của task queue nói chung và cách cài đặt task queue sử dụng Celery với Python nói riêng. Vậy câu hỏi đặt ra là với những trường hợp nào thì chúng ta nên sử dụng task queue. Dưới đây là một vài trường hợp sử dụng phổ biến:

  • Các tác vụ tính toán phức tạp (compute-intensive task). Những tác vụ này khi thực thi có thể chiếm dụng nhiều tài nguyên của hệ thống (CPU, memory…) dễ dàng gây quá tải cho server. Việc sử dụng task queue giúp các tác vụ được phân phối phù hợp với năng lực xử lý của worker. Một ví dụ dễ hình dung cho trường hợp sử dụng này thể hiện ở tính năng upload video lên Youtube hay Facebook. Bạn có để ý rằng sau khi một video được upload thành công thì Facebook hay Youtube sẽ cần mất một vài phút để hậu xử lý video không? Trong khoảng thời gian ấy, video sẽ được kiểm duyệt bởi rất nhiều thuật toán phức tạp khác nhau. Thay vì chạy lần lượt thì các thuật toán sẽ được đưa vào và xử lý bởi task queue. Quá trình kiểm duyệt kết thúc là lúc bạn nhận được thông báo rằng video đã sẵn sàng.
  • Các tác vụ tốn nhiều thời gian xử lý (long-running task). Chính là ví dụ đã đưa ra ở phần đầu, sử dụng task queue giúp rút ngắn thời gian xử lý request, hệ thống phản hồi nhanh hơn, đem lại trải nghiệm người dùng tốt hơn. Một ví dụ khác có thể thấy trong các ứng dụng đặt xe. Rõ ràng tại thời điểm bạn gửi yêu cầu đặt xe, hệ thống cần phải mất một thời gian tìm kiếm tài xế gần đó, gửi yêu cầu đặt xe cho tài xế và chờ tài xế phản hồi. Quy trình này không diễn ra liên tục và mất nhiều thời gian vì còn phụ thuộc vào phản hồi của tài xế, task queue có thể được sử dụng ở đây để quá trình này được xử lý hiệu quả.
  • Các tác vụ cần delay thời gian xử lý (delayed task). Đôi khi có những tác vụ ta muốn delay tới một khoảng thời gian nhất định trong tương lai. Ta có thể đưa chúng vào task queue, đến thời điểm đã hẹn tác vụ sẽ được lấy ra xử lý. Một ví dụ cho trường hợp này là xử lý timeout của các bài thi, tại thời điểm bắt đầu làm bài ta sẽ đưa vào task queue một task chấm điểm bài làm và delay nó một khoảng thời gian bằng thời gian tối đa người dùng có thể dùng để làm bài. Tại thời điểm timeout, hệ thống sẽ tự động chấm bài (nếu chưa submit trước đó) mà không cần phải chờ hành động phát sinh từ phía người dùng.
  • Các tác vụ có độ ưu tiên thấp (low priority task). Với những tác vụ bên lề tác vụ chính, không yêu cầu phải thực thi ngay ta cũng có thể đưa chúng vào queue qua đó giảm thời gian xử lý, rút ngắn thời gian chờ của người dùng. Chẳng hạn như tác vụ gửi email xác nhận sau khi đăng ký tài khoản thành công. Rõ ràng tác vụ này có độ ưu tiên thấp hơn so với việc tạo tài khoản trong hệ thống nên có thể được đưa vào task queue. Đó là lý do vì sao tại với nhiều hệ thống, bạn sẽ chỉ nhận được email thông báo xác nhận khoảng một vài giây sau khi tài khoản đăng ký thành công.

Cuối cùng là ví dụ thực tế về chức năng tạo câu hỏi trong hệ thống hỏi đáp của Got It. Đối với những bạn đọc chưa biết về Got It, các bạn có thể tìm hiểu ở đây. Khi người dùng gửi câu hỏi lên hệ thống của Got It, thông thường câu hỏi đó sẽ được gửi kèm cùng với một loại file nào đó. Đó có thể là file Excel, là một câu truy vấn SQL hay một bức ảnh chụp một phương trình toán học. Các file này sau khi upload thành công sẽ được đọc và xử lý để phục vụ quá trình trao đổi giữa người hỏi và chuyên gia sau đó. Vì các tác vụ đọc, xử lý file là một quá trình tốn thời gian và tương đối phức tạp nên chúng sẽ được đưa vào task queue để tránh ảnh hưởng đến trải nghiệm người dùng. Ngoài ra task queue còn được hệ thống của Got It sử dụng trong các các trường hợp như gửi dữ liệu tracking, logging, analytic…

Đánh giá và kết luận

Như bạn có thể thấy, task queue sẽ đem đến nhiều lợi ích nếu được sử dụng đúng cách. Bằng việc xử lý các tác vụ bất đồng bộ, task queue giúp chúng ta được unblock, cải thiện response time qua đó đem lại trải nghiệm người dùng tốt hơn.

Cũng bằng việc đưa các tác vụ vào queue và xử lý sau, task queue giúp ta chủ động hơn trong việc xử lý chúng. Các tác vụ này sẽ được lấy ra từ queue tùy theo khả năng của các worker. Trong trường hợp quá nhiều tác vụ được yêu cầu thực thi cùng lúc, task queue sẽ giúp tránh quá tải hệ thống. Vì worker xử lý tác vụ có thể tách ra thành một service riêng, ta cũng dễ dàng scale lên khi cần thiết mà không cần phải scale toàn bộ hệ thống.

Tuy nhiên việc lạm dụng task queue một cách không cần thiết sẽ làm tăng độ phức tạp của hệ thống, qua đó làm tăng chi phí vận hành. Bản thân ứng dụng cũng phải xử lý những trường hợp phức tạp hơn, chẳng hạn việc phải quản lý trạng thái của task và thông báo cho người dùng khi cần thiết, xử lý task thực thi bị lỗi, cơ chế retry…

Task queue cũng như bất kỳ giải pháp nào khác, có những ưu điểm và nhược điểm, chúng ta cần phân tích và đánh giá mức độ phù hợp cho từng trường hợp trước khi áp dụng. Trong tương lai, nếu có cơ hội, các kỹ sư của Got It sẽ gửi đến bạn đọc một bài viết chia sẻ thêm về kinh nghiệm trong quá trình sử dụng task queue cũng như cách giải quyết các vấn đề đã gặp phải.

Cảm ơn bạn đã đọc đến đây, hy vọng bài viết đã đem lại cho bạn những kiến thức bổ ích, hãy chia sẻ nếu bạn thấy hay. Đừng quên theo dõi blog để đón nhận những bài viết tiếp theo của team engineer ở Got It nhé!

Tài liệu tham khảo

Bình luận