primavera nube alibaba - principio nacos

        El uso básico de nacos se presentó anteriormente, y los nacos se presentarán más adelante en este artículo.

Sincronización de datos de un clúster de nacos: protocolo de distribución

        En el entorno de producción, para garantizar la estabilidad del servicio, generalmente adoptamos el método de implementación de clústeres. El método de implementación de clústeres también se presentó en el artículo anterior, entonces, ¿cómo sincronizar datos entre clústeres de nacos? A partir de la forma de implementación del clúster de nacos en el artículo anterior, se puede ver que nacos no distingue entre nodos maestros y nodos esclavos. En el clúster de nacos, cada nodo tiene el mismo peso y puede leer y escribir.

        nacos se compone de dos funciones: centro de configuración y centro de registro/descubrimiento. Para el centro de configuración, el cp utilizado por nacos es muy consistente, por lo que no lo presentaremos mucho; para el centro de registro/descubrimiento, la aplicación utilizada por nacos se implementa mediante el protocolo de distribución desarrollado por Ali. El protocolo de distribución será se describe más adelante en la introducción.

        Sabemos que ap es la consistencia final, entonces, ¿cómo garantiza el protocolo de distribución la consistencia final? Se lleva a cabo principalmente en los siguientes cuatro aspectos.

1 Agregar nodo de servicio de distribución

        Este escenario se refiere al hecho de que el clúster de distribución actual tiene nodos de servicio en ejecución. En este momento, se agrega un nuevo nodo de servicio de distribución. ¿Cómo realiza la sincronización de datos el nodo de servicio de distribución recién agregado?

        Cada nodo de servicio de distribución se comunica entre sí. Cuando se agrega un nodo de servicio de distribución, sondeará y extraerá todos los demás nodos de servicio en el clúster de servicio de distribución actual, y realizará una extracción completa, y luego almacenará en caché la información de registro en el local y el se realizará el registro del servicio correspondiente.

2 Sincronización de datos de nodos de servicio de distribución

        Los nodos del servicio de distribución enviarán una verificación de latido cada 5 s. La información de verificación no es la información de registro sino la información de metadatos. Si se utiliza la información de registro, los datos de solicitud en el paquete de latido serán demasiado grandes, por lo que la verificación de latido La solicitud los datos son información de metadatos. Cuando se descubre que los datos de la solicitud de latido actual no son coherentes con los datos de la memoria caché local durante el proceso de verificación, el servidor activará una operación de extracción completa para la sincronización de datos.

3 Operación de escritura de datos

       

         Cuando el cliente emite una operación de escritura, no se vinculará directamente al nodo del servicio de distribución para la operación de escritura. Primero irá al prefiltro uno por uno. El filtro calcula de acuerdo con el puerto ip +, encuentra el servicio de distribución nodo al que pertenece, y luego pasa La ruta se reenvía al filtro correspondiente, y luego se llama al nodo de servicio de distribución correspondiente para realizar la operación de escritura.La operación de escritura solo necesita asegurarse de que la escritura se complete en este nodo, y no hay necesidad de esperar a que se escriba sincrónicamente en otros nodos de servicio de distribución. El nodo de servicio que realiza la operación de escritura sincronizará periódicamente la información de cambio con otros nodos de servicio de forma incremental.

        El enrutamiento y el reenvío del filtro es un punto central de la implementación del protocolo de servicio de distribución. Garantiza que el nodo de servicio de distribución al que pertenece el cliente permanezca sin cambios. Cada vez que el cliente realiza operaciones de lectura y escritura, el servicio de distribución solicitado es lo mismo.

4 operación de lectura

         La operación de lectura es en realidad similar a la operación de escritura. Todos van primero al prefiltro, luego calculan de acuerdo con el puerto + ip y luego se enrutan al nodo de servicio de distribución al que pertenece. La introducción anterior decía que los nacos cluster es un ap, así que lea directamente Obtenga el caché local del servicio de distribución y regrese. Lo que ap garantiza es la consistencia final, que se puede garantizar a través del mecanismo de verificación de datos mencionado anteriormente.

        Echemos un vistazo a parte del código fuente de la sincronización de datos de clúster, solo el código fuente de la entrada y algunos pasos clave, los estudiantes interesados ​​pueden verlo por sí mismos.

        Lo siguiente es parte del código fuente para habilitar la verificación de datos e inicializar datos:

// 入口 com.alibaba.nacos.core.distributed.distro.DistroProtocol

// 在入口类的构造方法中,调用了startDistroTask()方法,接下来我们看一下这个方法

/**
 *该方法主要是开始两个任务,一个是验证任务,一个是初始化任务
 */
