Análisis del código fuente de Nacos Notas de seguimiento de Dark Horse

Análisis del código fuente de Nacos

1. Descarga el código fuente de Nacos y ejecútalo

Para estudiar el código fuente de Nacos, por supuesto, no puede usar el paquete jar del servidor de Nacos empaquetado para ejecutarlo. Debe descargar el código fuente y compilarlo usted mismo.

1.1 Descargar el código fuente de Nacos

Dirección de GitHub de Nacos: https://github.com/alibaba/nacos

El código fuente descargado de Nacos de la versión 1.4.2 se ha proporcionado en los materiales previos a la clase:
inserte la descripción de la imagen aquí

Si necesita estudiar otras versiones de los estudiantes, también puede descargarlo usted mismo:

Puede encontrar su página de lanzamiento: https://github.com/alibaba/nacos/tags, y encontrar la versión 1.4.2:
inserte la descripción de la imagen aquí

Después de hacer clic para ingresar, descargue el código fuente (zip):
inserte la descripción de la imagen aquí

1.2 Importar proyecto de demostración

Nuestros materiales previos a la clase brindan una demostración de microservicios, incluidos servicios como registro y descubrimiento de servicios.
inserte la descripción de la imagen aquí
Después de importar el proyecto, vea su estructura de proyecto:
inserte la descripción de la imagen aquí

Descripción de la estructura:

  • cloud-source-demo: directorio principal del proyecto
    • cloud-demo: el proyecto principal de microservicios, gestión de dependencias de microservicios
      • servicio de pedido: el microservicio de pedido, que necesita acceder al servicio de usuario en el negocio, es un consumidor de servicio
      • servicio de usuario: microservicio de usuario, que expone la interfaz de consulta de usuarios en función de la identificación, y es un proveedor de servicios

1.3 Importar código fuente de Nacos

Descomprima el código fuente de Nacos descargado previamente en el directorio del proyecto cloud-source-demo:
inserte la descripción de la imagen aquí

Luego, usa IDEA para importarlo como un módulo:

1) Seleccione la opción Estructura del proyecto:
inserte la descripción de la imagen aquí

Luego haga clic en importar módulo:
inserte la descripción de la imagen aquí

En la ventana emergente, seleccione el directorio del código fuente de nacos:
inserte la descripción de la imagen aquí

Luego seleccione el módulo maven, termine:

inserte la descripción de la imagen aquí

Finalmente, haga clic en Aceptar:

inserte la descripción de la imagen aquí

Estructura del proyecto importado:

inserte la descripción de la imagen aquí

compilación 1.4.proto

La comunicación de datos subyacente de Nacos serializará y deserializará los datos en función de protobuf. Y defina el archivo proto correspondiente en el submódulo de consistencia:
inserte la descripción de la imagen aquí

Primero debemos compilar el archivo proto en el código Java correspondiente.

1.4.1 Qué es protobuf

El nombre completo de protobuf es Protocol Buffer, que es un protocolo de serialización de datos proporcionado por Google. Esta es la definición oficial de Google:

Protocol Buffers es un formato de almacenamiento de datos estructurados liviano y eficiente que se puede usar para la serialización de datos estructurados y es muy adecuado para el almacenamiento de datos o el formato de intercambio de datos RPC. Se puede utilizar en formatos de datos estructurados serializados extensibles, independientes del idioma y de la plataforma en protocolos de comunicación, almacenamiento de datos y otros campos.

Puede entenderse simplemente como un formato de transmisión de datos multiplataforma y multilenguaje. La función es similar a json, pero tanto el rendimiento como el tamaño de los datos son mucho mejores que json.

La razón por la que protobuf puede ser multilenguaje es porque el formato de la definición de datos es .protoformat, que debe compilarse en el idioma correspondiente según protoc.

1.4.2 Instalar protocolo

Dirección de GitHub de Protobuf: https://github.com/protocolbuffers/protobuf/releases

Podemos descargar la versión de windows para usar:
inserte la descripción de la imagen aquí

Además, los materiales previos a la clase también proporcionan el paquete de instalación descargado:

inserte la descripción de la imagen aquí

Descomprímalo en cualquier directorio que no sea chino, y el protoc.exe en el directorio bin puede ayudarnos a compilar:
inserte la descripción de la imagen aquí

Luego configure este directorio bin en la ruta de la variable de entorno, puede consultar el método de configuración de JDK:
inserte la descripción de la imagen aquí

1.4.3 Compilar prototipo

Ingrese al directorio src/main bajo el módulo de consistencia de nacos-1.4.2:
inserte la descripción de la imagen aquí

Luego abra una ventana cmd y ejecute los siguientes dos comandos:

protoc --java_out=./java ./proto/consistency.proto
protoc --java_out=./java ./proto/Data.proto

Como se muestra en la imagen:

inserte la descripción de la imagen aquí

Estos códigos java serán compilados en el módulo de consistencia de nacos:

inserte la descripción de la imagen aquí

1.5 Ejecutar

La entrada del servidor nacos es la clase Nacos en el módulo de la consola:
inserte la descripción de la imagen aquí

Tenemos que hacer que comience de forma independiente:
inserte la descripción de la imagen aquí

Luego crea una nueva SpringBootApplication:

inserte la descripción de la imagen aquí

Luego complete la información de la aplicación:
inserte la descripción de la imagen aquí

Luego ejecute la función principal de Nacos:
inserte la descripción de la imagen aquí

Después de iniciar los servicios de servicio de pedido y servicio de usuario, puede ver la consola de nacos:
inserte la descripción de la imagen aquí

2. Registro de servicios

Después de que el servicio se registre en Nacos, se guardará en un registro local y su estructura es la siguiente:
inserte la descripción de la imagen aquí

En primer lugar, la capa más externa es un Mapa, la estructura es: Map<String, Map<String, Service>>:

  • clave: es namespace_id, que desempeña el papel de aislamiento ambiental. Puede haber varios grupos bajo el espacio de nombres
  • valor: Otro Map<String, Service>, que representa el grupo y los servicios dentro del grupo. Puede haber varios servicios en un grupo.
    • clave: representa la agrupación de grupos, pero cuando se usa como clave, el formato es nombre_grupo:nombre_servicio
    • valor: un determinado servicio en el grupo, como servicio de usuario, servicio de usuario. El tipo es Service, que también contiene uno internamente Map<String,Cluster>, y puede haber múltiples clústeres bajo un servicio
      • clave: nombre del clúster
      • valor: Clustertipo, que contiene la información específica del clúster. Un clúster puede contener varias instancias, es decir, información específica del nodo, incluido uno Set<Instance>, que es la colección de instancias bajo el clúster.
        • Instancia: información de la instancia, incluida la IP de la instancia, el puerto, el estado de salud, el peso, etc.

Cuando cada servicio se registre en Nacos, la información se organizará y almacenará en este Mapa.

2.1 Interfaz de registro de servicios

Nacos proporciona una interfaz API para el registro del servicio, el cliente solo necesita enviar una solicitud a esta interfaz para realizar el registro del servicio.

**Descripción de la interfaz:** Registre una instancia en el servicio Nacos.

Tipo de solicitud :POST

Solicitud de ruta :/nacos/v1/ns/instance

Parámetros de la solicitud :

nombre tipo ¿Es obligatorio? describir
ip cadena IP de instancia de servicio
puerto En t Puerto de instancia de servicio
ID de espacio de nombres cadena No Id. de espacio de nombres
peso doble No Pesos
activado booleano No ¿Está en línea?
saludable booleano No ya sea saludable
metadatos cadena No Información extendida
clusterName cadena No nombre del clúster
Nombre del Servicio cadena Nombre del Servicio
Nombre del grupo cadena No Nombre del grupo
efímero booleano No ¿Es una instancia temporal?

Código de error :

código de error describir Semántica
400 Solicitud incorrecta Error de sintaxis en la solicitud del cliente
403 Prohibido Permiso denegado
404 Extraviado recurso no encontrado
500 Error Interno del Servidor Error Interno del Servidor
200 DE ACUERDO normal

2.2 Cliente

Primero, necesitamos encontrar la entrada para el registro del servicio.

2.2.1.NacosServiceRegistryAutoConfiguration

Debido a que el cliente de Nacos se implementa en función del ensamblado automático de SpringBoot, podemos confiar en el descubrimiento de nacos:

spring-cloud-starter-alibaba-nacos-discovery-2.2.6.RELEASE.jar

La información de autoensamblaje de Nacos se encuentra en este paquete:
inserte la descripción de la imagen aquí

Se puede ver que se han cargado muchas clases de configuración automática, y la clase relacionada con el registro del servicio es NacosServiceRegistryAutoConfiguration, que seguimos.

Como puede ver, en la clase NacosServiceRegistryAutoConfiguration, hay un Bean relacionado con el registro automático:
inserte la descripción de la imagen aquí

2.2.2.NacosAutoServiceRegistration

NacosAutoServiceRegistrationEl código fuente se muestra en la figura:
inserte la descripción de la imagen aquí

