TDMQ 常用总结

一.产品简介

腾讯云消息队列 TDMQ(Tencent Distributed Message Queue,简称 TDMQ)是一款基于 Apache 顶级开源项目 Pulsar 自研的金融级分布式消息中间件,具备跨城高一致、高可靠、高并发的特性。 TDMQ 目前已应用在腾讯计费绝大部分场景,包括支付主路径、实时对账、实时监控、大数据实时分析等方面

1.1 产品概述

腾讯云消息队列 TDMQ(Tencent Distributed Message Queue,简称 TDMQ)是一款基于 Apache 顶级开源项目 Pulsar 自研的金融级分布式消息中间件,其计算与存储分离的架构设计,使得它具备极好的云原生和 Server less特性,用户按量使用,无需关心底层资源。

TDMQ 拥有原生 Java 、 C++、Python、GO 等多种 SDK,同时支持 HTTP 协议方式接入,可为分布式应用系统提供异步解耦和削峰填谷的能力,具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性

TDMQ 目前已应用在腾讯计费绝大部分场景,包括支付主路径、实时对账、实时监控、大数据实时分析等方面。

Pulsar结构图

主要特性

  • 具备高一致、高可靠、高并发特性
  • 采用服务和存储分离架构,支持水平动态扩容
  • 支持百万级消息主题
  • 非常低的消息发布和端到端的延迟
  • 支持多种订阅模式: 独占(exclusive)、共享(shared)、灾备(failover)
  • 一个 Server less 的轻量级计算框架 Functions 提供了原生的流数据处理
  • 支持多集群,能够无缝的基于地理位置进行跨集群的备份

1.2 产品优势

1.2.1 数据强一致

TDMQ 采用 Bookkeeper 一致性协议 实现数据强一致性(类似 RAFT 算法),将消息数据备份写到不同物理机上,并且要求是同步刷盘。当某台物理机出故障时,后台数据复制机制能够对数据快速迁移,保证用户数据备份可用。

1.2.2 高性能低延迟

TDMQ 能够高效支持百万级消息生产和消费,海量消息堆积且消息堆积容量不设上限,支撑了腾讯计费所有场景;性能方面,单集群 QPS 超过10万,同时在时耗方面有保护机制来保证低延迟,帮助您轻松满足业务性能需求。

1.2.3 百万级 Topic

TDMQ 计算与存储架构的分离设计,使得 TDMQ 可以轻松支持百万级消息主题。相比于市场上其他 MQ 产品,整个集群不会因为 Topic 数量增加而导致性能急剧下降。

1.2.4 丰富的消息类型

TDMQ 提供丰富的消息类型,涵盖普通消息、顺序消息(全局顺序 / 分区顺序)、分布式事务消息、定时消息,满足各种严苛场景下的高级特性需求。

1.2.5 消费者数量无限制

不同于 Kafka 的消息消费模式,TDMQ 的消费者数量不受限于 Topic 的分区个数,并且会按照一定的算法均衡每个消费者的消息量,业务可按需启动对应的消费者数量。

1.2.6 多协议接入

TDMQ 的 API 支持 Java、C++、Go 等多语言,并且支持 HTTP 协议,可扩展更多语言的接入,另外还支持开源RocketMQ、RabbitMQ客户端的接入。如果用户只是利用消息队列的基础功能进行消息的生产和消费,可以不用修改代码就完成到 TDMQ 的迁移。

1.2.7 隔离控制

提供按租户对 Topic 进行隔离的机制,同时可精确管控各个租户的生产和消费速率,保证租户之间互不影响,消息的处理不会出现资源竞争的现象。

1.2.8 全球部署

TDMQ 提供全球部署能力,对于拥有全球业务的企业,可以就近选取地域购买服务。

1.3 应用场景

1.3.1 异步解耦

