Hablando sobre el reinicio elegante de Dubbo Framework

1. Antecedentes

Recientemente, el servicio Dubbo se ha introducido en el entorno de producción. Cada vez que el servicio se reinicia en línea, habrá una alarma de tiempo de espera. Lo extraño es que el reinicio del cliente y del servidor tendrá un impacto, y la alarma se volverá más evidente cuando el volumen es grande.

La información general de las alarmas es la siguiente:

cause: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2021-09-09 11:59:56.822, end time: 2021-09-09 11:59:58.828, client elapsed: 0 ms, server elapsed: 2006 ms, timeout: 2000 ms, request: Request [id=307463, version=2.0.2, twoway=true, event=false, broken=false, data=null], channel: /XXXXXX:52149 -> /XXXXXX:20880] with root cause]

¿Cuál podría ser la razón?

  1. ¿Ningún apagado elegante?

  2. En el momento del reinicio, ¿el volumen de solicitud es demasiado grande y no hay calentamiento?

  3. ¿Después de que Dubbo se inicia con éxito, SpringBoot no se ha iniciado con éxito y no hay exposición de retraso?

  4. ¿Hay alguna configuración de parámetros que no sea razonable?

Todo lo anterior es posible. Después de casi medio mes de leer y verificar el código fuente del marco Dubbo, finalmente encontré todas las respuestas y, por la presente, puse mi corazón en clasificar los registros del pozo minero.

2. Descripción

  1. Versión

componentes Versión
dubbo 2.7.7
Neto 4.0.36.Final
cuidador del zoológico 3.4.9

  1. situación básica

Para las solicitudes de lectura, que son idempotentes, lo reintentamos de forma predeterminada, pero para las solicitudes de escritura, no lo reintentamos de forma predeterminada.

El tiempo de espera predeterminado es de 2000 ms.

Los servicios son todos contenedores docker, y el número de clientes de Dubbo es mucho mayor que el del proveedor de servicios, la proporción es de aproximadamente 10: 1

  1. Sugerencia Este artículo se centra en explicar los puntos y principios técnicos relacionados con el reinicio del servicio y no explicará las diferencias entre la base del marco Dubbo, la base Netty y las versiones anteriores.

3. Reinicie con gracia los puntos técnicos clave

Para los problemas anteriores, el marco Dubbo también proporciona soluciones, echemos un vistazo a ellos a su vez.

  1. Dubbo elegante mecanismo de tiempo de inactividad

Dubbo utiliza ShutdownHook de JDK para completar el apagado correcto. El elegante mecanismo de apagado implementado en Dubbo incluye principalmente 6 pasos: 

(1) Después de recibir la señal de salida del proceso PID de eliminación, el contenedor Spring activará el evento de destrucción del contenedor.

(2) El lado del proveedor cerrará la sesión de la información de metadatos del servicio (eliminará el nodo ZK).

(3) El consumidor obtendrá la última lista de proveedores de servicios.

(4) El proveedor enviará un mensaje de evento de solo lectura para notificar al consumidor que el servicio no está disponible.

(5) El servidor espera a que finalicen las tareas que ya se han ejecutado y se niega a ejecutar nuevas tareas.

salir con gracia

Código central:

  @Override
    public void close(final int timeout) {
        startClose();
        if (timeout > 0) {
            final long max = (long) timeout;
            final long start = System.currentTimeMillis();
            if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) {
                //发送 readonly 事件报文通知 consumer 服务不可用
                sendChannelReadOnlyEvent();
            }
            while (HeaderExchangeServer.this.isRunning()
                    && System.currentTimeMillis() - start < max) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        doClose();
        server.close(timeout);
    }

configuración relacionada

dubbo:
  application:
        shutwait: 10000 # 优雅退出等待时间,单位毫秒 默认等待 10s
  1. Mecanismo de precalentamiento Dubbo

El peso predeterminado del servicio Dubbo es 100. Dubbo en realidad proporciona un mecanismo de pseudo calentamiento, que calcula el peso de acuerdo con el tiempo de ejecución del proveedor del servicio y luego usa la estrategia de equilibrio de carga para realizar el tráfico de pequeño a grande. Empecemos por el código fuente de Dubbo y observemos la implementación específica del precalentamiento del servicio. El código fuente específico se encuentra enAbstractLoadBalance#getWeight

 /**
     * Get the weight of the invoker's invocation which takes warmup time into account
     * if the uptime is within the warmup time, the weight will be reduce proportionally
     *
     * @param invoker    the invoker
     * @param invocation the invocation of this invoker
     * @return weight
     */
    int getWeight(Invoker<?> invoker, Invocation invocation) {
        int weight;
        URL url = invoker.getUrl();
        // Multiple registry scenario, load balance among multiple registries.
        if (REGISTRY_SERVICE_REFERENCE_PATH.equals(url.getServiceInterface())) {
            weight = url.getParameter(REGISTRY_KEY + "." + WEIGHT_KEY, DEFAULT_WEIGHT);
        } else {
            weight = url.getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
            if (weight > 0) {
                //获取服务启动时间 timestamp
                long timestamp = invoker.getUrl().getParameter(TIMESTAMP_KEY, 0L);
                if (timestamp > 0L) {
                    //使用当前时间减去服务提供者启动时间,计算服务提供者已运行时间 `uptime`
                    long uptime = System.currentTimeMillis() - timestamp;
                    if (uptime < 0) {
                        return 1;
                    }
                    //获取服务预热时间基数,默认是10分钟
                    int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
                    //如果服务启动时间 小于 warmup 则重新计算权重
                    if (uptime > 0 && uptime < warmup) {
                        //根据已运行时间动态计算服务预热过程的权重
                        weight = calculateWarmupWeight((int)uptime, warmup, weight);
                    }
                }
            }
        }
        return Math.max(weight, 0);
    }

