6 điểm bởi GN⁺ 2023-09-25 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trước khi đưa vào một hệ thống hàng đợi riêng, nếu có thể xây dựng hàng đợi tác vụ nền chỉ bằng Postgres vốn đang dùng, bạn có thể tận dụng đáng kể sự đơn giản trong vận hành và mức độ quen thuộc của đội ngũ
  • NOTIFY/LISTENFOR UPDATE SKIP LOCKED từ Postgres 9.5 trở đi cùng giải quyết việc thông báo tác vụ mới và ngăn xử lý trùng lặp giữa các worker
  • Redis, Kafka, RabbitMQ, Amazon SQS là những lựa chọn mạnh, nhưng phụ thuộc mới sẽ bổ sung các chế độ lỗi và chi phí học tập vào môi trường phát triển, kiểm thử và vận hành
  • Hàng đợi Postgres cũng không phải vạn năng: push/pop có thể chậm hơn Redis, và có thể phát sinh chi phí vận hành như cơ sở dữ liệu hoặc máy chủ hàng đợi riêng, hay VACUUM thường xuyên hơn
  • Nên trừu tượng hóa mã tác vụ nền để không bị ràng buộc vào backend hàng đợi, và chỉ xem xét công nghệ hàng đợi khác khi công nghệ quen thuộc không đáp ứng được yêu cầu

Cách hàng đợi Postgres hoạt động

  • Công nghệ hàng đợi Postgres gồm hai thành phần
    • pub/sub để thông báo và nhận tác vụ mới
    • khóa hàng để ngăn nhiều worker xử lý cùng một tác vụ
  • Cả hai tính năng đều được cung cấp sẵn từ Postgres 9.5, phát hành năm 2016
  • Dùng NOTIFY cùng với LISTEN có thể gắn pub/sub vào ứng dụng
  • FOR UPDATE SKIP LOCKED sẽ lấy khóa hàng trên các bản ghi khớp điều kiện và bỏ qua những bản ghi đã bị khóa
    • Khi áp dụng cho bản ghi tác vụ, có thể tạo truy vấn xử lý hàng đợi như SELECT * FROM jobs ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1
  • SKIP LOCKED cung cấp một góc nhìn “không nhất quán” về dữ liệu, nhưng đặc tính này lại hữu ích trong hàng đợi
    • Các tác vụ đang được xử lý sẽ không hiển thị với worker khác do khóa hàng
    • Hành vi này cho phép loại trừ lẫn nhau phân tán
  • Khi dùng NOTIFY để báo tác vụ mới cho các worker đang LISTEN, có thể xây dựng cả xử lý theo lô định kỳ lẫn xử lý tác vụ thời gian thực

Vì sao Redis trở thành mặc định cho tác vụ nền

  • Xử lý các tác vụ chạy lâu là một lựa chọn kỹ thuật có nhiều trade-off
  • Các lựa chọn hàng đợi/message broker được dùng rộng rãi gồm
    • Redis: kho dữ liệu in-memory và backend của nhiều thư viện tác vụ nền
    • Apache Kafka: nền tảng streaming sự kiện phân tán do Apache Foundation quản lý
    • RabbitMQ: được giới thiệu là message broker được triển khai rộng rãi
    • Amazon SQS: SaaS của Amazon cung cấp hàng đợi có khả năng mở rộng cao
  • Trong background jobs topic trên GitHub, 5 thư viện phổ biến nhất đều dựa trên Redis
    1. Sidekiq — Ruby
    2. resque — Ruby
    3. rq — Python
    4. Hangfire — C#
    5. asynq — Go
  • Redis lưu dữ liệu trong bộ nhớ nên chèn và truy vấn nhanh, đồng thời cung cấp API pub/sub và các cấu trúc dữ liệu list, set, phù hợp để triển khai hàng đợi
  • Khả năng mở rộng của Redis khiến nó trở thành lựa chọn mặc định với nhiều developer, và mặc định có sức ảnh hưởng lớn trong lựa chọn công nghệ