交易引擎作为腾讯计费最核心的系统,每笔交易订单数据需要被几十个下游业务系统关注,包括物品批价、道具发货、积分、流计算分析等,多个系统对消息的处理逻辑不一致,单个系统不可能去适配每一个关联业务。此时,消息队列 TDMQ 可实现高效的异步通信和应用解耦,确保主站业务的连续性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BFetTrcc-1626158398649)(https://main.qcloudimg.com/raw/0d9879224dd334eb4ccee436be1a6f81.svg)]

1.3.2 削峰填谷

企业不定时举办的一些营销活动,新品发布上线,节日抢红包等,往往都会带来临时性的流量洪峰,这对后端的各个应用系统考验是十分巨大的,如果直接采用扩容方式应对又会带来一定的资源浪费。消息队列 TDMQ 此时便可以承担一个缓冲器的角色,将上游突增的请求集中收集,下游可以根据自己的实际处理能力来消费请求消息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WYQ2sc8I-1626158398654)(https://main.qcloudimg.com/raw/6bb7c310c0c4f6048df64705a0c4aff2.svg)]

1.3.3 顺序收发

顺序消息的应用出现在业务场景中。例如王者荣耀的皮肤道具购买与发放,过程中的订单创建、支付、退款等流程都是严格按照顺序执行的,与先进先出(First In First Out,FIFO)原理类似,消息队列 TDMQ 提供一种专门应对这种情形的顺序消息功能,即保证消息 FIFO。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1RZbgba-1626158398657)(https://main.qcloudimg.com/raw/964ecdc85396c26fb4863acaa9fd2222.svg)]

1.3.4 分布式事务一致性

腾讯计费(米大师)是孵化于支撑腾讯内部业务千亿级营收的互联网计费平台,承载了公司每天数亿收入大盘,解决的核心问题是如何确保钱货一致,使用 TDMQ 与分布式事务应用结合来处理交易事务,可以大大提升处理效率和性能。计费的交易链路通常比较长,出错或者超时的概率比较高,借助 TDMQ 的自动重推和海量堆积能力来实现事物补偿,以及支付 Tips 通知和交易流水推送可以通过 TDMQ 来实现最终一致性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VhOKHSkn-1626158398659)(https://main.qcloudimg.com/raw/60a8f143cd749516c2f5f03b4d19ed5c.svg)]

1.3.5 数据同步

如果有多个数据中心存在,需要在多个数据中心之间消费,那么 TDMQ 可以非常方便实现数据中心之间的同步。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mE8iIg98-1626158398661)(https://main.qcloudimg.com/raw/bf71094b56816dd68ad8c175e1c88e83.svg)]

1.3.6 大数据分析

数据在“流动”中产生价值,传统数据分析大多是基于批量计算模型,而无法做到实时的数据分析,利用 TDMQ 与流式计算引擎相结合,可以很方便地实现业务数据的实时分析。

1.4 使用限制

本文列举了消息队列 TDMQ 中对一些指标和性能的限制,请您在使用中注意不要超出对应的限制值,避免出现异常。

1.4.1 集群

限制类型限制说明
单地域内集群数量上限5个
集群名称长度不能超过64个字符
最大存储容量100GB

1.4.2 命名空间

限制类型限制说明
单集群内命名空间数量上限100个
单命名空间 TPS 上限10000
单命名空间带宽上限(生产 + 消费)400Mbps

1.4.3 Topic

限制类型限制说明
单命名空间内 Topic 数量上限2000
Topic 名称长度不能超过64个字符
单 Topic 最大生产者数量1000
单 Topic 最大消费者数量500

1.4.5 消息

限制类型限制说明
消息最大保留时间15天
消息最大延时10天
消息大小5MB
消费位点重置15天
最大已接收未确认消息数量5000条

二.快速入门

2.1 资源创建与准备

步骤一:新建集群并配置网络

  1. 登录 TDMQ 控制台,进入【集群管理】页面,选择目标地域。

  2. 单击【新建集群】,创建一个集群。

  3. 在创建好的集群中,单击操作列的【接入地址】。

    img

    接入地址获取方式如下:

    • 2.7.1及以上版本集群

      直接获取接入地址,如下图。

    • img

    • 2.6.1版本集群

    在接入点列表页,单击【新建】,新建一个 VPC 接入点(和运行客户端的资源所在 VPC 一致即可),创建后如下图。
    img

说明:

更多集群相关介绍与操作请参考 集群管理

步骤二:创建角色并授权

  1. 在 TDMQ 控制台的【角色管理】页面,单击【新建】进入新建角色页面。

  2. 填写角色名称和说明,单击【提交】完成角色创建。

  3. 进入【命名空间】页面,在系统预先创建好的 default 命名空间中,单击操作列的【配置权限】进入命名空间的权限列表。

    img

    说明:

    如果您希望自己重新创建命名空间,也可以自行创建。更多命名空间相关介绍与操作请参考 命名空间

  4. 在配置权限页面,单击【添加角色】,将刚刚创建的角色添加进来,分配生产和消费的权限。
    img

  5. 看到下图即代表配置成功。
    img

步骤三:创建 Topic 和订阅关系

  1. 在【Topic管理】页面,选择目标地域、当前集群和命名空间,单击【新建】,创建一个Topic。
  2. 单击操作列的【新增订阅】,为刚刚新建好的 Topic 创建一个订阅关系。
  3. 单击操作列的【更多】>【查看订阅】,可看到刚刚创建好的订阅。
    img

2.2 下载并运行 Demon

2.2.1 前置准备

下载 Demo(Demo下载地址),并配置相关参数。

添加 Maven 依赖
按照 Pulsar 官方文档 添加 Maven 依赖。

<!-- in your <properties> block -->
<pulsar.version>2.7.1</pulsar.version>
<!-- in your <dependencies> block -->
<dependency>
     <groupId>org.apache.pulsar</groupId>
     <artifactId>pulsar-client</artifactId>
     <version>${pulsar.version}</version>
</dependency>

创建 Client

  • 2.7.1版本及以上集群接入示例
PulsarClient client = PulsarClient.builder()
    .serviceUrl("http://*")//【集群管理】接入地址处复制
    .authentication(AuthenticationFactory.token("eyJr****"))//替换成角色密钥,位于【角色管理】页面
    .build();
System.out.println("&gt;&gt; pulsar client created.");
  • 2.6.1版本集群接入示例
PulsarClient client = PulsarClient.builder()
    .serviceUrl("pulsar://...:6000/")//接入地址到集群管理-接入点列表完整复制
    .listenerName("custom:pulsar-/vpc-/subnet-")//custom:替换成路由ID,位于【集群管理】接入点列表
    .authentication(AuthenticationFactory.token("eyJr"))//替换成角色密钥,位于【角色管理】页面
    .build();
System.out.println("&gt;&gt; pulsar client created.");
  • serviceUrl 即接入地址,可以在控制台【集群管理】页面查看并复制。

img

  • token 即角色的密钥,角色密钥可以在【角色管理】中复制。

注意

密钥泄露很可能导致您的数据泄露,请妥善保管您的密钥。

创建消费者进程

Consumer<byte[]> consumer = client.newConsumer()
            .topic("persistent://pulsar-****")//topic完整路径,格式为persistent://集群(租户)ID/命名空间/Topic名称
            .subscriptionName("****")//需要现在控制台或者通过控制台API创建好一个订阅,此处填写订阅名
            .subscriptionType(SubscriptionType.Exclusive)//声明消费模式为exclusive(独占)模式
            .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)//配置从最早开始消费,否则可能会消费不到历史消息
            .subscribe();
    System.out.println(">> pulsar consumer created.");

