消息中间件客户端消费控制实践

本文来自网易云社区,转载务必请注明出处。


背景

消息中间件是用来系统间通信、异步解耦、削峰填谷的重要手段,个人认为一个比较靠谱的Mq,应该具备以下特点

  • 控制投递:消息消费失败,支持消息有节奏的重新投递

  • 延迟消费:支持消息延迟消费,用来解决诸如消息乱序的场景

  • 流控消费:消费支持流控,真正的支持削峰填谷

  • 消费监控:消息消费的监控

目前考拉常用的消息中间件有rabbitMq和kafka,各自都有一些问题,不能完美胜任以上功能,因此本文试着探索并实践了一个消息控制的框架。


知己知彼


RabbitMq

先拿rabbitMq来说点事儿,这个mq的问题比较大,但是目前交易核心的消息都在rabbitMq上面。

对比原始需求

  • 控制投递:消息消费失败后,消息会被服务端无节制投递,一但处理逻辑有bug,触发无限投递,瞬间服务器会被消息打爆。

  • 延迟消费:乱序问题是mq共用的问题,rabbitMq并没有提供解决方案

  • 流控消费:消息对象保存在mq实例内存中,因此rabbitMq本身不支持堆积太多消息。

  • 消费监控:控制台数据简陋

Kafka

然后拿kafka做下对比,kafka性能、扩展性都优于rabbitMq,不过也不能完全满足我们的需求

对比原始需求

  • 控制投递:kafka消费有offset的概念,通过消费者代码实现可以主动控制消费节奏。

  • 延迟消费:kafka也没有提供结局方案

  • 流控消费:kafka消息数据落在磁盘上,可以堆积比较多的消息,但是对于消费方怎么流控并没有提供方案。

  • 消费监控:数据也比较简陋

设计思路


最理想情况,这些功能可以直接做在服务端上面,客户端不用做太多改造。不过,考虑现实情况,没办法直接去改kafka和rabbitMq源码,只能退而求其次去改造消息客户端,在消息客户端和消费者之间增加一个消息控制框架。

消息控制框架主要结构如下:

另外针对持久化到客户端的数据,还结合k-scheduler提供了一个消息重推模块,如下:

下面针对原始需求,看看消息控制框架都做了什么事情:


  • 控制投递:消息控制层catch消费异常,rabbitMq的消息会直接持久化消息后续重推,kafka消息异常,重置offset,有节制的重推。

  • 延迟消费:消息适配层提供延迟推送接口,需要业务方识别出消息乱序后,调用接口,接口会直接持久化乱序消息,在指定延迟时间后重新推送。

  • 流控消费:对于rabbitMq消息,控制层提供单机流控接口,被流控的消息直接持久化到DB,等待后续重推。另外针对kafka消息,集成了nfc全局流控,框架识别流控错误码,有节制的重推消息。

  • 消费监控:对接哨兵监控,所有消息消费、失败、流控等数据都会采集到哨兵


详细实现


RabbitMq消息的详细实现

rabbitMq和kafka都有各自特点,因此虽然整体框架的思路是一致的,但是一些细节处理还是略有不同,此处先拿rabbitMq的实现来作分析:

先上图

如图展示了一条rabbitMq消息是如何经过消息控制框架的,异常消息、延迟推送以及被流控的消息都会落库,然后等待重推。

被持久化的消息主要包含以下字段

  • 应用名

  • 业务名

  • 协议名(kafka或者rabbitMq)

  • 环境名(预发或者线上或者beta)

  • 消息体

  • 消息重推时间

  • 当前重试次数(根据重试次数实现了一个退避算法,来计算下次重推时间)

  • 消息状态

其中,为了表明一个消息和消费者之间的归属关系,提出了一个消费者分组的概念。

一个消费者分组包含应用名、业务名、协议名以及环境名,可以对应到唯一的消费者

重推逻辑如下

重推任务依赖于外部驱动,可以是cron可以是k-schedule,动动手指配置一下就ok。

目前重推任务只支持单机重试,因此大批量的消息重推消费速度不能得到保证。


kafka消息的差异实现

kafka本身可以堆积消息,因此摒弃了流控落库的逻辑,直接重置offset,有节奏的重试。另外,集成了nfc的全局流控,kafka的消费者直接使用nfc全局流控。

此外,对于kafka异常消息的处理,框架也是利用offset来重试,没有落库。


监控示例

核心代码实现


核心类图

针对交易消息做了定制化处理,对于kafka交易消息对接方只需要继承实现AbstractTradeKafkaControlProcessor,对于rabbitMq类型交易消息继承实现AbstractTradeRabbitControlListener即可。

