2 điểm bởi GN⁺ 2024-11-04 | 1 bình luận | Chia sẻ qua WhatsApp
  • Tổ hợp PostgreSQL+Redis phổ biến trong các dịch vụ web rất tiện lợi, nhưng một số phần trong hàng đợi tác vụ nền, khóa phân tán và Pub/Sub cũng có thể xử lý chỉ bằng PostgreSQL
  • FOR UPDATE SKIP LOCKED trong PostgreSQL 9.5 được dùng để triển khai hàng đợi bằng cách bỏ qua các hàng đang bị khóa thay vì chờ, ngăn nhiều worker cùng lấy một tác vụ
  • Khóa phân tán ở cấp ứng dụng có thể được tạo bằng advisory locks của PostgreSQL, tái sử dụng engine khóa nội bộ cho các mục đích do ứng dụng định nghĩa
  • LISTEN/NOTIFY trong PostgreSQL 9 hỗ trợ đăng ký kênh chuỗi tùy ý và gửi thông báo, nên có thể dùng làm tầng Pub/Sub; Rails ActionCable cũng hỗ trợ sẵn việc dùng PostgreSQL
  • Redis vẫn có thế mạnh về caching TTL và thao tác dữ liệu tạm thời, nhưng một số hệ thống có thể giảm phụ thuộc vào Redis để hạ chi phí vận hành và độ phức tạp phát triển

Những vai trò của Redis có thể được hấp thụ vào PostgreSQL

  • Một dịch vụ web thông thường dùng PostgreSQL làm kho dữ liệu và dùng Redis để điều phối hàng đợi tác vụ nền hoặc thực hiện các phép toán nguyên tử ở mức hạn chế
  • Bản thân Redis rất hữu ích, nhưng một số vai trò mà Redis đảm nhiệm trong tổ hợp này có thể được thay thế chỉ bằng các tính năng của PostgreSQL

Hàng đợi tác vụ nền

  • Redis thường được dùng để điều phối hàng đợi tác vụ nhằm chuyển việc cho pool worker nền trong các dịch vụ web
    • Ghi lại các tác vụ nền cần thực thi và dữ liệu đầu vào
    • Phải đảm bảo chỉ một trong nhiều worker lấy tác vụ đó
    • Redis có nhiều phép toán nguyên tử trên cấu trúc dữ liệu, nên rất phù hợp cho mục đích này
  • Từ PostgreSQL 9.5, có thể dùng tùy chọn SKIP LOCKED trong câu lệnh SELECT ... FOR ...
    • Khi chỉ định tùy chọn này, PostgreSQL sẽ bỏ qua các hàng mà nó phải chờ mở khóa
    • Khi chỉ định FOR UPDATE SKIP LOCKED, khóa cấp hàng được ngầm lấy đối với các hàng được trả về
    • Nhờ SKIP LOCKED, không có khả năng bị chặn bởi khóa của giao dịch khác
    • Nếu có tác vụ khác cần xử lý, tác vụ đó sẽ được trả về
    • Ngay cả khi nhiều worker chạy cùng một lệnh, chúng sẽ không nhận cùng một hàng do khóa cấp hàng
  • Luồng cơ bản là trong một transaction, chọn một tác vụ ở trạng thái pending, đổi tác vụ đó sang running, rồi trả về
    • BEGIN
    • SELECT ... FOR UPDATE SKIP LOCKED
    • UPDATE jobs SET status = 'running' ... RETURNING jobs.*
    • COMMIT
  • Điểm cần lưu ý là khi cả số worker và số tác vụ đều lớn, chi phí quét hàng đợi và thử lấy khóa có thể tăng lên
    • Trên thực tế, hầu hết các ứng dụng từng được xử lý có dưới 12 worker nền, nên khả năng chi phí này không lớn

Khóa ứng dụng

  • Có những trường hợp như routine đồng bộ với dịch vụ bên thứ ba, nơi chỉ một instance được phép chạy cho một người dùng cụ thể trên tất cả các tiến trình server
  • Loại khóa phân tán này là một use case phổ biến khác của Redis
  • PostgreSQL có thể đạt cùng mục đích bằng advisory locks
    • advisory locks cho phép sử dụng engine khóa mà PostgreSQL dùng nội bộ cho các mục đích do ứng dụng định nghĩa

