共享订阅

简介

共享订阅是在多个订阅者之间实现负载均衡的订阅方式:

                                                   [subscriber1] got msg1
             msg1, msg2, msg3                    /
[publisher]  ---------------->  "$share/g/topic"  -- [subscriber2] got msg2
                                                 \
                                                   [subscriber3] got msg3

上图中,共享 3 个 subscriber 用共享订阅的方式订阅了同一个主题 $share/g/topic,其中topic 是它们订阅的真实主题名,而 $share/g/ 是共享订阅前缀。EMQ X 支持两种格式的共享订阅前缀:

示例 前缀 真实主题名
$queue/t/1 $queue/ t/1
$share/abc/t/1 $share/abc t/1

带群组的共享订阅

$share/<group-name> 为前缀的共享订阅是带群组的共享订阅:

group-name 可以为任意字符串,属于同一个群组内部的订阅者将以负载均衡接收消息,但 EMQ X 会向不同群组广播消息。

例如,假设订阅者 s1,s2,s3 属于群组 g1,订阅者 s4,s5 属于群组 g2。那么当 EMQ X 向这个主题发布消息 msg1 的时候:

  • EMQ X 会向两个群组 g1 和 g2 同时发送 msg1
  • s1,s2,s3 中只有一个会收到 msg1
  • s4,s5 中只有一个会收到 msg1
                                       [s1]
           msg1                      /
[emqx]  ------>  "$share/g1/topic"    - [s2] got msg1
         |                           \
         |                             [s3]
         | msg1
          ---->  "$share/g2/topic"   --  [s4]
                                     \
                                      [s5] got msg1

下面对带群组的共享订阅进行测试:

添加两个订阅:

EMQ X:高级功能_重写规则EMQ X:高级功能_群组_02

客户端发送五条消息:

EMQ X:高级功能_MQTT_03

消息以负载均衡的方式发送到订阅者:

EMQ X:高级功能_群组_04

不带群组的共享订阅

$queue/ 为前缀的共享订阅是不带群组的共享订阅。它是 $share 订阅的一种特例,相当与所有订阅者都在一个订阅组里面:

                                       [s1] got msg1
        msg1,msg2,msg3               /
[emqx]  --------------->  "$queue/topic" - [s2] got msg2
                                     \
                                       [s3] got msg3

测试:

添加两个订阅者:

EMQ X:高级功能_客户端_05EMQ X:高级功能_客户端_06

发送五条消息,结果如下

EMQ X:高级功能_客户端_07

均衡策略与派发 Ack 配置

EMQ X 的共享订阅支持均衡策略与派发 Ack 配置:

# 均衡策略
broker.shared_subscription_strategy = random

# 适用于 QoS1 QoS2 消息,启用时在其中一个组离线时,将派发给另一个组
broker.shared_dispatch_ack_enabled = false
均衡策略 描述
random 在所有订阅者中随机选择
round_robin 按照订阅顺序
sticky 一直发往上次选取的订阅者
hash 按照发布者 ClientID 的哈希值

无论是单客户端订阅还是共享订阅都要注意客户端性能与消息接收速率,否则会引发消息堆积、客户端崩溃等错误。

延迟发布

EMQ X 的延迟发布功能可以实现按照用户配置的时间间隔延迟发布 PUBLISH 报文的功能。当客户端使用特殊主题前缀 $delayed/{DelayInteval} 发布消息到 EMQ X 时,将触发延迟发布功能。

延迟发布主题的具体格式如下:

$delayed/{DelayInterval}/{TopicName}
  • $delayed: 使用 $delay 作为主题前缀的消息都将被视为需要延迟发布的消息。延迟间隔由下一主题层级中的内容决定。
  • {DelayInterval}: 指定该 MQTT 消息延迟发布的时间间隔,单位是秒,允许的最大间隔是 4294967 秒。如果 {DelayInterval} 无法被解析为一个整型数字,EMQ X 将丢弃该消息,客户端不会收到任何信息。
  • {TopicName}: MQTT 消息的主题名称。

