Rocketmq之NameServer地址配置及更新

前言

NameServer在整个Rocketmq的模块划分中占据重要的地位,起到类似于注册中心的作用。BrokerServer启动时需要向NameServer注册自身元数据信息以及主题Topic信息,而Producer发送消息到BrokerServer、Consumer从BrokerServer订阅消息,则需要经过NameServer才能确定最终要进行数据通讯的BrokerServer的地址,所以,BrokerServer、Producer、Consumer程序启动均需要配置NameServer的地址。本篇文章就来聊一聊,BrokerServer、Producer、Consumer程序如何设置NameServer的地址以及NameServer的地址是否可以动态更新。

配置方式

首要我们需要清楚一点,代理服务器BrokerServer的配置信息最后会封装到BrokerConfig对象中,而生产者Producer、消费者Consumer的配置信息最终会封装到ClientConfig对象中,那么在这两个类中应该有NameServer地址相关的变量。

ClientConfig代码片段

public class ClientConfig {
    // ...省略部分代码
    private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses();
    // ...省略部分代码
}

NameServerAddressUtils#getNameServerAddresses

public class NameServerAddressUtils {
    // ...省略部分代码
    public static String getNameServerAddresses() {
        return System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV));
    }
    // ...省略部分代码
}

BrokerConfig代码片段

public class BrokerConfig {
    // ...省略部分代码
    @ImportantField
    private String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV));
    // ...省略部分代码
}

可见,不管是ClientConfig,还是BrokerConfig,NameServer地址变量的初始值,默认先取系统属性rocketmq.namesrv.addr的值,如果该系统属性未设置,则再取环境变量NAMESRV_ADDR的值,如果两者均未设置,那么配置类中NameServer地址变量初始值为null。
注:
通过上述分析,我们可以知晓两种配置NameServer地址的方法,不管你的程序是BrokerServer、Producer或者是Consumer
1.设置系统属性:rocketmq.namesrv.addr
2.设置环境变量:NAMESRV_ADDR

如果配置类中NameServer地址变量初始值为null,那BrokerServer、Producer、Consumer启动的时候是不是就会因为没有这个值而启动不了或者报错呢?其实并不会,还有额外的补偿手段,程序会通过http请求去访问一个特定的url获取NameServer地址,我们继续分析。

Producer或者Consumer

Producer或者Consumer底层都会持有一个MQClientInstance类对象,而在MQClientInstance类中,我们可以看到通过url请求NameServer地址的代码。原生API发送消息或者消费消息的代码大致如下所列,我们以消息发送者的start方法为切入口进行分析。

// 原生API发送消息
DefaultMQProducer producer = new DefaultMQProducer("test-group");
producer.start();
SendResult sendResult = producer.send(new Message("test-topic", "test-message".getBytes(StandardCharsets.UTF_8)));
System.out.println(sendResult);

// 原生API消费消息
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("test-group");
defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (msgList, context) -> {
    try {
        msgList.forEach(System.out::println);
    } catch (Exception e) {
        e.printStackTrace();
        return ConsumeConcurrentlyStatus.RECONSUME_LATER;
    }
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
defaultMQPushConsumer.subscribe("test-topic", "*");
defaultMQPushConsumer.start();

DefaultMQProducer#start

@Override
public void start() throws MQClientException {
    this.setProducerGroup(withNamespace(this.producerGroup));
    this.defaultMQProducerImpl.start();
    if (null != traceDispatcher) {
        try {
            traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
        } catch (MQClientException e) {
            log.warn("trace dispatcher start failed ", e);
        }
    }
}

我们可以看到,DefaultMQProducer的start方法主要逻辑委托给了DefaultMQProducerImpl的start方法

DefaultMQProducerImpl#start

public void start() throws MQClientException {
    this.start(true);
}

public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            this.serviceState = ServiceState.START_FAILED;

            this.checkConfig();

            if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                this.defaultMQProducer.changeInstanceNameToPID();
            }

            this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);

            boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
            if (!registerOK) {
                this.serviceState = ServiceState.CREATE_JUST;
                throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                    + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                    null);
            }

            this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

            if (startFactory) {
                mQClientFactory.start();
            }

            log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
                this.defaultMQProducer.isSendMessageWithVIPChannel());
            this.serviceState = ServiceState.RUNNING;
            break;
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("The producer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            break;
    }

    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();

    this.startScheduledTask();

}

在DefaultMQProducerImpl的start(final boolean startFactory)方法中,创建了MQClientInstance对象实例,并调用了其start方法