Se puede ver que durante la inicialización, su clase padre AbstractAutoServiceRegistrationtambién se inicializa.

AbstractAutoServiceRegistrationComo se muestra en la imagen:
inserte la descripción de la imagen aquí

Puede ver que implementa ApplicationListenerla interfaz y escucha eventos durante el inicio del contenedor Spring.

Después de escuchar WebServerInitializedEventel evento (inicialización del servicio web completada), se ejecuta el método bind.
inserte la descripción de la imagen aquí

El método de vinculación es el siguiente:

public void bind(WebServerInitializedEvent event) {
    
    
    // 获取 ApplicationContext
    ApplicationContext context = event.getApplicationContext();
    // 判断服务的 namespace,一般都是null
    if (context instanceof ConfigurableWebServerApplicationContext) {
    
    
        if ("management".equals(((ConfigurableWebServerApplicationContext) context)
                                .getServerNamespace())) {
    
    
            return;
        }
    }
    // 记录当前 web 服务的端口
    this.port.compareAndSet(0, event.getWebServer().getPort());
    // 启动当前服务注册流程
    this.start();
}

El proceso del método de inicio en él:

public void start() {
    
    
		if (!isEnabled()) {
    
    
			if (logger.isDebugEnabled()) {
    
    
				logger.debug("Discovery Lifecycle disabled. Not starting");
			}
			return;
		}

		// 当前服务处于未运行状态时,才进行初始化
		if (!this.running.get()) {
    
    
            // 发布服务开始注册的事件
			this.context.publishEvent(
					new InstancePreRegisteredEvent(this, getRegistration()));
            // ☆☆☆☆开始注册☆☆☆☆
			register();
			if (shouldRegisterManagement()) {
    
    
				registerManagement();
			}
            // 发布注册完成事件
			this.context.publishEvent(
					new InstanceRegisteredEvent<>(this, getConfiguration()));
            // 服务状态设置为运行状态,基于AtomicBoolean
			this.running.compareAndSet(false, true);
		}

	}

El método register() más crítico es la clave para completar el registro del servicio, el código es el siguiente:

protected void register() {
    
    
    this.serviceRegistry.register(getRegistration());
}

Este.serviceRegistry aquí es NacosServiceRegistry:
inserte la descripción de la imagen aquí

2.2.3.NacosServicioRegistro

NacosServiceRegistryEs la clase de implementación de ServiceRegistryla interfaz de Spring, y la interfaz ServiceRegistry es una interfaz de protocolo para el registro y descubrimiento de servicios, que define la declaración de registro, cancelación de registro y otros métodos.

NacosServiceRegistryLa implementación correcta es la siguiente register:

@Override
public void register(Registration registration) {
    
    
	// 判断serviceId是否为空,也就是spring.application.name不能为空
    if (StringUtils.isEmpty(registration.getServiceId())) {
    
    
        log.warn("No service to register for nacos client...");
        return;
    }
    // 获取Nacos的命名服务,其实就是注册中心服务
    NamingService namingService = namingService();
    // 获取 serviceId 和 Group
    String serviceId = registration.getServiceId();
    String group = nacosDiscoveryProperties.getGroup();
	// 封装服务实例的基本信息,如 cluster-name、是否为临时实例、权重、IP、端口等
    Instance instance = getNacosInstanceFromRegistration(registration);

    try {
    
    
        // 开始注册服务
        namingService.registerInstance(serviceId, group, instance);
        log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
                 instance.getIp(), instance.getPort());
    }
    catch (Exception e) {
    
    
        if (nacosDiscoveryProperties.isFailFast()) {
    
    
            log.error("nacos registry, {} register failed...{},", serviceId,
                      registration.toString(), e);
            rethrowRuntimeException(e);
        }
        else {
    
    
            log.warn("Failfast is false. {} register failed...{},", serviceId,
                     registration.toString(), e);
        }
    }
}

Se puede ver que en el método, finalmente se llama al método registerInstance de NamingService para realizar el registro.

La implementación predeterminada de la interfaz NamingService es NacosNamingService.

2.2.4.Servicio de nombres de Nacos

NacosNamingService proporciona funciones como registro y suscripción de servicios.

Entre ellos, registerInstance es la instancia del servicio de registro y el código fuente es el siguiente:

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    
    
    // 检查超时参数是否异常。心跳超时时间(默认15秒)必须大于心跳周期(默认5秒)
    NamingUtils.checkInstanceIsLegal(instance);
    // 拼接得到新的服务名,格式为:groupName@@serviceId
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    // 判断是否为临时实例,默认为 true。
    if (instance.isEphemeral()) {
    
    
        // 如果是临时实例,需要定时向 Nacos 服务发送心跳
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    // 发送注册服务实例的请求
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

Finalmente, el registro del servicio se completa mediante el método registerService de NacosProxy.

el código se muestra a continuación:

public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    
    

    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
                       instance);
	// 组织请求参数
    final Map<String, String> params = new HashMap<String, String>(16);
    params.put(CommonParams.NAMESPACE_ID, namespaceId);
    params.put(CommonParams.SERVICE_NAME, serviceName);
    params.put(CommonParams.GROUP_NAME, groupName);
    params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
    params.put("ip", instance.getIp());
    params.put("port", String.valueOf(instance.getPort()));
    params.put("weight", String.valueOf(instance.getWeight()));
    params.put("enable", String.valueOf(instance.isEnabled()));
    params.put("healthy", String.valueOf(instance.isHealthy()));
    params.put("ephemeral", String.valueOf(instance.isEphemeral()));
    params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
	// 通过POST请求将上述参数,发送到 /nacos/v1/ns/instance
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);

}

La información enviada aquí son los parámetros completos requeridos por la interfaz de registro del servicio Nacos. Los parámetros principales son:

  • namespace_id: entorno
  • service_name: nombre del servicio
  • group_name: nombre del grupo
  • cluster_name: nombre del clúster
  • ip: la dirección IP de la instancia actual
  • puerto: el puerto de la instancia actual

En el método registerInstance de NacosNamingService, hay una sección de código relacionada con el latido del servicio, que seguiremos aprendiendo más adelante.
inserte la descripción de la imagen aquí

2.2.5 Flujograma de registro de clientes

Como se muestra en la imagen:
inserte la descripción de la imagen aquí

2.3 Servidor

En el módulo nacos-console, se introducirá el módulo nacos-naming:
inserte la descripción de la imagen aquí
la estructura del módulo es la siguiente:
inserte la descripción de la imagen aquí

Entre ellos, el paquete com.alibaba.nacos.naming.controllers tiene varias interfaces relacionadas con el registro y descubrimiento de servicios, entre las cuales el registro de servicios pertenece a InstanceControllerla clase:
inserte la descripción de la imagen aquí

2.3.1.InstanceController

Al ingresar a la clase InstanceController, puede ver un método de registro, que es el método de registro del servicio:

@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
    
    
	// 尝试获取namespaceId
    final String namespaceId = WebUtils
        .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    // 尝试获取serviceName,其格式为 group_name@@service_name
    final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);
	// 解析出实例信息,封装为Instance对象
    final Instance instance = parseInstance(request);
	// 注册实例
    serviceManager.registerInstance(namespaceId, serviceName, instance);
    return "ok";
}

Aquí, ingresamos el método serviceManager.registerInstance().

2.3.2.Administrador de servicios

ServiceManager es la API central para administrar servicios e información de instancias en Nacos, que incluye el registro de servicios de Nacos:
inserte la descripción de la imagen aquí

Y el método registerInstance es el método para registrar instancias de servicio:

/**
     * 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(如果是第一次来注册实例,要先创建一个空service出来,放入注册表)
    // 此时不包含实例信息
    createEmptyService(namespaceId, serviceName, instance.isEphemeral());
    // 拿到创建好的service
    Service service = getService(namespaceId, serviceName);
    // 拿不到则抛异常
    if (service == null) {
    
    
        throw new NacosException(NacosException.INVALID_PARAM,
                                 "service not found, namespace: " + namespaceId + ", service: " + serviceName);
    }
    // 添加要注册的实例到service中
    addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}

Después de crear el servicio, el siguiente paso es agregar una instancia al servicio:

/**
     * 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 {
    
    
	// 监听服务列表用到的key,服务唯一标识,例如:com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@order-service
    String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
    // 获取服务
    Service service = getService(namespaceId, serviceName);
    // 同步锁,避免并发修改的安全问题
    synchronized (service) {
    
    
        // 1)获取要更新的实例列表
        List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
		// 2)封装实例列表到Instances对象
        Instances instances = new Instances();
        instances.setInstanceList(instanceList);
		// 3)完成 注册表更新 以及 Nacos集群的数据同步
        consistencyService.put(key, instances);
    }
}

En este método, la acción de modificar la lista de servicios está bloqueada para garantizar la seguridad de subprocesos. En el bloque de código síncrono se incluyen los siguientes pasos:

  • 1) Primero obtener la lista de instancias a actualizar,addIpAddresses(service, ephemeral, ips);
  • 2) Luego, encapsule los datos actualizados en Instancesun objeto, que se utilizará más adelante al actualizar el registro
  • 3) Finalmente, llame consistencyService.put()al método para completar la sincronización de datos del clúster de Nacos para garantizar la consistencia del clúster.

Nota: En addIPAddress en el paso 1, se copiará la lista de instancias anterior y se agregará una nueva instancia a la lista. En el paso 3, después de actualizar el estado de la instancia, la lista de instancias anterior se sobrescribirá directamente con la lista nueva. Durante el proceso de actualización, la lista de instancias anterior no se ve afectada y los usuarios aún pueden leerla.

De esta manera, en el proceso de actualización del estado de la lista, no hay necesidad de bloquear la operación de lectura del usuario, y no hará que el usuario lea datos sucios, y el rendimiento es mejor. Este esquema se llama esquema CopyOnWrite.

1) Actualizar lista de servicios

Echemos un vistazo a la actualización de la lista de instancias, el método correspondiente es addIpAddresses(service, ephemeral, ips);:

private List<Instance> addIpAddresses(Service service, boolean ephemeral, Instance... ips) throws NacosException {
    
    
    return updateIpAddresses(service, UtilsAndCommons.UPDATE_INSTANCE_ACTION_ADD, ephemeral, ips);
}

Continúe para ingresar updateIpAddressesel método:

public List<Instance> updateIpAddresses(Service service, String action, boolean ephemeral, Instance... ips)
    throws NacosException {
    
    
	// 根据namespaceId、serviceName获取当前服务的实例列表,返回值是Datum
    // 第一次来,肯定是null
    Datum datum = consistencyService
        .get(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), ephemeral));
	// 得到服务中现有的实例列表
    List<Instance> currentIPs = service.allIPs(ephemeral);
    // 创建map,保存实例列表,key为ip地址,value是Instance对象
    Map<String, Instance> currentInstances = new HashMap<>(currentIPs.size());
    // 创建Set集合,保存实例的instanceId
    Set<String> currentInstanceIds = Sets.newHashSet();
	// 遍历要现有的实例列表
    for (Instance instance : currentIPs) {
    
    
        // 添加到map中
        currentInstances.put(instance.toIpAddr(), instance);
        // 添加instanceId到set中
        currentInstanceIds.add(instance.getInstanceId());
    }
	
    // 创建map,用来保存更新后的实例列表
    Map<String, Instance> instanceMap;
    if (datum != null && null != datum.value) {
    
    
        // 如果服务中已经有旧的数据,则先保存旧的实例列表
        instanceMap = setValid(((Instances) datum.value).getInstanceList(), currentInstances);
    } else {
    
    
        // 如果没有旧数据,则直接创建新的map
        instanceMap = new HashMap<>(ips.length);
    }
	// 遍历实例列表
    for (Instance instance : ips) {
    
    
        // 判断服务中是否包含要注册的实例的cluster信息
        if (!service.getClusterMap().containsKey(instance.getClusterName())) {
    
    
            // 如果不包含,创建新的cluster
            Cluster cluster = new Cluster(instance.getClusterName(), service);
            cluster.init();
            // 将集群放入service的注册表
            service.getClusterMap().put(instance.getClusterName(), cluster);
            Loggers.SRV_LOG
                .warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
                      instance.getClusterName(), instance.toJson());
        }
		// 删除实例 or 新增实例 ?
        if (UtilsAndCommons.UPDATE_INSTANCE_ACTION_REMOVE.equals(action)) {
    
    
            instanceMap.remove(instance.getDatumKey());
        } else {
    
    
            // 新增实例,instance生成全新的instanceId
            Instance oldInstance = instanceMap.get(instance.getDatumKey());
            if (oldInstance != null) {
    
    
                instance.setInstanceId(oldInstance.getInstanceId());
            } else {
    
    
                instance.setInstanceId(instance.generateInstanceId(currentInstanceIds));
            }
            // 放入instance列表
            instanceMap.put(instance.getDatumKey(), instance);
        }

    }

    if (instanceMap.size() <= 0 && UtilsAndCommons.UPDATE_INSTANCE_ACTION_ADD.equals(action)) {
    
    
        throw new IllegalArgumentException(
            "ip list can not be empty, service: " + service.getName() + ", ip list: " + JacksonUtils
            .toJson(instanceMap.values()));
    }
	// 将instanceMap中的所有实例转为List返回
    return new ArrayList<>(instanceMap.values());
}

En pocas palabras, es obtener primero la lista de instancias anterior, luego comparar la información de la nueva instancia con la anterior, agregar la nueva instancia y sincronizar la ID de la instancia anterior. Luego devuelva la lista más reciente de instancias.

2) Consistencia del clúster de Nacos

Después de completar la actualización de la lista de servicios locales, Nacos implementa la actualización de consistencia del clúster nuevamente, llamando:

consistencyService.put(key, instances);

La interfaz de ConsistencyService aquí, que representa la interfaz de consistencia del clúster, tiene muchas implementaciones diferentes:
inserte la descripción de la imagen aquí

Vamos a DelegateConsistencyServiceImpl para ver:

@Override
public void put(String key, Record value) throws NacosException {
    
    
    // 根据实例是否是临时实例,判断委托对象
    mapConsistencyService(key).put(key, value);
}

Uno de mapConsistencyService(key)los métodos es elegir el método delegado:

private ConsistencyService mapConsistencyService(String key) {
    
    
    // 判断是否是临时实例:
    // 是,选择 ephemeralConsistencyService,也就是 DistroConsistencyServiceImpl类
    // 否,选择 persistentConsistencyService,也就是PersistentConsistencyServiceDelegateImpl
    return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
}

De forma predeterminada, todas las instancias son instancias temporales, solo nos enfocamos en DistroConsistencyServiceImpl.

2.3.4.DistroConsistencyServiceImpl

Veamos la implementación de consistencia de la instancia temporal: el método put de la clase DistroConsistencyServiceImpl:

public void put(String key, Record value) throws NacosException {
    
    
    // 先将要更新的实例信息写入本地实例列表
    onPut(key, value);
    // 开始集群同步
    distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
                        globalConfig.getTaskDispatchPeriod() / 2);
}

Este método tiene solo dos líneas:

  • onPut(key, value): donde el valor es Instancias, la información del servicio que se actualizará. Esta línea se basa principalmente en el método del grupo de subprocesos, escribiendo asíncronamente la información del servicio en el registro (es decir, el mapa múltiple)
  • distroProtocol.sync(): Es para sincronizar datos a otros nodos de Nacos en el clúster a través del protocolo Distro

Veamos primero el método onPut

2.3.4.1 Actualizar la lista de instancias locales

1) Poner en la cola de bloqueo

El método onPut es el siguiente:

public void onPut(String key, Record value) {
    
    
	// 判断是否是临时实例
    if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
    
    
        // 封装 Instances 信息到 数据集:Datum
        Datum<Instances> datum = new Datum<>();
        datum.value = (Instances) value;
        datum.key = key;
        datum.timestamp.incrementAndGet();
        // 放入DataStore
        dataStore.put(key, datum);
    }

    if (!listeners.containsKey(key)) {
    
    
        return;
    }
	// 放入阻塞队列,这里的 notifier维护了一个阻塞队列,并且基于线程池异步执行队列中的任务
    notifier.addTask(key, DataOperation.CHANGE);
}

El tipo de notificador es que DistroConsistencyServiceImpl.Notifierse mantiene una cola de bloqueo internamente para almacenar cambios en la lista de servicios:
inserte la descripción de la imagen aquí

Cuando addTask, agregue la tarea a la cola de bloqueo:

// DistroConsistencyServiceImpl.Notifier类的 addTask 方法:
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));
}
2) El notificador se actualiza de forma asíncrona

Al mismo tiempo, el notificador sigue siendo un Runnable, que obtiene continuamente tareas de la cola de bloqueo a través de un grupo de subprocesos de un solo subproceso y ejecuta la actualización de la lista de servicios. Echemos un vistazo al método de ejecución:

// DistroConsistencyServiceImpl.Notifier类的run方法:
@Override
public void run() {
    
    
    Loggers.DISTRO.info("distro notifier started");
	// 死循环,不断执行任务。因为是阻塞队列,不会导致CPU负载过高
    for (; ; ) {
    
    
        try {
    
    
            // 从阻塞队列中获取任务
            Pair<String, DataOperation> pair = tasks.take();
            // 处理任务,更新服务列表
            handle(pair);
        } catch (Throwable e) {
    
    
            Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
        }
    }
}

Echemos un vistazo al método handle:

// DistroConsistencyServiceImpl.Notifier类的 handle 方法:
private void handle(Pair<String, DataOperation> pair) {
    
    
    try {
    
    
        String datumKey = pair.getValue0();
        DataOperation action = pair.getValue1();

        services.remove(datumKey);

        int count = 0;

        if (!listeners.containsKey(datumKey)) {
    
    
            return;
        }
		// 遍历,找到变化的service,这里的 RecordListener就是 Service
        for (RecordListener listener : listeners.get(datumKey)) {
    
    

            count++;

            try {
    
    
                // 服务的实例列表CHANGE事件
                if (action == DataOperation.CHANGE) {
    
    
                    // 更新服务列表
                    listener.onChange(datumKey, dataStore.get(datumKey).value);
                    continue;
                }
				// 服务的实例列表 DELETE 事件
                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);
    }
}
3) Anular lista de instancias

En el método de servicio onChange, puede ver la lógica de actualizar la lista de instancias:

@Override
public void onChange(String key, Instances value) throws Exception {
    
    

    Loggers.SRV_LOG.info("[NACOS-RAFT] datum is changed, key: {}, value: {}", key, value);

	// 更新实例列表
    updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));

    recalculateChecksum();
}

método de actualización de IP:

public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
    
    
    // 准备一个Map,key是cluster,值是集群下的Instance集合
    Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
    // 获取服务的所有cluster名称
    for (String clusterName : clusterMap.keySet()) {
    
    
        ipMap.put(clusterName, new ArrayList<>());
    }
    // 遍历要更新的实例
    for (Instance instance : instances) {
    
    
        try {
    
    
            if (instance == null) {
    
    
                Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
                continue;
            }
			// 判断实例是否包含clusterName,没有的话用默认cluster
            if (StringUtils.isEmpty(instance.getClusterName())) {
    
    
                instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
            }
			// 判断cluster是否存在,不存在则创建新的cluster
            if (!clusterMap.containsKey(instance.getClusterName())) {
    
    
                Loggers.SRV_LOG
                    .warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
                          instance.getClusterName(), instance.toJson());
                Cluster cluster = new Cluster(instance.getClusterName(), this);
                cluster.init();
                getClusterMap().put(instance.getClusterName(), cluster);
            }
			// 获取当前cluster实例的集合,不存在则创建新的
            List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
            if (clusterIPs == null) {
    
    
                clusterIPs = new LinkedList<>();
                ipMap.put(instance.getClusterName(), clusterIPs);
            }
			// 添加新的实例到 Instance 集合
            clusterIPs.add(instance);
        } catch (Exception e) {
    
    
            Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
        }
    }

    for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
    
    
        //make every ip mine
        List<Instance> entryIPs = entry.getValue();
        // 将实例集合更新到 clusterMap(注册表)
        clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
    }

    setLastModifiedMillis(System.currentTimeMillis());
    // 发布服务变更的通知消息
    getPushService().serviceChanged(this);
    StringBuilder stringBuilder = new StringBuilder();

    for (Instance instance : allIPs()) {
    
    
        stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
    }

    Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
                         stringBuilder.toString());

}

En el código de la línea 45:clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);

Solo actualizando el registro:

public void updateIps(List<Instance> ips, boolean ephemeral) {
    
    
    // 获取旧实例列表
    Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;

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

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

	// 检查新加入实例的状态
    List<Instance> newIPs = subtract(ips, oldIpMap.values());
    if (newIPs.size() > 0) {
    
    
        Loggers.EVT_LOG
            .info("{} {SYNC} {IP-NEW} cluster: {}, new ips size: {}, content: {}", getService().getName(),
                  getName(), newIPs.size(), newIPs.toString());

        for (Instance ip : newIPs) {
    
    
            HealthCheckStatus.reset(ip);
        }
    }
	// 移除要删除的实例
    List<Instance> deadIPs = subtract(oldIpMap.values(), ips);

    if (deadIPs.size() > 0) {
    
    
        Loggers.EVT_LOG
            .info("{} {SYNC} {IP-DEAD} cluster: {}, dead ips size: {}, content: {}", getService().getName(),
                  getName(), deadIPs.size(), deadIPs.toString());

        for (Instance ip : deadIPs) {
    
    
            HealthCheckStatus.remv(ip);
        }
    }

    toUpdateInstances = new HashSet<>(ips);
	// 直接覆盖旧实例列表
    if (ephemeral) {
    
    
        ephemeralInstances = toUpdateInstances;
    } else {
    
    
        persistentInstances = toUpdateInstances;
    }
}

2.3.4.2 Sincronización de datos de clúster

Hay dos pasos en el método put de DistroConsistencyServiceImpl:
inserte la descripción de la imagen aquí

El método onPut ya ha sido analizado.

El siguiente distroProtocol.sync() es la lógica de la sincronización de clústeres.

El método de sincronización de la clase DistroProtocol es el siguiente:

public void sync(DistroKey distroKey, DataOperation action, long delay) {
    
    
    // 遍历 Nacos 集群中除自己以外的其它节点
    for (Member each : memberManager.allMembersWithoutSelf()) {
    
    
        DistroKey distroKeyWithTarget = new DistroKey(distroKey.getResourceKey(), distroKey.getResourceType(),
                                                      each.getAddress());
        // 定义一个Distro的同步任务
        DistroDelayTask distroDelayTask = new DistroDelayTask(distroKeyWithTarget, action, delay);
        // 交给线程池去执行
        distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask);
        if (Loggers.DISTRO.isDebugEnabled()) {
    
    
            Loggers.DISTRO.debug("[DISTRO-SCHEDULE] {} to {}", distroKey, each.getAddress());
        }
    }
}

La tarea sincronizada se encapsula como un DistroDelayTaskobjeto.

Entregado a distroTaskEngineHolder.getDelayTaskExecuteEngine()la ejecución, el valor de retorno de esta línea de código es:

NacosDelayTaskExecuteEngine, esta clase mantiene un grupo de subprocesos y recibe y ejecuta tareas.

El método de ejecución de tareas es el método processTasks():

protected void processTasks() {
    
    
    Collection<Object> keys = getAllTaskKeys();
    for (Object taskKey : keys) {
    
    
        AbstractDelayTask task = removeTask(taskKey);
        if (null == task) {
    
    
            continue;
        }
        NacosTaskProcessor processor = getProcessor(taskKey);
        if (null == processor) {
    
    
            getEngineLog().error("processor not found for task, so discarded. " + task);
            continue;
        }
        try {
    
    
            // 尝试执行同步任务,如果失败会重试
            if (!processor.process(task)) {
    
    
                retryFailedTask(taskKey, task);
            }
        } catch (Throwable e) {
    
    
            getEngineLog().error("Nacos task execute error : " + e.toString(), e);
            retryFailedTask(taskKey, task);
        }
    }
}

Se puede ver que la sincronización basada en el modo Distro se realiza de forma asíncrona, y cuando falla, la tarea se vuelve a poner en cola y se enriquece, por lo que no se garantiza la fuerte consistencia de los resultados de la sincronización, que pertenece a la estrategia de consistencia. del modo AP.

2.3.5 Diagrama de flujo del servidor

inserte la descripción de la imagen aquí

2.4 Resumen

  • ¿Cuál es la estructura registral de Nacos?

    • Respuesta: Nacos es un modelo de almacenamiento de varios niveles. La capa más externa implementa el aislamiento del entorno a través del espacio de nombres y luego la agrupación, bajo la cual se encuentran los servicios. Un servicio se puede dividir en diferentes clústeres, y un clúster contiene varias instancias. Por tanto, su estructura de registro es un Mapa, y su tipo es:

      Map<String, Map<String, Service>>,

      La clave externa es namespace_idy la clave interna es group+serviceName.

      El servicio mantiene un mapa en su interior, la estructura es: Map<String,Cluster>, la clave es clusterName y el valor es información del clúster.

      El clúster mantiene una colección de conjuntos internamente y los elementos son del tipo de instancia, lo que representa varias instancias en el clúster.

  • ¿Cómo garantiza Nacos la seguridad de la escritura concurrente?

    • R: En primer lugar, al registrar una instancia, el servicio se bloqueará, no hay problema de escritura simultánea entre diferentes servicios y no se afectan entre sí. Exclusión mutua mediante bloqueos para un mismo servicio. Además, al actualizar la lista de instancias, se realiza en función del grupo de subprocesos asíncronos, y la cantidad de subprocesos en el grupo de subprocesos es 1.
  • ¿Cómo evita Nacos los conflictos simultáneos de lectura y escritura?

    • Respuesta: Cuando Nacos actualice la lista de instancias, utilizará la tecnología CopyOnWrite. Primero, copie la lista de instancias antiguas, luego actualice la lista de instancias copiadas y luego use la lista de instancias actualizada para sobrescribir la lista de instancias anterior.
  • ¿Cómo trata Nacos las solicitudes de escritura simultáneas de cientos de miles de servicios dentro de Ali?

    • Respuesta: Nacos colocará las tareas de registro del servicio en la cola de bloqueo internamente y usará el grupo de subprocesos para completar la actualización de la instancia de forma asíncrona, mejorando así la capacidad de escritura simultánea.

3. Latido del servicio

Las instancias de Nacos se dividen en instancias temporales e instancias permanentes, que se pueden configurar en el archivo yaml:

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        ephemeral: false # 设置实例为永久实例。true:临时; false:永久
      server-addr: 192.168.150.1:8845

Las instancias temporales realizan comprobaciones de estado basadas en el latido del corazón, mientras que Nacos detecta activamente las instancias permanentes.

La interfaz API de latido proporcionada por Nacos es:

Descripción de la interfaz : envía el latido de una instancia

Tipo de solicitud : PUT

Solicitud de ruta :

/nacos/v1/ns/instance/beat

Parámetros de la solicitud :

nombre tipo ¿Es obligatorio? describir
Nombre del Servicio cadena Nombre del Servicio
Nombre del grupo cadena No Nombre del grupo
efímero booleano No ¿Es una instancia temporal?
derrotar Cadena de formato JSON Contenido del latido de la instancia

Código de error :

código de error describir Semántica
400 Solicitud incorrecta Error de sintaxis en la solicitud del cliente
403 Prohibido Permiso denegado
404 Extraviado recurso no encontrado
500 Error Interno del Servidor Error Interno del Servidor
200 DE ACUERDO normal

3.1 Cliente

En la sección 2.2.4 Registro del servicio, dijimos que la clase NacosNamingService implementa el registro del servicio y también implementa el latido del servicio:

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    
    
    NamingUtils.checkInstanceIsLegal(instance);
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    // 判断是否是临时实例。
    if (instance.isEphemeral()) {
    
    
        // 如果是临时实例,则构建心跳信息BeatInfo
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        // 添加心跳任务
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

3.1.1.Información de latido

El BeanInfo aquí contiene todo tipo de información necesaria para el latido del corazón:
inserte la descripción de la imagen aquí

3.1.2.BeatReactor

Y BeatReactoresta clase mantiene un grupo de subprocesos:
inserte la descripción de la imagen aquí

Se realiza un latido cuando se BeatReactorllama al método .addBeatInfo(groupedServiceName, beatInfo):

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
    
    
    NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
    String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
    BeatInfo existBeat = null;
    //fix #1733
    if ((existBeat = dom2Beat.remove(key)) != null) {
    
    
        existBeat.setStopped(true);
    }
    dom2Beat.put(key, beatInfo);
    // 利用线程池,定期执行心跳任务,周期为 beatInfo.getPeriod()
    executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
    MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}

El valor predeterminado del período de latido está en com.alibaba.nacos.api.common.Constantsla clase:
inserte la descripción de la imagen aquí
puede ver que son 5 segundos y el valor predeterminado es 5 segundos para un latido.

3.1.3.BeatTask

La tarea de latido está encapsulada en BeatTaskesta clase, que es un Runnable cuyo método de ejecución es el siguiente:

@Override
public void run() {
    
    
    if (beatInfo.isStopped()) {
    
    
        return;
    }
    // 获取心跳周期
    long nextTime = beatInfo.getPeriod();
    try {
    
    
        // 发送心跳
        JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
        long interval = result.get("clientBeatInterval").asLong();
        boolean lightBeatEnabled = false;
        if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
    
    
            lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
        }
        BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
        if (interval > 0) {
    
    
            nextTime = interval;
        }
        // 判断心跳结果
        int code = NamingResponseCode.OK;
        if (result.has(CommonParams.CODE)) {
    
    
            code = result.get(CommonParams.CODE).asInt();
        }
        if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
    
    
            // 如果失败,则需要 重新注册实例
            Instance instance = new Instance();
            instance.setPort(beatInfo.getPort());
            instance.setIp(beatInfo.getIp());
            instance.setWeight(beatInfo.getWeight());
            instance.setMetadata(beatInfo.getMetadata());
            instance.setClusterName(beatInfo.getCluster());
            instance.setServiceName(beatInfo.getServiceName());
            instance.setInstanceId(instance.getInstanceId());
            instance.setEphemeral(true);
            try {
    
    
                serverProxy.registerService(beatInfo.getServiceName(),
                                            NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
            } catch (Exception ignore) {
    
    
            }
        }
    } catch (NacosException ex) {
    
    
        NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                            JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());

    } catch (Exception unknownEx) {
    
    
        NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, unknown exception msg: {}",
                            JacksonUtils.toJson(beatInfo), unknownEx.getMessage(), unknownEx);
    } finally {
    
    
        executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
    }
}

3.1.5 Enviar latido

El envío del latido final todavía se logra a través del método NamingProxy:sendBeat

public JsonNode sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {
    
    

    if (NAMING_LOGGER.isDebugEnabled()) {
    
    
        NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
    }
    // 组织请求参数
    Map<String, String> params = new HashMap<String, String>(8);
    Map<String, String> bodyMap = new HashMap<String, String>(2);
    if (!lightBeatEnabled) {
    
    
        bodyMap.put("beat", JacksonUtils.toJson(beatInfo));
    }
    params.put(CommonParams.NAMESPACE_ID, namespaceId);
    params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
    params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
    params.put("ip", beatInfo.getIp());
    params.put("port", String.valueOf(beatInfo.getPort()));
    // 发送请求,这个地址就是:/v1/ns/instance/beat
    String result = reqApi(UtilAndComs.nacosUrlBase + "/instance/beat", params, bodyMap, HttpMethod.PUT);
    return JacksonUtils.toObj(result);
}

3.2 Servidor

Para instancias temporales, el código del servidor se divide en dos partes:

  • 1) InstanceController proporciona una interfaz para manejar la solicitud de latido del cliente
  • 2) Verifique regularmente si el latido de la instancia se ejecuta según lo programado

3.2.1.InstanciaControlador

Al igual que con el registro del servicio, en la clase InstanceController en el módulo de nombres de nacos, se define un método para manejar las solicitudes de latidos:

@CanDistro
@PutMapping("/beat")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public ObjectNode beat(HttpServletRequest request) throws Exception {
    
    
	// 解析心跳的请求参数
    ObjectNode result = JacksonUtils.createEmptyJsonNode();
    result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());

    String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);
    RsInfo clientBeat = null;
    if (StringUtils.isNotBlank(beat)) {
    
    
        clientBeat = JacksonUtils.toObj(beat, RsInfo.class);
    }
    String clusterName = WebUtils
        .optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME);
    String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);
    int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));
    if (clientBeat != null) {
    
    
        if (StringUtils.isNotBlank(clientBeat.getCluster())) {
    
    
            clusterName = clientBeat.getCluster();
        } else {
    
    
            // fix #2533
            clientBeat.setCluster(clusterName);
        }
        ip = clientBeat.getIp();
        port = clientBeat.getPort();
    }
    String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);
    Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}", clientBeat, serviceName);
    // 尝试根据参数中的namespaceId、serviceName、clusterName、ip、port等信息
    // 从Nacos的注册表中 获取实例
    Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);
	// 如果获取失败,说明心跳失败,实例尚未注册
    if (instance == null) {
    
    
        if (clientBeat == null) {
    
    
            result.put(CommonParams.CODE, NamingResponseCode.RESOURCE_NOT_FOUND);
            return result;
        }

        Loggers.SRV_LOG.warn("[CLIENT-BEAT] The instance has been removed for health mechanism, "
                             + "perform data compensation operations, beat: {}, serviceName: {}", clientBeat, serviceName);
		// 这里重新注册一个实例
        instance = new Instance();
        instance.setPort(clientBeat.getPort());
        instance.setIp(clientBeat.getIp());
        instance.setWeight(clientBeat.getWeight());
        instance.setMetadata(clientBeat.getMetadata());
        instance.setClusterName(clusterName);
        instance.setServiceName(serviceName);
        instance.setInstanceId(instance.getInstanceId());
        instance.setEphemeral(clientBeat.isEphemeral());

        serviceManager.registerInstance(namespaceId, serviceName, instance);
    }
	// 尝试基于namespaceId和serviceName从 注册表中获取Service服务
    Service service = serviceManager.getService(namespaceId, serviceName);
	// 如果不存在,说明服务不存在,返回404
    if (service == null) {
    
    
        throw new NacosException(NacosException.SERVER_ERROR,
                                 "service not found: " + serviceName + "@" + namespaceId);
    }
    if (clientBeat == null) {
    
    
        clientBeat = new RsInfo();
        clientBeat.setIp(ip);
        clientBeat.setPort(port);
        clientBeat.setCluster(clusterName);
    }
    // 如果心跳没问题,开始处理心跳结果
    service.processClientBeat(clientBeat);

    result.put(CommonParams.CODE, NamingResponseCode.OK);
    if (instance.containsMetadata(PreservedMetadataKeys.HEART_BEAT_INTERVAL)) {
    
    
        result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, instance.getInstanceHeartBeatInterval());
    }
    result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
    return result;
}

Finalmente, después de confirmar que el servicio y la instancia correspondientes a la solicitud de latido existen, comience a entregar la clase de servicio para procesar la solicitud de latido. El método de servicio processClientBeat se llama

3.2.2 Manejo de solicitudes de latidos

ServiceCómo comprobar service.processClientBeat(clientBeat);:

public void processClientBeat(final RsInfo rsInfo) {
    
    
    ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor();
    clientBeatProcessor.setService(this);
    clientBeatProcessor.setRsInfo(rsInfo);
    HealthCheckReactor.scheduleNow(clientBeatProcessor);
}

Se puede ver que la información del latido del corazón se encapsula en la clase ClientBeatProcessor y se entrega a HealthCheckReactor para su procesamiento.HealthCheckReactor es la encapsulación del grupo de subprocesos, por lo que no es necesario verificar demasiado.

La lógica comercial clave está en la clase ClientBeatProcessor, que es un Runnable, y el método de ejecución es el siguiente:

@Override
public void run() {
    
    
    Service service = this.service;
    if (Loggers.EVT_LOG.isDebugEnabled()) {
    
    
        Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());
    }

    String ip = rsInfo.getIp();
    String clusterName = rsInfo.getCluster();
    int port = rsInfo.getPort();
    // 获取集群信息
    Cluster cluster = service.getClusterMap().get(clusterName);
    // 获取集群中的所有实例信息
    List<Instance> instances = cluster.allIPs(true);

    for (Instance instance : instances) {
    
    
        // 找到心跳的这个实例
        if (instance.getIp().equals(ip) && instance.getPort() == port) {
    
    
            if (Loggers.EVT_LOG.isDebugEnabled()) {
    
    
                Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo.toString());
            }
            // 更新实例的最后一次心跳时间 lastBeat
            instance.setLastBeat(System.currentTimeMillis());
            if (!instance.isMarked()) {
    
    
                if (!instance.isHealthy()) {
    
    
                    instance.setHealthy(true);
                    Loggers.EVT_LOG
                        .info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok",
                              cluster.getService().getName(), ip, port, cluster.getName(),
                              UtilsAndCommons.LOCALHOST_SITE);
                    getPushService().serviceChanged(service);
                }
            }
        }
    }
}

El núcleo del procesamiento de solicitudes de latido es actualizar el tiempo del último latido de la instancia de latido, lastBeat, que se convertirá en un indicador clave para juzgar si el latido de la instancia ha expirado.

3.3.3 Detección de latidos cardíacos anormales

ServiceCuando se registra un servicio, se debe crear un objeto y Servicehay un initmétodo en él que se llamará durante el registro:

public void init() {
    
    
    // 开启心跳检测的任务
    HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
    for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
    
    
        entry.getValue().setService(this);
        entry.getValue().init();
    }
}

Entre ellos, HealthCheckReactor.scheduleCheck es una tarea programada para realizar la detección de latidos:
inserte la descripción de la imagen aquí

Se puede observar que esta tarea se ejecuta cada 5000ms, es decir, el estado de heartbeat de la instancia se detecta una vez cada 5 segundos.

El ClientBeatCheckTask aquí también es un Runnable, donde el método de ejecución es:

@Override
public void run() {
    
    
    try {
    
    
        // 找到所有临时实例的列表
        List<Instance> instances = service.allIPs(true);

        // first set health status of instances:
        for (Instance instance : instances) {
    
    
            // 判断 心跳间隔(当前时间 - 最后一次心跳时间) 是否大于 心跳超时时间,默认15秒
            if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
    
    
                if (!instance.isMarked()) {
    
    
                    if (instance.isHealthy()) {
    
    
                        // 如果超时,标记实例为不健康 healthy = false
                        instance.setHealthy(false);
 
                        // 发布实例状态变更的事件
                        getPushService().serviceChanged(service);
                        ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
                    }
                }
            }
        }

        if (!getGlobalConfig().isExpireInstance()) {
    
    
            return;
        }

        // then remove obsolete instances:
        for (Instance instance : instances) {
    
    

            if (instance.isMarked()) {
    
    
                continue;
            }
           // 判断心跳间隔(当前时间 - 最后一次心跳时间)是否大于 实例被删除的最长超时时间,默认30秒
            if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
    
    
                // 如果是超过了30秒,则删除实例
                Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(),
                                     JacksonUtils.toJson(instance));
                deleteIp(instance);
            }
        }

    } catch (Exception e) {
    
    
        Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
    }

}

El período de tiempo de espera también está en com.alibaba.nacos.api.common.Constantsesta clase:
inserte la descripción de la imagen aquí

3.3.4 Detección activa de salud

Para las instancias no temporales (efímeras = falsas), Nacos usará la detección de salud activa, enviará solicitudes a la instancia con regularidad y juzgará el estado de salud de la instancia en función de la respuesta.

El método registerInstance en la clase cuya entrada está en la Sección 2.3.2 ServiceManager:
inserte la descripción de la imagen aquí

Al crear un servicio vacío:

public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
    
    
    // 如果服务不存在,创建新的服务
    createServiceIfAbsent(namespaceId, serviceName, local, null);
}

Cree un proceso de servicio:

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);
        }
    }
}

La clave está putServiceAndInit(service)en el método:

private void putServiceAndInit(Service service) throws NacosException {
    
    
    // 将服务写入注册表
    putService(service);
    service = getService(service.getNamespaceId(), service.getName());
    // 完成服务的初始化
    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());
}

Ingrese la lógica de inicialización: service.init(), esto ingresará la clase de Servicio:

/**
     * Init service.
     */