例如:

  • $delayed/15/x/y: 15 秒后将 MQTT 消息发布到主题 x/y
  • $delayed/60/a/b: 1 分钟后将 MQTT 消息发布到 a/b
  • $delayed/3600/$SYS/topic: 1 小时后将 MQTT 消息发布到 $SYS/topic

测试:

先开启该功能:

EMQ X:高级功能_客户端_08

然后重启emqx服务。

添加一个监听者监听d1主题:

EMQ X:高级功能_群组_09

发送一个10秒延迟的消息

EMQ X:高级功能_群组_10

10秒后,接收到消息:

EMQ X:高级功能_MQTT_11

代理订阅

EMQ X 的代理订阅功能使得客户端在连接建立时,不需要发送额外的 SUBSCRIBE 报文,便能自动建立用户预设的订阅关系。

内置代理订阅

EMQ X 通过内置代理订阅模块就可以通过配置文件来指定代理订阅规则从而实现代理订阅,适用于有规律可循的静态的代理订阅需求。

代理订阅功能由 emqx_mod_subscription 内置模块提供,此功能默认关闭,支持在 EMQ X Broker 运行期间动态启停

EMQ X:高级功能_群组_12

相关订阅选项

  • 服务质量( QoS )

    服务端可以向客户端发送的应用消息的最大 QoS 等级。

  • NL( No Local )

    应用消息是否能够被转发到发布此消息的客户端。

    • NL 值为 0 时,表示应用消息可以被转发给发布此消息的客户端。
    • NL 值为 1 时,表示应用消息不能被转发给发布此消息的客户端。
  • RAP( Retain As Published )

    向此订阅转发应用消息时,是否保持消息被发布时设置的保留(RETAIN)标志。

    • RAP 值为 0 时,表示向此订阅转发应用消息时把保留标志设置为 0。
    • RAP 值为 1 时,表示向此订阅转发应用消息时保持消息被发布时设置的保留标志。
  • RH( Retain Handling )

    当订阅建立时,是否发送保留消息

    • 0:订阅建立时发送保留消息
    • 1:订阅建立时,若该订阅当前不存在则发送保留消息
    • 2:订阅建立时不要发送保留消息

代理订阅规则

代理订阅规则的格式如下:

## 代理订阅的主题
module.subscription.<number>.topic = <topic>

## 代理订阅的订阅选项:QoS
## 可选值: 0、1、2
## 默认值:1
module.subscription.<number>.qos = <qos>

## 代理订阅的订阅选项:No Local
## 可选值: 0、1
## 默认值:0
module.subscription.<number>.nl = <nl>

## 代理订阅的订阅选项:Retain As Published
## 可选值: 0、1
## 默认值:0
module.subscription.<number>.rap = <rap>

## 代理订阅的订阅选项:Retain Handling
## 可选值: 0、1、2
## 默认值:0
module.subscription.<number>.rh = <rh>

在配置代理订阅的主题时,EMQ X 提供了 %c%u 两个占位符供用户使用,EMQ X 会在执行代理订阅时将配置中的 %c%u 分别替换为客户端的 Client IDUsername,需要注意的是,%c%u 必须占用一整个主题层级。

修改emqx.conf配置,并重启emqx服务

module.subscription.1.topic = testtopic/%c
module.subscription.1.qos = 2
module.subscription.1.nl = 1
module.subscription.1.rap = 1
module.subscription.1.rh = 1

设置连接:

EMQ X:高级功能_群组_13

这里并没有添加订阅,发送消息到主题testtopic/%c,会自动添加代理订阅,收到消息

EMQ X:高级功能_MQTT_14

在EMQ X Enterprise 版本中支持动态代理订阅,即通过外部数据库设置主题列表在设备连接时读取列表实现代理订阅。

主题重写

EMQ X 的主题重写功能支持根据用户配置的规则在客户端订阅主题、发布消息、取消订阅的时候将 A 主题重写为 B 主题。

