mq使用经验
1、Producer使用指南--发送消息注意事项
1、正常情况下一个业务系统尽可能用一个Topic,消息子类型用tags来标识,tags可以由业务系统自由设置。只有发送消息设置了tags,消费方在订阅消息时,才可以利用tags在broker做消息过滤。
MQCPMessage msg = new MQCPMessage(); // 初始化消息对象
message.setTags("TagA"); // 设置消息TAG
2、每个消息在业务层面的唯一标识码,要设置到keys字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过topic,key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key尽可能唯一,这样可以避免潜在的哈希冲突。
MQCPMessage msg = new MQCPMessage(); // 初始化消息对象
message.setTags("TagA"); // 设置消息TAG
String orderId = "20034568923546"; // 订单Id
message.setKeys(orderId);
3、消息发送成功或者失败,要打印消息日志,务必要打印sendresult和key字段。
4、send消息方法,只要不抛异常,就代表发送成功。但是发送成功会有多个状态,在sendResult里定义
5、对于消息不可丢失应用,务必要有消息重发机制。例如如果消息发送失败,存储到数据库,能有定时程序尝试重发,或者人工触发重发。
2、Consumer使用指南--消费端去重
RocketMQ无法避免消息重复,所以如果业务对消费重复非常敏感,务必要在业务层面去重,有以下几种去重方式:
1. 将消息的唯一键,可以是msgID,也可以是消息内容中的唯一标识字段,例如订单Id等,消费之前判断是否在Db或Tair(全局KV存储)中存在,如果不存在则插入,并消费,否则跳过。(实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过)msgId一定是全局唯一标识符,但是可能会存在同样的消息有两个不同msgId的情况(有多种原因),这种情况可能会使业务上重复消费,建议最好使用消息内容中的唯一标识字段去重。
2. 使用业务层面的状态机去重
5、如何判断发送消息是否成功?
客户端Producer调用send消息方法,只要不抛异常,就代表发送成功。但是发送成功会有多个状态,在sendResult里定义。
返回状态 |
状态释义 |
SEND_OK |
消息发送成功 |
FLUSH_DISK_TIMEOUT |
消息发送成功,但是服务器刷盘超时,消息已经进入服务器队列,只有此时MASTER服务器宕机,消息才会丢失 |
FLUSH_SLAVE_TIMEOUT |
消息发送成功,但是服务器同步到Slave时超时,消息已经进入服务器队列,只有此时SLAVE服务器宕机,消息才会丢失 |
SLAVE_NOT_AVAILABLE |
消息发送成功,但是此时SLAVE不可用,消息已经进入服务器队列,只有此时SLAVE服务器宕机,消息才会丢失 |
目前MQCP测试和生产环境集群都采用两主两从共4台Broker机器,针对大部分业务系统来讲,只要MQCP没有抛出异常,可以默认消息已成功发送。建议业务系统针对发送消息后所有非SEND_OK状态的消息,打印Warning日志,并在运营端设置对应的监控规则,及时发邮件提醒。
6、如何判断消费消息是否成功?
客户端Consumer在MQCPMessageListener中实现pushMessage(),遍历并处理消息后会返回给MQCP端消费的状态,状态只有消费成功或者消费失败两种状态。
消费状态 |
状态释义 |
CONSUME_OK |
消费成功 |
CONSUME_FAIL |
消费失败 |
7、消费端如何实现定时消费?
在某些业务场景下,消费端希望在业务低峰(例如半夜12点后)时开始从MQCP拉取消息,在业务高峰期前关闭掉消费功能,以此来降低系统负载。这种类似场景涉及到如何在不停业务服务的场景下,多次的开启和关闭MQCP消费服务。
MQCP的消费者本身是可以多实例初始化的,每个实例的消费者服务开启和关闭也是独立的,所以可以很良好的支持定时消费的场景。
如果业务系统有类似的需求,我们建议:
1. 业务系统本身需要添加功能开关,支持配置化的方式来开启或关闭消费服务。其实现本身比较简单,就是调用MQCP消费者的start || shutdown方法。必要时需要对方法添加上层逻辑封装,来实现定制化的需求。
2. 调用完consumer对象的shutdown方法后,不要立即初始化下一个consumer对象并启用服务,建议至少延迟几秒种,等相关的资源回收完毕。
3. 完善业务端开启或关闭消息服务的日志,方便后续运维处理问题。
4. 具体实现方式,可以参考下面TestTimedConsumeImpl类。
import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.Properties; import com.paic.mqcp.client.MQCPFactory; import com.paic.mqcp.client.common.MQCPException; import com.paic.mqcp.client.common.MQCPMessageListener; import com.paic.mqcp.client.consumer.MQCPConsumer; import com.paic.mqcp.client.dto.MQCPMessage; import com.paic.mqcp.client.dto.MQCPMessageFilter; import com.paic.mqcp.client.util.MQCPConstant; import com.paic.mqcp.client.util.MQCPConsumeStatus; import com.paic.mqcp.common.consumer.ConsumeFromWhere; /** * 模拟业务系统需要在不停应用的场景下,开启、关闭接受消息服务 * * @author WUJING754 * */ public class TestTimedConsumeImpl { /** 属性对象 */ private static Properties p = null; /** * 获取属性值对象 * * @return Properties */ public static Properties initialProperties() { p = new Properties(); // 业务系统可以从配置文件中取出该属性,demo中写死的 p.setProperty(MQCPConstant.NAME_SERVER_ADDRESS, "10.20.22.148:9876;10.20.22.149:9876"); // 消费者ID,业务系统需要在MQCP-ADMIN中注册,否则无法正常发送消息 // demo中已经初始化好了的,后续请联系MQCP开发注册系统 p.setProperty(MQCPConstant.CONSUMER_ID, "CID_PARP_TEST_DEFAULT"); // INSTANCE_NAME属性建议业务系统不设置,用默认的即可 //p.setProperty(MQCPConstant.INSTANCE_NAME,"_PACP"); return p; } public static MQCPConsumer initialConsumer(){ MQCPConsumer pushConsumer = null; try { // 初始化过滤对象 MQCPMessageFilter mqcpFilter = new MQCPMessageFilter(); List<String> list = new ArrayList<String>(); // 设置tag // tag的作用是过滤消息,如果设置改值,MQCP只会取出发送消息时设置了该tag值的消息 // 需要注意的是同一CID下的多个应用实例需要设置为同样的tag列表来过滤消息,以保证消息不会被过滤取走但未被业务系统处理 list.add("testByWUJING754"); mqcpFilter.setTags(list); // 初始化MQCPPushConsumer并指定为集群消费(消息只会被消费一次,无论应用的实例有多少个) // PUSH消费模式下,客户端包会启动后台线程不断的从MQCP中拉去消息(准实时方式,毫秒级延时) // 业务系统收取到消息后,需要实现MQCPMessageListener,来处理消息 pushConsumer = MQCPFactory .createConsumer(initialProperties()); // 设置第一次CID消费消息的时间点,这里设置从最后的消息开始获取 // 如果不设置该值,默认是队列的最开始出消费消息 pushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); // 订阅 T_PAFA5_LOG_P 主题的消息(创建topic需要在MQCP-ADMIN中,如需创建请联系MQCP开发) pushConsumer.subscribe("T_PARP_TEST_DEMO", mqcpFilter, new MQCPMessageListener() { @Override public MQCPConsumeStatus pushMessage( List<MQCPMessage> messageList) { // 监听到有消息抵达后,业务系统需要遍历messageList对象,来获取消息 // messageList的默认大小为1,即消息是一条一条的推送到客户端的 for (MQCPMessage msg : messageList) { // 消息明细 MQCPMessage对象 // 建议业务系统将从消息平台拉取消息和处理消息的逻辑解耦, // 在consumer监听器中只监听到消息,建议简单的将获取的消息解析存储,然后返回 MQCPConsumeStatus.CONSUME_OK, // 后端可以异步来处理接收到的消息 try { System.out.println("###########receive message\n[msgTopic:" + msg.getTopic() + "\nmsgId:" + msg.getMsgId() + "\nmsgContent:" + new String(msg.getConent(),"UTF-8") + "\nmsgKey:" + msg.getKey() + "]"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return MQCPConsumeStatus.CONSUME_OK; } }); } catch (Exception ex) { ex.printStackTrace(); } return pushConsumer; } /** * 入口 * @param args */ public static void main(String[] args) { // 初始化一个pushConsumer对象 MQCPConsumer pushConsumer = initialConsumer(); // 启动该consumer对象,开始消费消息 startConsumingMsg(pushConsumer); // 业务系统不断收取消息,处理消息过程中... // 根据业务需要,可以调用stopConsumingMsg()方法来停止收取消息 stopConsumingMsg(pushConsumer); // 主线程休眠30秒 try { Thread.sleep(30000L); } catch (InterruptedException e) { e.printStackTrace(); } // 模拟业务系统再次初始化一个consumer对象 pushConsumer = initialConsumer(); // 启动consumer,开始收取消息 startConsumingMsg(pushConsumer); // 业务系统不断收取消息,处理消息过程中... // 业务系统根据需要,停止取消息服务 stopConsumingMsg(pushConsumer); } /* * 启动传入的消费者对象 */ public static boolean startConsumingMsg(final MQCPConsumer pushConsumer){ boolean flag = true; try { pushConsumer.start(); System.out.println("-->start consumer success."); } catch (MQCPException e) { System.out.println("-->start consumer fail due to " + e.getMessage()); e.printStackTrace(); flag = false; } return flag; } /* * 关闭传入的消费者对象 */ public static void stopConsumingMsg(final MQCPConsumer pushConsumer){ pushConsumer.shutdown(); System.out.println("-->shutdown consumer success."); } }
9、客户端生产者是否有消息重发机制?
(摘至RocketMq官方文档)
RocketMq消息发送失败如何处理,Producer的send方法本身支持内部重试,重试逻辑如下:
1. 至多重试5次。
2. 如果发送失败,则轮转到下一个Broker。
3. 这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认5s。
所以,如果本身向broker发送消息产生超时异常,就不会再做重试。
以上策略仍然不能保证消息一定发送成功,为保证消息一定成功,建议应用这样做:如果调用send同步方法发送失败,则尝试将消息存储到db,由后台线程定时重试,保证消息一定到达Broker。
上述DB重试方式为什么没有集成到MQ客户端内部做,而是要求应用自己去完成,我们基于以下几点考虑
1. MQ的客户端设计为无状态模式,方便任意的水平扩展,且对机器资源的消耗仅仅是cpu、内存、网络。
2. 如果MQ客户端内部集成一个KV存储模块,那么数据只有同步落盘才能较可靠,而同步落盘本身性能开销较大,所以通常会采用 异步落盘,又由于应用关闭过程不受MQ运维人员控制,可能经常会发生kill -9这样暴力方式关闭,造成数据没有及时落盘而丢 失。
3. Producer所在机器的可靠性较低,一般为虚拟机,不适合存储重要数据。
综上,建议重试过程交由应用来控制。
10、客户端消费者是否有消息重发机制?
(摘至RocketMq官方文档)
消息重试机制如下:
注意:重试的消息,MsgKey不变,MsgId会变。
11、业务消费端没有取到消息,如何去定位问题?
正常情况下生产者发送消息到MQCP,消息被投递到消费端的延时应该在毫秒级。如果消费端迟迟没有收到消息,建议采用下面的步骤来排查问题:
1、 获得消息的消息ID或者KEY,去MQCP-ADMIN的消息查询模块,根据自己的消费者ID找到其对应的消费状态。
2、 常见的投递状态有:
SUBSCRIBED_AND_CONSUMED |
订阅了,而且消费了(Offset越过了) |
SUBSCRIBED_BUT_FILTERD |
订阅了,但是被过滤掉了 |
the consumer group[***] not online |
订阅了,但是消费者未启动 |
SUBSCRIBED_AND_NOT_CONSUME_YET |
订阅了,但是没有消费(Offset小) |
UNKNOW_EXCEPTION |
未知异常 |
注: SUBSCRIBED_AND_CONSUMED状态,表示消息已被正常消费掉,如果此时有异常,需要业务系统检查日志,分析看看是否因为解析消息时有异常,导致消息未被正确处理。
SUBSCRIBED_BUT_FILTERD状态,需要业务系统检查初始化Consumer对象时传入的TagList是否和生产者定义的tag匹配。
SUBSCRIBED_AND_NOT_CONSUME_YET状态,可能的原因是由于消息有积压,消息还未被取走,可以稍等几十秒再去查询一下状态。
the consumer group[***] not online状态,表示对应CID的消费者还未正确启动。业务系统需要检查消费者是否已启动,如果已启动请检查是否启动时有报错。有可能相关的配置项配置错误,导致consumer启动时校验失败。
UNKNOW_EXCEPTION表示消息平台有异常,请联系MQCP开发同事。
13、如何避免接收到的消息是乱码?
对于生产者来说,建议将消息body转为byte数组时显示指定为UTF-8编码。对于消费者来说,建议在接收到消息后将byte数组转为String时指定UTF-8编码。这样可以避免因为消息body中有中文或者特殊字符,消费端解析时乱码,进而造成消息解析失败。
Example of producer
MQCPMessage msg = new MQCPMessage();
msg.setConent("test msg body".getBytes("UTF-8")); // 生产者组装消息body时指定urf-8编码
Example of consumer
14、MQCP中的消息标签(tag)如何使用?
在消息中间件实际的使用场景中,消费者只需要消息队列中的部分消息,其余消息希望默认不被接收,直接丢弃掉。针对类似这种场景 ,MQCP提供通过合理使用消息标签(Tag)的方式来实现消费端灵活过滤队列中消息的功能。
实现方式如下:
1、 生产者和消费者双方约定消息标签具体的设置值及其代表的含义。
2、 生产者在发送消息时,组装消息对象的时候,需要给对应消息设置正确的消息标签。
MQCPMessage msg = new MQCPMessage();
//设置过滤标签---大小写敏感
msg.setTag("SystemTag");
3、 消费者在组装消费者对象时,需要正确设置消息过滤的过滤器
MQCPMessageFilter mqcpFilter = new MQCPMessageFilter();
List<String> list = new ArrayList<String>();
// 设置tag
// tag的作用是过滤消息,如果设置改值,MQCP只会取出发送消息时设置了该tag值的消息
// 需要注意的是同一CID下的多个应用实例需要设置为同样的tag列表来过滤消息,以保证消息不会被过滤取走但未被业务系统处理list.add("SystemTag");
mqcpFilter.setTags(list);
上述生产端、消费端的实现代码具体可查阅MQCP官网提供的Demo。
16、The consumer’s subscription not exist报错?
现象:业务系统开发环境报异常:The consumer’s subscription not exist,检查消费者状态,多台实例在线,检查消息消费情况,消息一直不被消费,状态为:SUBSCRIBED_AND_NOT_CONSUME_YET(订阅未消费)。
原因:业务系统之前已经接入过MQCP,且开发环境有多个开发人员在同时进行开发,多个主机同时连接MQCP,本次该consumer新增了1个订阅关系(subscription-A),但业务系统只有1个同事(小Y)的代码才新增了订阅关系,其他几个开发人员的代码没有配置,一旦他们的consumer启动后,给broker保持通信时发送的订阅关系中,并没有subscription-A,broker会remove该订阅关系,因此小Y的consumer启动后,与broker通信时发现没有subscription-A,就会报:The consumer’s subscription not exist。
解决方案:1、不同的cid,订阅不同的topic,避免同一个cid订阅多个topic
2、业务系统开发环境,只开发相关功能的主机才启动consumer
17、业务系统消费端两种常见的错误实现?
18、申请生产环境mq消息查询权限
请使用IE浏览器,在itsm系统申请对应的权限,通道如下:
IT权限管理-申请->平安科技_消息协作平台监控应用(MQCP_ADMIN)=>帐户管理组
19、测试环境admin平台查询消息已消费,但消费端未查询到消息
同一个消费者是集群消费模式,在测试环境中,只想在某一个环境测试MQ功能,需要每个环境配置不同的CID。比如某系统测试环境有多套环境:STG1和STG2环境,测试人员正在STG1测试,如果两个环境的CID相同,则消息有可能就被STG2取走,测试人员在STG1上查不到该消息。
解决方法:
1、若为刚新增的发布订阅关系,请联系MQCP同事申请给每一个环境配置独立的CID,并创建发布订阅关系
2、若每个环境都申请了独立的CID,请检查是否其他某个环境使用了该环境的CID
20、java.lang.reflect.InvocationTargetException告警
业务系统引用客户端包在1.0.15之前版本在启动时可能会出现该异常,不影响消息的收发,1.0.15的客户端版本修复了该问题
解决方法:
更新客户端包为最新的版本,最新包版本可在包库查询。客户端包不断在迭代优化,建议业务系统及时更新客户端包。客户端包每个版本更新内容可在官网“相关文档---CLIENT包版本线”查询。
需注意:客户端包在1.0.20优化了发布订阅关系的校验方式,配置文件的配置项有变化,从原来的name_server_address、cluster_id
、virtual_account变为了:server_address、virtual_account,具体请查阅demo
21、服务启动时报错:MQCP_CLIENT->Initial cache failed,no cache message received after waiting for 90s
客户端在重启的时报错初始化发布订阅关系缓存数据失败。
解决方法:
1、检查是否新增了配置文件:mqcp_client.properties,需新增
2、检查mqcp_client.properties配置文件,客户端版本为1.0.20之前的版本,需检查name_server_address、cluster_id的值是否正确,前后是否包含空格,检查virtual_account配置的虚拟用户是否与admin平台配置的一致,客户端版本为1.0.15之前的,还需在代码里新增name_server_address的配置:p.setProperty(MQCPConstant.NAME_SERVER_ADDRESS,"不同环境配置不同的nameserver地址" ); 客户端版本为1.0.20及之后的,需要检查server_address、virtual_account是否正确,是否包含空格。
3、建议更新客户端包为最新的包。