MQClientInstance#start

public void start() throws MQClientException {

    synchronized (this) {
        switch (this.serviceState) {
            case CREATE_JUST:
                // 省略部分代码
                
                // 没有手动指定NameSrv的值,从远端服务器获取并更新本地缓存
                if (null == this.clientConfig.getNamesrvAddr()) {
                    this.mQClientAPIImpl.fetchNameServerAddr();
                }
                
                // 省略部分代码
                
                // Start various schedule tasks
                this.startScheduledTask();
                
                // 省略部分代码
                break;
            case START_FAILED:
                throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
            default:
                break;
        }
    }
}

MQClientInstance的start方法中,与我们该篇分析的内容相关的大致就是上述代码所列的两处。
第一,判断ClientConfig对象的namesrvAddr值是否为null,若是,调用MQClientAPIImpl的fetchNameServerAddr方法从远端服务器拉取NameServer服务器的地址,并更新用到的地方

MQClientAPIImpl#fetchNameServerAddr

public String fetchNameServerAddr() {
    try {
        String addrs = this.topAddressing.fetchNSAddr();
        if (addrs != null) {
            if (!addrs.equals(this.nameSrvAddr)) {
                log.info("name server address changed, old=" + this.nameSrvAddr + ", new=" + addrs);
                this.updateNameServerAddressList(addrs);
                this.nameSrvAddr = addrs;
                return nameSrvAddr;
            }
        }
    } catch (Exception e) {
        log.error("fetchNameServerAddr Exception", e);
    }
    return nameSrvAddr;
}

TopAddressing#fetchNSAddr

public final String fetchNSAddr() {
    return fetchNSAddr(true, 3000);
}

public final String fetchNSAddr(boolean verbose, long timeoutMills) {
    String url = this.wsAddr;
    try {
        if (!UtilAll.isBlank(this.unitName)) {
            url = url + "-" + this.unitName + "?nofix=1";
        }
        HttpTinyClient.HttpResult result = HttpTinyClient.httpGet(url, null, null, "UTF-8", timeoutMills);
        if (200 == result.code) {
            String responseStr = result.content;
            if (responseStr != null) {
                return clearNewLine(responseStr);
            } else {
                log.error("fetch nameserver address is null");
            }
        } else {
            log.error("fetch nameserver address failed. statusCode=" + result.code);
        }
    } catch (IOException e) {
        if (verbose) {
            log.error("fetch name server address exception", e);
        }
    }

    if (verbose) {
        String errorMsg =
            "connect to " + url + " failed, maybe the domain name " + MixAll.getWSAddr() + " not bind in /etc/hosts";
        errorMsg += FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL);

        log.warn(errorMsg);
    }
    return null;
}

向远端请求的url地址就是wsAddr变量值,那么这个变量是在什么地方赋值的呢?发现该变量是在TopAddressing的构造方法中赋值,而TopAddressing对象又是在MQClientAPIImpl中创建,我们找到具体的创建逻辑

MQClientAPIImpl#Constructor

public MQClientAPIImpl(final NettyClientConfig nettyClientConfig,
    final ClientRemotingProcessor clientRemotingProcessor,
    RPCHook rpcHook, final ClientConfig clientConfig) {
    this.clientConfig = clientConfig;
    topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName());
    
    // 省略部分逻辑
}

传入TopAddressing构造方法的值取自MixAll的getWSAddr方法的返回值

MixAll#getWSAddr

public static String getWSAddr() {
    String wsDomainName = System.getProperty("rocketmq.namesrv.domain", DEFAULT_NAMESRV_ADDR_LOOKUP);
    String wsDomainSubgroup = System.getProperty("rocketmq.namesrv.domain.subgroup", "nsaddr");
    String wsAddr = "http://" + wsDomainName + ":8080/rocketmq/" + wsDomainSubgroup;
    if (wsDomainName.indexOf(":") > 0) {
        wsAddr = "http://" + wsDomainName + "/rocketmq/" + wsDomainSubgroup;
    }
    return wsAddr;
}

通过分析上述方法,可以知道,默认会从http://jmenv.tbsite.net:8080/rocketmq/nsaddr这个链接处拉取NameServer的地址。当然,如果你想更改这个链接,可以修改系统属性"rocketmq.namesrv.domain"和"rocketmq.namesrv.domain.subgroup"的值以达到目的。

第二,判断ClientConfig对象的namesrvAddr值是否为null,若是,开启定时任务,周期性地更新NameServer服务器的地址

MQClientInstance#startScheduledTask