Pub/Sub

  • Redis cũng thường được dùng khi đẩy sự kiện tới các client đang hoạt động
    • Thông báo rằng người dùng có thể đọc tin nhắn mới
    • Stream dữ liệu tới client ngay khi dữ liệu sẵn sàng
    • Thông thường websocket là tầng truyền sự kiện, còn Redis đóng vai trò engine Pub/Sub
  • Từ PostgreSQL 9, các câu lệnh LISTENNOTIFY cung cấp chức năng Pub/Sub
    • Client PostgreSQL có thể đăng ký bằng LISTEN vào một kênh thông điệp cụ thể là chuỗi tùy ý
    • Khi client khác gửi NOTIFY tới kênh đó, mọi client đang đăng ký sẽ nhận được thông báo
    • Có thể đính kèm thêm một thông điệp nhỏ nếu muốn
  • Nếu đang dùng Rails và ActionCable, việc sử dụng PostgreSQL được hỗ trợ sẵn

Những lĩnh vực đáng để giữ Redis lại

  • Redis đảm nhiệm các lĩnh vực khác PostgreSQL và mạnh ở những việc mà PostgreSQL không nhắm tới
    • Caching dữ liệu có TTL
    • Lưu trữ và thao tác dữ liệu tạm thời
  • PostgreSQL có phạm vi tính năng rộng hơn nhiều so với cách nhìn nhận nó chỉ là một cơ sở dữ liệu SQL đơn giản hay một thành phần mờ phía sau ORM
  • Một số tác vụ đã giao cho Redis có thể cũng phù hợp với PostgreSQL
  • Bỏ qua Redis có thể là một lựa chọn giúp giảm chi phí vận hành và độ phức tạp phát triển phát sinh khi phụ thuộc vào nhiều dịch vụ dữ liệu