public void init() {
    
    
    // 开启临时实例的心跳监测任务
    HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
    // 遍历注册表中的集群
    for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
    
    
        entry.getValue().setService(this);
        // 完成集群初识化
        entry.getValue().init();
    }
}

Aquí la inicialización del clúster entry.getValue().init();entra en un método Clusterde tipo init():

/**
     * Init cluster.
     */
public void init() {
    
    
    if (inited) {
    
    
        return;
    }
    // 创建健康检测的任务
    checkTask = new HealthCheckTask(this);
	// 这里会开启对 非临时实例的 定时健康检测
    HealthCheckReactor.scheduleCheck(checkTask);
    inited = true;
}

Aquí HealthCheckReactor.scheduleCheck(checkTask);, las tareas programadas se iniciarán para realizar controles de salud en instancias no temporales. La lógica de detección se define en HealthCheckTaskesta clase, que es un Runnable, en el que el método de ejecución:

public void run() {
    
    

    try {
    
    
        if (distroMapper.responsible(cluster.getService().getName()) && switchDomain
            .isHealthCheckEnabled(cluster.getService().getName())) {
    
    
            // 开始健康检测
            healthCheckProcessor.process(this);
			// 记录日志 。。。
        }
    } catch (Throwable e) {
    
    
       // 记录日志 。。。
    } finally {
    
    
        if (!cancelled) {
    
    
            // 结束后,再次进行任务调度,一定延迟后执行
            HealthCheckReactor.scheduleCheck(this);
            
            // 。。。
        }
    }
}

