Phần trước đo xem hàng đợi đổi được cái gì. Bài này dựng một broker thật để nghịch suốt 38 bài còn lại — và đi qua bốn cái bẫy tôi đã vấp đúng trong lúc viết bài này, không phải nghe kể lại.

Tệp compose

services:
  rabbitmq:
    image: rabbitmq:4-management
    container_name: rmq
    hostname: rmq
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - rmq-data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "check_running"]
      interval: 5s
      timeout: 10s
      retries: 10

volumes:
  rmq-data:

docker compose up -d, xong. Bốn dòng đáng nói:

Dòng Vì sao có
4-management Bản kèm giao diện quản trị. Bản trần không có cổng 15672
hostname: rmq Tên node RabbitMQ là rabbit@<hostname>. Bẫy thứ ba ở dưới
volumes Không có nó thì mọi thứ bốc hơi cùng container
healthcheck Compose mới biết lúc nào broker thật sự lên — bẫy thứ nhất

Image nặng 111 MB và khai báo tám cổng, bạn chỉ cần công bố hai. Broker rỗng ăn 146 MiB RAM.

Broker rỗng có sẵn gì

$ docker exec -u rabbitmq rmq rabbitmqctl -q list_vhosts
/
$ docker exec -u rabbitmq rmq rabbitmqctl -q list_users
guest   [administrator]
$ docker exec -u rabbitmq rmq rabbitmqctl -q list_queues
(rỗng)

Một vhost /, một tài khoản guest quyền quản trị, không hàng đợi nào. Mọi thứ khác bạn tự tạo.

Thông điệp đầu tiên

Java thuần, chỉ cần thư viện com.rabbitmq:amqp-client:

var cf = new ConnectionFactory();
cf.setHost("localhost");

try (Connection conn = cf.newConnection();
     Channel ch = conn.createChannel()) {

    ch.queueDeclare("xin-chao", true, false, false, null);
    ch.basicPublish("", "xin-chao", MessageProperties.PERSISTENT_TEXT_PLAIN,
            "Thông điệp đầu tiên".getBytes(UTF_8));

    ch.basicConsume("xin-chao", false, (tag, msg) -> {
        System.out.println("Đã nhận : " + new String(msg.getBody(), UTF_8));
        ch.basicAck(msg.getEnvelope().getDeliveryTag(), false);
    }, tag -> {});
}
Đã khai báo hàng đợi 'xin-chao'
Đã gửi  : Thông điệp đầu tiên
Đã nhận : Thông điệp đầu tiên
  qua exchange '', routing key 'xin-chao'
Còn lại trong hàng đợi: 0 thông điệp

Để ý dòng áp chót: exchange là chuỗi rỗng, routing key trùng tên hàng đợi. Đó không phải "gửi thẳng vào hàng đợi" — nó là default exchange, và nó có một cái bẫy riêng đủ lớn cho cả một bài (phần 12).

Bẫy 1: "cổng đã mở" không phải "broker đã sẵn sàng"

Đo ba lần, từ lúc container khởi động tới lúc hai mốc khác nhau xảy ra:

Lần TCP tới cổng 5672 chấp nhận Bắt tay AMQP thành công
1 6 ms 2 130 ms
2 2 ms 2 496 ms
3 3 ms 2 396 ms
Bộ chuyển cổng của Docker nhận kết nối TCP ngay lập tức, từ trước khi broker kịp lắng nghe. Chờ bằng nc -z hay wait-for-it.sh là chờ nhầm thứ, và sớm hơn sự thật khoảng 400 tới 1 000 lần.

Chờ đúng thì có hai cách: để healthcheck của compose làm, hoặc thử mở hẳn một kết nối AMQP rồi lặp lại tới khi thành công. Đây là lý do CI hay đỏ ở lần chạy đầu rồi xanh khi chạy lại.

Bẫy 2: guest mở ra toàn mạng

Ai cũng nghe câu "tài khoản guest chỉ dùng được từ localhost". Với bản cài thường thì đúng. Với image Docker chính thức thì sai, và chính image nói ra điều đó:

$ docker exec rmq cat /etc/rabbitmq/conf.d/10-defaults.conf
## DEFAULT SETTINGS ARE NOT MEANT TO BE TAKEN STRAIGHT INTO PRODUCTION
## allow access to the guest user from anywhere on the network
loopback_users.guest = false