Tiêu chí cần xem xét trước khả năng mở rộng

  • Nỗi ám ảnh “scale” của ngành công nghệ có thể đẩy sự đơn giản, khả năng bảo trì và việc giảm tải nhận thức cho developer xuống phía sau
  • Nhiều ứng dụng không cần mở rộng ở mức Google, Facebook hay Uber
  • Khi bắt đầu một dự án hoặc doanh nghiệp, thay vì tối ưu khả năng mở rộng ngay từ đầu, cần xem xét trước các tiêu chí sau
    • Công nghệ mà đội ngũ hiểu rõ
    • Công nghệ đủ tốt để đáp ứng nhu cầu người dùng
    • Giải pháp phù hợp với năng lực đội ngũ và tốn ít công sức nhất
  • Postgres trên thực tế cũng mở rộng tốt, nhưng trong use case hàng đợi, nó không phải hệ thống hàng đợi chuyên dụng có khả năng mở rộng mạnh nhất
  • Là phần mềm đa dụng, Postgres xử lý khá tốt nhiều việc, và hàng đợi cũng có thể được xem là một trong số đó

Đánh giá theo “công nghệ nhàm chán”

  • Khi chọn công nghệ hàng đợi, câu hỏi quan trọng nhất là hiện bạn đang dùng và hiểu rõ công nghệ nào
  • Công nghệ đã dùng và đã hiểu rõ sẽ tạo ít gánh nặng hơn khi thêm vào software stack
  • Nếu bạn đã dùng cơ sở dữ liệu quan hệ và cơ sở dữ liệu đó là Postgres, có thể cân nhắc hàng đợi Postgres trước các phần mềm khác
  • “Công nghệ nhàm chán” không phải là một danh sách cố định mà thay đổi tùy theo công nghệ hiện đang sử dụng
    • Với ứng dụng tập trung vào truyền thông điệp, RabbitMQ có thể là công nghệ nhàm chán
    • Với ứng dụng tập trung vào caching, Redis có thể là công nghệ nhàm chán
    • Với ứng dụng có nhiều dữ liệu quan hệ, Postgres có thể là công nghệ nhàm chán
  • Nếu đưa Redis, Kafka, RabbitMQ, SQS vào chỉ để phục vụ tác vụ nền, chi phí sẽ lớn
    • Thêm phụ thuộc hệ thống mới vào môi trường phát triển, kiểm thử và production
    • Trong tương lai, Developer, DBA, SRE phải biết các chế độ lỗi và cấu hình của hệ thống mới
    • Cần kiến thức về khôi phục sự cố, chẩn đoán vấn đề và giám sát hiệu năng
    • Những unknown unknowns mà đội ngũ chưa biết vẫn là rủi ro

Cái giá của hàng đợi Postgres và tiêu chí lựa chọn

  • Công nghệ nhàm chán không phải vạn năng, và Postgres cũng không ngoại lệ
  • Chọn hàng đợi Postgres mang lại các lợi ích như sự quen thuộc, các chế độ lỗi đã biết và phân tán chi phí, nhưng đổi lại có thể phải trả các cái giá sau
    • Push/pop của hàng đợi Postgres chậm hơn Redis đáng kể
    • Thay vì một cơ sở dữ liệu quan hệ duy nhất, có thể cần cơ sở dữ liệu ứng dụng và cơ sở dữ liệu hàng đợi riêng
    • Có thể cần máy chủ cơ sở dữ liệu riêng để mở rộng tác vụ nền một cách độc lập
    • Có thể cần VACUUM thường xuyên hơn, tạo thêm gánh nặng hiệu năng
  • Không nên để Postgres, Redis, Kafka, RabbitMQ hay SQS trở thành mặc định tự động
  • Lựa chọn công nghệ là một trade-off liên tục, cần được đánh giá theo yêu cầu của đội ngũ và ứng dụng
  • Khi còn nghi ngờ, có thể áp dụng tiêu chí “chỉ cân nhắc phương án thay thế khi đã chứng minh được rằng công nghệ nhàm chán không đáp ứng yêu cầu”