private void startDistroTask() {
    if (EnvUtil.getStandaloneMode()) {
        isInitialized = true;
        return;
    }
    // 验证任务
    startVerifyTask();
    // 初始化任务
    startLoadTask();
}
/**
 * 开启数据验证的定时任务,每5s发起一次
 * DEFAULT_DATA_VERIFY_INTERVAL_MILLISECONDS = 5000L;
 */    
private void startVerifyTask() {
    GlobalExecutor.schedulePartitionDataTimedSync(new DistroVerifyTimedTask(memberManager, distroComponentHolder,
                    distroTaskEngineHolder.getExecuteWorkersManager()),
            DistroConfig.getInstance().getVerifyIntervalMillis());
}

/**
 * 初始化任务,通过线程池执行任务,并传入一个回调函数,用来标识是否完成
 */  
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));
}


// 接下来再看一下DistroLoadDataTask这个类的实现,在它的run方法中主要是执行了一个load方法
// 我们直接看一下这个load()方法

// 这个方法主要是进行了一些条件的判断,需要注意它使用的while循环操作,如果前面的天剑一直满足
// 则会进入死循环中,因此必须打破前面的两个while条件才会进入最终的数据初始化任务
private void load() throws Exception {
    // 除了自身节点以外没有其它节点,则休眠1s
    while (memberManager.allMembersWithoutSelf().isEmpty()) {
        Loggers.DISTRO.info("[DISTRO-INIT] waiting server list init...");
        TimeUnit.SECONDS.sleep(1);
    }
    // 若数据类型为空,说明distroComponent的组件注册还未初始化完毕
    while (distroComponentHolder.getDataStorageTypes().isEmpty()) {
        Loggers.DISTRO.info("[DISTRO-INIT] waiting distro data storage register...");
        TimeUnit.SECONDS.sleep(1);
    }
    // 加载每个类型的数据
    for (String each : distroComponentHolder.getDataStorageTypes()) {
        if (!loadCompletedMap.containsKey(each) || !loadCompletedMap.get(each)) {
            // 调用加载方法,标记已处理
            loadCompletedMap.put(each, loadAllDataSnapshotFromRemote(each));
        }
    }
}


// 本方法就是具体的去拉取各个distro服务节点,并更新数据的方法
private boolean loadAllDataSnapshotFromRemote(String resourceType) {
    // 获取数传输对象
    DistroTransportAgent transportAgent = distroComponentHolder.findTransportAgent(resourceType);
    // 获取数据处理器
    DistroDataProcessor dataProcessor = distroComponentHolder.findDataProcessor(resourceType);
    if (null == transportAgent || null == dataProcessor) {
        Loggers.DISTRO.warn("[DISTRO-INIT] Can't find component for type {}, transportAgent: {}, dataProcessor: {}",
                resourceType, transportAgent, dataProcessor);
        return false;
    }
    // 向每个节点请求数据
    for (Member each : memberManager.allMembersWithoutSelf()) {
        try {
            Loggers.DISTRO.info("[DISTRO-INIT] load snapshot {} from {}", resourceType, each.getAddress());
            // 获取数据
            DistroData distroData = transportAgent.getDatumSnapshot(each.getAddress());
            // 解析数据并且更新数据
            boolean result = dataProcessor.processSnapshot(distroData);
            Loggers.DISTRO
                    .info("[DISTRO-INIT] load snapshot {} from {} result: {}", resourceType, each.getAddress(),
                            result);
            // 如果解析成功,标记此类型数据已经加载完毕
            if (result) {
                distroComponentHolder.findDataStorage(resourceType).finishInitial();
                return true;
            }
        } catch (Exception e) {
            Loggers.DISTRO.error("[DISTRO-INIT] load snapshot {} from {} failed.", resourceType, each.getAddress(), e);
        }
    }
    return false;
}

        El siguiente es el código fuente para la sincronización de datos incrementales

// 入口为com.alibaba.nacos.naming.consistency.ephemeral.distro.v2.DistroClientDataProcessor
// 增量数据同步是采用发布订阅的方式进行的数据同步


// 主要关注下面的客户端变更事件即可
@Override
public List<Class<? extends Event>> subscribeTypes() {
    List<Class<? extends Event>> result = new LinkedList<>();
    // 客户端变更的时事件
    result.add(ClientEvent.ClientChangedEvent.class);
    // 客户端断开的事件
    result.add(ClientEvent.ClientDisconnectEvent.class);
    // 服务验证失败的事件
    result.add(ClientEvent.ClientVerifyFailedEvent.class);
    return result;
}
// 当事件触发的时候,会调用该类的onEvent()方法
@Override
public void onEvent(Event event) {
    if (EnvUtil.getStandaloneMode()) {
        return;
    }
    if (!upgradeJudgement.isUseGrpcFeatures()) {
        return;
    }
    if (event instanceof ClientEvent.ClientVerifyFailedEvent) {
        syncToVerifyFailedServer((ClientEvent.ClientVerifyFailedEvent) event);
    } else {
        // 将该事件同步到其它distro服务节点,
        // 延迟1s进行同步,DEFAULT_DATA_SYNC_DELAY_MILLISECONDS = 1000L
        syncToAllServer((ClientEvent) event);
    }
}

