RocketMQ 消息过滤、存储和ACL

文章目录

  • 消息过滤
    • 消息过滤的方式
    • tag 标签过滤
    • SQL属性过滤
      • SQL 属性过滤规则
  • 消息存储和清理机制
  • 消息持久化机制
  • RocketMQ 开启 ACL 权限控制
    • plain_acl.yml 配置文件
    • 权限说明
    • 权限校验的步骤
    • 启动配置
    • 新建或修改 broker.conf
    • 重启 broker
    • Remoting 协议设置 ACL 信息
    • gRPC 协议设置 ACL 信息
    • 其他的 broker 配置

消息过滤

我们前面提到,一个主题只能有一种消息类型,那么只要是同一个消息类型,我们就可以发送到同一个主题。但这些消息中可能涉及到多个业务,他们对应的消费者的消费逻辑可能不一样。如果我们单个消费者接收该主题的所有消息进行消费,那么在我们的消费逻辑中需要很多的判断来判断其走哪个业务逻辑,无疑增加了消费逻辑的复杂度。使用消息过滤的方式,即可在 MQ 服务端进行消息的过滤,将消息投递给特定的消费者,使每个消费者只需关注与自身相关的消息进行消费。

消息过滤的方式

  • tag 标签过滤:我们之前的所有示例都是通过 tag 进行过滤的。其实现简单,方便。
  • SQL 属性过滤:通过生产者为消息设置的属性(Key)及属性值(Value)进行匹配。适用于复杂逻辑。

tag 标签过滤

tag 标签过滤方式为,生产者发送消息时为消息设置一个 Tag 标签,消费者在消费时匹配该 Tag ,如果匹配则消费发送给该消费者,如果不匹配则不发送。

  1. 消费者,设置 tag
Message message = messageBuilder.setTopic("topic")
//设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey")
//设置消息Tag,用于消费端根据指定Tag过滤消息。
//该示例表示消息的Tag设置为"TagA"。
// Tag使用可见字符,建议长度不超过128字符
.setTag("TagA")
//消息体。
.setBody("messageBody".getBytes())
.build();
  1. 消费者,订阅消息
    2.1 匹配单个标签
String topic = "Your Topic";
//只订阅消息标签为"TagA"的消息。
FilterExpression filterExpression = new FilterExpression("TagA", FilterExpressionType.TAG);
pushConsumer.subscribe(topic, filterExpression);

2.2 匹配多个Tag标签

String topic = "Your Topic";
//只订阅消息标签为"TagA"、"TagB"或"TagC"的消息。
FilterExpression filterExpression = new FilterExpression("TagA||TagB||TagC", FilterExpressionType.TAG);
pushConsumer.subscribe(topic, filterExpression);

2.3 匹配所有消息

String topic = "Your Topic";
//使用Tag标签过滤消息,订阅所有消息。
FilterExpression filterExpression = new FilterExpression("*", FilterExpressionType.TAG);
pushConsumer.subscribe(topic, filterExpression);

SQL属性过滤

SQL属性过滤是 RocketMQ 提供的高级消息过滤方式,通过生产者为消息设置的属性(Key)及属性值(Value)进行匹配。生产者在发送消息时可设置多个属性,消费者订阅时可设置SQL语法的过滤表达式过滤多个属性。

注:Tag是一种系统属性,所以SQL过滤方式也兼容Tag标签过滤。在SQL语法中,Tag的属性名称为TAGS。

  1. 生产者设置消息tag标签和自定义属性
Message message = messageBuilder.setTopic("topic")
//设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey")
//设置消息Tag,用于消费端根据指定Tag过滤消息。
//该示例表示消息的Tag设置为"messageTag"。
.setTag("messageTag")
//消息也可以设置自定义的分类属性,例如环境标签、地域、逻辑分支。
//该示例表示为消息自定义一个属性,该属性为地域,属性值为杭州。
.addProperty("Region", "Hangzhou")
//消息体。
.setBody("messageBody".getBytes())
.build();
  1. 消费者订阅消息
    2.1 根据单个自定义属性匹配消息
