bigywater.tech
RabbitMQ · 1

AMQP 模型:connection、channel、exchange、queue、binding

connection、channel、exchange、queue、binding 这五个词是 AMQP 的全部家当,但它们各自在网线上长什么样、在 broker 里占什么?这篇用一个 TCP 代理把 AMQP 0-9-1 的帧一帧一帧打出来,再用 rabbitmqctl 和管理 API 看这几类对象在 RabbitMQ 4.3.6 里的真实样子和开销。

7 帧
一次 AMQP 连接握手用到的帧数(协议头不算帧),4 个来自客户端
3 帧
发布一条 5 字节的消息:方法帧 + 内容头 + 内容体
+1100 vs +407
100 个连接(各 1 个 channel)与 1 个连接 100 个 channel,在 broker 里多出的 Erlang 进程数

连接与握手

实测 · 本机执行输出
  C→S  ch0  ProtocolHeader            8 字节
  S→C  ch0  Connection.Start        528 字节
  C→S  ch0  Connection.StartOk      328 字节
  S→C  ch0  Connection.Tune          20 字节
  C→S  ch0  Connection.TuneOk        20 字节
  C→S  ch0  Connection.Open          16 字节
  S→C  ch0  Connection.OpenOk        13 字节
  握手共 7 帧,客户端发 4 帧、broker 发 3 帧;连接上协商出来的 heartbeat = 60 秒,frame_max = 131072
  broker 在 Connection.Start 里告知的产品和版本: RabbitMQ 4.3.6;它支持的能力(capabilities)里,值为 True 的有: ['authentication_failure_close', 'basic.nack', 'connection.blocked', 'consumer_cancel_notify', 'consumer_priorities', 'direct_reply_to', 'exchange_exchange_bindings', 'per_consumer_qos', 'publisher_confirms']

一条 AMQP 连接建立在一条 TCP 连接上:客户端先发 8 字节协议头,接着是 Connection.Start / StartOk(版本、能力、认证)、Tune / TuneOk(协商心跳间隔和最大帧长)、Open / OpenOk(选 vhost)。这些帧都走 0 号通道。握手完才有“channel”可言。

方法帧、内容头、内容体

实测 · 本机执行输出
  C→S  ch1  Exchange.Declare         33 字节
  S→C  ch1  Exchange.DeclareOk       12 字节
  C→S  ch1  Queue.Declare            26 字节
  S→C  ch1  Queue.DeclareOk          27 字节
  C→S  ch1  Queue.Bind               36 字节
  S→C  ch1  Queue.BindOk             12 字节
  C→S  ch1  Basic.Publish            25 字节
  C→S  ch1  ContentHeader            22 字节
  C→S  ch1  ContentBody              13 字节
  C→S  ch1  Basic.Get                22 字节
  S→C  ch1  Basic.GetOk              35 字节
  S→C  ch1  ContentHeader            22 字节
  S→C  ch1  ContentBody              13 字节
  C→S  ch1  Basic.Ack                21 字节
  发布 5 个字节的消息 b"hello" 用了三个帧:Basic.Publish(方法帧)+ ContentHeader(属性与消息总长度)+ ContentBody(消息体);收到的 body = b'hello'

AMQP 的帧有四种:方法(method)、内容头(header)、内容体(body)和心跳。Exchange.Declare 这类操作只有一个方法帧加一个 Ok;发布则是 Basic.Publish + 内容头 + 内容体三个帧,没开 confirm 时不会有回包。每个帧固定 8 字节开销(7 字节头 + 1 字节结尾)。

channel

实测 · 本机执行输出
  $ rabbitmqctl list_connections name state channels peer_port
    Listing connections ...
    name	state	channels	peer_port
    127.0.0.1:58737 -> 127.0.0.1:9504	running	3	58737
  $ rabbitmqctl list_channels number consumer_count messages_unacknowledged
    Listing channels ...
    number	consumer_count	messages_unacknowledged
    1	0	0
    2	0	0
    3	0	0
  pika 里三个 channel 的编号: [1, 2, 3]

channel 是连接内的逻辑通道,共用一条 TCP 连接,各有自己的消费者、确认状态和 prefetch。rabbitmqctl list_connections 的 channels 列和 list_channels 能直接看到。

错误的范围