private void startScheduledTask() {
    // 没有手动指定name-server地址的情况下,两分钟更新一次name-server地址
    // 这个就是name-server可以动态变化的唯一途径
    if (null == this.clientConfig.getNamesrvAddr()) {
        // 两分钟拉取更新一次name-server地址
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
                } catch (Exception e) {
                    log.error("ScheduledTask fetchNameServerAddr exception", e);
                }
            }
        }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
    }
}

可以看出,默认每隔2分钟会调用MQClientAPIImpl的fetchNameServerAddr方法更新NameServer地址,具体逻辑上述第一条已经解释过不再赘述

BrokerServer

BrokerServer的启动类是BrokerStartup,在BrokerStartup的main方法中,会创建BrokerController的实例对象并调用其start方法,而在创建BrokerController的实例对象时会一并调用其initialize方法进行初始化,就在这个初始化方法中,有跟我们该篇分析相关的内容。

BrokerStartup#main

public static void main(String[] args) {
    start(createBrokerController(args));
}

BrokerStartup#createBrokerController

public static BrokerController createBrokerController(String[] args) {
    // 省略部分代码
    try {
        // 省略部分代码
        boolean initResult = controller.initialize();
        // 省略部分代码
        return controller;
    } catch (Throwable e) {
        e.printStackTrace();
        System.exit(-1);
    }

    return null;
}

BrokerController#initialize

public boolean initialize() throws CloneNotSupportedException {
    // 将本地文件中存储的数据加载至内存
    boolean result = this.topicConfigManager.load();
    result = result && this.consumerOffsetManager.load();
    result = result && this.subscriptionGroupManager.load();
    result = result && this.consumerFilterManager.load();

    // 省略部分代码

    result = result && this.messageStore.load();

    if (result) {
        // 省略部分代码
        if (this.brokerConfig.getNamesrvAddr() != null) {
            this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr());
            log.info("Set user specified name server address: {}", this.brokerConfig.getNamesrvAddr());
        } else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) {
            // 没有明确指定name-server的地址,且配置了允许从地址服务器获取name-server地址
            // 每隔2分钟从name-server地址服务器拉取最新的配置
            this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

                @Override
                public void run() {
                    try {
                        BrokerController.this.brokerOuterAPI.fetchNameServerAddr();
                    } catch (Throwable e) {
                        log.error("ScheduledTask fetchNameServerAddr exception", e);
                    }
                }
            }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
        }

        // 省略部分代码
    }
    return result;
}

可以看到,BrokerConfig中如果namesrvAddr变量值为null,默认每隔2分钟会调用BrokerOuterAPI的fetchNameServerAddr方法更新NameServer地址

BrokerOuterAPI#fetchNameServerAddr

public String fetchNameServerAddr() {
    try {
        String addrs = this.topAddressing.fetchNSAddr();
        if (addrs != null) {
            if (!addrs.equals(this.nameSrvAddr)) {
                log.info("name server address changed, old: {} new: {}", this.nameSrvAddr, addrs);
                this.updateNameServerAddressList(addrs);
                this.nameSrvAddr = addrs;
                return nameSrvAddr;
            }
        }
    } catch (Exception e) {
        log.error("fetchNameServerAddr Exception", e);
    }
    return nameSrvAddr;
}

该方法与MQClientAPIImpl的fetchNameServerAddr方法逻辑几乎一模一样,也不再进行赘述。

注:
第三种配置NameServer地址的方法,那就是系统启动的时候,不配置系统属性rocketmq.namesrv.addr和环境变量NAMESRV_ADDR,这样系统会自动访问一个url从远端服务器拉取NameServer的地址,同时会开启相应的定时任务定时刷新NameServer地址。


总结:
BrokerServer、Producer、Consumer程序启动获取NameServer地址的三种方式
1.配置系统属性:rocketmq.namesrv.addr
2.配置环境变量:NAMESRV_ADDR
3.不配置系统属性rocketmq.namesrv.addr和环境变量NAMESRV_ADDR,让程序自动访问一个url从远端服务器拉取NameServer的地址,同时会开启相应的定时任务定时刷新NameServer地址。该url地址默认为http://jmenv.tbsite.net:8080/rocketmq/nsaddr,可以通过修改系统属性rocketmq.namesrv.domain和rocketmq.namesrv.domain.subgroup以达到修改url地址的目的。
只有上述第3种方式才可以实现程序运行过程中动态更新NameServer地址值。

你可能感兴趣的:(rocketmq学习,rocketmq,java,java-rocketmq)