说明

  • Topic 名称需要填入完整路径,即“persistent://clusterid/namespace/Topic”,clusterid/namespace/topic 的部分可以从控制台上【Topic管理】页面直接复制。

    img

  • subscriptionName需要写入订阅名,可在【消费管理】界面查看。

创建生产者进程

Producer<byte[]> producer = client.newProducer()
               .topic("persistent://pulsar-****")//topic完整路径,格式为persistent://集群(租户)ID/命名空间/Topic名称
               .create();
       System.out.println(">> pulsar producer created.");

说明

Topic 名称需要填入完整路径,即“persistent://clusterid/namespace/Topic”,clusterid/namespace/topic 的部分可以从控制台上【Topic管理】页面直接复制。

生产消息

for (int i = 0; i < 1000; i++) {
           String value = "my-sync-message-" + i;
           MessageId msgId = producer.newMessage().value(value.getBytes()).send();//发送消息
           System.out.println("deliver msg " + msgId + ",value:" + value);
       }
       producer.close();//关闭生产进程

消费消息

for (int i = 0; i < 1000; i++) {
           Message<byte[]> msg = consumer.receive();//接收当前offset对应的一条消息
           String msgId = msg.getMessageId().toString();
           String value = new String(msg.getValue());
           System.out.println("receive msg " + msgId + ",value:" + value);
           consumer.acknowledge(msg);//接收到之后必须要ack,否则offset会一直停留在当前消息,无法继续消费
       }

2.2.2 打包

pom.xml 所在目录执行命令mvn clean package,或者通过IDE自带的功能打包整个工程,在target目录下生成一个可运行的jar文件。
img

2.2.3 上传

运行成功后将 jar 文件上传到云服务器,具体操作参考 如何将本地文件拷贝到云服务器

2.2.4 执行

登录云服务器,进入到刚刚上传jar文件所在的目录,可看到文件已上传到云服务器。
img
执行命令 java -jar tdmq-demo-1.0.0.jar,运行 Demo,可查看运行日志。
img

2.2.5 查看记录

登录 TDMQ 控制台,依次点击【Topic管理】>【Topic名称】进入消费管理页面,点开订阅名下方右三角号,可查看生产消费记录。

img

2.2.6 查看消息

img

消息轨迹如下:

img

三.开发指南

3.1 原理解析

3.1.1 Apache Pulsar 架构