Veamos el algoritmo de peso de cálculo.

 /**
     * Calculate the weight according to the uptime proportion of warmup time
     * the new weight will be within 1(inclusive) to weight(inclusive)
     *
     * @param uptime the uptime in milliseconds
     * @param warmup the warmup time in milliseconds
     * @param weight the weight of an invoker
     * @return weight which takes warmup into account
     */
    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        int ww = (int) ( uptime / ((float) warmup / weight));
        return ww < 1 ? 1 : (Math.min(ww, weight));
    }

El método de cálculo aquí es realmente muy simple. En pocas palabras, cuanto más se ejecute el servicio, mayor será el peso. Cuando el tiempo de actividad = calentamiento, se restaurará el peso normal.

De forma predeterminada (el peso predeterminado del servicio Dubbo es 100 y el tiempo de calentamiento es de 10 minutos)

Si el proveedor de servicios ha estado funcionando durante 1 minuto, el peso terminará siendo 10.

Si el proveedor de servicios ha estado funcionando durante 5 minutos, el peso terminará siendo 50.

Si el proveedor de servicios ha estado funcionando durante 11 minutos y supera el umbral de tiempo de calentamiento predeterminado de 10 minutos, no se realizarán más cálculos y se devolverá directamente el peso de peso predeterminado.

Recordatorio: la estrategia de balanceo de carga consistentehash (hash de consistencia) no admite el precalentamiento del servicio.

configuración relacionada

dubbo:
    provider:
         warmup: 600000 # 单位毫秒 默认10分钟
  1. exposición retardada

Algunos contenedores externos (como tomcat) bloquearán las llamadas al servicio dubbo antes de que se inicien por completo, lo que provocará un tiempo de espera en el lado del consumidor. Esta situación puede ocurrir con cierta probabilidad durante la liberación. Para evitar este problema, establezca un cierto tiempo de retraso (garantizado después de que se inicie Tomcat) para lograr una liberación suave.

La exposición retrasada de dubbo en el código fuente se refleja principalmente en ServiceBeanla clase y su clase principal ServiceConfig#export.

 public synchronized void export() {
        //是否已经暴露
        if (!shouldExport()) {
            return;
        }

        if (bootstrap == null) {
            bootstrap = DubboBootstrap.getInstance();
            bootstrap.init();
        }

        checkAndUpdateSubConfigs();

        //init serviceMetadata
        serviceMetadata.setVersion(version);
        serviceMetadata.setGroup(group);
        serviceMetadata.setDefaultGroup(group);
        serviceMetadata.setServiceType(getInterfaceClass());
        serviceMetadata.setServiceInterfaceName(getInterface());
        serviceMetadata.setTarget(getRef());

        if (shouldDelay()) { //是否需要延迟暴露
            DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
        } else {
            //真正执行服务暴露的方法
            doExport();
        }

        exported();
    }

Se puede ver en el código anterior que Dubbo usa una tarea de retraso de programación para retrasar la ejecución de doExport.

El diagrama de secuencia de exposición retardada es el siguiente:

configuración relacionada

dubbo:
    provider:
         delay: 5000 # 默认null不延迟, 单位毫秒
  1. otro

Después de resolver esto, todavía hay muchos tiempos de espera al reiniciar el servicio, que se encuentran al verificar los registros del cliente.

/XXX:57330 -> /XXXX:20880 is established., dubbo version: 2.7.7, current host: XXXX
2021-09-07 15:01:07.748 [NettyClientWorker-1-16] INFO  o.a.d.r.t.netty4.NettyClientHandler   -  [DUBBO] The connection of /XXXX:57332 -> /XXXX:20880 is established., dubbo version: 2.7.7, current host: XXXX

# 简单统计一下发现 客户端启动时建立了3600个长连接
$ less /u01/logs/order-service-api_XXX/dubbo.log  | grep NettyClientWorker- |grep  '2021-09-07 15' | wc -l
3600

