nacos的服务端,接收注册请求--源码分析

提示:源码为nacos-1.4.1版本。


前言

听说nacos异步任务+内存队列来支持高并发。那么是不是呢?我们从源码来验证一下。

源码分析:

提示:注意看源码里的注释,都是关键代码。其余代码由于篇幅原因,省略。如有错误,请留言。

上一篇讲了客户端注册是发送v1/ns/instance的POST请求,那么我们寻找这个controller。

程序入口com.alibaba.nacos.naming.controllers.InstanceController#register

/**
 * 服务端注册实例
 * Register new instance.
 *
 * @param request http request
 * @return 'ok' if success
 * @throws Exception any error during register
 */
@CanDistro
@PostMapping //=v1/ns/instance
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {

    final String namespaceId = WebUtils
            .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);

    //组装instance
    final Instance instance = parseInstance(request);
    //注册instance
    serviceManager.registerInstance(namespaceId, serviceName, instance);
    return "ok";
}

追进registerInstance方法查看

/**
     * Register an instance to a service in AP mode.
     *
     * <p>This method creates service or cluster silently if they don't exist.
     *
     * @param namespaceId id of namespace
     * @param serviceName service name
     * @param instance    instance to register
     * @throws Exception any error occurred in the process
     */
    public void registerInstance(String namespaceId, String serviceName, Instance instance) throws 				NacosException {

        //组建空的Service
        createEmptyService(namespaceId, serviceName, instance.isEphemeral());

        Service service = getService(namespaceId, serviceName);

        if (service == null) {
            throw new NacosException(NacosException.INVALID_PARAM,
                    "service not found, namespace: " + namespaceId + ", service: " + serviceName);
        }
		//添加实例
        addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
    }

创建和初始化Service

点进createEmptyService方法,进去追踪com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent

    /**
     * Create service if not exist.
     *
     * @param namespaceId namespace
     * @param serviceName service name
     * @param local       whether create service by local
     * @param cluster     cluster
     * @throws NacosException nacos exception
     */
    public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster 			cluster)throws NacosException {
        //第一次为空
        Service service = getService(namespaceId, serviceName);
        if (service == null) {

            Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
            service = new Service();
            service.setName(serviceName);
            service.setNamespaceId(namespaceId);
            service.setGroupName(NamingUtils.getGroupName(serviceName));
            // now validate the service. if failed, exception will be thrown
            service.setLastModifiedMillis(System.currentTimeMillis());
            service.recalculateChecksum();
            if (cluster != null) {
                cluster.setService(service);
                service.getClusterMap().put(cluster.getName(), cluster);
            }
            service.validate();

            //初始化
            putServiceAndInit(service);
            if (!local) {
                addOrReplaceService(service);
            }
        }
    }

putServiceAndInit方法

	private void putServiceAndInit(Service service) throws NacosException {
        //构建空的服务列表
        putService(service);
        //客户端心跳检查
        service.init();
        consistencyService
                .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), 					true), service);
        consistencyService
                .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), 					false), service);
        Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
    }

点进putService方法

	/**
     * Put service into manager.
     *
     * @param service service
     */
    public void putService(Service service) {
        if (!serviceMap.containsKey(service.getNamespaceId())) {
            synchronized (putServiceLock) {
                if (!serviceMap.containsKey(service.getNamespaceId())) {
                    //构建NamespaceId为key
                    serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
                }
            }
        }
        //构建服务列表
        serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
    }

这里介绍serviceMap,就是存放实例的注册表。结构为Map(namespace, Map(group::serviceName, Service)).

/**
* Nacos服务注册表
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();

在这里插入图片描述

这里Service构建完成。继续执行service.init();

	/**
     * Init service.
     */
    public void init() {
        //客户端心跳检查定时任务
        HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
        for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
            entry.getValue().setService(this);
            entry.getValue().init();
        }
    }

点进scheduleCheck方法,发现就是延时任务,这里具体逻辑后续会出。

	public static void scheduleCheck(ClientBeatCheckTask task) {
        //延时任务,延时5秒,每5秒执行一次
        futureMap.putIfAbsent(task.taskKey(), GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, 				TimeUnit.MILLISECONDS));
    }

到这里service部分完成。