2. Uso avanzado y principio del centro de configuración

        En el artículo anterior, hice una introducción básica al uso simple de nacos como centro de configuración, y este artículo presenta dos usos avanzados.

1 Uso avanzado

        El uso avanzado es principalmente para la configuración de los siguientes dos escenarios:

        El primer escenario es que para algunas configuraciones, ya sea que estén en el entorno de desarrollo, prueba o producción, sus configuraciones son las mismas. Para esta configuración, no es necesario configurarla en cada entorno. Puede usar archivos de configuración públicos para configurar Se puede lograr agregando la siguiente configuración en el archivo de configuración

spring:
  cloud:
    nacos: 
      config:
        server-addr: 172.30.10.103:8848
        file-extension: yaml
        namespace: 4b57e563-2039-42f4-86b1-9c4c7cf58bfc
        # 公共配置文件,可以配置多个
        shared-configs[0]:
          # 公共配置文件名称
          dataId: file.yaml
          # 公共配置文件所属组
          group: DEFAULT_GROUP
          # 公共配置文件是否刷新
          refresh: true

        El segundo escenario es que en un proyecto, para distinguir el contenido responsable del archivo de configuración, se deben usar varios archivos de configuración, como la configuración relacionada con el pedido y la configuración relacionada con el usuario, que se pueden realizar a través de la siguiente configuración

spring:
  cloud:
    nacos:
      config:
        server-addr: 172.30.10.103:8848
        file-extension: yaml
        namespace: 4b57e563-2039-42f4-86b1-9c4c7cf58bfc
        # 扩展配置文件,允许配置多个
        extension-configs[0]:
          # 扩展配置文件名称
          dataId: order.yaml
          # 扩展配置文件所属组
          group: DEFAULT_GROUP
          # 扩展配置文件动态刷新
          refresh: true
        extension-configs[1]:
          dataId: user.yaml
          group: DEFAULT_GROUP
          refresh: true

2 Principio del centro de configuración

        Todos sabemos que el cliente puede obtener dinámicamente la configuración relevante del centro de configuración, que no es más que los dos métodos de tirar activamente del cliente o empujar activamente el servidor. Luego, el cliente nscos obtiene la configuración del servidor mediante extracción o inserción. ?

        El cliente nacos obtiene la configuración dinámica tirando. El cliente establece un sondeo largo con el servidor. Durante el establecimiento del sondeo largo, si la configuración del servidor cambia, le dice al cliente que la configuración ha cambiado, y luego el cliente El terminal inicia una solicitud para obtener el contenido específico de la configuración; si no hay cambios, regresa vacío. Echa un vistazo al diagrama de flujo a continuación:

        De la figura anterior, podemos ver que el cliente establecerá una conexión larga con el servidor, y hay dos situaciones en las que responde la conexión larga: una es que el período de espera supera los 29,5 s; la otra es que el contenido de la configuración cambia . Con respecto al tiempo de espera, el tiempo de espera, el tiempo de respuesta y otros contenidos relacionados, lo reflejaremos en el análisis del código fuente. A través de la imagen de arriba, tenemos una cierta comprensión del principio de adquisición de configuración dinámica del centro de configuración de nacos, luego echemos un breve vistazo al código fuente, y los estudiantes que estén interesados ​​pueden aprender más sobre él. El contenido del código fuente se divide en dos partes, una parte es para que el cliente establezca un enlace largo para obtener la configuración de actualización de configuración, y la otra parte es para que el servidor reciba los cambios de configuración y responda al cliente.

   2.1 Código fuente del cliente (nacos-config-2.2.5.release, nacos-client-1.4.1)

        El siguiente código fuente presenta principalmente la entrada del código fuente del cliente y presenta la importante clase ClientWorker.