String topic = "topic";
//只订阅地域属性为杭州的消息。
FilterExpression filterExpression = new FilterExpression("Region IS NOT NULL AND Region='Hangzhou'", FilterExpressionType.SQL92);
simpleConsumer.subscribe(topic, filterExpression);

2.2 同时根据多个自定义属性匹配消息

String topic = "topic";
//只订阅地域属性为杭州且价格属性大于30的消息。
FilterExpression filterExpression = new FilterExpression("Region IS NOT NULL AND price IS NOT NULL AND Region = 'Hangzhou' AND price > 30", FilterExpressionType.SQL92);
simpleConsumer.subscribe(topic, filterExpression);

2.3 匹配Topic中的所有消息,不进行过滤

String topic = "topic";
//订阅所有消息。
FilterExpression filterExpression = new FilterExpression("True", FilterExpressionType.SQL92);
simpleConsumer.subscribe(topic, filterExpression);

SQL 属性过滤规则

语法 说明 示例
IS NULL 判断属性不存在。 a IS NULL :属性a不存在。
IS NOT NULL 判断属性存在。 a IS NOT NULL:属性a存在。
> >= < <= 用于比较数字,不能用于比较字符串,否则消费者客户端启动时会报错。 说明 可转化为数字的字符串也被认为是数字。 a IS NOT NULL AND a > 100:属性a存在且属性a的值大于100。 a IS NOT NULL AND a > ‘abc’:错误示例,abc为字符串,不能用于比较大小。
BETWEEN xxx AND xxx 用于比较数字,不能用于比较字符串,否则消费者客户端启动时会报错。等价于>= xxx AND <= xxx。表示属性值在两个数字之间。 a IS NOT NULL AND (a BETWEEN 10 AND 100):属性a存在且属性a的值大于等于10且小于等于100。
NOT BETWEEN xxx AND xxx 用于比较数字,不能用于比较字符串,否则消费者客户端启动会报错。等价于< xxx OR > xxx,表示属性值在两个值的区间之外。 a IS NOT NULL AND (a NOT BETWEEN 10 AND 100):属性a存在且属性a的值小于10或大于100。
IN (xxx, xxx) 表示属性的值在某个集合内。集合的元素只能是字符串。 a IS NOT NULL AND (a IN (‘abc’, ‘def’)):属性a存在且属性a的值为abc或def。
= <> 等于和不等于。可用于比较数字和字符串。 a IS NOT NULL AND (a = ‘abc’ OR a<>‘def’):属性a存在且属性a的值为abc或a的值不为def。
AND OR 逻辑与、逻辑或。可用于组合任意简单的逻辑判断,需要将每个逻辑判断内容放入括号内。 a IS NOT NULL AND (a > 100) OR (b IS NULL):属性a存在且属性a的值大于100或属性b不存在。

由于SQL属性过滤是生产者定义消息属性,消费者设置SQL过滤条件,因此过滤条件的计算结果具有不确定性,服务端的处理方式如下:

  • 异常情况处理:如果过滤条件的表达式计算抛异常,消息默认被过滤,不会被投递给消费者。例如比较数字和非数字类型的值。
  • 空值情况处理:如果过滤条件的表达式计算值为null或不是布尔类型(true和false),则消息默认被过滤,不会被投递给消费者。例如发送消息时未定义某个属性,在订阅时过滤条件中直接使用该属性,则过滤条件的表达式计算结果为null。
  • 数值类型不符处理:如果消息自定义属性为浮点型,但过滤条件中使用整数进行判断,则消息默认被过滤,不会被投递给消费者。

消息存储和清理机制

消息按照达到服务器的先后顺序被存储到队列中,理论上每个队列都支持无限存储。但实际情况下,服务端节点物理存储空间有限,消息时无法永久存储的。

RocketMQ 使用存储时长作为消息存储的依据,即每个节点对外承诺消息的存储时长。在存储时长范围内的消息都会被保留,无论消息是否被消费;超过时长限制的消息则会被清理掉。

