Có hai cách đưa dữ liệu từ nơi này sang nơi khác, và chúng khác nhau y như chuyển nhà khác với đăng ký nhận bản sao. Chuyển nhà thì bê hết đồ sang chỗ mới, nhà cũ trống trơn. Đăng ký nhận bản sao thì bản gốc vẫn nằm nguyên, bạn chỉ nhận một bản chép đều đặn về tay. Shovel và Federation của RabbitMQ là đúng hai cách đó — cùng nối hai broker rời nhau, nhưng khác nhau ở đúng một chữ: chuyển hay chép. Và chọn nhầm chữ đó là âm thầm mất dữ liệu hoặc âm thầm nhân đôi nó.

Phần 34 nối ba node thành một cụm — chúng chia sẻ metadata và cần mạng ổn định. Bài này nối hai broker hoàn toàn rời nhau: hai cụm khác nhau, có thể ở hai trung tâm dữ liệu, chỉ nói chuyện qua AMQP thường.

Hai công cụ, và khác biệt giữa chúng nằm ở đúng một chữ.

Shovel: chuyển thông điệp

docker exec -u rabbitmq bkA rabbitmqctl set_parameter shovel chuyen-sang-B '{
  "src-protocol":"amqp091", "src-uri":"amqp://",        "src-queue":"nguon",
  "dest-protocol":"amqp091","dest-uri":"amqp://guest:guest@bkB","dest-queue":"dich"}'

Gửi 20 000 thông điệp vào nguon của broker A:

trước khi gửi : A:nguon=0      B:dich=0
sau 12 giây   : A:nguon=0      B:dich=20000

Toàn bộ thông điệp đã sang B, và A không còn gì. Shovel là một consumer bình thường: nó đọc từ hàng đợi nguồn rồi publish sang đích, nên thông điệp rời khỏi nguồn.

Tốc độ chuyển, đo bằng cách nạp sẵn 50 000 rồi bật shovel:

chuyển 50 000 thông điệp sang broker khác: 1,2 s (40 234 msg/s)

Nhanh, nhưng nhớ rằng đó là một consumer đơn: nó không nhân lên theo số node, và với thông điệp lớn hoặc đường truyền xa thì con số này sẽ khác hẳn.

Federation: chép thông điệp

Federation làm điều ngược lại. Trên broker B (bên nhận), khai một upstream trỏ về A rồi gắn policy lên exchange:

docker exec -u rabbitmq bkB rabbitmqctl set_parameter federation-upstream tu-A \
  '{"uri":"amqp://guest:guest@bkA","expires":3600000}'
docker exec -u rabbitmq bkB rabbitmqctl set_policy fed-su-kien "^su-kien$" \
  '{"federation-upstream":"tu-A"}' --priority 1 --apply-to exchanges

Gửi 1 000 thông điệp vào exchange su-kien của A:

A:nghe-A=1000   B:nghe-B=1000

Cả hai bên đều nhận đủ. Bên nghe ở A không mất gì, còn B có thêm bản của mình.

Cơ chế lộ ra khi nhìn danh sách hàng đợi của A:

name                                 messages
nghe-A                               1000
federation: su-kien -> rabbit@bkB    0

Federation tự tạo một hàng đợi trên broker nguồn, bind vào exchange cần chép, rồi kéo về. Biết điều này rất có ích lúc gỡ lỗi: nếu hàng đợi đó phình lên, nghĩa là đường truyền sang B đang không theo kịp.

Federation cần plugin ở cả hai broker

Lần đầu tôi chỉ bật rabbitmq_federation trên B — bên nhận, nơi link chạy. Kết quả:

trạng thái: {server_initiated_close, 404,
  "NOT_FOUND - no exchange 'federation: su-kien -> rabbit@bkB B' in vhost '/'"}
log: Federation link could not create a disposable (one-off) connection: function_clause

Bật plugin trên A nữa thì mọi thứ chạy ngay. Thông báo lỗi không hề gợi ý điều đó — nó nói về một exchange không tồn tại, khiến người ta đi tìm sai chỗ.