// 入口为com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
// 通过jar包进入,这个类创建了三个bean,我们主要关注的是第二个bean:NacosConfigManager
// 通过查看该类的构造方法,发现其调用了createConfigService方法,在这个方法中通过工厂的方式创建
// service,最终是通过反射的方式创建:
// Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService")
// 我们的源码从NacosConfigService的构造方法开始
    public NacosConfigService(Properties properties) throws NacosException {
        // 参数校验
        ValidatorUtils.checkInitParam(properties);
        String encodeTmp = properties.getProperty("encode");
        if (StringUtils.isBlank(encodeTmp)) {
            this.encode = "UTF-8";
        } else {
            this.encode = encodeTmp.trim();
        }
        // 根据配置文件获取namespace
        this.initNamespace(properties);
        this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        this.agent.start();
        // 该构造方法的主要目的就是创建ClientWorker对象,所有的相关操作都是在该对象中实现的
        this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
    }

        En el constructor de la clase ClientWorker, se inicializan algunos parámetros clave, incluido el período de tiempo de espera, se han creado dos grupos de subprocesos cronometrados y se ha iniciado la tarea de verificación de configuración.

    public ClientWorker(final HttpAgent agent, ConfigFilterChainManager configFilterChainManager, Properties properties) {
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;
        // 初始化参数
        this.init(properties);
        // 创建第一个线程池,用于启动配置检查任务
        this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });
        // 第二个定时任务线程池,具体功能后续会出现
        this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });
        // 启动第一个定时任务线程池,用于检查配置,通过线程池参数可以发现,该任务每10s执行一次
        this.executor.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                try {
                    ClientWorker.this.checkConfigInfo();
                } catch (Throwable var2) {
                    ClientWorker.LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", var2);
                }

            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }

    // 根据配置信息,初始化相关参数
    private void init(Properties properties) {
        // 超时时间,properties这个参数是根据配置文件生成的,如果没有配置超时时间,我们可以发现,超时时间为30s
        this.timeout = (long)Math.max(ConvertUtils.toInt(properties.getProperty("configLongPollTimeout"), 30000), 10000);
        this.taskPenaltyTime = ConvertUtils.toInt(properties.getProperty("configRetryTime"), 2000);
        this.enableRemoteSyncConfig = Boolean.parseBoolean(properties.getProperty("enableRemoteSyncConfig"));
    }

        El siguiente paso es el método checkConfigInfo. En este método, la tarea principal es agrupar las tareas de sondeo largas y luego ejecutar las tareas de sondeo largas a través del grupo de subprocesos executorService creado anteriormente, LongPollingRunnable. Verifiquemos directamente las tareas de sondeo largas, es decir , su método de ejecución. En este código se realizan principalmente tres cosas: el primer paso es verificar la configuración local y asignar valores relevantes de acuerdo a diferentes situaciones; el segundo paso es establecer un enlace largo con el servidor para obtener la configuración modificada; el tercero paso, la información de cambio devuelta en el paso anterior no es el contenido de la configuración, sino la información relacionada con el cambio de dataId+group+teanant.Este paso es llamar al servidor para obtener el contenido de configuración específico y realizar actualizaciones locales basadas en esta información:

        public void run() {
            List<CacheData> cacheDatas = new ArrayList();
            ArrayList inInitializingCacheList = new ArrayList();

            try {
                // cacheMap即为缓存的配置信息
                Iterator var3 = ClientWorker.this.cacheMap.values().iterator();
                // 第一个for循环,比较本地配置
                while(var3.hasNext()) {
                    CacheData cacheData = (CacheData)var3.next();
                    if (cacheData.getTaskId() == this.taskId) {
                        cacheDatas.add(cacheData);

                        try {
                            // 第一步,检查本地配置,并根据配置的不同信息,进行相关赋值
                            ClientWorker.this.checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception var13) {
                            ClientWorker.LOGGER.error("get local config info error", var13);
                        }
                    }
                }
                
                // 第二步 与服务端建立长轮询,获取变更的配置信息
                List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                    ClientWorker.LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
                }

                Iterator var16 = changedGroupKeys.iterator();
                // 第二个for循环,根据上面获取到的变更的配置信息集合,更新本地配置
                while(var16.hasNext()) {
                    String groupKey = (String)var16.next();
                    String[] key = GroupKey.parseKey(groupKey);
                    String dataId = key[0];
                    String group = key[1];
                    String tenant = null;
                    if (key.length == 3) {
                        tenant = key[2];
                    }

                    try {
                        // 第三步 调用服务端获取变更的配置信息,根据dataId, group以及tenant,调用地址为/v1/cs/configs
                        String[] ct = ClientWorker.this.getServerConfig(dataId, group, tenant, 3000L);
                        CacheData cache = (CacheData)ClientWorker.this.cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                        cache.setContent(ct[0]);
                        if (null != ct[1]) {
                            cache.setType(ct[1]);
                        }

                        ClientWorker.LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}", new Object[]{ClientWorker.this.agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(ct[0]), ct[1]});
                    } catch (NacosException var12) {
                        String message = String.format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s", ClientWorker.this.agent.getName(), dataId, group, tenant);
                        ClientWorker.LOGGER.error(message, var12);
                    }
                }

                var16 = cacheDatas.iterator();

                while(true) {
                    CacheData cacheDatax;
                    do {
                        if (!var16.hasNext()) {
                            inInitializingCacheList.clear();
                            ClientWorker.this.executorService.execute(this);
                            return;
                        }

                        cacheDatax = (CacheData)var16.next();
                    } while(cacheDatax.isInitializing() && !inInitializingCacheList.contains(GroupKey.getKeyTenant(cacheDatax.dataId, cacheDatax.group, cacheDatax.tenant)));

                    cacheDatax.checkListenerMd5();
                    cacheDatax.setInitializing(false);
                }
            } catch (Throwable var14) {
                ClientWorker.LOGGER.error("longPolling error : ", var14);
                ClientWorker.this.executorService.schedule(this, (long)ClientWorker.this.taskPenaltyTime, TimeUnit.MILLISECONDS);
            }
        }

        A continuación, mire el contenido de establecer un enlace largo con el servidor en el segundo paso