La lógica de detección de estado se define en healthCheckProcessor.process(this);el método. En la interfaz HealthCheckProcessor, hay muchas implementaciones de esta interfaz. El valor predeterminado es TcpSuperSenseProcessor:
inserte la descripción de la imagen aquí

Método de proceso ingresado TcpSuperSenseProcessor:

@Override
public void process(HealthCheckTask task) {
    
    
    // 获取所有 非临时实例的 集合
    List<Instance> ips = task.getCluster().allIPs(false);

    if (CollectionUtils.isEmpty(ips)) {
    
    
        return;
    }

    for (Instance ip : ips) {
    
    
		// 封装健康检测信息到 Beat
        Beat beat = new Beat(ip, task);
        // 放入一个阻塞队列中
        taskQueue.add(beat);
        MetricsMonitor.getTcpHealthCheckMonitor().incrementAndGet();
    }
}

Se puede ver que todas las tareas de detección de estado se colocan en una cola de bloqueo en lugar de ejecutarse de inmediato. Aquí nuevamente se adopta la estrategia de ejecución asíncrona, y se puede ver una gran cantidad de tales diseños en Nacos.

Y TcpSuperSenseProcessoren sí mismo es un Runnable, en su constructor se colocará en el grupo de subprocesos para ejecutar, su método de ejecución es el siguiente:

public void run() {
    
    
    while (true) {
    
    
        try {
    
    
            // 处理任务
            processTask();
            // ...
        } catch (Throwable e) {
    
    
            SRV_LOG.error("[HEALTH-CHECK] error while processing NIO task", e);
        }
    }
}

Procese la tarea de detección de salud a través de processTask:

private void processTask() throws Exception {
    
    
    // 将任务封装为一个 TaskProcessor,并放入集合
    Collection<Callable<Void>> tasks = new LinkedList<>();
    do {
    
    
        Beat beat = taskQueue.poll(CONNECT_TIMEOUT_MS / 2, TimeUnit.MILLISECONDS);
        if (beat == null) {
    
    
            return;
        }

        tasks.add(new TaskProcessor(beat));
    } while (taskQueue.size() > 0 && tasks.size() < NIO_THREAD_COUNT * 64);
	// 批量处理集合中的任务
    for (Future<?> f : GlobalExecutor.invokeAllTcpSuperSenseTask(tasks)) {
    
    
        f.get();
    }
}

Las tareas se encapsulan en TaskProcessor para su ejecución, TaskProcessor es un Callable, el método de llamada que contiene:

@Override
public Void call() {
    
    
    // 获取检测任务已经等待的时长
    long waited = System.currentTimeMillis() - beat.getStartTime();
    if (waited > MAX_WAIT_TIME_MILLISECONDS) {
    
    
        Loggers.SRV_LOG.warn("beat task waited too long: " + waited + "ms");
    }
	
    SocketChannel channel = null;
    try {
    
    
        // 获取实例信息
        Instance instance = beat.getIp();
		// 通过NIO建立TCP连接
        channel = SocketChannel.open();
        channel.configureBlocking(false);
        // only by setting this can we make the socket close event asynchronous
        channel.socket().setSoLinger(false, -1);
        channel.socket().setReuseAddress(true);
        channel.socket().setKeepAlive(true);
        channel.socket().setTcpNoDelay(true);

        Cluster cluster = beat.getTask().getCluster();
        int port = cluster.isUseIPPort4Check() ? instance.getPort() : cluster.getDefCkport();
        channel.connect(new InetSocketAddress(instance.getIp(), port));
		// 注册连接、读取事件
        SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
        key.attach(beat);
        keyMap.put(beat.toString(), new BeatKey(key));

        beat.setStartTime(System.currentTimeMillis());

        GlobalExecutor
            .scheduleTcpSuperSenseTask(new TimeOutTask(key), CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
    } catch (Exception e) {
    
    
        beat.finishCheck(false, false, switchDomain.getTcpHealthParams().getMax(),
                         "tcp:error:" + e.getMessage());

        if (channel != null) {
    
    
            try {
    
    
                channel.close();
            } catch (Exception ignore) {
    
    
            }
        }
    }

    return null;
}

3.3 Resumen

Hay dos modos de detección de salud en Nacos:

  • Instancia temporal:
    • Adopte el modo de detección de latidos del corazón del cliente, el período de latidos del corazón es de 5 segundos
    • Los intervalos de latidos cardíacos de más de 15 segundos se marcan como no saludables
    • Si el intervalo de latido supera los 30 segundos, se eliminará de la lista de servicios
  • Instancia permanente:
    • Adopte el método de detección de estado activo del lado del servidor
    • Un número aleatorio con un período de 2000 + 5000 milisegundos
    • Las anomalías detectadas solo se marcarán como incorrectas y no se eliminarán

Entonces, ¿por qué Nacos tiene instancias temporales y permanentes?

Tome Taobao como ejemplo. Durante el período de promoción de Double Eleven, el tráfico será mucho más alto de lo habitual. En este momento, el servicio debe agregar más instancias para hacer frente a la alta concurrencia. Estas instancias ya no necesitarán usarse después de Double Eleven. Los ejemplos temporales son más apropiados. Para algunas instancias permanentes de servicios, es más apropiado usar instancias permanentes .

En comparación con eureka, tanto Nacos como Eureka se implementan en función del modo de latido en instancias temporales, y la diferencia no es grande, principalmente porque el ciclo de latido es diferente, eureka es de 30 segundos y Nacos es de 5 segundos.

Además, Nacos admite instancias permanentes, pero Eureka no. Eureka solo proporciona monitoreo de salud en modo de latido, sin detección activa.

4. Descubrimiento de servicios

Nacos proporciona una interfaz para consultar la lista de instancias en función de serviceId:

Descripción de la interfaz : consulta la lista de instancias bajo el servicio

Tipo de solicitud : OBTENER

Solicitud de ruta :

/nacos/v1/ns/instance/list

Parámetros de la solicitud :

nombre tipo ¿Es obligatorio? describir
Nombre del Servicio cadena Nombre del Servicio
Nombre del grupo cadena No Nombre del grupo
ID de espacio de nombres cadena No Id. de espacio de nombres
racimos Cadena, varios clústeres separados por comas No nombre del clúster
solosaludable boolean 否,默认为false 是否只返回健康实例

错误编码

错误代码 描述 语义
400 Bad Request 客户端请求中的语法错误
403 Forbidden 没有权限
404 Not Found 无法找到资源
500 Internal Server Error 服务器内部错误
200 OK 正常

4.1.客户端

4.1.1.定时更新服务列表

4.1.1.1.NacosNamingService

在2.2.4小节中,我们讲到一个类NacosNamingService,这个类不仅仅提供了服务注册功能,同样提供了服务发现的功能。
inserte la descripción de la imagen aquí

多个重载的方法最终都会进入一个方法:

@Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
                                      boolean subscribe) throws NacosException {
    
    

    ServiceInfo serviceInfo;
    // 1.判断是否需要订阅服务信息(默认为 true)
    if (subscribe) {
    
    
        // 1.1.订阅服务信息
        serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
                                                 StringUtils.join(clusters, ","));
    } else {
    
    
        // 1.2.直接去nacos拉取服务信息
        serviceInfo = hostReactor
            .getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
                                              StringUtils.join(clusters, ","));
    }
    // 2.从服务信息中获取实例列表并返回
    List<Instance> list;
    if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
    
    
        return new ArrayList<Instance>();
    }
    return list;
}