消息保存时长并不能完整控制消息的实际保存时间,因为消息存储仍然使用本地磁盘,本地磁盘空间不足时,为保证服务稳定性消息仍然会被强制清理,导致消息的实际保存时长小于设置的保存时长。 意味着当消费能力严重不足时,消息会产生大量堆积,导致没有可以磁盘空间,进一步导致未消费消息被清理机制清理掉。

消息持久化机制

RocketMQ 消息默认存储在本地磁盘文件中,存储文件的根目录由 Broker 配置参数 storePathRootDir 决定,其默认路径为:~/store。用户路径下的 store 目录。该目录的目录结构如下:

├── timerwheel
├── timerlog
│   └── 00000000000000000000
├── lock
├── index
│   └── 20230908092441339
├── consumequeue
│   ├── TestTopic
│   │   ├── 7
│   │   │   └── 00000000000000000000
│   │   └── 4
│   │       └── 00000000000000000000
│   ├── SCHEDULE_TOPIC_XXXX
│   │   ├── 2
│   │   │   └── 00000000000000000000
│   │   └── 17
│   │       └── 00000000000000000000
│   ├── rmq_sys_wheel_timer
│   │   └── 0
│   │       └── 00000000000000000000
│   ├── RMQ_SYS_TRANS_OP_HALF_TOPIC
│   │   └── 0
│   │       └── 00000000000000000000
│   ├── RMQ_SYS_TRANS_HALF_TOPIC
│   │   └── 0
│   │       └── 00000000000000000000
│   ├── rmq_sys_REVIVE_LOG_DefaultCluster
│   │   ├── 7
│   │   │   └── 00000000000000000000
│   │   ├── 6
│   │   │   └── 00000000000000000000
│   │   ├── 5
│   │   │   └── 00000000000000000000
│   │   ├── 4
│   │   │   └── 00000000000000000000
│   │   ├── 3
│   │   │   └── 00000000000000000000
│   │   ├── 2
│   │   │   └── 00000000000000000000
│   │   ├── 1
│   │   │   └── 00000000000000000000
│   │   └── 0
│   │       └── 00000000000000000000
│   ├── %RETRY%MY_RETRY_GROUP_MY_NORMAL_TOPIC
│   │   └── 0
│   │       └── 00000000000000000000
│   ├── %RETRY%DLQ_CONSUMER_GROUP_%DLQ%MY_RETRY_GROUP
│   │   └── 0
│   │       └── 00000000000000000000
│   ├── MY_TRANSACTION_TOPIC
│   │   ├── 4
│   │   │   └── 00000000000000000000
│   │   └── 1
│   │       └── 00000000000000000000
│   ├── MY_NORMAL_TOPIC
│   │   ├── 7
│   │   │   └── 00000000000000000000
│   │   ├── 5
│   │   │   └── 00000000000000000000
│   │   ├── 4
│   │   │   └── 00000000000000000000
│   │   ├── 2
│   │   │   └── 00000000000000000000
│   │   └── 0
│   │       └── 00000000000000000000
│   ├── MY_FIFO_TOPIC
│   │   ├── 6
│   │   │   └── 00000000000000000000
│   │   ├── 5
│   │   │   └── 00000000000000000000
│   │   ├── 3
│   │   │   └── 00000000000000000000
│   │   ├── 2
│   │   │   └── 00000000000000000000
│   │   └── 1
│   │       └── 00000000000000000000
│   ├── MY_DELAY_TOPIC
│   │   ├── 6
│   │   │   └── 00000000000000000000
│   │   ├── 5
│   │   │   └── 00000000000000000000
│   │   ├── 3
│   │   │   └── 00000000000000000000
│   │   └── 1
│   │       └── 00000000000000000000
│   ├── %DLQ%MY_RETRY_GROUP
│   │   └── 0
│   │       └── 00000000000000000000
│   └── %DLQ%DLQ_CONSUMER_GROUP
│       └── 0
│           └── 00000000000000000000
├── config
│   ├── topics.json.bak
│   ├── topics.json
│   ├── timermetrics.bak
│   ├── timermetrics
│   ├── timercheck
│   ├── subscriptionGroup.json.bak
│   ├── subscriptionGroup.json
│   ├── delayOffset.json.bak
│   ├── delayOffset.json
│   ├── consumerOrderInfo.json.bak
│   ├── consumerOrderInfo.json
│   ├── consumerOffset.json.bak
│   ├── consumerOffset.json
│   ├── consumerFilter.json.bak
│   └── consumerFilter.json
├── compaction
│   ├── position-checkpoint.bak
│   └── position-checkpoint
├── commitlog
│   ├── 00000000001073741824
│   └── 00000000000000000000
├── checkpoint
├── abort.bak
└── abort
  • index: 该文件夹下的文件是以创建时的时间戳为文件名,其作为消息的索引来使用。
    一个 index 存储单元包含如:key 的hashCode、物理偏移位置、时间差异、下一个 index 的位置,这些内容的写入也是按顺序的