// 在checkUpdateDataIds里面主要是组装了调用服务端时的请求信息,
// 每个配置文件对应的相关信息为dataId++group++md5+(+teaant)
// 组装完请求参数以后,调用checkUpdateConfigStr方法,接下来我们看一下相关代码

    List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
        // 设置请求参数以及请求头
        Map<String, String> params = new HashMap(2);
        params.put("Listening-Configs", probeUpdateString);
        Map<String, String> headers = new HashMap(2);
        // 超时时间在前面的源码中说过,默认为30s
        headers.put("Long-Pulling-Timeout", "" + this.timeout);
        // 如果是第一次请求,不需要进行挂起
        if (isInitializingCacheList) {
            headers.put("Long-Pulling-Timeout-No-Hangup", "true");
        }

        if (StringUtils.isBlank(probeUpdateString)) {
            return Collections.emptyList();
        } else {
            try {
                long readTimeoutMs = this.timeout + (long)Math.round((float)(this.timeout >> 1));
                // 这里的agent就是在最开始的NcaosConfigService的构造方法中创建的agent
                // 开始远程调用服务端的服务,记住这个地址,后面查看服务端源码的时候,即从这个地址入手
                HttpRestResult<String> result = this.agent.httpPost("/v1/cs/configs/listener", headers, params, this.agent.getEncode(), readTimeoutMs);
                if (result.ok()) {
                    this.setHealthServer(true);
                    // 格式化响应信息
                    return this.parseUpdateDataIdResponse((String)result.getData());
                }

                this.setHealthServer(false);
                LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", this.agent.getName(), result.getCode());
            } catch (Exception var8) {
                this.setHealthServer(false);
                LOGGER.error("[" + this.agent.getName() + "] [check-update] get changed dataId exception", var8);
                throw var8;
            }

            return Collections.emptyList();
        }
    }

El código fuente del cliente se analiza aquí, si desea saber más sobre la implementación específica en su interior, puede consultarlo en detalle en el orden anterior.

2.2 Código del servidor (2.1.0)

        En el proceso de análisis del código fuente del cliente anterior, sabemos que la dirección para el sondeo entre el cliente y el servidor es: /v1/cs/configs/listener, y el protocolo es post Luego encontramos la ubicación correspondiente en el servidor En el método, nos enfocamos principalmente en el método inner.doPollingConfig:

// com.alibaba.nacos.config.server.controller.ConfigController的listener方法
// 然后再改方法中,我们关注的重点是doPollingConfig方法调用
    public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
            Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
        .
        // 判断当前请求是否为长轮询,通过对客户端源码的分析,这里走的是长轮询的机制
        if (LongPollingService.isSupportLongPolling(request)) {
            longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
            return HttpServletResponse.SC_OK + "";
        }
        ...
}

A través del análisis del código anterior, sabemos que vamos a ir a la rama de sondeo largo, en esta rama se hacen principalmente tres cosas: 1. Obtener el tiempo de espera 2. Convertir solicitudes síncronas en solicitudes asíncronas, reduciendo la carga del servidor Número de solicitudes de sincronización 3. Iniciar sondeo largo

    public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
            int probeRequestSize) {
        // 获取超时时间
        String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
        // 不允许断开的标记
        String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
        // 应用名称
        String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
        String tag = req.getHeader("Vipserver-Tag");
        // 延时时间
        int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
        
        // 提前500s返回一个响应,避免客户端出现超时,超时时间计算为29.5s
        long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
        if (isFixedPolling()) {
            timeout = Math.max(10000, getFixedPollingInterval());
            // Do nothing but set fix polling timeout.
        } else {
            long start = System.currentTimeMillis();
            List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
            if (changedGroups.size() > 0) {
                generateResponse(req, rsp, changedGroups);
                LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
                        RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                        changedGroups.size());
                return;
            } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
                LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                        RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                        changedGroups.size());
                return;
            }
        }
        // 获取客户端ip
        String ip = RequestUtil.getRemoteIp(req);
        
        // 把当前请求转换为一个异步请求(意味着tomcat线程被释放,最后需要asyncContext来手动完成响应)
        final AsyncContext asyncContext = req.startAsync();
        
        asyncContext.setTimeout(0L);
        // 开始执行长轮询,通过线程池创建执行任务,线程池类型为SingleScheduledExecutorService
        ConfigExecutor.executeLongPolling(
                new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
    }