4.1.1.2.HostReactor

进入1.1.订阅服务消息,这里是由HostReactor类的getServiceInfo()方法来实现的:

public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
    
    

    NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
    // 由 服务名@@集群名拼接 key
    String key = ServiceInfo.getKey(serviceName, clusters);
    if (failoverReactor.isFailoverSwitch()) {
    
    
        return failoverReactor.getService(key);
    }
    // 读取本地服务列表的缓存,缓存是一个Map,格式:Map<String, ServiceInfo>
    ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
    // 判断缓存是否存在
    if (null == serviceObj) {
    
    
        // 不存在,创建空ServiceInfo
        serviceObj = new ServiceInfo(serviceName, clusters);
        // 放入缓存
        serviceInfoMap.put(serviceObj.getKey(), serviceObj);
        // 放入待更新的服务列表(updatingMap)中
        updatingMap.put(serviceName, new Object());
        // 立即更新服务列表
        updateServiceNow(serviceName, clusters);
        // 从待更新列表中移除
        updatingMap.remove(serviceName);

    } else if (updatingMap.containsKey(serviceName)) {
    
    
        // 缓存中有,但是需要更新
        if (UPDATE_HOLD_INTERVAL > 0) {
    
    
            // hold a moment waiting for update finish 等待5秒中,待更新完成
            synchronized (serviceObj) {
    
    
                try {
    
    
                    serviceObj.wait(UPDATE_HOLD_INTERVAL);
                } catch (InterruptedException e) {
    
    
                    NAMING_LOGGER
                        .error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
                }
            }
        }
    }
    // 开启定时更新服务列表的功能
    scheduleUpdateIfAbsent(serviceName, clusters);
    // 返回缓存中的服务信息
    return serviceInfoMap.get(serviceObj.getKey());
}

基本逻辑就是先从本地缓存读,根据结果来选择:

  • 如果本地缓存没有,立即去nacos读取,updateServiceNow(serviceName, clusters)
    inserte la descripción de la imagen aquí

  • 如果本地缓存有,则开启定时更新功能,并返回缓存结果:

    • scheduleUpdateIfAbsent(serviceName, clusters)

inserte la descripción de la imagen aquí

在UpdateTask中,最终还是调用updateService方法:
inserte la descripción de la imagen aquí

不管是立即更新服务列表,还是定时更新服务列表,最终都会执行HostReactor中的updateService()方法:

public void updateService(String serviceName, String clusters) throws NacosException {
    
    
    ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
    try {
    
    
		// 基于ServerProxy发起远程调用,查询服务列表
        String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);

        if (StringUtils.isNotEmpty(result)) {
    
    
            // 处理查询结果
            processServiceJson(result);
        }
    } finally {
    
    
        if (oldService != null) {
    
    
            synchronized (oldService) {
    
    
                oldService.notifyAll();
            }
        }
    }
}

4.1.1.3.ServerProxy

而ServerProxy的queryList方法如下:

public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly)
    throws NacosException {
    
    
	// 准备请求参数
    final Map<String, String> params = new HashMap<String, String>(8);
    params.put(CommonParams.NAMESPACE_ID, namespaceId);
    params.put(CommonParams.SERVICE_NAME, serviceName);
    params.put("clusters", clusters);
    params.put("udpPort", String.valueOf(udpPort));
    params.put("clientIP", NetUtils.localIP());
    params.put("healthyOnly", String.valueOf(healthyOnly));
	// 发起请求,地址与API接口一致
    return reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, HttpMethod.GET);
}

4.1.2.处理服务变更通知

除了定时更新服务列表的功能外,Nacos还支持服务列表变更时的主动推送功能。

在HostReactor类的构造函数中,有非常重要的几个步骤:
inserte la descripción de la imagen aquí