┌───────────────┬───────────────────────────────┬───────────────┬───────────────┐
│ Key HashCode  │        Physical Offset        │   Time Diff   │ Next Index Pos│
│   (4 Bytes)(8 Bytes)(4 Bytes)(4 Bytes)   │
├───────────────┴───────────────────────────────┴───────────────┴───────────────┤
│                                 Index Store Unit                              │
│                                                                               │
  • consumequeue:该文件夹下以各个主题命名的文件夹中(如:TestTopic),有按队列序号的文件夹(如:0~7,默认8个队列)。这里存储的是逻辑队列,其包含队列在 commitlog 文件中的位置 offset,消息内容的大小和消息 tag 的 hash 值。这些信息在文件中是顺序写入的。
  • config:该文件夹下存储了主题、消费者分组等重要的配置信息。比如:我们的消费者分组信息就存储在 subscriptionGroup.json。
  • commitlog:日志数据文件。消息实际被持久化存储的文件,其按照收到消息的顺序写入文件(写入过程是加锁的),之后将消息在 commitlog 存储的位置 offset、消息大小、tag 等信息发送给 consumequeue。

commitlog 文件夹下,00000000000000000000 代表了第一个日志数据文件,其最大大小为 1G = 1073741824;其起始偏移量为 0,当第一个文件写满,则生成第二个文件,其文件名为 00000000001073741824,其起始偏移量为 1073741824,依此类推。

RocketMQ 开启 ACL 权限控制

在安全要求较高的服务器环境下,或者多项目组共享一个 RocketMQ 集群时,建议大家开启权限控制,这样可以防止多项目混用 RocketMQ 资源,也可以控制非法的访问请求。

plain_acl.yml 配置文件

plain_acl.yml 配置文件,在 ${RocketMQ_HOME}/conf/plain_acl.yml,其默认配置如下:

# 全局IP白名单
globalWhiteRemoteAddresses:
  - 10.10.103.*
  - 192.168.0.*
# 账号配置列表
accounts:
	# Access Key 类似账号名
  - accessKey: RocketMQ
  	# Secret Key
    secretKey: 12345678
    # 用户IP白名单列表
    whiteRemoteAddress:
    # 当前用户是否为管理员用户
    admin: false
    # 默认的Topic权限
    defaultTopicPerm: DENY
    # 默认的ConsumerGroup权限
    defaultGroupPerm: SUB
    # 各个Topic的权限
    topicPerms:
      - topicA=DENY
      - topicB=PUB|SUB
      - topicC=SUB
	# 各个ConsumerGroup的权限
    groupPerms:
      # the group should convert to retry topic
      - groupA=DENY
      - groupB=PUB|SUB
      - groupC=SUB

  - accessKey: rocketmq2
    secretKey: 12345678
    whiteRemoteAddress: 192.168.1.*
    # 如果是管理员用户,则拥有所有权限
    admin: true