Apache Pulsar 是一个发布-订阅模型的消息系统,由 Broker、Apache BookKeeper、Producer、Consumer 等组件组成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nkIPDWud-1626158398679)(https://main.qcloudimg.com/raw/f71d23920b92ca8b093cd22ae21913a4.svg)]

  • Producer : 消息的生产者,负责发布消息到 Topic。
  • Consumer:消息的消费者,负责从 Topic 订阅消息。
  • Broker:无状态服务层,负责接收和传递消息,集群负载均衡等工作,Broker 不会持久化保存元数据,因此可以快速的上、下线。
  • Apache BookKeeper:有状态持久层,由一组 Bookie 存储节点组成,可以持久化地存储消息。

Apache Pulsar 在架构设计上采用了计算与存储分离的模式,消息发布和订阅相关的计算逻辑在 Broker 中完成,数据存储在 Apache BookKeeper 集群的 Bookie 节点上。

3.1.2 Topic与分区

Topic(主题)是某一种分类的名字,消息在 Topic 中可以被存储和发布。生产者往 Topic 中写消息,消费者从 Topic 中读消息。

Pulsar 的 Topic 分为 Partitioned Topic 和 Non-Partitioned Topic 两类,Non-Partitioned Topic 可以理解为一个分区数为1的 Topic。实际上在 Pulsar 中,Topic 是一个虚拟的概念,创建一个3分区的 Topic,实际上是创建了3个“分区Topic”,发给这个 Topic 的消息会被发往这个 Topic 对应的多个 “分区Topic”。
例如:生产者发送消息给一个分区数为3,名为my-topic的 Topic,在数据流向上是均匀或者按一定规则(如果指定了key)发送给了 my-topic-partition-0my-topic-partition-1my-topic-partition-2 三个“分区 Topic”。

分区 Topic 做数据持久化时,分区是逻辑上的概念,实际存储的单位是分片(Segment)的。

如下图所示,分区 Topic1-Part2 的数据由N个 Segment 组成, 每个 Segment 均匀分布并存储在 Apache BookKeeper 群集中的多个 Bookie 节点中, 每个 Segment 具有3个副本。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4bwBigjK-1626158398679)(https://main.qcloudimg.com/raw/66aeaa4a39be02e3c61245694ec6b07c.svg)]

3.1.3 物理分区与逻辑分区

逻辑分区和物理分区对比如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3OlFtksq-1626158398680)(https://main.qcloudimg.com/raw/8fa46c108d316e3cc3bf299b0be7e775.svg)]

**物理分区:**计算与存储耦合,容错需要拷贝物理分区,扩容需要迁移物理分区来达到负载均衡。

逻辑分区:物理“分片”,计算层与存储层隔离,这种结构使得 Apache Pulsar 具备以下优点。

  • Broker 和 Bookie 相互独立,方便实现独立的扩展以及独立的容错。
  • Broker 无状态,便于快速上、下线,更加适合于云原生场景。
  • 分区存储不受限于单个节点存储容量。
  • 分区数据分布均匀,单个分区数据量突出不会使整个集群出现木桶效应。
  • 存储不足扩容时,能迅速利用新增节点平摊存储负载。

3.2 使用实践

3.2.1 订阅模式

为了适用不同场景的需求,TDMQ 提供多种订阅方式。订阅可以灵活组合出很多可能性:

  • 如果您想实现传统的 “发布-订阅消息”形式 ,可以让每个消费者都有一个唯一的订阅名称(独占)。
  • 如果您想实现传统的“消息队列” 形式,可以使多个消费者使用同一个的订阅名称(共享、灾备)。
  • 如果您想同时实现以上两点,可以让一些消费者使用独占方式,剩余消费者使用其他方式。

img

3.2.1.1 独占模式(Exclusive)

如果两个及以上的消费者尝试以同样方式去订阅主题,消费者将会收到错误,适用于全局有序消费的场景。

 Consumer<byte[]> consumer1 = client.newConsumer()
                .subscriptionType(SubscriptionType.Exclusive)
                .topic(topic)
                .subscriptionName(groupName)
                .subscribe();
//consumer1启动成功
 Consumer<byte[]> consumer2 = client.newConsumer()
                .subscriptionType(SubscriptionType.Exclusive)
                .topic(topic)
                .subscriptionName(groupName)
                .subscribe();
//consumer2启动失败
3.2.1.2 灾备模式(Failover)

consumer 将会按字典顺序排序,第一个 consumer 被初始化为唯一接受消息的消费者。

 Consumer<byte[]> consumer1 = client.newConsumer()
                .subscriptionType(SubscriptionType.Failover)
                .topic(topic)
                .subscriptionName(groupName)
                .subscribe();
//consumer1启动成功
 Consumer<byte[]> consumer2 = client.newConsumer()
                .subscriptionType(SubscriptionType.Failover)
                .topic(topic)
                .subscriptionName(groupName)
                .subscribe();
//consumer2启动成功

当 master consumer 断开时,所有的消息(未被确认和后续进入的)将会被分发给队列中的下一个consumer。

3.2.1.3 共享模式(Shared)

消息通过 round robin 轮询机制(也可以自定义)分发给不同的消费者,并且每个消息仅会被分发给一个消费者。当消费者断开连接,所有被发送给他,但没有被确认的消息将被重新安排,分发给其它存活的消费者。

 Consumer<byte[]> consumer = client.newConsumer()
                .subscriptionType(SubscriptionType.Shared)
                .topic(topic)
                .subscriptionName(groupName)
                .subscribe();

3.2.2 定时和延时消息

3.2.2.1 相关概念
  • 定时消息:消息在发送至服务端后,实际业务并不希望消费端马上收到这条消息,而是推迟到某个时间点被消费,这类消息统称为定时消息。
  • 延时消息:消息在发送至服务端后,实际业务并不希望消费端马上收到这条消息,而是推迟一段时间后再被消费,这类消息统称为延时消息。

实际上,定时消息可以看成是延时消息的一种特殊用法,其实现的最终效果和延时消息是一致的。

3.2.2.2 使用场景

如果系统是一个单体架构,则通过业务代码自己实现延时或利用第三方组件实现基本没有差别;一旦架构复杂起来,形成了一个大型分布式系统,有几十上百个微服务,这时通过应用自己实现定时逻辑会带来各种问题。一旦运行着延时程序的某个节点出现问题,整个延时的逻辑都会受到影响。

针对以上问题,利用延时消息的特性投递到消息队列里,便是一个较好的解决方案,能统一计算延时时间,同时重试和死信机制确保消息不丢失。

具体场景的示例如下:

  • 微信红包发出后,生产端发送一条延时24小时的消息,到了24小时消费端程序收到消息,进行用户是否已经领走红包的判断,如果没有则退还到原账户。
  • 小程序下单某商品后,后台存放一条延时30分钟的消息,到时间之后消费端收到消息触发对支付结果的判断,如果没有支付就取消订单,这样就实现了超过30分钟未完成支付就取消订单的逻辑。
  • 微信上用户将某条信息设置待办后,也可以通过发送一条定时消息,服务端到点收到这条定时消息,对用户进行待办项提醒。
3.2.2.3 使用方式

在 TDMQ 的 SDK 中提供了专门的 API 来实现定时消息和延时消息。

  • 对于定时消息,您需要提供一个消息发送的时刻。
  • 对于延时消息,您需要提供一个时间长度作为延时的时长。
3.2.2.3.1 定时消息

定时消息通过生产者producer的 deliverAt() 方法实现,代码示例如下:

String value = "message content";
try {
    //需要先将显式的时间转换为 Timestamp
     long timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2020-11-11 00:00:00").getTime();
     //通过调用 producer 的 deliverAt 方法来实现定时消息
    MessageId msgId = producer.newMessage()
            .value(value.getBytes())
            .deliverAt(timeStamp)
            .send();
} catch (ParseException e) {
    //TODO 添加对 Timestamp 解析失败的处理方法
    e.printStackTrace();
}

注意:

定时消息的时间范围为当前时间开始计算,864000秒(10天)以内的任意时刻。如10月1日12:00开始,最长可以设置到10月11日12:00。

3.2.2.3.2 延时消息

延时消息通过生产者produce的 deliverAfter() 方法实现,代码示例如下:

String value = "message content";
//需要指定延时的时长
long delayTime = 10L;
//通过调用 producer 的 deliverAfter 方法来实现定时消息
MessageId msgId = producer.newMessage()
   .value(value.getBytes())
   .deliverAfter(delayTime, TimeUnit.SECONDS) //单位可以自由选择
   .send();

注意:

延时消息的时长取值范围为0 - 864000秒(0秒 - 10天)。如10月1日12:00开始,最长可以设置864000秒。如果设置的时间超过这个时间,则直接按864000秒计算,到时会直接投递。

3.2.2.4 使用说明和限制
  • 使用定时和延时两种类型的消息时,请确保客户端的机器时钟和服务端的机器时钟(所有地域均为UTC+8 北京时间)保持一致,否则会有时差。
  • 定时和延时消息在精度上会有1秒左右的偏差。
  • 定时和延时消息单个 Topic 下同时存在的最大数量为10万条,超过这个并发数服务端会限制生产端继续生产这两种消息。为预防此类现象,请留意控制台上关于定时和延时消息指标的监控。
  • 关于定时和延时消息的时间范围,最大均为10天。
  • 使用定时消息时,设置的时刻在当前时刻以后才会有定时效果,否则消息将被立即发送给消费者。
  • 设定定时时间后,从定时的时间点开始计算消息最长保留时间,例如定时到3天后发送,消息最长保留7天,则到了第10天仍未被消费时,消息会被删除。延时消息同理。

3.2.3 消息重试与死信机制

重试 Topic 是一种为了确保消息被正常消费而设计的 Topic 。当某些消息第一次被消费者消费后,没有得到正常的回应,则会进入重试 Topic 中,当重试达到一定次数后,停止重试,投递到死信 Topic 中。

当消息进入到死信队列中,表示 TDMQ 已经无法自动处理这批消息,一般这时就需要人为介入来处理这批消息。您可以通过编写专门的客户端来订阅死信 Topic,处理这批之前处理失败的消息。

3.2.3.1 自动重试
3.2.3.1.1 相关概念

重试 Topic:一个重试 Topic 对应一个订阅名(一个订阅者组的唯一标识),以 Topic 形式存在于 TDMQ 中。当您新建了一个订阅后,会自动创建一个重试 Topic,该 Topic 会自主实现消息重试的机制。

该 Topic 命名为:

  • 2.7.1及以上版本集群:[订阅名]-RETRY
  • 2.6.1版本集群:[订阅名]-retry
3.2.3.1.2 实现原理

您创建的消费者使用某个订阅名以共享模式订阅了一个 Topic 后,如果开启了 enableRetry 属性,就会自动订阅这个订阅名对应的重试队列。

说明:

仅共享模式支持自动化重试和死信机制,独占和灾备模式不支持。

这里以 Java 语言客户端为例,在 topic1 创建了一个 sub1 的订阅,客户端使用 sub1 订阅名订阅了 topic1 并开启了 enableRetry,如下所示:

Consumer consumer = client.newConsumer()
  .topic("persistent://1******30/my-ns/topic1")
  .subscriptionType(SubscriptionType.Shared)//仅共享消费模式支持重试和死信
  .enableRetry(true)
  .subscriptionName("sub1")
  .subscribe();

此时,topic1sub1 的订阅就形成了带有重试机制的投递模式,sub1 会自动订阅之前在新建订阅时自动创建的重试 Topic 中(可以在控制台 Topic 列表中找到)。当 topic1 中的消息投递第一次未收到消费端 ACK 时,这条消息就会被自动投递到重试 Topic ,并且由于 consumer 自动订阅了这个主题,后续这条消息会在一定的 重试规则下重新被消费。当达到最大重试次数后仍失败,消息会被投递到对应的死信队列,等待人工处理。

说明:

如果是 client 端自动创建的订阅,可以通过控制台上的【Topic管理】>【更多】>【查看订阅】进入消费管理页面手动重建重试和死信队列。
img

3.2.3.1.3 自定义参数设置

TDMQ 会默认配置一套重试和死信参数,具体如下:

  • 2.7.1版本及以上集群
    • 指定重试次数为16次(失败16次后,第17次会投递到死信队列)
    • 指定重试队列为[订阅名]-RETRY
    • 指定死信队列为[订阅名]-DLQ
  • 2.6.1版本集群
    • 指定重试次数为16次(失败16次后,第17次会投递到死信队列)
    • 指定重试队列为[订阅名]-retry
    • 指定死信队列为[订阅名]-dlq

如果希望自定义配置这些参数,可以使用 deadLetterPolicy API 进行配置,代码如下:

Consumer<byte[]> consumer = pulsarClient.newConsumer()
  .topic("persistent://pulsar-****")
  .subscriptionName("sub1")
.subscriptionType(SubscriptionType.Shared)
  .enableRetry(true)//开启重试消费
  .deadLetterPolicy(DeadLetterPolicy.builder()
        .maxRedeliverCount(maxRedeliveryCount)//可以指定最大重试次数
        .retryLetterTopic("persistent://my-property/my-ns/sub1-retry")//可以指定重试队列
        .deadLetterTopic("persistent://my-property/my-ns/sub1-dlq")//可以指定死信队列
        .build())
  .subscribe();
3.2.3.1.4 重试规则

重试规则由 reconsumerLater API 实现,有三种模式:

//指定任意延迟时间
consumer.reconsumeLater(msg, 1000L, TimeUnit.MILLISECONDS);
//指定延迟等级
consumer.reconsumeLater(msg, 1);
//等级递增
consumer.reconsumeLater(msg);
  • 第一种:指定任意延迟时间。第二个参数填写延迟时间,第三个参数指定时间单位。延迟时间和延时消息的取值范围一致,范围在1 - 864000(单位:秒)。

  • 第二种:指定任意延迟等级(仅限存量腾讯云版SDK的用户使用)。实现效果和第一种基本一致,更方便统一管理分布式系统中的延时时长,延迟等级说明如下:

    1. reconsumeLater(msg, 1)

      中的第二个参数即为消息等级

    2. 默认MESSAGE_DELAYLEVEL = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h",这个常数决定了每级对应的延时时间,例如1级对应1s,3级对应10s。如果默认值不符合实际业务需求,用户可以重新自定义。

  • 第三种:等级递增(仅限存量腾讯云版SDK的用户使用)。实现的效果不同于以上两种,为退避式的重试,即第一次失败后重试间隔为1秒,第二次失败后重试间隔为5秒,以此类推,次数越多,间隔时间越长。具体时间间隔同样由第二种中介绍的 MESSAGE_DELAYLEVEL 决定。
    这种重试机制往往在业务场景中有更实际的应用,如果消费失败,一般服务不会立刻恢复,使用这种渐进式的重试方式更为合理。

注意:

如果您使用的是 Pulsar 社区的 SDK,则不支持延迟等级和等级递增两种模式。

3.2.3.1.5 重试消息的消息属性

一条重试消息会给消息带上如下 property。

{
REAL_TOPIC="persistent://my-property/my-ns/test, 
ORIGIN_MESSAGE_ID=314:28:-1, 
RETRY_TOPIC="persistent://my-property/my-ns/my-subscription-retry, 
RECONSUMETIMES=16
}
  • REAL_TOPIC:原 Topic
  • ORIGIN_MESSAGE_ID:最初生产的消息 ID
  • RETRY_TOPIC:重试 Topic
  • RECONSUMETIMES:代表该消息重试的次数
3.2.3.1.6 重试消息的消息 ID 流转

消息 ID 流转过程如下所示,您可以借助此规则对相关日志进行分析。

原始消费: msgid=1:1:0:1
第一次重试: msgid=2:1:-1
第二次重试: msgid=2:2:-1
第三次重试: msgid=2:3:-1
.......
第16次重试: msgid=2:16:0:1
第17次写入死信队列: msgid=3:1:-1
3.2.3.1.7 完整代码示例

以下为借助 TDMQ 实现完整消息重试机制的代码示例,供开发者参考。

3.2.3.1.7.1 订阅主题
Consumer<byte[]> consumer1 = client.newConsumer()
.topic("persistent://pulsar-****")
.subscriptionName("my-subscription")
.subscriptionType(SubscriptionType.Shared)
.enableRetry(true)//开启重试消费
//.deadLetterPolicy(DeadLetterPolicy.builder()
//         .maxRedeliverCount(maxRedeliveryCount)
//         .retryLetterTopic("persistent://my-property/my-ns/my-subscription-retry")//可以指定重试队列
//         .deadLetterTopic("persistent://my-property/my-ns/my-subscription-dlq")//可以指定死信队列
//         .build())
.subscribe();
3.2.3.1.7.2 执行消费
while (true) {
 Message msg = consumer.receive();
 try {
    // Do something with the message
    System.out.printf("Message received: %s", new String(msg.getData()));
    // Acknowledge the message so that it can be deleted by the message broker
    consumer.acknowledge(msg);
 } catch (Exception e) {
    // select reconsume policy
    consumer.reconsumeLater(msg, 1000L, TimeUnit.MILLISECONDS);
    //consumer.reconsumeLater(msg, 1);
    //consumer.reconsumeLater(msg);
 }
}
3.2.3.2 主动重试

当消费者在某个时间没有成功消费某条消息,如果想重新消费到这条消息时,消费者可以发送一条取消确认消息到 TDMQ 服务端,TDMQ 会将这条消息重新发给消费者。 这种方式重试时不会产生新的消息,所以也不能自定义重试间隔。

以下为主动重试的 Java 代码示例:

while (true) {
  Message msg = consumer.receive();
  try {
      // Do something with the message
      System.out.printf("Message received: %s", new String(msg.getData()));
      // Acknowledge the message so that it can be deleted by the message broker
      consumer.acknowledge(msg);
  } catch (Exception e) {
      // Message failed to process, redeliver later
      consumer.negativeAcknowledge(msg);
  }
  consumer.negativeAcknowledge(msg);
}

3.2.4 客户端连接与生产消费者

本文主要介绍 TDMQ Pulsar 客户端与连接、客户端与生产/消费者之间的关系,并向开发者介绍客户端合理的使用方式,以便更高效、稳定地使用 TDMQ Pulsar 版的服务。

核心原则:

  • 一个进程一个 PulsarClient 即可。
  • Producer、Consumer 是线程安全的,对于同一个 Topic,可以复用且最好复用。
3.2.4.1 客户端与连接

TDMQ Pulsar 客户端(以下简称 PulsarClient )是应用程序连接到 TDMQ Pulsar 版的一个基本单位,一个 PulsarClient 对应一个 TCP 连接。一般来说,用户侧的一个应用程序或者进程对应使用一个 PulsarClient,有多少个应用节点,对应就有多少个 Client 数量。若长时间不使用 TDMQ 服务的应用节点,应回收 Client 以节省资源消耗(当前 TDMQ Pulsar 版的连接上限是单个 Topic 2000个 Client 连接)。

3.2.4.2 客户端与生产/消费者

一个 Client 下可以创建多个生产和消费者,用于提升生产和消费的速度。比较常见的用法是,一个 Client 下,利用多线程创建多个 Producer 或 Consumer 对象,用于生产消费,不同 Producer 和 Consumer 之间数据相互隔离。

当前 TDMQ 对生产/消费者的限制为:

  • 单个 Topic 生产者上限1000个。
  • 单个 Topic 消费者上限500个。
3.2.4.3 最佳实践

生产/消费者的数量不一定取决于业务对象,它们是一个可以复用的资源,通过名称作为唯一标识进行区分。

3.2.4.3.1 生产者

假设有1000个业务对象在同时生产消息,并不是要创建1000个 Producer,只要是向同一个 Topic 进行投递,每个应用节点可以先统一使用一个 Producer 来进行生产(单例模式),往往单个 Producer 就能吃满单个应用节点的硬件配置。

以下给出一段 Java 消息生产的代码示例。

//从配置文件中获取 serviceURL 接入地址、Token 密钥、Topic 全名和 Subscription 名称(均可从控制台复制)
@Value("${tdmq.serviceUrl}")
private String serviceUrl;
@Value("${tdmq.token}")
private String token;
@Value("${tdmq.topic}")
private String topic;

//声明1个 Client 对象、producer 对象
private PulsarClient pulsarClient;
private Producer<string> producer;

//在一段初始化程序中创建好客户端和生产者对象
public void init() throws Exception {
    pulsarClient = PulsarClient.builder()
            .serviceUrl(serviceUrl)
            .authentication(AuthenticationFactory.token(token))
            .build();
    producer = pulsarClient.newProducer(Schema.STRING)
            .topic(topic)
            .create();
}

在实际生产消息的业务逻辑中直接引用 producer 完成消息的发送。

//在实际生产消息的业务逻辑中直接引用,注意 Producer 通过范式声明的 Schema 类型要和传入对象匹配
public void onProduce(Producer<string> producer){
    //添加业务逻辑
    String msg = "my-message";//模拟从业务逻辑拿到消息
    try {
        //TDMQ 默认开启 Schema 校验, 消息对象一定需要和 producer 声明的 Schema 类型匹配
        MessageId messageId = producer.newMessage()
              .key("msgKey")
                  .value(msg)
                  .send();
        System.out.println("delivered msg " + msgId + ", value:" + value);
    } catch (PulsarClientException e) {
          System.out.println("delivered msg failed, value:" + value);
          e.printStackTrace();
    }
}

public void onProduceAsync(Producer<string> producer){
    //添加业务逻辑
    String msg = "my-asnyc-message";//模拟从业务逻辑拿到消息
    //异步发送消息,无线程阻塞,提升发送速率
    CompletableFuture<messageid> messageIdFuture = producer.newMessage()
          .key("msgKey")
              .value(msg)
              .sendAsync();
    //通过异步回调得知消息发送成功与否
    messageIdFuture.whenComplete(((messageId, throwable) -> {
        if( null != throwable ) {
            System.out.println("delivery failed, value: " + msg );
            //此处可以添加延时重试的逻辑
        } else {
            System.out.println("delivered msg " + messageId + ", value:" + msg);
        }
    }));
}

当一个生产者长时间不使用时需要调用 close 方法关闭,以避免占用资源;当一个客户端实例长时间不使用时,同样需要调用 close 方法关闭,以避免连接池被占满。

public void destroy(){
    if (producer != null) {
        producer.close();
    }
    if (pulsarClient != null) {
        pulsarClient.close();
    }
}
3.2.4.3.2 消费者

如同生产者,消费者也最好按照单例模式进行使用,单个消费节点只需要一个客户端实例以及一个消费者实例。一般来说,一个消息队列的消费端的性能瓶颈都在于消费者按照自己业务逻辑处理消息的过程,而并非在接收消息的动作上。所以当出现了消费性能不足的时候,先看消费者的网络带宽消耗,如果趋势上看没有达到一个明显的上限,就应该先根据日志以及消息轨迹信息分析自身处理消息的业务逻辑耗时。

注意:

  • 当使用 Shared 或者 Key-Shared 模式时,消费者数量不一定小于等于分区数。服务端会有一个负责分发消息的模块按照一定的方式(Shared 模式默认是轮询,Key-Shared 则是在同一个 key 内轮询)将消息分发给所有的消费者。
  • 当使用 Shared模式,如果生产侧暂停了生产,则到了末尾一部分消息时,可能会出现消费分布不均的情况。
  • 使用多线程消费,即使复用一个 consumer 对象,消息的顺序也将无法得到保证。

以下给出一个 Java 基于 Spring boot 框架用线程池进行多线程消费的完整代码示例。

import org.apache.pulsar.client.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Service
public class ConsumerService implements Runnable {

    //从配置文件中获取 serviceURL 接入地址、Token 密钥、Topic 全名和 Subscription 名称(均可从控制台复制)
    @Value("${tdmq.serviceUrl}")
    private String serviceUrl;
    @Value("${tdmq.token}")
    private String token;
    @Value("${tdmq.topic}")
    private String topic;
    @Value("${tdmq.subscription}")
    private String subscription;

    private volatile boolean start = false;
    private PulsarClient pulsarClient;
    private Consumer<string> consumer;
    private static final int corePoolSize = 10;
    private static final int maximumPoolSize = 10;

    private ExecutorService executor;
    private static final Logger logger = LoggerFactory.getLogger(ConsumerService.class);

    @PostConstruct
    public void init() throws Exception {
        pulsarClient = PulsarClient.builder()
                .serviceUrl(serviceUrl)
                .authentication(AuthenticationFactory.token(token))
                .build();
        consumer = pulsarClient.newConsumer(Schema.STRING)
                .topic(topic)
                //.subscriptionType(SubscriptionType.Shared)
                .subscriptionName(subscription)
                .subscribe();
        executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.AbortPolicy());
        start = true;
    }

    @PreDestroy
    public void destroy() throws Exception {
        start = false;
        if (consumer != null) {
            consumer.close();
        }
        if (pulsarClient != null) {
            pulsarClient.close();
        }
        if (executor != null) {
            executor.shutdownNow();
        }
    }

    @Override
    public void run() {
        logger.info("tdmq consumer started...");
        for (int i = 0; i < maximumPoolSize; i++) {
            executor.submit(() -> {
                while (start) {
                    try {
                        Message<string> message = consumer.receive();
                        if (message == null) {
                            continue;
                        }
                        onConsumer(message);
                    } catch (Exception e) {
                        logger.warn("tdmq consumer business error", e);
                    }
                }
            });
        }
        logger.info("tdmq consumer stopped...");
    }

    /**
     * 这里写消费业务逻辑
     *
     * @param message
     * @return return true: 消息ack  return false: 消息nack
     * @throws Exception 消息nack
     */
    private void onConsumer(Message<string> message) {
        //业务逻辑,延时类操作
        try {
            System.out.println(Thread.currentThread().getName() + " - message receive: " + message.getValue());
            Thread.sleep(1000);//模拟业务逻辑处理
            consumer.acknowledge(message);
            logger.info(Thread.currentThread().getName() + " - message processing succeed:" + message.getValue());
        } catch (Exception exception) {
            consumer.negativeAcknowledge(message);
            logger.error(Thread.currentThread().getName() + " - message processing failed:" + message.getValue());
        }
    }
}
  • 6
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值