nacos如何实现服务注册?

nacos为什么服务注册如此高效?

源码分析

nacos服务端使用内存方式存储服务实例时,底层采用异步+阻塞队列的方式实现服务的注册。当服务注册时,把服务实例数据写入阻塞队列,返回注册成功,然后异步的从阻塞队列获取实例数据进行注册,其实现流程如下:

nacos如何实现服务注册?_第1张图片

nacos服务端提供的restfull API接口为/v1/ns/instance,controller源码如下:

@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + UtilsAndCommons.NACOS_NAMING_INSTANCE_CONTEXT)
public class InstanceController {

/**
 * nacos服务端服务实例注册接口
 */
@CanDistro
    @PostMapping
    @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);
        
        final Instance instance = HttpRequestInstanceBuilder.newBuilder()
                .setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();
        
        getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
        return "ok";
    }
}



/**
 * nacos服务端服务实例注册实现方法
 */
@Component
public class InstanceOperatorServiceImpl implements InstanceOperator {
 
 @Override
    public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        com.alibaba.nacos.naming.core.Instance coreInstance = parseInstance(instance);
        serviceManager.registerInstance(namespaceId, serviceName, coreInstance);
    }
}



@Component
public class ServiceManager implements RecordListener {

/**
 * nacos服务端服务实例注册服务实例
 */
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        
        createEmptyService(namespaceId, serviceName, instance.isEphemeral());
        
        Service service = getService(namespaceId, serviceName);
        
        checkServiceIsNull(service, namespaceId, serviceName);
        
        addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
    }


/**
 * nacos服务端根据服务名注册服务实例
 */
  public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
            throws NacosException {
        
        String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
        
        Service service = getService(namespaceId, serviceName);
        
        synchronized (service) {
            List instanceList = addIpAddresses(service, ephemeral, ips);
            
            Instances instances = new Instances();
            instances.setInstanceList(instanceList);
            
            consistencyService.put(key, instances);
        }
    }
}

在服务注册中心,为了提高效率,一般都是采用内存不持久化的方式存储服务实例,EphemeralConsistencyService接口定义了该种方式的存储实现。实现代码如下:

@DependsOn("ProtocolManager")
@org.springframework.stereotype.Service("distroConsistencyService")
public class DistroConsistencyServiceImpl implements EphemeralConsistencyService, DistroDataProcessor {

/**
 * 保存服务实例
 */
 @Override
    public void put(String key, Record value) throws NacosException {
        onPut(key, value);
        // If upgrade to 2.0.X, do not sync for v1.
        if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {
            return;
        }
        distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
                DistroConfig.getInstance().getSyncDelayMillis());
    }

}

添加服务实例到阻塞队列的实现逻辑在onPut(String key, Record value)方法中,服务实例存储在ConcurrentHashMap中,注册的数据存储在阻塞队列ArrayBlockingQueue中。其源码如下:

/**
     * 异步注册服务实例
     */
    public void onPut(String key, Record value) {
        
        if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
            Datum 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);
    }

/**
     * 异步注册服务实例
     */
 public class Notifier implements Runnable {



/**
         * 阻塞队列添加服务实例数据
         */
        public void addTask(String datumKey, DataOperation action) {
            
            if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
                return;
            }
            if (action == DataOperation.CHANGE) {
                services.put(datumKey, StringUtils.EMPTY);
            }
            tasks.offer(Pair.with(datumKey, action));
        }

}

从阻塞队列异步获取服务实例,进行数据实例数据的新增,源码如下:


/**
 * 线程任务
 */
public class Notifier implements Runnable {



        /**
         * 异步执行服务实例注册
         */
         @Override
        public void run() {
            Loggers.DISTRO.info("distro notifier started");
            
            for (; ; ) {
                try {
                    Pair pair = tasks.take();
                    handle(pair);
                } catch (Throwable e) {
                    Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
                }
            }
        }

        /**
         * 异步执行服务实例注册具体逻辑
         */
private void handle(Pair pair) {
            try {
                String datumKey = pair.getValue0();
                DataOperation action = pair.getValue1();
                
                services.remove(datumKey);
                
                int count = 0;
                
                if (!listeners.containsKey(datumKey)) {
                    return;
                }
                
                for (RecordListener listener : listeners.get(datumKey)) {
                    
                    count++;
                    
                    try {
                        if (action == DataOperation.CHANGE) {
                            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);
                    }
                }
                
                if (Loggers.DISTRO.isDebugEnabled()) {
                    Loggers.DISTRO
                            .debug("[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}",
                                    datumKey, count, action.name());
                }
            } catch (Throwable e) {
                Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
            }
        }

}

nacos如何剔除无效的服务实例? 

nacos采用客户端主动上报,告诉服务端自己健康状态。对于健康检查机制采用了 TTL(Time To Live)机制,即客户端在 一定时间没有向注册中心发送心跳,那么注册中心会认为此服务不健康,进而触发后续的剔除逻辑。

nacos数据模型比eureka有哪些优化?