添加实例

我们回到最开始的方法,查看addInstance方法

	public void registerInstance(String namespaceId, String serviceName, Instance instance) throws 				NacosException {
        ...省略
            
		//添加实例
        addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
    }

这里需要关注一下buildInstanceListKey方法,这个方法构建了一个key。key=com.alibaba.nacos.naming.iplist.ephemeral.dev##DEFAULT_GROUP@@stock-service

简单来说,就是包含ephemeral的一个字符串,用来判断是临时实例还是永久实例。并且还跟nacos的AP CP模式相关。后续在展开。

    /**
     * Add instance to service.
     *
     * @param namespaceId namespace
     * @param serviceName service name
     * @param ephemeral   whether instance is ephemeral
     * @param ips         instances
     * @throws NacosException nacos exception
     */
    public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
            throws NacosException {

        // ephemeral 默认true。ephemeral ture,临时的。
        String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);

        Service service = getService(namespaceId, serviceName);

        synchronized (service) {
            //返回instance集合
            List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);

            Instances instances = new Instances();
            instances.setInstanceList(instanceList);

            //继续追踪
            consistencyService.put(key, instances);
        }
    }

进入com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl#put方法

    @Override
    public void put(String key, Record value) throws NacosException {
        //根据key,判断是临时实例,还是持久实例。
        //DistroConsistencyServiceImpl.put
        mapConsistencyService(key).put(key, value);
    }

	//根据key,返回ephemeralConsistencyService(AP)
    private ConsistencyService mapConsistencyService(String key) {
        return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : 									persistentConsistencyService;
    }

进入com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#put方法

	@Override
    public void put(String key, Record value) throws NacosException {
        //关键代码 将instance放到dataStore
        onPut(key, value);
        distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), 								DataOperation.CHANGE,globalConfig.getTaskDispatchPeriod() / 2);
    }


    /**
     * Put a new record.
     *
     * @param key   key of record
     * @param value record
     */
    public void onPut(String key, Record value) {

        if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
            Datum<Instances> datum = new Datum<>();
            datum.value = (Instances) value;
            datum.key = key;
            datum.timestamp.incrementAndGet();
            dataStore.put(key, datum);
        }

        if (!listeners.containsKey(key)) {
            return;
        }
        //加入到阻塞队列
        notifier.addTask(key, DataOperation.CHANGE);
    }

addTask方法

    public class Notifier implements Runnable {

        private ConcurrentHashMap<String, String> services = new ConcurrentHashMap<>(10 * 1024);

        //阻塞队列 1mb
        private BlockingQueue<Pair<String, DataOperation>> tasks = new ArrayBlockingQueue<>(1024 * 					1024);

        /**
         * Add new notify task to queue.
         *
         * @param datumKey data key
         * @param action   action for data
         */
        public void addTask(String datumKey, DataOperation action) {

            if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
                return;
            }
            if (action == DataOperation.CHANGE) {
                services.put(datumKey, StringUtils.EMPTY);
            }
            //封装为Pair,加入队列
            tasks.offer(Pair.with(datumKey, action));
        }
        
        @Override
        public void run() {
            Loggers.DISTRO.info("distro notifier started");

            for (; ; ) {
                try {
                    //从阻塞队列取出
                    //死循环,阻塞队列内没有内容,tasks.take会阻塞住,不会占用cpu
                    Pair<String, DataOperation> pair = tasks.take();
                    //处理
                    handle(pair);
                } catch (Throwable e) {
                    Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
                }
            }
        }

那么问题来了,将instance加入到队列后,什么时候从队列中取出呢?

从上面Notifier看出,是一个线程,那么看run方法实现即可。handle(pair);方法即处理pair。

	/**
     * spring容器初始化后,执行
     */
    @PostConstruct
    public void init() {
        //实现异步注册
        GlobalExecutor.submitDistroNotifyTask(notifier);
    }

    private void handle(Pair<String, DataOperation> pair) {
        ...省略
        for (RecordListener listener : listeners.get(datumKey)) {

            count++;

            try {
                if (action == DataOperation.CHANGE) {
                    //dataStore.get(datumKey).value就是instance
                    //servce.onChange
                    listener.onChange(datumKey, dataStore.get(datumKey).value);
                    continue;
                }

                if (action == DataOperation.DELETE) {
                    listener.onDelete(datumKey);
                    continue;
                }
            } catch (Throwable e) {
                Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", 							datumKey, e);
            }
        }
        ...省略
    }