EMQ X 的保留消息 和 延迟发布可以与主题重写配合使用,例如,当用户想使用延迟发布功能,但不方便修改客户端发布的主题时,可以使用主题重写将相关主题重写为延迟发布的主题格式。

由于 ACL 检查会在主题重写之前执行,所以只要确保重写之前的主题能够通过 ACL 检查即可。

启停主题重写功能

主题重写功能由 emqx_mod_rewrite 内置模块提供, 此功能默认关闭,支持在 EMQ X Broker 运行期间动态启停

配置主题重写规则

EMQ X 的主题重写规则需要用户自行配置,用户可以自行添加多条主题重写规则,规则的数量没有限制,但由于任何携带主题的 MQTT 报文都需要匹配一遍重写规则,因此此功能在高吞吐场景下带来的性能损耗与规则数量是成正比的,用户需要谨慎地使用此功能。

每条主题重写规则的格式如下:

module.rewrite.pub.rule.<number> = 主题过滤器 正则表达式 目标表达式
module.rewrite.sub.rule.<number> = 主题过滤器 正则表达式 目标表达式

重写规则分为 Pub 规则和 Sub 规则,Pub 规则匹配 PUSHLISH 报文携带的主题,Sub 规则匹配 SUBSCRIBE、UNSUBSCRIBE 报文携带的主题。

每条重写规则都由以空格分隔的主题过滤器、正则表达式、目标表达式三部分组成。在主题重写功能开启的前提下,EMQ X 在收到诸如 PUBLISH 报文等带有主题的 MQTT 报文时,将使用报文中的主题去依次匹配配置文件中规则的主题过滤器部分,一旦成功匹配,则使用正则表达式提取主题中的信息,然后替换至目标表达式以构成新的主题。

目标表达式中可以使用 $N 这种格式的变量匹配正则表达中提取出来的元素,$N 的值为正则表达式中提取出来的第 N 个元素,比如 $1 即为正则表达式提取的第一个元素。

需要注意的是,EMQ X 使用倒序读取配置文件中的重写规则,当一条主题可以同时匹配多条主题重写规则的主题过滤器时,EMQ X 仅会使用它匹配到的第一条规则进行重写,如果该条规则中的正则表达式与 MQTT 报文主题不匹配,则重写失败,不会再尝试使用其他的规则进行重写。因此用户在使用时需要谨慎的设计 MQTT 报文主题以及主题重写规则。

示例

假设 emqx.conf 文件中已经添加了以下主题重写规则,修改完成后,重启emqx服务

module.rewrite.sub.rule.1 = y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2
module.rewrite.pub.rule.1 = x/# ^x/y/(.+)$ z/y/x/$1
module.rewrite.pub.rule.2 = x/y/+ ^x/y/(\d+)$ z/y/$1

此时我们分别订阅 y/a/z/by/defx/1/2x/y/2x/y/z 五个主题:

  • 当客户端订阅 y/def 主题时,y/def 不匹配任何一个主题过滤器,因此不执行主题重写,直接订阅 y/def 主题。
  • 当客户端订阅 y/a/z/b 主题时,y/a/z/b 匹配 y/+/z/# 主题过滤器,EMQ X 执行 module.rewrite.sub.rule.1 规则,通过正则正则表达式匹配出元素 [a、b] ,将匹配出来的第二个元素带入 y/z/$2,实际订阅了 y/z/b 主题。
  • 当客户端向 x/1/2 主题发送消息时,x/1/2 匹配 x/# 主题过滤器,EMQ X 执行 module.rewrite.pub.rule.1 规则,通过正则表达式未匹配到元素,不执行主题重写,因此直接向 x/1/2 主题发送消息。
  • 当客户端向 x/y/2 主题发送消息时,x/y/2 同时匹配 x/#x/y/+ 两个主题过滤器,EMQ X 通过倒序读取配置,所以优先匹配 module.rewrite.pub.rule.2,通过正则替换,实际向 z/y/2 主题发送消息。
  • 当客户端向 x/y/z 主题发送消息时,x/y/z 同时匹配 x/#x/y/+ 两个主题过滤器,EMQ X 通过倒序读取配置,所以优先匹配 module.rewrite.pub.rule.2,通过正则表达式未匹配到元素,不执行主题重写,直接向x/y/z主题发送消息。需要注意的是,即使module.rewrite.pub.rule.2的正则表达式匹配失败,也不会再次去匹配module.rewrite.pub.rule.1` 的规则。

