zoukankan      html  css  js  c++  java
  • 消息中间件客户端消费控制实践

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


    背景

    消息中间件是用来系统间通信、异步解耦、削峰填谷的重要手段,个人认为一个比较靠谱的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+款云产品!

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


    相关文章:
    【推荐】 HBase-数据写入流程解析

  • 相关阅读:
    socket阻塞与非阻塞,同步与异步
    Python列表切成多个/生成多个空列表
    virtualbox 下windows与虚拟机实现文件共享---挂载
    centos安装mysql
    centos安装Python2.7
    在遍历或者迭代过程中对集合进行增删 都会发生异常(迭代器的并发异常)
    List
    LinkedList
    增强for循环
    Collection中的迭代器
  • 原文地址:https://www.cnblogs.com/zyfd/p/9773679.html
Copyright © 2011-2022 走看看