1 bình luận

 
GN⁺ 2024-11-04
Các ý kiến trên Hacker News
  • Vì mọi người đều khăng khăng theo kiến trúc phân tán quá mức, nên nhiều khi họ không thấy được lợi thế thật sự của Redis. Nếu chạy trên cùng máy với ứng dụng, nó có thể phản hồi nhanh hơn rất nhiều so với 1 mili giây, và nhờ vậy ứng dụng có thể làm những việc mà Postgres khó làm được
    Đúng là Postgres rất tuyệt, nhưng nó không chạy trong bộ nhớ trên cùng máy với ứng dụng. Nếu chỉ cần những thứ như hàng đợi, có thể bạn không cần kho key-value in-memory. Điểm cốt lõi của kho key-value in-memory là làm các việc cần đặc tính hiệu năng của RAM, và bạn không thể có được đặc tính đó qua một kết nối mạng

    • Nếu chỉ có một tiến trình cục bộ dùng Redis trong máy làm cache in-memory, thì tốt hơn là dùng luôn cấu trúc dữ liệu của ngôn ngữ lập trình đang dùng
    • Tôi tò mò overhead của Postgres và Redis khác nhau bao nhiêu khi chạy cục bộ. Cũng không hiểu vì sao lại xem Postgres là không chạy cục bộ được
      Postgres không có phép màu đặc biệt nào cả, nó chỉ là một chương trình chạy trong tiến trình khác giống Redis. Với kết nối cục bộ, nó dùng pipe nhanh để giảm độ trễ, và cũng có thể dùng các phương thức truyền dữ liệu khối lượng lớn nhanh hơn. Tôi đã dùng kiểu này nhiều lần
    • Django có cache tích hợp hỗ trợ Redis, và cũng có tùy chọn cache in-memory, nhưng được ghi là “không dùng cho production”. Lý do là nếu có nhiều instance Django, mỗi cache in-memory sẽ khác nhau
      Nhưng với các công cụ nghiệp vụ nội bộ, bạn có thể dùng một instance duy nhất trong thời gian dài, và cache in-memory này giúp nó rất nhanh. django-cachalot là thư viện tự động xử lý invalidation cache mỗi khi có ghi vào bảng. Cách làm hơi thô, nhưng đem lại cải thiện hiệu năng gần như không tốn công sức; các ứng dụng nghiệp vụ nội bộ ít cập nhật về cơ bản chạy từ RAM, và chỉ quay lại truy vấn cơ sở dữ liệu thông thường khi cache miss
      https://github.com/noripyt/django-cachalot
    • Thứ nhanh hơn Redis trong cấu hình này là hashmap. Cũng không có lý do gì Postgres không thể chạy trên cùng máy chủ với app, và thực tế đây là cấu hình khá phổ biến
    • Thiết kế quá mức và phân tán hóa quá sớm là vấn đề có thật, nhưng Redis là viết tắt của “Remote Dictionary Server”. Mục đích ban đầu hoàn toàn không phải để chạy cục bộ. Tất nhiên, chạy cục bộ cũng có thể là một lựa chọn thiết kế hợp lý, chẳng hạn khi dictionary mặc định của ngôn ngữ không hỗ trợ truy vấn theo khoảng
  • Thảo luận ở đây có nhiều ý mang tính phòng thủ từ góc nhìn Redis, nhưng tất nhiên Redis có những lĩnh vực cụ thể mà nó tốt hơn
    Tuy vậy tôi nghĩ trọng tâm của bài viết không phải là điều đó. Câu cốt lõi có thể tóm gọn là: “PostgreSQL có nhiều tính năng hơn rất nhiều so với những gì bạn kỳ vọng khi tiếp cận nó như một cơ sở dữ liệu SQL đơn giản hoặc một thực thể bí ẩn ẩn sau ORM”. Nếu bạn chỉ dùng cơ sở dữ liệu đằng sau ORM, rất có thể bạn đang bỏ lỡ tính năng của bất kỳ cơ sở dữ liệu nào. Nếu cần thêm một dịch vụ khác như Redis, có thể dùng cơ sở dữ liệu đã cấu hình sẵn sẽ tốt hơn là thêm một dependency mới

  • Hiểu được Postgres có thể làm gì là điều tốt. Đây là một cơ sở dữ liệu mạnh
    Phản biện là rào cản để dùng Redis rất thấp, và đổi lại bạn có hiệu năng cao, hỗ trợ thư viện phong phú, đồng thời giảm tải cho cơ sở dữ liệu chính. Ví dụ, bạn có thể tạo cache phản hồi API bằng Postgres. TTL có thể được xử lý bằng một cron job quét các giá trị cache cũ. Hoặc đơn giản là dùng Redis
    Advisory lock rất hay và hữu ích, nhưng nếu dùng thứ như PgBouncer, có thể phát sinh vấn đề giữa session advisory lock và transaction interleaving. Một hệ thống riêng có các nhược điểm như gọi qua mạng, tính sẵn sàng, kiến thức miền, nhưng mức đánh đổi của Redis thì khá thấp

    • Giảm được một thứ phải quản lý trong production là một lợi thế. Hãy bắt đầu với PostgreSQL, rồi thêm hệ thống chuyên biệt khi hiệu năng, mở rộng hoặc chi phí khiến nó trở nên cần thiết
  • Đây là bài khá cũ, nhưng giờ đã trở thành một pattern rất phổ biến. 90% dự án chỉ cần hàng đợi tác vụ để gửi email hoặc tạo báo cáo không xử lý hàng triệu message mỗi giây, nên cách đơn giản hóa stack rất đáng được cân nhắc
    Tôi thường dùng pattern này để né các vấn đề gặp phải với Celery, rồi tách nó thành một framework riêng: https://github.com/TkTech/chancy Rất hoan nghênh phản hồi
    Có nhiều công cụ như vậy, trong đó một số là dịch vụ thương mại, nên rõ ràng có nhu cầu
    https://worker.graphile.org/ (Node.js)
    https://riverqueue.com/ (Go)
    https://github.com/acaloiaro/neoq (Go)
    https://github.com/contribsys/faktory (Go)
    https://github.com/sorentwo/oban (Elixir)
    https://github.com/procrastinate-org/procrastinate (Python)

    • Tôi đang tìm một hàng đợi tác vụ đơn giản. Huey cũng ổn, nhưng tôi đã dùng Postgres rồi, và tác vụ đưa vào hàng đợi chỉ khoảng một lần mỗi giờ, nên giải pháp dựa trên Redis luôn cảm giác hơi quá mức
    • Bên Node cũng có PG-Boss: https://github.com/timgit/pg-boss
  • PGQueuer dùng FOR UPDATE SKIP LOCKEDLISTEN/NOTIFY của PostgreSQL để cung cấp hàng đợi tác vụ, khóa và thông báo thời gian thực
    Đây là một lựa chọn thay thế tối giản không cần Redis nếu bạn đã dùng PostgreSQL
    https://github.com/janbjorge/PGQueuer
    Nhân tiện, tôi là người tạo ra nó

  • Tôi thích Postgres, nhưng nó có một số giới hạn
    Nếu cần kho lưu trữ khóa-giá trị, bạn phải cân nhắc liệu mình có hiểu autovacuum không, có biết giới hạn của connection pool không, và muốn ưu tiên thông lượng hay độ an toàn. Nếu cần hàng đợi, cần xem đó là xử lý tuần tự, có giới hạn tốc độ, fan-out hay tách theo chủ đề. Nếu cần publish/subscribe, cần cân nhắc có quan tâm đến việc nhận trùng lặp không, có quan tâm đến mất tin nhắn không, và có cần phát lại không. Nếu cần khóa, bạn phải hiểu giới hạn của connection pool và statement_timeout. Phần lớn các vấn đề trên đều có thể giải quyết, nhưng không đơn giản đến vậy

    • Cũng cần xem liệu phần triển khai có sụp đổ khi các transaction chạy lâu ngăn vacuum loại bỏ tuple hay không
  • Trở ngại lớn trong publish/subscribe của Postgres là kích thước tối đa của tin nhắn chỉ 8000 byte
    Cách né được khuyến nghị là đưa dữ liệu vào bảng rồi chỉ gửi ID, nhưng nếu không muốn giữ dữ liệu đó mãi mãi thì phải garbage collect nó, và mỗi tin nhắn lại phát sinh thêm việc. Tất nhiên có những trường hợp cách này vẫn ổn, nhưng trong nhiều use case của Redis, giới hạn này khiến khó có thể xem là tương đương

  • Hãy xem pgsql có xử lý được 15000 kết nối client không

    • Khác biệt giữa Postgres và MySQL ở điểm này thật sự lớn đến đáng ngạc nhiên
      Một người bên Planetscale từng nói trên podcast rằng mỗi instance MySQL của GitHub xử lý hơn 50.000 kết nối. Trong khi đó, với Postgres, nếu cần hơn 100 kết nối thì gần như đã cần PgBouncer rồi
    • Có thể dùng connection pooling. Có thể dùng chỉ với vài cú nhấp chuột, như AWS RDS Proxy
  • Hàng đợi, khóa, publish/subscribe đều có thể làm được. Nhưng mục đích quan trọng nhất của Redis là caching thì lại bị bỏ sót
    Update trong Postgres nổi tiếng là tốn kém hơn insert, tạo ra rác và cần vacuum. Việc ghi cũng chậm đi đáng kể vì các bảo đảm về độ bền dữ liệu vốn không quan trọng đối với caching. Tự động hết hạn rất tiện và giúp giảm sai sót

    • Phần lớn các tính năng không cần thiết đều có thể tắt. Ví dụ có thể tạo bảng ở dạng unlogged: https://www.postgresql.org/docs/current/sql-createtable.html...
      Cũng có thể tắt synchronous commit, autovacuum, v.v. Tất nhiên Redis có lẽ vẫn nhanh hơn, nhưng mức chênh lệch có thể không đáng để một công ty thông thường phải bận tâm
  • Nói rõ hơn ý chính của bài viết: hãy bắt đầu với Postgres, rồi nếu phát sinh nhu cầu thì chuyển sang Redis
    Tốt nhất là giữ số lượng thành phần vận hành ở mức ít nhất có thể

    • Redis không hẳn là khó triển khai. Thay vì lên kế hoạch cho một lần chuyển đổi mà không biết sau này sẽ có tác dụng phụ ngoài dự kiến nào, dùng Redis ngay từ đầu cũng là một lựa chọn ổn