测试:

我这里向y/z/b主题发送消息

EMQ X:高级功能_MQTT_15

订阅了y/a/z/b主题的收到了消息,说明主题重写生效。

EMQ X:高级功能_重写规则_16

黑名单

EMQ X 为用户提供了黑名单功能,用户可以通过相关的 HTTP API 将指定客户端加入黑名单以拒绝该客户端访问,除了客户端标识符以外,还支持直接封禁用户名甚至 IP 地址。

黑名单只适用于少量客户端封禁需求,如果有大量客户端需要认证管理,请使用 认证功能。

在黑名单功能的基础上,EMQ X 支持自动封禁那些被检测到短时间内频繁登录的客户端,并且在一段时间内拒绝这些客户端的登录,以避免此类客户端过多占用服务器资源而影响其他客户端的正常使用。

需要注意的是,自动封禁功能只封禁客户端标识符,并不封禁用户名和 IP 地址,即该机器只要更换客户端标识符就能够继续登录。

此功能默认关闭,用户可以在 emqx.conf 配置文件中将 enable_flapping_detect 配置项设为 on 以启用此功能。

zone.external.enable_flapping_detect = off

用户可以为此功能调整触发阈值和封禁时长,对应配置项如下:

flapping_detect_policy = 30, 1m, 5m

此配置项的值以 , 分隔,依次表示客户端离线次数,检测的时间范围以及封禁时长,因此上述默认配置即表示如果客户端在 1 分钟内离线次数达到 30 次,那么该客户端使用的客户端标识符将被封禁 5 分钟

黑名单 HTTP API

GET /api/v4/banned

获取黑名单

Query String Parameters:

/api/v4/clients

Success Response Body (JSON):

Name Type Description
code Integer 0
data Array 由对象构成的数组,对象中的字段与 POST 方法中的 Request Body 相同
meta Object /api/v4/clients

Examples:

获取黑名单列表:

$ curl -i --basic -u admin:public -vX GET "http://localhost:8081/api/v4/banned"

{"meta":{"page":1,"limit":10000,"count":1},"data":[{"who":"example","until":1582265833,"reason":"undefined","by":"user","at":1582265533,"as":"clientid"}],"code":0}

POST /api/v4/banned

将对象添加至黑名单

Parameters (json):

Name Type Required Default Description
who String Required 添加至黑名单的对象,可以是客户端标识符、用户名和 IP 地址
as String Required 用于区分黑名单对象类型,可以是 clientidusernamepeerhost
reason String Required 详细信息
by String Optional user 指示该对象被谁添加至黑名单
at Integer Optional 当前系统时间 添加至黑名单的时间,单位:秒
until Integer Optional 当前系统时间 + 5 分钟 何时从黑名单中解除,单位:秒

Success Response Body (JSON):

Name Type Description
code Integer 0
data Object 与传入的 Request Body 相同

Examples:

将 client 添加到黑名单:

$ curl -i --basic -u admin:public -vX POST "http://localhost:8081/api/v4/banned" -d '{"who":"example","as":"clientid","reason":"example"}'

{"data":{"who":"example","as":"clientid"},"code":0}

DELETE /api/v4/banned/{as}/{who}

将对象从黑名单中删除

Parameters:

Success Response Body (JSON):

Name Type Description
code Integer 0
message String 仅在发生错误时返回,用于提供更详细的错误信息

xamples:

将 client 从黑名单中移除:

$ curl -i --basic -u admin:public -X DELETE "http://localhost:8081/api/v4/banned/clientid/example"