一个服务部署多套环境

使用eureka作为服务注册中心时,需要单独部署dev,test,preprod,prod环境。而nacos由命名空间namespace,分组group和服务service三个元素确定服务实例。在使用中,部署一套nacos服务,可以根据命名空间可以区分,dev,test,preprod环境配置,节省了部署资源。元素的模式如图:

nacos如何实现服务注册?_第2张图片

 在源码中使用ConcurrentHashMap进行存储,key由命名空间namespaceId,服务名serviceName构建唯一主键,示例如下:

 
    /**
     * 添加服务实例
     */
 public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
            throws NacosException {
        // 构建唯一key
        String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
        
        Service service = getService(namespaceId, serviceName);
        
        synchronized (service) {
            List instanceList = addIpAddresses(service, ephemeral, ips);
            
            Instances instances = new Instances();
            instances.setInstanceList(instanceList);
            
            consistencyService.put(key, instances);
        }
    }


    /**
     * 根据命名空间id和服务名称生成key
     */
public static String buildInstanceListKey(String namespaceId, String serviceName, boolean ephemeral) {
        return ephemeral ? buildEphemeralInstanceListKey(namespaceId, serviceName)
                : buildPersistentInstanceListKey(namespaceId, serviceName);
    }
 private static String buildEphemeralInstanceListKey(String namespaceId, String serviceName) {
        return INSTANCE_LIST_KEY_PREFIX + EPHEMERAL_KEY_PREFIX + namespaceId + NAMESPACE_KEY_CONNECTOR + serviceName;
    }

同一个服务支持集群部署

针对单个服务实例,nacos兼容服务集群部署。例如,中大型电商公司的订单服务,在北京,上海,成都等不同地域部署了服务集群,实现异地多机房部署。 其官网服务领域模型如图:

nacos源码中表示服务实例类Service,包含集群Cluster,集群Cluster中包含服务实例Instance,起源吗如下:

/**
 * canos服务实例
 */
public class Service extends com.alibaba.nacos.api.naming.pojo.Service implements Record, RecordListener {

    // 服务对应集群集合
    private Map clusterMap = new HashMap<>();
}

/**
 * canos服务集群
 */
public class Cluster extends com.alibaba.nacos.api.naming.pojo.Cluster implements Cloneable {
    
    // 临时服务实例集合
    @JsonIgnore
    private Set persistentInstances = new HashSet<>();
    
}

nacos如何保证临时实例的高可用?

nacos服务端集群部署,基于最终一致性原则,采用Distro 协议(AP模式),保证在某些 Nacos 节点宕机后,整个临时实例处理系统依旧可以正常工作。Distro 协议的主要设计思想如下:

  1. Nacos 每个节点是平等的都可以处理写请求,同时把新数据同步到其他节点。
  2. 每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据一致性。
  3. 每个节点独立处理读请求,及时从本地发出响应。

数据初始化和校验可以参考官网文档,对于一个已经启动完成的 Distro 集群,在一次客户端发起写操作的流程中,当注册非持久化的实例 的写请求打到某台 Nacos 服务器时,Distro 集群处理的流程图如下:

nacos如何实现服务注册?_第3张图片

 

操作流程如下:

  1. 前置的 Filter 拦截请求,并根据请求中包含的 IP 和 port 信息计算其所属的 Distro 责任节点, 并将该请求转发到所属的 Distro 责任节点上。
  2. 责任节点上的 Controller 将写请求进行解析。
  3. Distro 协议定期执行 Sync 任务,将本机所负责的所有的实例信息同步到其他节点上。

 在创建实例的api上定义CanDistro注解,使用DistroFilter对请求进行拦截,核心代码如下:

public class DistroFilter implements Filter {

 @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        ReuseHttpServletRequest req = new ReuseHttpServletRequest((HttpServletRequest) servletRequest);
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        
        String urlString = req.getRequestURI();
        
        if (StringUtils.isNotBlank(req.getQueryString())) {
            urlString += "?" + req.getQueryString();
        }
        
        try {
            Method method = controllerMethodsCache.getMethod(req);
            
            String path = new URI(req.getRequestURI()).getPath();
            if (method == null) {
                throw new NoSuchMethodException(req.getMethod() + " " + path);
            }
            // 当该节点接收到任何读请求时,都直接在本机查询并返回
            if (!method.isAnnotationPresent(CanDistro.class)) {
                filterChain.doFilter(req, resp);
                return;
            }
            String distroTag = distroTagGenerator.getResponsibleTag(req);
            // 当该节点接收到属于该节点负责的实例的写请求时
            if (distroMapper.responsible(distroTag)) {
                filterChain.doFilter(req, resp);
                return;
            }
            
            // proxy request to other server if necessary:
            String userAgent = req.getHeader(HttpHeaderConsts.USER_AGENT_HEADER);
            
            if (StringUtils.isNotBlank(userAgent) && userAgent.contains(UtilsAndCommons.NACOS_SERVER_HEADER)) {
                // This request is sent from peer server, should not be redirected again:
                Loggers.SRV_LOG.error("receive invalid redirect request from peer {}", req.getRemoteAddr());
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "receive invalid redirect request from peer " + req.getRemoteAddr());
                return;
            }
            // 当该节点接收到不属于该节点负责的实例的写请求时,将在集群内部路由,转发给对应的节点,从而完成读写。
            final String targetServer = distroMapper.mapSrv(distroTag);
            