Kiến trúc tác vụ nền có thể thay đổi hàng đợi

  • Mã xử lý tác vụ nền không nên phụ thuộc vào một công nghệ hàng đợi cụ thể
  • Khi ứng dụng phát triển, các công nghệ như memcached hay Redis có thể được thêm vào khi cần, và theo thời gian chúng cũng có thể trở thành “công nghệ nhàm chán” quen thuộc với đội ngũ
  • Nếu trừu tượng hóa công nghệ hàng đợi, người dùng có thể chọn hàng đợi phù hợp với tác vụ
  • Trong các thư viện tác vụ nền phổ biến nhất trên GitHub, ngoại trừ Hangfire, các thư viện còn lại không cung cấp lối thoát sang công nghệ hàng đợi khác ngoài Redis
    • Với cấu trúc như vậy, muốn đổi hàng đợi thì phải viết lại mã ứng dụng
  • Neoq là thư viện Go được xây dựng theo triết lý này
    • Backend hàng đợi có thể dùng in-memory, Postgres hoặc Redis
    • Người dùng có thể khởi tạo bằng backend hàng đợi khác mà không cần đổi mã ứng dụng
    • Triển khai in-memory và Postgres được cung cấp trực tiếp, còn triển khai Redis dùng asynq
  • Redis, Kafka, RabbitMQ, SQS có thể là những công nghệ tuyệt vời, nhưng không phải lúc nào cũng là hàng đợi phù hợp cho công việc đó hoặc có mức độ phức tạp thích hợp