{"code":0}

速率限制

简介

EMQ X 提供对接入速度、消息速度的限制:当客户端连接请求速度超过指定限制的时候,暂停新连接的建立;当消息接收速度超过指定限制的时候,暂停接收消息。

速率限制是一种 backpressure 方案,从入口处避免了系统过载,保证了系统的稳定和可预测的吞吐。速率限制可在 emqx.conf 中配置:

配置项 类型 默认值 描述
listener.tcp.external.max_conn_rate Number 1000 本节点上允许的最大连接速率 (conn/s)
zone.external.rate_limit.conn_messagess_in Number,Duration 无限制 单连接上允许的最大发布速率 (msg/s)
zone.external.rate_limit.conn_bytes_in Size,Duration 无限制 单连接上允许的最大报文速率 (bytes/s)
  • max_conn_rate 是单个 emqx 节点上连接建立的速度限制。1000 代表秒最多允许 1000 个客户端接入。
  • conn_messages_in 是单个连接上接收 PUBLISH 报文的速率限制。100,10s 代表每个连接上允许收到的最大 PUBLISH 消息速率是每 10 秒 100 个。
  • conn_bytes_in 是单个连接上接收 TCP数据包的速率限制。100KB,10s 代表每个连接上允许收到的最大 TCP 报文速率是每 10 秒 100KB。

conn_messages_inconn_bytes_in 提供的都是针对单个连接的限制,EMQ X 目前没有提供全局的消息速率限制。

速率限制原理

EMQ X 使⽤令牌桶 (Token Bucket)算法来对所有的 Rate Limit 来做控制。 令牌桶算法 的逻辑如下图:

EMQ X:高级功能_正则表达式_17
  • 存在一个可容纳令牌(Token) 的最大值 burst 的桶(Bucket),最大值 burst 简记为 b 。
  • 存在一个 rate 为每秒向桶添加令牌的速率,简记为 r 。当桶满时则不不再向桶中加⼊入令牌。
  • 每当有 1 个(或 N 个)请求抵达时,则从桶中拿出 1 个 (或 N 个) 令牌。如果令牌不不够则阻塞,等待令牌的⽣生成。

由此可知该算法中:

  • 长期来看,所限制的请求速率的平均值等于 rate 的值。

  • 记实际请求达到速度为 M,且 M > r,那么,实际运⾏中能达到的最大(峰值)速率为 M = b + r,证明:

    容易想到,最大速率 M 为:能在1个单位时间内消耗完满状态令牌桶的速度。而桶中令牌的消耗速度为 M - r,故可知:b / (M - r) = 1,得 M = b + r

令牌桶算法在 EMQ X 中的应用

当使用如下配置做报文速率限制的时候:

zone.external.rate_limit.conn_bytes_in = 100KB,10s

EMQ X 将使用两个值初始化每个连接的 rate-limit 处理器:

  • rate = 100 KB / 10s = 10240 B/s
  • burst = 100 KB = 102400 B

根据 消息速率限制原理 中的算法,可知:

  • 长期来看允许的平均速率限制为 10240 B/s
  • 允许的峰值速率为 102400 + 10240 = 112640 B/s

为提高系统吞吐,EMQ X 的接入模块不会一条一条的从 socket 读取报文,而是每次从 socket 读取 N 条报文。rate-limit 检查的时机就是在收到这 N 条报文之后,准备继续收取下个 N 条报文之前。故实际的限制速率不会如算法一样精准。EMQ X 只提供了一个大概的速率限制。N 的值可以在 etc/emqx.conf 中配置:

配置项 类型 默认值 描述
listener.tcp.external.active_n Number 100 emqx 每次从 TCP 栈读取多少条消息

飞行窗口和消息队列

简介

为了提高消息吞吐效率和减少网络波动带来的影响,EMQ X 允许多个未确认的 QoS 1 和 QoS 2 报文同时存在于网路链路上。这些已发送但未确认的报文将被存放在 Inflight Window 中直至完成确认。