Lo siguiente a lo que debemos prestar atención es a esta larga tarea de sondeo, que es ejecutada por el grupo de subprocesos. A continuación, debemos prestar atención al método run() de ClientLongPolling. Este método se divide principalmente en dos partes, una parte comienza después de un retraso de 29,5 segundos Haga respuestas de enlaces largos; parte de eso es poner enlaces largos en todos los subs.

        public void run() {
            // 构建一个异步任务,延后29.5s执行,如果达到29.5秒以后没有做任何配置的修改,则自行触发执行,即进行长链接响应
            asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(() -> {
                try {
                    getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());

                    // 获取删除标识,该标识是一个防止重复响应的标识,在前面讲述原理的时候说过,
                    // 长链接的响应有两种情况,一种是超时响应,一种是变更响应,就是使用该标识进行判断
                    boolean removeFlag = allSubs.remove(ClientLongPolling.this);
                    // 如果删除成功,代表的就是超时响应;删除失败,代表是变更响应
                    if (removeFlag) {
                        if (isFixedPolling()) {
                            LogUtil.CLIENT_LOG
                                    .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
                                            RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                            "polling", clientMd5Map.size(), probeRequestSize);
                            List<String> changedGroups = MD5Util
                                    .compareMd5((HttpServletRequest) asyncContext.getRequest(),
                                            (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
                            if (changedGroups.size() > 0) {
                                sendResponse(changedGroups);
                            } else {
                                // 没有变更,返回null
                                sendResponse(null);
                            }
                        } else {
                            LogUtil.CLIENT_LOG
                                    .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
                                            RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                            "polling", clientMd5Map.size(), probeRequestSize);
                            sendResponse(null);
                        }
                    } else {
                        LogUtil.DEFAULT_LOG.warn("client subsciber's relations delete fail.");
                    }
                } catch (Throwable t) {
                    LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
                }

            }, timeoutTime, TimeUnit.MILLISECONDS);
            // 将当前请求放入长轮询队列
            allSubs.add(this);
        }

Lo anterior es la información del código fuente sobre la respuesta de tiempo de espera en la descripción principal, entonces, ¿dónde está el código fuente sobre la respuesta de la operación? Ahora vamos al método de construcción de LongPollingService, en el que se registra el evento de suscripción para escuchar cambios de datos

    public LongPollingService() {
        allSubs = new ConcurrentLinkedQueue<>();
        
        ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);
        
        NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);
        
        // 注册订阅事件,用来监听配置变化
        NotifyCenter.registerSubscriber(new Subscriber() {
            
            @Override
            public void onEvent(Event event) {
                if (isFixedPolling()) {
                    // Ignore.
                } else {
                    if (event instanceof LocalDataChangeEvent) {
                        LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                        ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                    }
                }
            }
            
            @Override
            public Class<? extends Event> subscribeType() {
                return LocalDataChangeEvent.class;
            }
        });
        
    }

Luego hacemos un seguimiento de DataChangeTask y encontramos el método de ejecución, que es el código fuente de la respuesta de la operación:

        public void run() {
            try {
                ConfigCacheService.getContentBetaMd5(groupKey);
                // 遍历所有客户端建立的获取配置变化信息的长轮询
                for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
                    
                    ClientLongPolling clientSub = iter.next();
                    // 判断当前的ClientLongPolling中,请求的key是否包含当前修改的groupkey
                    if (clientSub.clientMd5Map.containsKey(groupKey)) {
                        if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
                            continue;
                        }
                        if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                            continue;
                        }
                        
                        getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                        // 将该长轮询从等待队列中移除,当移除以后,上面源码中的removeFlag则为false
                        iter.remove(); 
                        LogUtil.CLIENT_LOG
                                .info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
                                        RequestUtil
                                                .getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
                                        "polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
                        // 响应客户端
                        clientSub.sendResponse(Arrays.asList(groupKey));
                    }
                }
                
            } catch (Throwable t) {
                LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
            }
        }