权限说明

  • DENY 拒绝
  • ANY PUB 或者 SUB 权限
  • PUB 发送权限
  • SUB 订阅权限

注1:特殊的权限,比如 UPDATE_AND_CREATE_TOPIC 等,只能由 admin 账号操作。
注2:对于某个资源(比如:topic),如果有其对应的权限配置,则使用配置的权限,如果没有配置则使用默认的权限。

权限校验的步骤

  1. 检查是否命中全局 IP 白名单,如果是,则校验通过。否则验证第二步
  2. 检查是否命中用户 IP 白名单;如果是,则认为校验通过;否则验证第三步
  3. 校验签名,校验不通过,抛出异常;校验通过,则验证第四步
  4. 对用户请求所需的权限 和 用户所拥有的权限进行校验;不通过,抛出异常

注意:IP 白名单如果通过,则不会验证签名,如果你要使用签名方式认证,最好就不要设置对应的 IP 白名单。

启动配置

通过 broker 的 aclEnable 配置来开启权限校验。

我们前面还提到建议关闭 autoCreateSubscriptionGroup (是否自动创建消费者分组)、autoCreateTopicEnable (是否自动创建Topic),这两个参数也都是 broker 的配置参数,我们在此一并配置了。

新建或修改 broker.conf


brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH

# 以上为 broker.conf 的默认配置
# 开启权限验证
aclEnable = true
# 关闭自动创建Topic
autoCreateTopicEnable = false
# 关闭自动创建消费者分组
autoCreateSubscriptionGroup = false

关闭自动创建 Topic 后,生产者执行时将报没有对应主题的错误。

关闭自动创建消费者分组后,消费者示例可以成功启动,但其无法消费任何消息。

重启 broker

$> ./bin/mqshutdown proxy
$> ./bin/mqshutdown broker
$> nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf --enable-proxy &

注:以上命令在 RocketMQ home 目录下执行的

Remoting 协议设置 ACL 信息

DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName", 
	new AclClientRPCHook(new SessionCredentials("accessKey","secretKey"));
);

gRPC 协议设置 ACL 信息

ClientConfiguration configuration = ClientConfiguration.newBuilder()
                // endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
                .setEndpoints(MyMQProperties.ENDPOINTS)
.setCredentialProvider(new StaticSessionCredentialsProvider("accessKey","secretKey"))
.build();

其他的 broker 配置

属性名 默认值 说明
listenPort 10911 接受客户端连接的监听端口
namesrvAddr null nameServer 地址
brokerIP1 网卡的 InetAddress 当前 broker 监听的 IP
brokerIP2 跟 brokerIP1 一样 存在主从 broker 时,如果在 broker 主节点上配置了 brokerIP2 属性,broker 从节点会连接主节点配置的 brokerIP2 进行同步
brokerName null broker 的名称
brokerClusterName DefaultCluster 当前 broker 所属的 Cluser 名称
brokerId 0 broker id, 0 表示 master, 其他的正整数表示 slave
storePathCommitLog $HOME/store/commitlog/ 存储 commit log 的路径
storePathConsumerQueue $HOME/store/consumequeue/ 存储 consume queue 的路径
mapedFileSizeCommitLog 1024 * 1024 * 1024(1G) commit log 的映射文件大小
deleteWhen 04 在每天的什么时间删除已经超过文件保留时间的 commit log
fileReservedTime 72 以小时计算的文件保留时间
brokerRole ASYNC_MASTER SYNC_MASTER/ASYNC_MASTER/SLAVE
flushDiskType ASYNC_FLUSH SYNC_FLUSH/ASYNC_FLUSH SYNC_FLUSH 模式下的 broker 保证在收到确认生产者之前将消息刷盘。ASYNC_FLUSH 模式下的 broker 则利用刷盘一组消息的模式,可以取得更好的性能。

你可能感兴趣的:(人在江湖之RocketMQ,rocketmq,消息过滤,消息存储机制,ACL,权限管理)