进入com.alibaba.nacos.naming.core.Service#onChange方法

省略其他代码,进入最关键的updateIps方法

	@Override
    public void onChange(String key, Instances value) throws Exception {
		...省略
		//真正的注册逻辑
        updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));

        recalculateChecksum();
    }

	/**
     * Update instances.
     *
     * @param instances instances
     * @param ephemeral whether is ephemeral instance
     */
    public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
        Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
        ...省略

        for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
            //make every ip mine
            List<Instance> entryIPs = entry.getValue();
            //updateIps 真正的注册逻辑
            clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
        }
        ...省略

    }

从clusterMap中取出cluster。调用cluster.updateIps方法。传入的是Instance集合。

	/**
     * Update instance list.
     *
     * @param ips       instance list
     * @param ephemeral whether these instances are ephemeral
     */
    public void updateIps(List<Instance> ips, boolean ephemeral) {

        //ephemeralInstances真正的注册表,是不操作的。这里创建了oldIpMap,是注册表的副本,进行增删改,最后在赋值给				ephemeralInstances
        Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;

        HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());

        for (Instance ip : toUpdateInstances) {
            oldIpMap.put(ip.getDatumKey(), ip);
        }

        ...省略

        toUpdateInstances = new HashSet<>(ips);

        if (ephemeral) {
            //存Instance的集合,是注册列表map的一部分
            ephemeralInstances = toUpdateInstances;
        } else {
            persistentInstances = toUpdateInstances;
        }
    }

查看ephemeralInstances是什么?ephemeralInstances是存放所有注册实例的。

	//instance注册表set
    @JsonIgnore
    private Set<Instance> ephemeralInstances = new HashSet<>();

总结

现在我们可以印证异步注册+内存队列的说法了。

具体将要注册instance实例放入阻塞队列BlockingQueue。

//阻塞队列 1mb
private BlockingQueue<Pair<String, DataOperation>> tasks = new ArrayBlockingQueue<>(1024 * 1024);

然后采用异步线程,从队列中拿取进行handle(pair)处理,最终放入到

//存放instance
Set<Instance> ephemeralInstances = new HashSet<>();

服务注册表的高并发读写

问题1:我们关注一下注册表的结构,是Set集合。那么在高并发情况下,它的读写是怎么做的呢?加锁?

从源码来看,根据ephemeral,将真正的注册表ephemeralInstances,赋值给toUpdateInstances(副本)。下面的操作都是操作toUpdateInstances。最后将其替换真正的注册表ephemeralInstances。

所有nacos针对高并发读写问题,采用copy on write设计,读写分离。采用修改一个副本,然后替换真正的注册表。

	/**
     * Update instance list.
     *
     * @param ips       instance list
     * @param ephemeral whether these instances are ephemeral
     */
    public void updateIps(List<Instance> ips, boolean ephemeral) {

        //ephemeralInstances真正的注册表,是不操作的。这里创建了oldIpMap,是注册表的副本,进行增删改,最后在赋值给				ephemeralInstances
        Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;

        HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());

        for (Instance ip : toUpdateInstances) {
            oldIpMap.put(ip.getDatumKey(), ip);
        }

        ...省略

        toUpdateInstances = new HashSet<>(ips);

        if (ephemeral) {
            //存Instance的集合,是注册列表map的一部分
            ephemeralInstances = toUpdateInstances;
        } else {
            persistentInstances = toUpdateInstances;
        }
    }

问题2:用副本是否会有延时问题呢?客户端拉取时,正好还在复制,不就读取的是老的注册表吗?

是的,会有这个问题。但是为了高可用,牺牲一点一致性。在读取时,可能会延迟1s得到新的注册表。

问题3:如果有上千个注册实例,复制副本是否会大量占用内存?

从clusterMap.get(entry.getKey())可以看出是取的对应的cluter的集合。只复制cluster粒度的Set集合。

//updateIps 真正的注册逻辑
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);

创作不易,如果对您有所帮助,请点赞关注!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值