实测 · 本机执行输出
  对不存在的队列做 passive declare → ChannelClosedByBroker: (404, "NOT_FOUND - no queue 'q.nonexistent' in vhost '/'")
  channel 1 状态: is_open=True;出错的 channel: is_open=False;连接: is_open=True
  $ rabbitmqctl list_channels number consumer_count
    Listing channels ...
    number	consumer_count
    1	0
    3	0
  对没发过的 delivery_tag 做 ack → channel 被关: ["(406, 'PRECONDITION_FAILED - unknown delivery tag 999999')"]
  线上帧: [('C→S', 3, 'Basic.Ack', 21), ('S→C', 3, 'Channel.Close', 68), ('C→S', 3, 'Channel.CloseOk', 12)]
  之后:出错的 channel is_open=False;连接 is_open=True
  声明一个不存在的 exchange 类型 → ChannelClosedByBroker: (406, "PRECONDITION_FAILED - unknown exchange type 'nosuchtype'")
  (broker 发的是 Channel.Close,reply-code 406;只关出错的 channel,连接 c2.is_open=True)
  连一个不存在的 vhost → ProbableAccessDeniedError: ConnectionClosedByBroker: (530) 'NOT_ALLOWED - vhost nope not found'
  用错误的密码 → ProbableAuthenticationError: ConnectionClosedByBroker: (403) 'ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For 
  这两种是连接级错误:broker 发 Connection.Close(或直接断开),整条 TCP 连接都没了,上面所有 channel 一起作废

资源或使用错误(404 找不到队列、406 前置条件不满足)只关出错的 channel:broker 发 Channel.Close,客户端回 CloseOk,别的 channel 继续用。认证失败(403)、vhost 不存在(530)这类是连接级错误,整条连接被关闭。

exchange、queue、binding

实测 · 本机执行输出
  $ rabbitmqctl list_exchanges name type durable auto_delete
    Listing exchanges for vhost / ...
    name	type	durable	auto_delete
    	direct	true	false
    amq.direct	direct	true	false
    amq.fanout	fanout	true	false
    amq.headers	headers	true	false
    amq.match	headers	true	false
    amq.rabbitmq.log	topic	true	false
    amq.rabbitmq.trace	topic	true	false
    amq.topic	topic	true	false
    x.demo	direct	true	false
  $ rabbitmqctl list_queues name durable messages
    Timeout: 60.0 seconds ...
    Listing queues for vhost / ...
    name	durable	messages
    q.demo	true	0
  $ rabbitmqctl list_bindings source_name source_kind destination_name destination_kind routing_key
    Listing bindings for vhost /...
    source_name	source_kind	destination_name	destination_kind	routing_key
    	exchange	q.demo	queue	q.demo
    x.demo	exchange	q.demo	queue	k1

消息先到 exchange,按 binding 里的规则转到队列。list_exchanges 里有一个名字是空串的默认 exchange 和一组 amq.* 预置 exchange;每个队列都自动以队列名为 routing key 绑在默认 exchange 上,所以“直接发到队列”其实也是走了 exchange。

开销

实测 · 本机执行输出
  基线: Erlang 进程 385,内存 62.8 MiB
  100 个连接 × 1 个 channel: 进程 1485(+1100),内存 70.0 MiB(+7.2)
  关掉后回到: 进程 385,内存 64.5 MiB
  1 个连接 × 100 个 channel: 进程 792(+407),内存 66.7 MiB(+2.3)

在 broker 一侧,一个连接(带 1 个 channel)约 11 个 Erlang 进程,每多一个 channel 约多 4 个。连接数多比 channel 数多贵得多——这也是客户端通常用少量连接、多个 channel 的原因(具体多少合适见本系列「连接与 channel 的数量」那一篇)。

分步演示

AMQP 模型
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / RabbitMQ 4.3.6 / Erlang/OTP 28.5 / pika 1.4.4(Homebrew 与临时虚拟环境);所有节点都是本机自己起的 RabbitMQ 进程(127.0.0.1,端口 9500 以上,每个实验独立的数据目录与节点名,不连任何外部服务);命令输出来自真实的 rabbitmqctl 与管理 API,发布与消费用 pika 的真实连接;计时用 time.perf_counter,数字每次运行会有小幅抖动。第一、二、四节经过自己写的 TCP 代理(按 AMQP 0-9-1 帧格式解析两个方向的字节);第六节的进程数和内存来自 rabbitmqctl eval 调用 erlang:system_info(process_count) 与 erlang:memory(total),每一项只取一次测量
    • 没有验证:TLS 连接与证书认证;AMQP 1.0 和其它协议(MQTT、STOMP)的连接模型;多 vhost、多用户的权限;连接和 channel 数很大(几万)时的行为;channel_max 的上限与超过时的错误;不同版本 RabbitMQ 或 Erlang 的开销数字
    京ICP备2026067502号-1京公网安备11010502063487号