Tres principios del centro de registro de nacos

        Cuando se utiliza nacos como centro de registro, se utiliza AP para garantizar la disponibilidad del proyecto. Después de la versión 2.0, el cliente y el servidor han establecido un enlace largo del protocolo grpc. El cliente envía una tarea de latido al servidor cada 5 segundos, y el servidor verificará regularmente la tarea de latido. Marque la instancia como en mal estado o elimine la instancia .

 

         El servidor y cada cliente tendrán información de registro, y cuando los clientes realizan llamadas de servicio, la información de registro se lee localmente en lugar de extraerse de forma remota.

        El cliente establece un enlace largo con el servidor. Antes de 2.0, el cliente envía una tarea de latido al servidor. El cliente envía una tarea de latido al servidor cada 5 s. El servidor detectará regularmente la tarea de latido. No se envía ningún latido para más de 15 s., marque el cliente como una instancia en mal estado y, si no se envía ninguna tarea de latido durante más de 30 s, la instancia se eliminará; sin embargo, después de 2.0, se usa el vínculo largo grpc y ya no se usa el latido , y el servidor crea activamente una tarea programada dentro de los 3 segundos Ejecute una vez, verifique el cliente que no se ha comunicado durante más de 20 segundos y luego recuerde enviar una solicitud de sondeo, si la respuesta está dentro de los 1 segundos, la detección pasa, de lo contrario, el enlace es remoto.

        Cuando un cliente se desconecta voluntariamente o el servidor lo elimina, el servidor enviará un evento de cambio de registro a cada instancia en buen estado y luego el cliente lo recuperará. Habrá una tarea programada en el lado del cliente, que extrae la lista de registro del centro de registro cada 6 segundos, publica eventos de cambio y luego los suscriptores actualizan los datos del registro local.

3.1 Cliente

        Usamos la clase NamingTest en el código fuente de nacos para simular el registro de clientes. Por supuesto, también puede analizarlo a través del paquete jar de nacos correspondiente en su proyecto. La entrada es NacosServiceRegistryAutoConfiguation#nacosServiceRegistry. Los estudiantes interesados ​​pueden verla a través de esta entrada.

        La razón por la que se usa la clase NamingTest en el código fuente para el análisis es que es más amigable para los estudiantes que no tienen experiencia en la lectura del código fuente, y la información relevante y los pasos son relativamente claros y obvios. En la entrada, se realizan principalmente las siguientes cosas relacionadas: 1. Establecer la información relevante del servidor, 2. Establecer la información de la instancia que se registrará, 3. Obtener y registrar NacosNamingService.

    public void testServiceList() throws Exception {
        // nacos服务端的地址以及用户名和密码
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
        properties.put(PropertyKeyConst.USERNAME, "nacos");
        properties.put(PropertyKeyConst.PASSWORD, "nacos");
        // 客户端实例的相关信息,包含ip,port,原信息等相关数据
        Instance instance = new Instance();
        instance.setIp("1.1.1.1");
        instance.setPort(800);
        instance.setWeight(2);
        // 元数据信息,对客户端的相关描述
        Map<String, String> map = new HashMap<String, String>();
        map.put("netType", "external");
        map.put("version", "2.0");
        instance.setMetadata(map);

        // 通过工厂方法创建namingService,最终是通过反射的方式进行创建,com.alibaba.nacos.client.naming.NacosNamingService
        NamingService namingService = NacosFactory.createNamingService(properties);
        // 进入具体的注册流程
        namingService.registerInstance("nacos.test.1", instance);
        
        ...

        
    }

A continuación, ingrese al proceso de registro específico y salte al método sobrecargado de NacosNamingService#registerInstance.En este método, se realizan principalmente dos cosas: 1. Verificar el tiempo de latido 2. Registrar la instancia a través del proxy. ¿Por qué usar un agente? Es por compatibilidad con versiones anteriores. Antes de la 2.0, se usaba el protocolo http para el registro, y después de la 2.0, se usaba el protocolo grpc para el registro. Nuestro análisis del código fuente se basa en la versión 2.x, que es la versión de enlace largo .

    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        // 检查心跳,心跳间隔时间以及服务删除时间必须大于心跳间隔,否则抛出异常
        NamingUtils.checkInstanceIsLegal(instance);
        // 通过代理
        clientProxy.registerService(serviceName, groupName, instance);
    }

// 看一下NamingUtils.checkInstanceIsLegal(instance);的相关代码
    public static void checkInstanceIsLegal(Instance instance) throws NacosException {
        // 心跳超时时间以及服务删除时间必须大于心跳时间,否则抛出异常
        // 当我们点击进入心跳时间,心跳超时时间,实例删除时间就会发现,这些时间与上面的原理图上面的时间是对应的
        if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
                || instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {
            throw new NacosException(NacosException.INVALID_PARAM,
                    "Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");
        }
    }
// 查看一下心跳的相关时间

    // 心跳间隔时间,默认为5s
    public long getInstanceHeartBeatInterval() {
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
                Constants.DEFAULT_HEART_BEAT_INTERVAL);
    }
    // 心跳超时时间,默认为15s
    public long getInstanceHeartBeatTimeOut() {
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
                Constants.DEFAULT_HEART_BEAT_TIMEOUT);
    }
    // 删除时间,默认为30s
    public long getIpDeleteTimeout() {
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
                Constants.DEFAULT_IP_DELETE_TIMEOUT);
    }