Shovel thì ngược lại: chỉ cần plugin ở phía chạy shovel, đầu kia không cần biết gì cả, vì với nó shovel chỉ là một client AMQP bình thường.

Bảng so sánh

Shovel Federation
Thông điệp ở nguồn bị lấy đi còn nguyên
Đơn vị nối hàng đợi → hàng đợi (hoặc exchange) exchange → exchange (hoặc hàng đợi)
Khai ở đâu broker chạy shovel broker nhận
Plugin cần bật một phía cả hai phía
Dấu vết trên nguồn không một hàng đợi federation: ...

Dùng để làm gì

Di trú không dừng dịch vụ. Đây là chỗ shovel toả sáng. Dựng broker mới, shovel hàng đợi cũ sang, đợi nguon=0, rồi chuyển consumer sang broker mới. Không mất thông điệp, không có cửa sổ dừng — và nếu hỏng thì chỉ cần xoá shovel.

Chuyển vùng địa lý. Federation đúng hơn khi mỗi vùng cần bản sao của cùng một luồng sự kiện mà vẫn phục vụ được người dùng tại chỗ. Mỗi vùng có exchange riêng, nghe upstream của vùng kia.

Gom về trung tâm. Nhiều broker biên, một broker trung tâm: shovel từng broker biên đẩy về, vì dữ liệu chỉ cần đến đúng một nơi.

Và nhớ điều phần 22 đo được: cả hai cơ chế đều là at-least-once. Đường truyền đứt giữa lúc chuyển thì thông điệp có thể sang hai lần — consumer phía bên kia vẫn phải lũy đẳng.

Muốn biết đường nối đang sống hay chết, hỏi thẳng từng broker:

docker exec -u rabbitmq bkA rabbitmqctl shovel_status
docker exec -u rabbitmq bkB rabbitmqctl federation_status

Với shovel, cột cuối là số thông điệp đã chuyển. Với federation, status khác running nghĩa là đường nối đang hỏng — và thông điệp vẫn dồn trong hàng đợi federation: ... ở đầu bên kia.

Mẫu số chung

Trước mọi thiết kế đồng bộ dữ liệu có đúng một ngã ba, và nó gói trong một câu hỏi: sau khi đưa đi, nguồn còn giữ dữ liệu đó nữa không? Chuyển (shovel) làm nguồn trống — hợp khi dữ liệu chỉ nên tồn tại ở một nơi: di trú, gom về trung tâm, dọn hàng đợi cũ. Chép (federation) giữ nguyên nguồn — hợp khi nhiều nơi cùng cần một bản: nhân vùng địa lý, fan-out cho nhiều bên đọc. Cùng ngã ba ấy hiện ra ở khắp nơi mà người ta hay gọi lẫn tên: mv với cp, git push (chuyển nhánh sang remote) với git fork (nhân cả kho), một database dump-and-load một lần với replication chạy liên tục, std::move với sao chép trong C++. Chọn nhầm nửa này lấy nửa kia là hoặc mất bản duy nhất, hoặc nhân ra hai bản phải hoà giải. Nên đừng hỏi "công cụ nào nhanh hơn" trước; hỏi "tôi muốn nguồn trống hay muốn nguồn còn" — trả lời xong là đã chọn xong.

Điều thứ hai, một bài học gỡ lỗi: thông báo lỗi mô tả triệu chứng ở tầng nó nổi lên, không phải nguyên nhân ở tầng nó bắt nguồn. Federation báo "404 không có exchange" trong khi thủ phạm thật là thiếu plugin ở broker bên kia — đúng chỗ không ai nghĩ tới nhìn. Cùng kiểu lạc hướng ở một NoClassDefFoundError thật ra do sai phiên bản thư viện, một "connection refused" thật ra do sai cấu hình DNS, một "permission denied" thật ra do thiếu thư mục cha. Quy tắc: khi lỗi chỉ vào một chỗ mà chỗ đó trông vô lý, đừng vá đúng câu chữ của nó — lùi xuống một tầng, hoặc sang đầu bên kia của đường nối, vì lỗi được báo ở nơi nó bị phát hiện, hiếm khi ở nơi nó được sinh ra.

Bài sau: vhost, phân quyền và TLS.