细读经典——RocketMQ技术NM(1)

本文深入剖析RocketMQ的整体架构,重点讲解NameServer的角色和配置,包括NameServerConfig和NettyServerConfig的设置,以及定时任务的实现。此外,还介绍了Broker如何向NameServer注册并保持心跳,以确保服务的高可用性。
摘要由CSDN通过智能技术生成

消息中间件目前是RocketMQ和Kafka两家独大,RocketMQ作为java开源代码,很适合中间件开发人员作为一手的学习材料,设计中的很多思想都是值得学习的。废话不多,进入正题。

一、整体架构

书中是从Nameserver切入整个RocketMQ的,但我这里从整体入手,希望你能先牢记这张图

包含几个重要部分:

Producer集群,NameServer集群,Broker集群以及Consumer集群。如果你熟悉微服务架构,那么整个集群和Eureka是非常像的:由Nameserver集群提供注册和发现,Producer集群负责生产消息,Consumer集群负责消息消费,Broker集群负责转发并持久化消息;NameServer集群保证AP,各个节点通过NameServer集群进行通信并确认状态。

是不是感觉很简单,没错架构是很简单。如果你了解分布式架构,那么你一定会提出一个问题:RocketMQ是否需要保证一致性?如果你了解过Kafka你也会问,为什么RocketMQ里没有ZK去保证Broker集群的高可用?这是因为RockerMQ在设计之初,就希望通过规避复杂的ZK集群而设计了自己的NameServer,这也导致RocketMQ在最初无法保证高可用,Broker节点虽然可以主从,但是Master写节点宕机之后该Broker是无法提供服务的。以至于后来,RocketMQ又根据Raft协议实现了自己的高可用架构Dledger,这都是后话了。

注意,如果你想部署一个RocketMQ,你要分别部署NameServer,Broker,Producer,Consumer。

看不懂没关系,先往下走。

1.1 NamerServer

书中是直接上代码的,虽然我不太推崇这么做,因为这样会让你预先陷入细节之中,我还是希望在看的你先从整体入手,尝试理解整个架构,代码只作为理解的工具,到你自己真正进入源码阅读阶段的时候,再进行详细的整理。

NameServer启动类包括NamerSrvStartup和NameSrvController,startup负责启动,controller负责配置信息录入和状态存放,我们聚焦在controller上:

先关注controller的成员变量: 

public class NamesrvController{
    private final NamesrvConfig namesrvConfig;

    private final NettyServerConfig nettyServerConfig;

    private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(
        "NSScheduledThread"));
    private final KVConfigManager kvConfigManager;
    //Broker路由信息管理
    private final RouteInfoManager routeInfoManager;

    private RemotingServer remotingServer;

    private BrokerHousekeepingService brokerHousekeepingService;

    private ExecutorService remotingExecutor;

    private Configuration configuration;
    private FileWatchService fileWatchService;
}

1.1.1NamesrvConfig和NettyServerConfig

定义了两个NS所需要的初始化配置:

// nameServer 初始化配置
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
public class NamesrvConfig{
//rocketMq主目录
    private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
    //kv配置的持久化路径
    private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
    //默认配置文件路径
    private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
}

public class NettyServerConfig{
    private int listenPort = 8888;
    private int serverWorkerThreads = 8;
    private int serverCallbackExecutorThreads = 0;
    private int serverSelectorThreads = 3;
    private int serverOnewaySemaphoreValue = 256;
    private int serverAsyncSemaphoreValue = 64;
    private int serverChannelMaxIdleTimeSeconds = 120;

    private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
    private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
    private boolean serverPooledByteBufAllocatorEnable = true;
    private boolean useEpollNativeSelector = false;
}