            List headerList = new ArrayList<>(16);
            Enumeration headers = req.getHeaderNames();
            while (headers.hasMoreElements()) {
                String headerName = headers.nextElement();
                headerList.add(headerName);
                headerList.add(req.getHeader(headerName));
            }
            
            final String body = IoUtils.toString(req.getInputStream(), Charsets.UTF_8.name());
            final Map paramsValue = HttpClient.translateParameterMap(req.getParameterMap());
            
            RestResult result = HttpClient
                    .request("http://" + targetServer + req.getRequestURI(), headerList, paramsValue, body,
                            PROXY_CONNECT_TIMEOUT, PROXY_READ_TIMEOUT, Charsets.UTF_8.name(), req.getMethod());
            String data = result.ok() ? result.getData() : result.getMessage();
            try {
                WebUtils.response(resp, data, result.getCode());
            } catch (Exception ignore) {
                Loggers.SRV_LOG.warn("[DISTRO-FILTER] request failed: " + distroMapper.mapSrv(distroTag) + urlString);
            }
        } catch (AccessControlException e) {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN, "access denied: " + ExceptionUtil.getAllExceptionMsg(e));
        } catch (NoSuchMethodException e) {
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED,
                    "no such api:" + req.getMethod() + ":" + req.getRequestURI());
        } catch (Exception e) {
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    "Server failed," + ExceptionUtil.getAllExceptionMsg(e));
        }
        
    }
}

Distro集群写入服务实例时,完成实例临时存储后,会同步数据到其他节点,源码如下:

/**
 * Distro协议持久化服务
 */
public class DistroConsistencyServiceImpl implements EphemeralConsistencyService, DistroDataProcessor {

   /**
    * 存储临时服务实例
    */
    @Override
    public void put(String key, Record value) throws NacosException {
        onPut(key, value);
        // If upgrade to 2.0.X, do not sync for v1.
        if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {
            return;
        }
        // 同步数据到其他服务节点
        distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
                DistroConfig.getInstance().getSyncDelayMillis());
    }

}
定义DistroProtocol类,进行数据的同步,数据初始化,数据校验等操作。源码如下:
public class DistroProtocol {

 public DistroProtocol(ServerMemberManager memberManager, DistroComponentHolder distroComponentHolder,
            DistroTaskEngineHolder distroTaskEngineHolder) {
        this.memberManager = memberManager;
        this.distroComponentHolder = distroComponentHolder;
        this.distroTaskEngineHolder = distroTaskEngineHolder;
        // 初始化定时任务
        startDistroTask();
    }

private void startDistroTask() {
        if (EnvUtil.getStandaloneMode()) {
            isInitialized = true;
            return;
        }
        // 数据校验定时任务
        startVerifyTask();
        // 数据拉取定时任务
        startLoadTask();
    }

private void startLoadTask() {
        DistroCallback loadCallback = new DistroCallback() {
            @Override
            public void onSuccess() {
                isInitialized = true;
            }
            
            @Override
            public void onFailed(Throwable throwable) {
                isInitialized = false;
            }
        };
        GlobalExecutor.submitLoadDataTask(
                new DistroLoadDataTask(memberManager, distroComponentHolder, DistroConfig.getInstance(), loadCallback));
    }
    
    private void startVerifyTask() {
        GlobalExecutor.schedulePartitionDataTimedSync(new DistroVerifyTimedTask(memberManager, distroComponentHolder,
                        distroTaskEngineHolder.getExecuteWorkersManager()),
                DistroConfig.getInstance().getVerifyIntervalMillis());
    }
  /**
    * 数据同步
    */
  public void sync(DistroKey distroKey, DataOperation action) {
        sync(distroKey, action, DistroConfig.getInstance().getSyncDelayMillis());
    }

public void sync(DistroKey distroKey, DataOperation action, long delay) {
        for (Member each : memberManager.allMembersWithoutSelf()) {
            syncToTarget(distroKey, action, each.getAddress(), delay);
        }
    }

 public void syncToTarget(DistroKey distroKey, DataOperation action, String targetServer, long delay) {
        DistroKey distroKeyWithTarget = new DistroKey(distroKey.getResourceKey(), distroKey.getResourceType(),
                targetServer);
        DistroDelayTask distroDelayTask = new DistroDelayTask(distroKeyWithTarget, action, delay);
        distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask);
        if (Loggers.DISTRO.isDebugEnabled()) {
            Loggers.DISTRO.debug("[DISTRO-SCHEDULE] {} to {}", distroKey, targetServer);
        }
    }

}

你可能感兴趣的:(分布式中间件,nacos)