基本思路是:

  • 通过PushReceiver监听服务端推送的变更数据
  • 解析数据后,通过NotifyCenter发布服务变更的事件
  • InstanceChangeNotifier监听变更事件,完成对服务列表的更新

4.1.2.1.PushReceiver

我们先看PushReceiver,这个类会以UDP方式接收Nacos服务端推送的服务变更数据。

先看构造函数:

public PushReceiver(HostReactor hostReactor) {
    
    
    try {
    
    
        this.hostReactor = hostReactor;
        // 创建 UDP客户端
        String udpPort = getPushReceiverUdpPort();
        if (StringUtils.isEmpty(udpPort)) {
    
    
            this.udpSocket = new DatagramSocket();
        } else {
    
    
            this.udpSocket = new DatagramSocket(new InetSocketAddress(Integer.parseInt(udpPort)));
        }
        // 准备线程池
        this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
    
    
            @Override
            public Thread newThread(Runnable r) {
    
    
                Thread thread = new Thread(r);
                thread.setDaemon(true);
                thread.setName("com.alibaba.nacos.naming.push.receiver");
                return thread;
            }
        });
		// 开启线程任务,准备接收变更数据
        this.executorService.execute(this);
    } catch (Exception e) {
    
    
        NAMING_LOGGER.error("[NA] init udp socket failed", e);
    }
}

El constructor PushReceiver ejecuta tareas basadas en el grupo de subprocesos. Esto se debe a que PushReceiver también es un Runnable y la lógica comercial del método de ejecución es la siguiente:

@Override
public void run() {
    
    
    while (!closed) {
    
    
        try {
    
    
            // byte[] is initialized with 0 full filled by default
            byte[] buffer = new byte[UDP_MSS];
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
			// 接收推送数据
            udpSocket.receive(packet);
			// 解析为json字符串
            String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
            NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
			// 反序列化为对象
            PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
            String ack;
            if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
    
    
                // 交给 HostReactor去处理
                hostReactor.processServiceJson(pushPacket.data);

                // send ack to server 发送ACK回执,略。。
        } catch (Exception e) {
    
    
            if (closed) {
    
    
                return;
            }
            NAMING_LOGGER.error("[NA] error while receiving push data", e);
        }
    }
}

4.1.2.2.Reactor anfitrión

El procesamiento de los datos de notificación se entrega al HostReactormétodo processServiceJson:

public ServiceInfo processServiceJson(String json) {
    
    
    // 解析出ServiceInfo信息
    ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);
    String serviceKey = serviceInfo.getKey();
    if (serviceKey == null) {
    
    
        return null;
    }
    // 查询缓存中的 ServiceInfo
    ServiceInfo oldService = serviceInfoMap.get(serviceKey);

    // 如果缓存存在,则需要校验哪些数据要更新
    boolean changed = false;
    if (oldService != null) {
    
    
		// 拉取的数据是否已经过期
        if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {
    
    
            NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "
                               + serviceInfo.getLastRefTime());
        }
        // 放入缓存
        serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
		
        // 中间是缓存与新数据的对比,得到newHosts:新增的实例;remvHosts:待移除的实例;
        // modHosts:需要修改的实例
        if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
    
    
            // 发布实例变更的事件
            NotifyCenter.publishEvent(new InstancesChangeEvent(
                serviceInfo.getName(), serviceInfo.getGroupName(),
                serviceInfo.getClusters(), serviceInfo.getHosts()));
            DiskCache.write(serviceInfo, cacheDir);
        }

    } else {
    
    
        // 本地缓存不存在
        changed = true;
        // 放入缓存
        serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
        // 直接发布实例变更的事件
        NotifyCenter.publishEvent(new InstancesChangeEvent(
            serviceInfo.getName(), serviceInfo.getGroupName(),
            serviceInfo.getClusters(), serviceInfo.getHosts()));
        serviceInfo.setJsonFromServer(json);
        DiskCache.write(serviceInfo, cacheDir);
    }
	// 。。。
    return serviceInfo;
}

4.2 Servidor

4.2.1 Interfaz de la lista de servicios de extracción

En el InstanceController presentado en la Sección 2.3.1, se proporciona una interfaz para extraer la lista de servicios:

/**
     * Get all instance of input service.
     *
     * @param request http request
     * @return list of instance
     * @throws Exception any error during list
     */
@GetMapping("/list")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
public ObjectNode list(HttpServletRequest request) throws Exception {
    
    
    // 从request中获取namespaceId和serviceName
    String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);

    String agent = WebUtils.getUserAgent(request);
    String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY);
    String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);
    // 获取客户端的 UDP端口
    int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));
    String env = WebUtils.optional(request, "env", StringUtils.EMPTY);
    boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, "isCheck", "false"));

    String app = WebUtils.optional(request, "app", StringUtils.EMPTY);

    String tenant = WebUtils.optional(request, "tid", StringUtils.EMPTY);

    boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false"));

    // 获取服务列表
    return doSrvIpxt(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant,
                     healthyOnly);
}

Introduzca doSrvIpxt()el método para obtener la lista de servicios:

public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent,
                            String clusters, String clientIP,
                            int udpPort, String env, boolean isCheck,
                            String app, String tid, boolean healthyOnly) throws Exception {
    
    
    ClientInfo clientInfo = new ClientInfo(agent);
    ObjectNode result = JacksonUtils.createEmptyJsonNode();
    // 获取服务列表信息
    Service service = serviceManager.getService(namespaceId, serviceName);
    long cacheMillis = switchDomain.getDefaultCacheMillis();

    // now try to enable the push
    try {
    
    
        if (udpPort > 0 && pushService.canEnablePush(agent)) {
    
    
			// 添加当前客户端 IP、UDP端口到 PushService 中
            pushService
                .addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort),
                           pushDataSource, tid, app);
            cacheMillis = switchDomain.getPushCacheMillis(serviceName);
        }
    } catch (Exception e) {
    
    
        Loggers.SRV_LOG
            .error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP, udpPort, e);
        cacheMillis = switchDomain.getDefaultCacheMillis();
    }

    if (service == null) {
    
    
        // 如果没找到,返回空
        if (Loggers.SRV_LOG.isDebugEnabled()) {
    
    
            Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName);
        }
        result.put("name", serviceName);
        result.put("clusters", clusters);
        result.put("cacheMillis", cacheMillis);
        result.replace("hosts", JacksonUtils.createEmptyArrayNode());
        return result;
    }
	// 结果的检测,异常实例的剔除等逻辑省略
    // 最终封装结果并返回 。。。

    result.replace("hosts", hosts);
    if (clientInfo.type == ClientInfo.ClientType.JAVA
        && clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
    
    
        result.put("dom", serviceName);
    } else {
    
    
        result.put("dom", NamingUtils.getServiceName(serviceName));
    }
    result.put("name", serviceName);
    result.put("cacheMillis", cacheMillis);
    result.put("lastRefTime", System.currentTimeMillis());
    result.put("checksum", service.getChecksum());
    result.put("useSpecifiedURL", false);
    result.put("clusters", clusters);
    result.put("env", env);
    result.replace("metadata", JacksonUtils.transferToJsonNode(service.getMetadata()));
    return result;
}

4.2.2 Publicar notificación UDP de cambio de servicio

InstanceControllerEn el método de la sección anterior doSrvIpxt(), hay una línea de código de este tipo:

pushService.addClient(namespaceId, serviceName, clusters, agent,
                      new InetSocketAddress(clientIP, udpPort),
                           pushDataSource, tid, app);

De hecho, el puerto UDP, la IP y otra información del consumidor se encapsulan en un objeto PushClient y se almacenan en PushService. Es conveniente enviar mensajes después de cambios en el servicio en el futuro.

La propia clase PushService implementa ApplicationListenerla interfaz:
inserte la descripción de la imagen aquí

Esta es la interfaz de escucha de eventos, que escucha ServiceChangeEvent (evento de cambio de servicio).

Seremos notificados cuando la lista de servicios cambie:
inserte la descripción de la imagen aquí

4.3 Resumen

El descubrimiento de servicios de Nacos se divide en dos modos:

  • Modo 1: modo de extracción activo, los consumidores extraen activamente la lista de servicios de Nacos regularmente y la almacenan en caché, y luego leen la lista de servicios en el caché local primero cuando llaman al servicio.
  • Modo 2: Modo de suscripción Los consumidores se suscriben a la lista de servicios en Nacos y reciben notificaciones de cambio de servicio basadas en el protocolo UDP. Cuando se actualice la lista de servicios en Nacos, se enviará una transmisión UDP a todos los suscriptores.

En comparación con Eureka, la actualización del estado del servicio del modo de suscripción de Nacos es más oportuna y es más fácil para los consumidores descubrir cambios en la lista de servicios de manera oportuna y eliminar los servicios defectuosos.

Supongo que te gusta

Origin blog.csdn.net/sinat_38316216/article/details/129862342
Recomendado
Clasificación