很明显,NettyServerConfig是为Netty服务的,也就是说NS也使用了NIO作为通信框架。具体配置我不再多说,英文也是一门很直接的语言,变量的作用和英文一模一样,不用怀疑,你熟悉netty的话应该完全不是问题。

    public boolean initialize() {
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
            }
        }, 5, 10, TimeUnit.SECONDS);

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                NamesrvController.this.kvConfigManager.printAllPeriodically();
            }
        }, 1, 10, TimeUnit.MINUTES);
}

 在Controller的initialize()方法中定义了两个定时任务,通过scheduledExecutorService构建,每隔10S分别扫描活跃的Broker,维持心跳,同时每隔10S打印一次KV配置。

    public static NamesrvController start(final NamesrvController controller) throws Exception {

        if (null == controller) {
            throw new IllegalArgumentException("NamesrvController is null");
        }

        boolean initResult = controller.initialize();
        if (!initResult) {
            controller.shutdown();
            System.exit(-3);
        }
        //钩子函数
        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));

        controller.start();

        return controller;
    }

 startup里还有个钩子函数,会在start()调用之前,去关闭需要释放的资源,类似监听者模式。

 1.1.2 RouteInfoManager 

 Broker管理的核心类,我是建议对这部分参数要非常非常的熟悉甚至是背诵,好处你会懂的,重要的成员变量:

public class RouteInfoManager {
    //负载均衡表Topic级别
    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
    //broker基础信息
    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
    //集群中所有的broker名称
    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
    // broker状态信息 ,每次收到心跳包时更新
    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
    // 消息过滤,用于类模式消息过滤
    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}

不走许愿派风格,我们感性的从一个实例说明一下这些参数存了什么:

topicQueueTable:存储topic和broker的映射用于消息路由

brokerAddrTable:broker的基础信息,存放broker所在的集群,主备broker地址,0代表master,其余代表slave

clusterAddrTable:broker集群信息

brokerLiveTable:活跃的broker信息,NS和broker维持长tcp连接,每当NS收到broker心跳时,都会更新lastupdatetimestamp,确保broker的存活状态。

filterServerTable:用于消息过滤,这个之后会提到,类似kafka里的拦截器

1.1.3 Broker和NS

broker需要向NS注册自己,并需要在注册之后,维持和NS的心跳,确保自己的存活状态被感知。

在BrokerController的start方法中,新建任务周期性的向NS更新自己的状态

public void start() throws Exception {
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
          @Override
          public void run() {
               try {
                    BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
                } catch (Throwable e) {
                    log.error("registerBrokerAll Exception", e);
                }
            }
        }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
}

看一下注册方法底层,这段注册代码可以拿来当所有注册的模板了,你要自己开发中间件,就这么写,经典;registerBroker方法跟到底层,你会发现又是netty编程,所以这块需要你任务调度和netty编程的基础知识。

    public List<RegisterBrokerResult> registerBrokerAll(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId,
        final String haServerAddr,
        final TopicConfigSerializeWrapper topicConfigWrapper,
        final List<String> filterServerList,
        final boolean oneway,
        final int timeoutMills,
        final boolean compressed) {
        //broker向所有nameserver注册
        final List<RegisterBrokerResult> registerBrokerResultList = Lists.newArrayList();
        List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
        if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
            //组装请求报文
            final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
            requestHeader.setBrokerAddr(brokerAddr);
            requestHeader.setBrokerId(brokerId);
            requestHeader.setBrokerName(brokerName);
            requestHeader.setClusterName(clusterName);
            requestHeader.setHaServerAddr(haServerAddr);
            requestHeader.setCompressed(compressed);

            RegisterBrokerBody requestBody = new RegisterBrokerBody();
            requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
            requestBody.setFilterServerList(filterServerList);
            final byte[] body = requestBody.encode(compressed);
            final int bodyCrc32 = UtilAll.crc32(body);
            requestHeader.setBodyCrc32(bodyCrc32);
            // 要一个countDownLatch保证所有任务执行完
            final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
            for (final String namesrvAddr : nameServerAddressList) {
                brokerOuterExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
                            if (result != null) {
                                //注册有结果就记录结果,子线程结束
                                registerBrokerResultList.add(result);
                            }
                            log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                        } catch (Exception e) {
                            log.warn("registerBroker Exception, {}", namesrvAddr, e);
                        } finally {
                            // 一定要在finally里countDown
                            countDownLatch.countDown();
                        }
                    }
                });
            }

            try {
                //等待向所有NS注册线程完成或者失败
                countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }
        }

        return registerBrokerResultList;
    }

路由发现和删除我就不展开了,源码很有意思,这里其实和使用关系不大,不过源码确实很精彩,希望大家自己看看。

最后放一张NS路由,注册,发现的状态转换图

NS先到这里,下一节开Producer!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值