AbstractControlListener中核心的消息控制代码如下,AbstractTradeKafkaControlProcessor中有针对kafka的特点做改动,不再赘述。


     /**
     * 消息处理流程
     *
     * @param message
     * @param controlDTO
     */
    protected void processControlMessage(T message, ControlDTO controlDTO) {
        BizIdTypeBond bizIdTypeBond = buildBizIdTypeBond(message);
        MonitorNameSpace monitorNameSpace = buildMonitorNameSpace(bizIdTypeBond);        // 统计消息处理个数
        MonitorFactory.getMonitorService().onNewMessage(monitorNameSpace, 1, false);		// 是否流控
        boolean needRelease = false;        if (isOpenFlowControl()) {
            flowControlService.aquireResource();
            needRelease = true;
        }        // 执行业务逻辑
        try {
            onControlMessage(message, controlDTO);            if (controlDTO.getDelayPush() != null) {                // 延迟推送
                storeService.storeMessage(encodeStoreMessage(message, bizIdTypeBond), controlDTO.getDelayPush(),                        "delay push");
                MonitorFactory.getMonitorService().onStoreMessage4DelayPush(monitorNameSpace, 1, false);
                NotifyConstants.NOTIFY_LOG.warn("delay push messageDTO=", message);
            }
        } catch (Throwable t) {            // 异常控制
            String note = NotifyCommonUtil.buildCallStatck(t, 500);
            storeService.storeMessage(encodeStoreMessage(message, bizIdTypeBond), null, note);
            MonitorFactory.getMonitorService().onStoreMessage4Exception(monitorNameSpace, 1, false, note);
            NotifyConstants.NOTIFY_LOG.warn("process failed messageDTO=", message);            return;
        } finally {            if (needRelease) {
                flowControlService.releaseResoure();
            }
        }
    }复制代码


对接示例

xml配置

    <bean id="globalControlConfig"
          >
        <property name="applicationName" value="order"/>
        <property name="enviroment" value="${message.control.environment}"/>
        <property name="tableName" value="tb_mq_message_control"/>
        <property name="dataSource" ref="rdsDataSource"/>
    </bean>

 <!--交易事件变更监听器-->
    <bean id="tradeEventListener"
          >
        <property name="notifyControlConfig" ref="notifyControlConfigTrade"/>
    </bean>

    <bean id="notifyControlConfigTrade"
          >
        <property name="bizGroup" value="trade"/>
    </bean>

 <!-- 交易事件兜底重试任务 -->复制代码
<bean id="retryTaskEntry" class="com.netease.haitao.notify.base.task.runner.RetryTaskEntry"/>复制代码


代码示例


rabbitMq

public class TradeEventListener extends AbstractTradeRabbitControlListener {

   @Resource
   private OrderComposeConfigHolder orderComposeConfigHolder;

   @Resource
   private TradeEventService tradeEventService;

   @Override
   protected boolean isOpenMessageControl() {
      return orderComposeConfigHolder.isOpenTradeMessageControl();
   }

   @Override
   protected int flowControlThreshold() {
      return orderComposeConfigHolder.tradeEventFlowControlThreshold();
   }

   @Override
   protected boolean isOpenFlowControl() {
      return orderComposeConfigHolder.isOpenFlowControl();
   }

   @Override
   public void onControlTradeEvent(TradeEvent tradeEvent, ControlDTO controlDTO) throws Exception {

        OrderComposeLogConstants.notifyLog.info("onTradeEvent,message=" + tradeEvent.toString());
      try {
         tradeEventService.processTradeEvent(tradeEvent);
      } catch (OrderComposeException e) {
         if (e.getErrorCode().equals(OrderComposeErrorEnum.TRADE_EVENT_WRONG_ORDER.intValue())) {
            OrderComposeLogConstants.notifyLog.warn("message wrong order,tradeEvent=" + tradeEvent.toString()
                  + ",delayPush=" + orderComposeConfigHolder.tradeEventWrongOrderDelayPushTime());
            // 乱序之后的延迟消费
            controlDTO.setDelayPush(orderComposeConfigHolder.tradeEventWrongOrderDelayPushTime());
         } else {
            OrderComposeLogConstants.notifyLog.warn("process failed,tradeEvent=" + tradeEvent.toString(), e);
            throw e;
         }
      } catch (Exception e) {
         OrderComposeLogConstants.notifyLog.warn("process failed,tradeEvent=" + tradeEvent.toString(), e);
         throw e;
      }
   }
}复制代码

kafka

/**
 * 订单创建订单占用库存消息处理
 */
public class OrderInvUnpayCloseEventProcessor extends AbstractTradeKafkaControlProcessor<UnpayCancelEvent> {

    @Resource
    private OrderInvModule orderInvModule;

    @Override
    @GlobalResource(resourceName = NfcResources.orderInvNotify)
    public void onControlMessage(UnpayCancelEvent unpayCancelEvent, ControlDTO controlDTO) throws Exception {
        OrderComposeLogConstants.notifyLog.info("OrderInvCreateEventProcessor,unpayCancelEvent=" + unpayCancelEvent);
        orderInvModule.processUnpayClose(unpayCancelEvent.getGorderId());
    }

}复制代码
复制代码

本文来自网易云社区 ,经作者程汉授权发布。

网易云免费体验馆,0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区


相关文章:
【推荐】 3分钟带你了解负载均衡服务
【推荐】 Puppeteer入门初探
【推荐】 双11背后的黑科技:大数据实时计算如何为你量身定制?


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值