connection、channel、exchange、queue、binding 这五个词是 AMQP 的全部家当,但它们各自在网线上长什么样、在 broker 里占什么?这篇用一个 TCP 代理把 AMQP 0-9-1 的帧一帧一帧打出来,再用 rabbitmqctl 和管理 API 看这几类对象在 RabbitMQ 4.3.6 里的真实样子和开销。
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 字节结尾)。
$ 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)这类是连接级错误,整条连接被关闭。
$ 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 的数量」那一篇)。