Con esta pregunta en mente, consulte el código fuente para averiguarlo.

DubboProtocol#getClients

private ExchangeClient[] getClients(URL url) {
        boolean useShareConnect = false;

        //获取配置连接数, 如果没有配置默认0
        int connections = url.getParameter(CONNECTIONS_KEY, 0);
        List<ReferenceCountExchangeClient> shareClients = null;
        // if not configured, connection is shared, otherwise, one connection for one service
        if (connections == 0) {
            //注意: 如果Provider 配置了connections, 就不会使用共享连接,Consumer就算配置了shareConnections也不会生效
            useShareConnect = true;

            /*
             * The xml configuration should have a higher priority than properties.
             */
            String shareConnectionsStr = url.getParameter(SHARE_CONNECTIONS_KEY, (String) null);
            connections = Integer.parseInt(StringUtils.isBlank(shareConnectionsStr) ? ConfigUtils.getProperty(SHARE_CONNECTIONS_KEY,
                    DEFAULT_SHARE_CONNECTIONS) : shareConnectionsStr);
            shareClients = getSharedClient(url, connections);
        }

        ExchangeClient[] clients = new ExchangeClient[connections];
        for (int i = 0; i < clients.length; i++) {
            if (useShareConnect) {
                clients[i] = shareClients.get(i);

            } else {
                //初始化创连接
                clients[i] = initClient(url);
            }
        }

        return clients;
    }

El problema es que la configuración de nuestro servidor

dubbo:
  provider:
    connections: 200

Explique el código anterior. Si no se configuran conexiones, se utilizarán conexiones compartidas. El número de conexiones compartidas está determinado por el número de conexiones compartidas configuradas por el Consumidor. El valor predeterminado es 1. Por el contrario, si se configuran conexiones, el Se establecerá el número de conexiones para cada servicio.

Echemos un vistazo al proceso initClient

initClient(URL url) {

        // client type setting.
        String str = url.getParameter(CLIENT_KEY, url.getParameter(SERVER_KEY, DEFAULT_REMOTING_CLIENT));

        url = url.addParameter(CODEC_KEY, DubboCodec.NAME);
        // enable heartbeat by default
        url = url.addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT));

        // BIO is not allowed since it has severe performance issue.
        if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
            throw new RpcException("Unsupported client type: " + str + "," +
                    " supported client type is " + StringUtils.join(ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(), " "));
        }

        ExchangeClient client;
        try {
            // 是否配置了懒加载
            if (url.getParameter(LAZY_CONNECT_KEY, false)) {
                client = new LazyConnectExchangeClient(url, requestHandler);

            } else {
                //没有配置懒加载会初始化长连接
                client = Exchangers.connect(url, requestHandler);
            }

        } catch (RemotingException e) {
            throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e);
        }

        return client;
    }

Como se puede ver en el código anterior, si la carga diferida no está configurada, la conexión larga se inicializará directamente. Es decir, siempre que reinicie nuestro consumidor, la cantidad de servicios * 200 * se establecerán varias conexiones largas de servicios docker en el servidor. El número de nuestros servicios es 3, y el número de servicios docker es 6, que son exactamente 3600 conexiones largas.

Luego, cuando el servidor se reinicie, ZK notificará al consumidor (alrededor de 60 servicios docker) cuando el servidor se reinicie y establecerá una conexión con el servicio docker recién iniciado. Un consumidor establecerá 200 * 3, y un total de 36,000 conexiones largas se establecerá. .

A partir de esto, se puede ver que cada vez que se reinicia el servicio, se debe establecer una gran cantidad de conexiones largas, lo que resulta en un consumo de tiempo particularmente largo (calculado aproximadamente, alrededor de 10 segundos).

Optimización: Reducir el número de pools de conexiones.Después de las pruebas de estrés, la configuración 2 es suficiente.

dubbo:
  provider:
    connections: 2

Por supuesto, el servidor tampoco se puede configurar de forma predeterminada, y el consumidor puede determinar la cantidad de conexiones largas. La carga diferida se puede utilizar cuando se requieren muchas conexiones largas. Se recomienda que el número total de conexiones persistentes establecidas inmediatamente después de reiniciar el servidor no supere las 500. Después de resolver los problemas anteriores, el problema del tiempo de espera de reinicio finalmente se resuelve.

Resumir

El problema del reinicio elegante de Dubbo es un gran pozo, y también muestra la importancia de conocer el motivo de la configuración de parámetros, de lo contrario, puede generar problemas impredecibles.

Además, también pisamos un pozo de piscina de hilos, que se presentará en el próximo artículo.

Sígueme, no te pierdas, bienvenido a dar me gusta y coleccionar.

Supongo que te gusta

Origin blog.csdn.net/weixin_38130500/article/details/120279023
Recomendado
Clasificación