Nghĩa là công bố cổng 5672 ra Internet với cấu hình mặc định là trao quyền quản trị cho bất kỳ ai gõ guest/guest. Cách chữa gọn nhất là khai user ngay từ đầu:

environment:
  RABBITMQ_DEFAULT_USER: blog
  RABBITMQ_DEFAULT_PASS: mat-khau-that

Đo lại sau khi đặt: danh sách user chỉ còn blog [administrator], guest biến mất hẳn, và thử đăng nhập bằng nó nhận AuthenticationFailureException.

Bẫy 3: volume đúng, hostname sai, dữ liệu vẫn bốc hơi

Với tệp compose ở trên, gửi 5 thông điệp persistent vào một hàng đợi durable rồi khởi động lại ba kiểu:

trước khi khởi động lại : ben-vung  5
sau 'compose restart'   : ben-vung  5
sau 'down' rồi 'up'     : ben-vung  5
sau 'down -v' rồi 'up'  : không còn hàng đợi

Đúng như mong đợi — -v xoá volume nên mất sạch. Nhưng bỏ compose, chạy docker run trần với đúng volume đó mà quên --hostname:

node lần 1     : rabbit@8b45488fb99a   → tạo hàng đợi 'ben-vung'
node lần 2     : rabbit@b9945d513055   → KHÔNG THẤY, dù volume vẫn nguyên

$ ls /var/lib/rabbitmq/mnesia
rabbit@8b45488fb99a       rabbit@b9945d513055

Dữ liệu không mất — nó nằm nguyên trong thư mục của node cũ, chỉ là node mới không biết đến. Docker đặt hostname ngẫu nhiên cho mỗi container, mà tên node RabbitMQ lại gắn chặt vào hostname. Compose thoát bẫy này nhờ tự đặt hostname theo tên service; docker run thì không.

Bẫy 4: đừng gọi rabbitmq-diagnostics bằng root lúc broker đang lên

Cái này tôi vấp lúc viết chính bài này, và nó ăn mất nửa buổi. Vòng lặp chờ tưởng chừng vô hại:

until docker exec rmq rabbitmq-diagnostics -q ping; do :; done

Chạy 3 lần, container chết cả 3, log đầy Error when reading /var/lib/rabbitmq/.erlang.cookie: eacces.

Nguyên nhân: server chạy bằng user rabbitmq (uid 999), còn docker exec mặc định chạy bằng root. Công cụ CLI cũng là chương trình Erlang, nên nó cần cookie — không thấy thì nó tự tạo. Gọi đủ sớm thì root tạo cookie trước, 0400, và server không đọc nổi tệp của chính thư mục nhà mình.

Thêm đúng một cờ là hết:

until docker exec -u rabbitmq rmq rabbitmq-diagnostics -q ping; do :; done

Chạy lại 2 lần, sống cả 2, không dòng lỗi nào. healthcheck trong compose cũng chạy bằng root nhưng không sao — chu kỳ 5 giây nên broker (sẵn sàng ở giây 2,4) luôn tạo cookie trước.

Chốt lại

Dựng broker mất một tệp compose và khoảng ba giây. Bốn cái bẫy trên đều không báo lỗi rõ ràng: một cái làm CI đỏ ngẫu nhiên, một cái mở toang cửa, một cái làm dữ liệu "biến mất" trong khi vẫn nằm trên đĩa, một cái giết broker bằng chính lệnh kiểm tra sức khoẻ.

Bài sau: năm khái niệm của AMQP 0-9-1 — connection, channel, exchange, queue, binding — và vì sao channel mới là đơn vị làm việc chứ không phải connection.

Thử ba mươi giây

docker run -d --name rmq --hostname rmq -p 5672:5672 -p 15672:15672 \
  -v rmq-data:/var/lib/rabbitmq rabbitmq:4-management

# xem broker lên tới đâu — KHÔNG dùng nc -z
docker logs -f rmq | grep -m1 "Server startup complete"

Rồi mở http://localhost:15672 với guest/guest. Vào tab Queues, tạo hàng đợi xin-chao, và để ý là nó đã có sẵn --hostname lẫn volume — hai thứ mà mọi lệnh docker run chép trên mạng đều thiếu.