Có cần Redis không? PostgreSQL cung cấp hàng đợi, khóa và Pub/Sub (2021)
(spin.atomicobject.com)- 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 LOCKEDtrong 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/NOTIFYtrong 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 LOCKEDtrong câu lệnhSELECT ... 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ụ đó sangrunning, rồi trả vềBEGINSELECT ... FOR UPDATE SKIP LOCKEDUPDATE 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
LISTENvàNOTIFYcung cấp chức năng Pub/Sub- Client PostgreSQL có thể đăng ký bằng
LISTENvào một kênh thông điệp cụ thể là chuỗi tùy ý - Khi client khác gửi
NOTIFYtớ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
- Client PostgreSQL có thể đăng ký bằng
- 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
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
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
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ả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
Đâ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)
PGQueuer dùng FOR UPDATE SKIP LOCKED và LISTEN/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
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
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
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
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ể