clientProxy es un proxy, a través del método init en el método de construcción, encontraremos que su tipo de implementación es NamingClientProxyDelegate, donde encontraremos la clase de implementación que realmente se encarga del registro

// 通过代理获取到真正进行注册的类
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
    }
// 会根据当前的实例类型进行区分,获取到真正的实现类,如果是瞬时对象(也就是注册实例),
// 则会采用grpcClientProxy,这里默认为true
    private NamingClientProxy getExecuteClientProxy(Instance instance) {
        return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
    }

A través del código anterior, encontramos la clase que es realmente responsable del registro, luego continuamos con el seguimiento, seguimos con NamingGrpcClientProxy#registerService y hacemos dos cosas en este método: 1. Almacenar en caché la información de la instancia actual, 2. Grpc remoto llamar

    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
                instance);
        // 缓存当前注册的实例信息,key为服务信息+组信息,value为服务信息,组信息,实例信息
        redoService.cacheInstanceForRedo(serviceName, groupName, instance);
        // grpc远程调用
        doRegisterService(serviceName, groupName, instance);
    }
    
    public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {
        InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,
                NamingRemoteConstants.REGISTER_INSTANCE, instance);
        // grpc远程调用
        requestToServer(request, Response.class);
        // 注册完成以后,将缓存中的注册状态变为true
        redoService.instanceRegistered(serviceName, groupName);
    }

El código fuente específico en el protocolo grpc no se analizará aquí temporalmente. Los estudiantes interesados ​​pueden ubicar rpcClient.start() a través del método start() en el constructor NamingGrpcClientProxy y ver el código fuente a través de esta entrada.

3.2 Servidor

        A través del registro del servicio en la guía de API abierta en el sitio web oficial de nacous, podemos encontrar su dirección de llamada, que corresponde al controlador en el código fuente, es decir, InstanceController, y encontrar el método de registro. Lo más importante en él es el método de registro de instancias, el análisis es el siguiente:

// getInstanceOperator().registerInstance(namespaceId, serviceName, instance);调用实例注册
// getInstanceOperator()方法会根据使用的协议不同,选择不同的service
private InstanceOperator getInstanceOperator() {
    // 现在使用的grpc,因此选择instanceServiceV2,类型为InstanceOperatorClientImpl
    return upgradeJudgement.isUseGrpcFeatures() ? instanceServiceV2 : instanceServiceV1;
}

A continuación, ingresamos al proceso de registro del servicio específico, haciendo principalmente dos cosas: primero, establecer una conexión con el cliente, a través del clientId generado; segundo, registrar la instancia del cliente

public void registerInstance(String namespaceId, String serviceName, Instance instance) {
    // 判断是否为临时实例
    boolean ephemeral = instance.isEphemeral();
    // 获取客户端id,ip:port+#+true
    String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
    // 创建与客户端的链接
    createIpPortClientIfAbsent(clientId);
    // 获取服务
    Service service = getService(namespaceId, serviceName, ephemeral);
    // 注册服务实例
    clientOperationService.registerInstance(service, instance, clientId);
}

Al dar de alta una instancia, se seleccionará una ruta en función de si la instancia actual es una instancia temporal o una instancia permanente. Para el alta del servicio, la instancia registrada es una instancia temporal, por lo que se utilizará la ruta de la instancia temporal. La última el registro de la instancia se realiza en esta ruta

public void registerInstance(Service service, Instance instance, String clientId) {
    Service singleton = ServiceManager.getInstance().getSingleton(service);
    if (!singleton.isEphemeral()) {
        throw new NacosRuntimeException(NacosException.INVALID_PARAM,
                String.format("Current service %s is persistent service, can't register ephemeral instance.",
                        singleton.getGroupedServiceName()));
    }
    Client client = clientManager.getClient(clientId);
    if (!clientIsLegal(client, clientId)) {
        return;
    }
    // 获取实例信息
    InstancePublishInfo instanceInfo = getPublishInfo(instance);
    // 将instance添加到client中
    client.addServiceInstance(singleton, instanceInfo);
    client.setLastUpdatedTime();
    // 建立service与clientId关系
    NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
    NotifyCenter
            .publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
}

Bueno, el código fuente de naocs se analizará aquí por el momento. Si existe la posibilidad más adelante, analizaremos el código fuente relacionado con la detección de salud, el descubrimiento de servicios y los eventos de publicación y suscripción entre el cliente y el servidor.

Supongo que te gusta

Origin blog.csdn.net/weixin_38612401/article/details/125431230
Recomendado
Clasificación