当网络链路中同时存在的报文超出限制,即 Inflight Window 到达长度限制(见 max_inflight)时,EMQ X 将不再发送后续的报文,而是将这些报文存储在 Message Queue 中。一旦 Inflight Window 中有报文完成确认,Message Queue 中的报文就会以先入先出的顺序被发送,同时存储到 Inflight Window 中。

当客户端离线时,Message Queue 还会被用来存储 QoS 0 消息,这些消息将在客户端下次上线时被发送。这功能默认开启,当然你也可以手动关闭,见 mqueue_store_qos0

需要注意的是,如果 Message Queue 也到达了长度限制,后续的报文将依然缓存到 Message Queue,但相应的 Message Queue 中最先缓存的消息将被丢弃。如果队列中存在 QoS 0 消息,那么将优先丢弃 QoS 0 消息。因此,根据你的实际情况配置一个合适的 Message Queue 长度限制(见 max_mqueue_len)是非常重要的。

飞行队列与 Receive Maximum

MQTT v5.0 协议为 CONNECT 报文新增了一个 Receive Maximum 的属性,官方对它的解释是:

客户端使用此值限制客户端愿意同时处理的 QoS 为 1 和 QoS 为 2 的发布消息最大数量。没有机制可以限制服务端试图发送的 QoS 为 0 的发布消息 。

也就是说,服务端可以在等待确认时使用不同的报文标识符向客户端发送后续的 PUBLISH 报文,直到未被确认的报文数量到达 Receive Maximum 限制。

不难看出,Receive Maximum 其实与 EMQ X 中的 Inflight Window 机制如出一辙,只是在 MQTT v5.0 协议发布前,EMQ X 就已经对接入的 MQTT 客户端提供了这一功能。现在,使用 MQTT v5.0 协议的客户端将按照 Receive Maximum 的规范来设置 Inflight Window 的最大长度,而更低版本 MQTT 协议的客户端则依然按照配置来设置。

配置项

配置项 类型 可取值 默认值 说明
max_inflight integer >= 0 32 (external), 128 (internal) Inflight Window 长度限制,0 即无限制
max_mqueue_len integer >= 0 1000 (external), 10000 (internal) Message Queue 长度限制,0 即无限制
mqueue_store_qos0 enum true, false true 客户端离线时 EMQ X 是否存储 QoS 0 消息至 Message Queue

消息重传

简介

消息重传 (Message Retransmission) 是属于 MQTT 协议标准规范的一部分。

协议中规定了作为通信的双方 服务端客户端 对于自己发送到对端的 PUBLISH 消息都应满足其 服务质量 (Quality of Service levels) 的要求。如:

  • QoS 1:表示 消息至少送达一次 (At least once delivery);即发送端会一直重发该消息,除非收到了对端对该消息的确认。意思是在 MQTT 协议的上层(即业务的应用层)相同的 QoS 1 消息可能会收到多次。
  • QoS 2:表示 消息只送达一次 (Exactly once delivery);即该消息在上层仅会接收到一次。

虽然,QoS 1 和 QoS 2 的 PUBLISH 报文在 MQTT 协议栈这一层都会发生重传,但请你谨记的是:

  • QoS 1 消息发生重传后,在 MQTT 协议栈上层,也会收到这些重发的 PUBLISH 消息。
  • QoS 2 消息无论如何重传,最终在 MQTT 协议栈上层,都只会收到一条 PUBLISH 消息

基础配置

有两种场景会导致消息重发:

  1. PUBLISH 报文发送给对端后,规定时间内未收到应答。则重发这个报文。
  2. 在保持会话的情况下,客户端重连后;EMQ X 会自动重发 未应答的消息,以确保 QoS 流程的正确。

etc/emqx.conf 中可配置:

配置项 类型 可取值 默认值 说明
retry_interval duration - 30s 等待一个超时间隔,如果没收到应答则重传消息

一般来说,你只需要关心以上内容就足够了。