1 bình luận

 
GN⁺ 2023-09-25
Các ý kiến trên Hacker News
  • Đến nay tôi đã xây dựng hệ thống tác vụ phân tán ba lần, và kinh nghiệm thực tế mà tôi khuyên trong nhiều năm là “hãy thiết kế dựa trên quy mô gấp 10 lần hiện tại”
    Nếu hiện tại là 70 request/giây thì lấy mốc 700 request/giây; nếu cần 20 máy chủ chạy batch job thì lấy mốc 200 máy. Ngay cả startup tăng trưởng 100% mỗi năm thì sau 3 năm cũng chỉ lớn hơn 8 lần, nên vẫn có thời gian để viết lại khi tăng trưởng
    Hệ thống đầu tiên tôi làm đã tránh SQL vì “khả năng mở rộng”, nhưng rồi gặp rất nhiều tình huống ngoại lệ cần tính toàn vẹn giao dịch và khá khổ sở
    Hai hệ thống tác vụ phân tán sau đó dùng PostgreSQL làm bộ điều phối, và đúng nghĩa là được xây quanh SELECT FOR UPDATE SKIP LOCKED. Một hệ thống thường điều khiển 350 worker, hệ thống kia áp dụng mức ưu tiên phức tạp cho hàng nghìn tác vụ
    Cả hai sẽ trụ tốt cho đến khi đạt quy mô doanh thu hằng năm hàng triệu đô la, và chẳng hạn hệ thống 350 worker kia chỉ cần chỉnh một chút là có thể mở rộng tới khoảng 2.000 CPU
    Công nghệ siêu quy mô trông có vẻ khá rẻ cho đến khi bạn nhận ra mình cần transaction. Nhưng từ khoảnh khắc đó, việc giả lập ngữ nghĩa transaction trên một kho lưu trữ eventual consistency trở thành ác mộng kỹ thuật
    Vì vậy hãy ngồi xuống và tính thử. Nếu công ty kiếm 100 triệu đô la mỗi năm thì hệ thống phân tán cần lớn đến mức nào, liệu có dễ kiếm một instance PostgreSQL xử lý được tải đó không, hoặc shard theo từng client có đủ không; nếu được thì rất đáng cân nhắc PostgreSQL một cách nghiêm túc. Một trăm chuyện nhỏ sẽ trở nên dễ hơn

    • Tôi cũng có trải nghiệm tương tự. Đã nhiều lần tôi đẩy queue dựa trên SQL đi xa hơn hàng chục lần so với quy mô mà người ta thường nói “SQL sẽ sập ở đây và bắt buộc phải dùng giải pháp phân tán”
      Giải pháp SQL thường đơn giản hơn, ăn ít tài nguyên tính toán hơn và dễ hỗ trợ vận hành hơn
      Tuy nhiên để làm cho nó chạy được, bạn phải hiểu database đủ rõ để biết ngay từ đầu rằng có những tính năng như SELECT FOR UPDATE SKIP LOCKED. Ngày nay ngày càng nhiều kỹ sư lớn lên trong môi trường các ORM nặng nề che khuất năng lực thật sự của DBMS, nên kiến thức kiểu này đang trở nên khá hiếm
    • Hãy thiết kế và kiểm thử theo quy mô gấp 10 lần hiện tại, nhưng triển khai thì chỉ cần đúng mức hiện tại cần
      Hệ thống phải chịu được tải đỉnh, và nếu không biết tải đỉnh là bao nhiêu thì phải chừa biên an toàn hoặc thêm cách để bỏ tác vụ hay trì hoãn khi cần
      Mọi thứ đều là đánh đổi; chỉ tối ưu những gì cần tối ưu, và khả năng phán đoán đâu là thứ đó là đặc điểm của một kỹ sư giỏi
    • Câu “giả lập ngữ nghĩa transaction trên một kho lưu trữ eventual consistency là ác mộng kỹ thuật” thật sự rất quan trọng
      Nếu điều này còn mới với bạn, thì bạn vừa thu lại đủ giá trị cho một tuần đọc HN trong giờ làm rồi; đáng để viết ra giấy và dán lên thẻ ATM
    • Trong PostgreSQL cũng có thể tạo queue dựa trên advisory lock, chậm hơn nhiều nhưng cũng có ưu điểm
  • Ở nhiều dự án, tôi chọn cách tiếp cận đơn giản hơn: dùng bảng thông thường và SELECT FOR UPDATE SKIP LOCKED, thứ có sẵn ngay trong ORM/query DSL framework của mọi ngôn ngữ
    https://www.pgcasts.com/episodes/the-skip-locked-feature-in-...
    Không phải “web scale”, nhưng theo kinh nghiệm của tôi thì có thể dễ dàng mở rộng đến hàng nghìn background job

    • Tôi từng làm còn đơn giản hơn, không dùng lock vì không có logic transaction. Chọn một hàng, rồi thử cập nhật trường đánh dấu “đã lấy”; nếu số hàng bị ảnh hưởng là 1 thì đó là việc của tôi, nếu là 0 thì ai đó đã lấy trước, nên chọn hàng mới
      Tôi đã dùng cách này cho công việc ở một tổ chức lớn mà không gặp vấn đề. Không cần triển khai đặc biệt hay hạ tầng mới, chỉ cần chạy vài worker thread trong app. Cũng có thể có một thread đặt lại các tác vụ bị bỏ rơi
      Tuy nhiên trong 3 năm thực tế chưa từng xảy ra tình huống đó; mọi thứ đều nằm trong try/catch, nếu thất bại thì đưa lại vào queue, và ứng dụng Java cũng rất ổn định
    • Gần đây có người đã công bố một manifesto và đoạn mã đúng cho cách này trong PostgreSQL
      delete from task
      where task_id in
      ( select task_id
      from task
      order by random() -- use tablesample for better performance
      for update
      skip locked
      limit 1
      )
      returning task_id, task_type, params::jsonb as params
      [1] https://taylor.town/pg-task
    • Việc hệ thống queue không mở rộng đúng cách là loại vấn đề không nên để đến cuối cùng mới biết. Vì sau khi đã phát hiện hệ thống queue về mặt cấu trúc là không thể mở rộng, rất khó sửa mà không gây mất dữ liệu
      Bạn nói “hàng nghìn background job”, nhưng queue thường nên được nhìn theo định luật Little, tức là phải nói đến các tỷ lệ như tốc độ đưa tác vụ vào trung bình mỗi giây và thời lượng trung bình của tác vụ. Chỉ số lượng thô không có nhiều ý nghĩa
      Ban đầu dùng UPDATE ... SET ngây thơ cũng được, nhưng nó giữ quá nhiều lock. Bạn có thể dùng subquery SELECT trong UPDATE, hoặc dùng SELECT FOR UPDATE SKIP LOCKED để tối ưu lock khi lấy tác vụ, nhưng cuối cùng các truy vấn lấy tác vụ có thể chặn lock của nhau và làm queue dừng lại
      Khi đó bạn có thể tắt luồng đưa tác vụ vào để DB thở được, nhưng các tác vụ đưa vào bị mất sẽ gây mất dữ liệu, và thường vấn đề cốt lõi là các thao tác lấy tác vụ chặn lẫn nhau
      Cũng có thể vội vàng shard bảng tác vụ để tránh lock, nhưng việc phân phối trên nhiều worker rất mong manh và có thể dẫn đến mất dữ liệu. Bạn cũng có thể bỏ một phần tác vụ ngẫu nhiên, nhưng dĩ nhiên đó là mất dữ liệu
      Những lựa chọn này cực kỳ căng thẳng khi đang vận hành, và nếu không thiết kế lại từ đầu thì cũng rất khó khôi phục. Trừ khi bạn chỉ có vài khách hàng và chắc chắn mãi mãi chỉ ở mức vài chục tác vụ mỗi giây, còn sau khi tự trải qua tình huống này, nếu có thể tôi sẽ chọn công nghệ queue thật sự thay vì database
    • https://github.com/graphile/worker của Graphile cũng được triển khai đại khái theo cách này
  • Tôi thường thấy kiểu phê phán như “các kỹ sư bắt chước hạ tầng của FAANG để trông ngầu, dù nó hoàn toàn khác nhu cầu của họ”, nhưng tôi nghĩ phần lớn là vấn đề về kiến thức và tài liệu hóa
    Nếu muốn làm theo FAANG hoặc các startup khác để xây dựng kiến trúc dựa trên hàng đợi có thể mở rộng vô hạn, có hàng chục hướng dẫn, tutorial và white paper chất lượng cao giúp bạn thiết lập các giải pháp có khả năng mở rộng như Redis, SQS trong vài giờ
    Chi phí bảo trì cao hơn, nhưng bạn có thể bắt đầu bằng cách copy-paste lệnh, code và cấu hình từ các nguồn có uy tín
    Ngược lại, nếu muốn dùng NOTIFY của PostgreSQL và tìm “SQLALchemy notify listen postgres”, bạn chỉ thấy vài câu hỏi Stack Overflow không có lời đáp và vài GitHub Gist thiếu ngữ cảnh
    Tôi rất muốn thử cách này trong side project, nhưng không có dư 2–3 ngày tự mày mò một mình
    Nhìn bề ngoài, lựa chọn có vẻ là giữa “đơn giản nhưng không mở rộng được, tức chỉ dùng PostgreSQL” và “phức tạp nhưng mở rộng được, tức Redis/SQS, v.v.”; khi đó người ta sẽ nói tôi bị công nghệ hào nhoáng và FAANG làm mờ mắt nên chọn khả năng mở rộng không cần thiết
    Nhưng nếu tính cả hướng dẫn và tài liệu, lựa chọn thực tế gần với “phức tạp và khả năng mở rộng cũng không dự đoán được, vì tôi không biết cách triển khai và các cạm bẫy” so với “đơn giản và có thể mở rộng, cách mà mọi người thực sự đang làm”. Khi đó lựa chọn của kỹ sư đi theo FAANG trông hợp lý hơn nhiều

    • Ưu điểm của những công nghệ “nhàm chán” như PostgreSQL là tài liệu rất tốt. Muốn dùng NOTIFY thì chỉ cần đọc https://www.postgresql.org/docs/current/sql-notify.html, không cần kỹ năng tìm kiếm
    • Khả năng mở rộng có cái giá của nó. Nếu đưa vào khi không cần, nó làm giảm tính linh hoạt, và với startup thì đó chính là trạng thái không mong muốn
      Ví dụ, nếu dùng PostgreSQL với tải thấp, các việc như migration schema, thêm ràng buộc mới, phân tích gần như là chuyện nhỏ
      Dùng SQS, Cassandra, v.v. thì có được khả năng mở rộng/tính sẵn sàng, nhưng khi nhận ra thiết kế ban đầu không phù hợp, việc thay đổi sẽ mất thời gian hơn nhiều. Chẳng hạn khi phía kinh doanh yêu cầu thêm ràng buộc kiểu “người dùng loại foo không được kết hợp đồng thời giá trị bar”
      Không có PostgreSQL vẫn có thể triển khai, nhưng nhất là khi cần thay đổi thì không dễ hoặc đơn giản
      Vì vậy để giữ tính linh hoạt, tôi nghĩ nên dùng PostgreSQL, và khi biết chắc điều gì sẽ không thay đổi thì hãy dùng thêm thứ khác trên nền PostgreSQL. Tất nhiên trường hợp này sẽ phát sinh overhead về hạ tầng và bảo trì
      Rốt cuộc luôn là sự đánh đổi, và cần biết đánh đổi cái gì, khi nào, với cái gì
    • Bài này nhắm nhiều hơn tới nhà phát triển thư viện hơn là nhà phát triển ứng dụng
      Tôi không nghĩ việc mọi người tự triển khai worker tác vụ nền tùy chỉnh trong ứng dụng, dù backend là PostgreSQL hay hàng đợi khác, là lý tưởng
      Tác vụ nền có nhiều điểm tinh vi và chi tiết triển khai rất dễ sai, nên thường tốt hơn là để một thư viện hoặc framework chuyên dụng, bao quát hơn đảm nhận
      Nếu mọi ứng dụng Rails đều không có Sidekiq/Active Jobs mà mỗi nơi tự triển khai worker nền tùy chỉnh, có lẽ ứng dụng Rails đã mang tiếng tệ hơn nhiều về độ tin cậy so với hiện nay
    • Điểm “bảo trì cao hơn” sẽ giết chết các startup nhỏ. Tất nhiên cũng nhận được nhiều thứ, nhưng nếu công ty phá sản thì chẳng còn ý nghĩa gì
      Chỉ cần chọn giải pháp mang lại lợi ích lớn nhất mà không đi quá xa mục tiêu chính
      Tôi từng thấy một startup nơi các lập trình viên dùng khoảng 80% thời gian để vật lộn với công cụ và hạ tầng. Runway còn 3 tháng, và giờ ở cuối đường băng đó chỉ còn một cái hố lớn. Nghĩ lại vẫn còn rùng mình
    • Nếu lập luận là đi theo thiết kế cấp FAANG sẽ tiết kiệm thời gian, thì cơ sở cốt lõi chỉ là có thể tìm thấy hướng dẫn online
      Có vẻ chưa đủ sự tôn trọng đối với độ phức tạp
  • Nhược điểm lớn nhất khi dùng PostgreSQL làm bus publish/subscribe cùng LISTEN/NOTIFYLISTEN là tính năng theo session, nên không phù hợp với connection pooling theo từng câu lệnh
    Nếu dùng cách này, nên dùng advisory lock. Các loại khóa tường minh khác tạo gánh nặng lớn hơn cho database, còn advisory lock được thiết kế có chủ đích để rất nhẹ
    Ví dụ triển khai tôi thích là que, đã được port sang nhiều ngôn ngữ

    • Một trong những lý do tôi không thích NOTIFY/LISTEN là khi có vấn đề thì khó chẩn đoán
      Gần đây cũng có vấn đề là sau một thời gian, tất cả NOTIFY/LISTEN đều dừng, chỉ khởi động lại database mới giải quyết được, nên đã phải ngừng dùng https://dba.stackexchange.com/questions/325104/error-could-n...
    • Không biết có phiên bản Node của Que không
  • Skype nhiều năm trước đã dùng PostgreSQL làm hàng đợi cùng với một plugin nhỏ để xử lý toàn bộ CDR. Không biết bây giờ họ còn dùng không, nhưng theo chuẩn 10 năm trước thì đó là “quy mô web”, và trong khi mọi người trên Internet tranh luận rằng dùng database làm queue là anti-pattern, nó vẫn cứ chạy tốt
    Có transaction thì khá tiện
    https://wiki.postgresql.org/wiki/SkyTools
    Vì đã dùng khá nhiều trong công việc nên tôi cũng từng vài lần thuyết trình về chủ đề này ở Sydpy. Nếu bạn đã vận hành và hỗ trợ PostgreSQL tốt rồi thì nó hữu ích
    Tuy nhiên nếu là bây giờ thì có lẽ tôi sẽ dùng queue chuyên dụng, và riêng RabbitMQ thì sẽ tránh

    • Rất muốn nghe giải thích thêm về phần “riêng RabbitMQ thì sẽ tránh”. Cũng tò mò về kinh nghiệm hoặc suy nghĩ của những người khác
    • Tôi đồng ý với việc bây giờ nên dùng queue chuyên dụng. Lý do chính là trong môi trường cloud, hầu hết nhà cung cấp cloud đều có công nghệ queue được host sẵn rất dễ dùng và rẻ
      Ngay cả khi lo bị khóa vào vendor, các thao tác cơ bản của queue thực ra nhỏ đến mức gần như chỉ là push/pop, nên viết theo cách dễ chuyển đi khi cần cũng tương đối dễ
    • Tôi tò mò không biết bạn đã có trải nghiệm tệ với RabbitMQ chưa. Nếu có, đó là do bản thân công nghệ, hay do các yếu tố xung quanh như tài liệu hoặc cộng đồng?
    • Cách dùng database làm queue khi chạy tốt thì rất tuyệt, nhưng khi hỏng thì đẩy bạn vào trạng thái rất khó khôi phục
      Nếu vì đã vận hành database sẵn mà nhét cả queue vào database, thì lập luận đó là con dao hai lưỡi. Không có gì đáng chúc mừng, nhưng mớ hỗn độn của queue giờ có thể kéo sập cả kho dữ liệu chính
  • Một trong những lợi thế lớn nhất khi dùng PostgreSQL làm queue ứng dụng là mọi tác vụ bất đồng bộ đã lập lịch đều được hưởng lợi từ tính transaction
    Ví dụ có một thay đổi backend phức tạp sau khi đăng ký thành viên để gửi email; nếu sau khi đưa tác vụ gửi email vào mà phần sau đó trong cùng transaction thất bại và rollback, thì tác vụ email đó ngay từ đầu sẽ không được đưa vào queue

    • Cần nói rõ rằng khi kết nối với các hệ thống không idempotent khác, chắc chắn phải chọn một trong hai ngữ nghĩa ít nhất một lần hoặc nhiều nhất một lần
      Với email, nếu chờ xác nhận gửi rồi thất bại, bạn phải chọn giữa làm transaction thất bại và chấp nhận khả năng email bị gửi trùng, hoặc tiếp tục và chấp nhận khả năng mất email
      Lợi thế lớn nằm ở các đường code sửa DB bất đồng bộ. Vì việc consume tác vụ và cập nhật DB nằm trong cùng một transaction, nên có thể xử lý hoàn toàn bằng transaction cho ngữ nghĩa đúng một lần
    • Đây là một điểm thật sự quan trọng. Vì SQS giúp dễ auto scale cụm xử lý tác vụ, tôi thường dùng kết hợp PostgreSQL và SQS
      Trong PostgreSQL, tôi đặt một bảng log transaction chứa sự kiện được trigger và pg_current_xact_id() của transaction đó. Cũng có thể dùng xmin tích hợp của hàng, nhưng phải để ý transaction wraparound. Khi insert vào hàng này thì NOTIFY sẽ xảy ra
      Một background process chạy lặp lại, chọn tất cả các hàng trong bảng transaction có transaction ID nằm giữa xmin của lần chạy trước và pg_snapshot_xmin(pg_current_snapshot()) hiện tại. Nó map các sự kiện đó thành tác vụ rồi gửi lên SQS, ghi lại xmin, sau đó dùng LISTEN để chờ NOTIFY tiếp theo
    • Chúng tôi cũng giảm nhẹ vấn đề này một chút bằng cách lên lịch để việc thêm vào queue được thực hiện sau khi commit. Dù vậy vẫn còn phần không an toàn, và nếu kết nối RabbitMQ bị sập thì sẽ rắc rối
    • Việc báo cho database rằng thứ gì đó đã được xử lý và việc bắn message sang nơi như RabbitMQ không thể là transaction 100%. Đây chính là lý do lớn nhất của cách làm này
      Với queue PostgreSQL, đúng là nếu sau khi đưa tác vụ gửi email vào mà transaction thất bại về sau, email sẽ không vào queue
      Code database cần tách biệt hoàn toàn thì đáng được tách thành các transaction khác nhau. Ngược lại, nếu user chưa được tạo trong DB thì bạn có thể cũng không muốn gửi email đăng ký, nên còn tùy tình huống
    • Một lợi thế khác là đảm bảo transaction hoàn tất trước khi tác vụ được lấy
      Với queue dựa trên Redis hoặc gần như bất kỳ queue nào khác, bạn sẽ nhanh chóng gặp tình huống một tác vụ chạy với giả định rằng bản ghi database đã tồn tại trước khi transaction được commit. Code để sửa việc này thường gượng gạo và phức tạp
  • Điều tôi thích ở Kafka là nó đơn giản là một log chỉ append, và client về bản chất chỉ giữ offset
    Về mặt khái niệm thì rất dễ hiểu, bền vững, và khá chống chịu lỗi, vì có thể quay lại bất kỳ offset nào để đọc lại
    Đáng tiếc là do đặc tính phân tán, Kafka kéo theo đủ độ phức tạp, nên cuối cùng với hầu hết use case thì không đáng
    Cá nhân tôi muốn có thứ gì đó tương tự nhưng dễ vận hành hơn. Trên một node đơn lẻ, nó có thể xử lý hàng trăm, thậm chí hàng nghìn sự kiện mỗi giây; nếu không có độ phức tạp phân tán thì sẽ khá tốt
    Về lý thuyết PostgreSQL cũng có thể làm được, chỉ cần không bao giờ xóa hàng. Có lẽ đó cũng có thể là câu trả lời

    • PostgreSQL có ID tự tăng tương đương “offset” native và cũng có thể partition theo ngày, nên tôi xem đây là ứng viên rất tốt để thay thế Kafka đơn giản
      Nếu không cần toàn bộ tính năng như consumer group, partition, thì cấu hình consumer cũng sẽ đơn giản hơn nhiều
    • Tôi từng hy vọng FasterLog một ngày nào đó sẽ đủ trưởng thành
  • Windmill đang dùng đúng cách này. Windmill là một lựa chọn mã nguồn mở thay thế Retool và là công cụ gần với một Airflow hiện đại; họ chạy benchmark hằng ngày
    Khi chạy một worker Windmill và PostgreSQL bằng container trên một instance GitHub CI vừa phải, benchmark xử lý 1.200 tác vụ mỗi giây. Thêm worker thì mở rộng ổn định lên đến 5.000 tác vụ mỗi giây
    Họ đang xem xét dùng Citus để vượt rào cản 5.000 tác vụ mỗi giây trên instance multi-tenant
    https://github.com/windmill-labs/windmill/tree/benchmarks

  • Thời còn ở mức khoảng 10 thông điệp mỗi giây, một số hàng đợi từng dùng PostgreSQL và cũng mở rộng được khá tốt. Nhưng nói thật, việc thiết lập SQS hay các stack hàng đợi khác trên AWS, GCP, Azure quá dễ, chúng được tạo ra đúng cho mục đích đó, và những thứ như DLQ cũng được tích hợp sẵn
    Tôi không thấy lý do gì để chọn con đường này, rồi phải lo hệ thống đó sụp đổ và ảnh hưởng đến cả trạng thái của phần DB còn lại
    Tôi thích nguyên tắc “hãy dùng công cụ đơn giản nhất”, nhưng đôi khi có vẻ các kỹ sư đi quá xa, để rồi dù các lựa chọn thay thế phổ biến khá rẻ và đơn giản, cuối cùng chỉ còn lại công cụ ngớ ngẩn nhất kèm theo những lưu ý mơ hồ

    • Cách tôi chọn là lưu hầu hết trạng thái công việc trong DB, còn hàng đợi công việc chỉ dùng để đánh thức worker xử lý công việc đó
      Lưu trạng thái công việc trong DB giúp có thể truy vấn trạng thái một cách dễ nhìn. Dù không thể hiện chính xác trạng thái thực tế, nó vẫn hữu ích khi phân tích sự cố trong vận hành. Đặc biệt là hầu hết hàng đợi công việc chỉ đơn giản xóa các bản ghi đã xử lý
      Và nếu biến mọi tác vụ nền thành idempotent, thì cách làm kiểu “gửi một thông điệp vào hàng đợi công việc để yêu cầu xử lý tác vụ này” gần như luôn an toàn
      Nếu chỉ dựa vào message queue, khi có vấn đề hiệu năng thường rất khó biết chuyện gì đang xảy ra. Ví dụ RabbitMQ có thể cho biết kích thước hàng đợi, nhưng gần như không có, hoặc rất hạn chế, chức năng kiểm tra dữ liệu bên trong
    • Vì transaction và tính nhất quán dữ liệu. Đây là câu trả lời không thể có được với SQS
    • Vì mỗi phần mềm có yêu cầu khác nhau. Với appliance on-premise/B2B, việc không cần dịch vụ bên ngoài PostgreSQL tự nó có thể là một tính năng
      Có những phần mềm có thể được dự đoán là sẽ không bao giờ vượt quá năng lực của PostgreSQL, và ngay cả nếu vượt quá thì cũng có thể thiết kế để dễ chuyển sang dịch vụ khác
      Và cũng có thể đơn giản là muốn xây dựng một hệ thống công việc có transaction ngay trong PostgreSQL
  • Chúng tôi dùng Oban của Elixir để xử lý hàng trăm nghìn đến hàng triệu tác vụ mỗi ngày bên trong PostgreSQL
    Việc có ngữ nghĩa transaction quanh các tác vụ nền, chẳng hạn chỉ lên lịch gửi email khi transaction thành công, cực kỳ tiện lợi
    Cần tinh chỉnh autovacuum một chút, nhưng sau khi dọn dẹp xong thì nó rất phù hợp với chúng tôi