Эволюция Dubbo Graceful Shutdown

Java

Введение

существует«ShutdownHook — решение для корректного завершения работы Java»В этой статье мы рассказали о принципе изящного завершения работы в Java. Далее, основываясь на приведенных выше знаниях, мы углубимся в Dubbo, чтобы понять, как Dubbo может добиться корректного завершения работы.

2. Проблемы с изящным завершением работы Dubbo, которые необходимо решить

Чтобы добиться корректного завершения работы, Dubbo необходимо решить несколько проблем:

  1. Новые запросы больше не могут быть отправлены поставщику услуг Dubbo, который не работает.
  2. Если поставщик услуг закрыт, запрос на обслуживание получен, и перед переходом в автономный режим необходимо обработать услугу.
  3. Если потребитель службы закрыт, отправленный запрос службы должен дождаться возврата ответа.

Только решив три вышеупомянутые проблемы, можно свести к минимуму влияние простоя на бизнес и добиться «мягкого» простоя.

3. 2.5.Х

Реализация корректного завершения работы Dubbo относительно завершена в версии 2.5.X Реализация этой версии относительно проста и понятна. Итак, давайте начнем с исходного кода версии Dubbo 2.5.X и посмотрим, как Dubbo обеспечивает плавное завершение работы.

3.1 Общая схема реализации корректного завершения работы

Класс записи изящного завершения работы находится по адресуAbstractConfigВ статическом коде исходный код выглядит следующим образом:

static {
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        public void run() {
            if (logger.isInfoEnabled()) {
                logger.info("Run shutdown hook now.");
            }
            ProtocolConfig.destroyAll();
        }
    }, "DubboShutdownHook"));
}

Будет регистрацияShutdownHook, который инициирует вызов после закрытия приложения.ProtocolConfig.destroyAll().

ProtocolConfig.destroyAll()Исходный код выглядит следующим образом:

public static void destroyAll() {
    // 防止并发调用
    if (!destroyed.compareAndSet(false, true)) {
        return;
    }
    // 先注销注册中心
    AbstractRegistryFactory.destroyAll();

    // Wait for registry notification
    try {
        Thread.sleep(ConfigUtils.getServerShutdownTimeout());
    } catch (InterruptedException e) {
        logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
    }

    ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
    // 再注销 Protocol
    for (String protocolName : loader.getLoadedExtensions()) {
        try {
            Protocol protocol = loader.getLoadedExtension(protocolName);
            if (protocol != null) {
                protocol.destroy();
            }
        } catch (Throwable t) {
            logger.warn(t.getMessage(), t);
        }
    }
    }

Как видно из вышеизложенного, корректное завершение работы Dubbo в основном делится на два этапа:

  1. Отменить реестр
  2. выйти из системыProtocol

3.2, отменить центр регистрации

Исходный код для отмены центра регистрации выглядит следующим образом:

public static void destroyAll() {
    if (LOGGER.isInfoEnabled()) {
        LOGGER.info("Close all registries " + getRegistries());
    }
    // Lock up the registry shutdown process
    LOCK.lock();
    try {
        for (Registry registry : getRegistries()) {
            try {
                registry.destroy();
            } catch (Throwable e) {
                LOGGER.error(e.getMessage(), e);
            }
        }
        REGISTRIES.clear();
    } finally {
        // Release the lock
        LOCK.unlock();
    }
}

Этот метод отменит регистрацию созданной внутри службы реестра. Внутренняя логика отмены регистрационного центра относительно проста, здесь мы не будем углубляться в исходный код, а покажем его прямо с картинками.

p.s. Исходный код находится по адресу:AbstractRegistry

Взяв ZK в качестве примера, Dubbo удалит соответствующий сервисный узел, а затем отменит подписку. Из-за изменения информации узла ZK сервер ZK уведомит потребителя dubbo об отключении узла службы и, наконец, закроет службу и соединение ZK.

Через центр регистрации Dubbo может своевременно оповещать потребителей офлайн-сервисов, а новые запросы больше не будут отправляться на офлайн-узлы, что также решает первую упомянутую выше проблему: новые запросы не могут быть отправлены в Dubbo, который закрывается. поставщики услуг.

Однако некоторые недостатки все же есть: из-за изолированности сети может быть определенная задержка в соединении между сервером ZK и Dubbo, и ZK-уведомление может не уведомить потребителя с первого раза. Учитывая эту ситуацию, после отмены центра регистрации добавить систему ожидания, код такой:

// Wait for registry notification
try {
    Thread.sleep(ConfigUtils.getServerShutdownTimeout());
} catch (InterruptedException e) {
    logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
}

Время ожидания по умолчанию равно10000ms, можно установитьdubbo.service.shutdown.waitПереопределить параметры по умолчанию. 10 секунд — это просто значение опыта, которое можно установить в соответствии с реальной ситуацией. Однако настройка этого времени ожидания достаточно специфична, и его нельзя сделать слишком коротким, иначе потребитель не получит ZK-уведомление, а провайдер отключится. Его нельзя установить слишком длинным. Если он слишком длинный, закрытие приложения займет много времени, что повлияет на процесс публикации.

3.3. Протокол выхода

ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
for (String protocolName : loader.getLoadedExtensions()) {
    try {
        Protocol protocol = loader.getLoadedExtension(protocolName);
        if (protocol != null) {
            protocol.destroy();
        }
    } catch (Throwable t) {
        logger.warn(t.getMessage(), t);
    }
}

loader#getLoadedExtensionsвернет дваProtocolподклассы соответственноDubboProtocolиInjvmProtocol.

DubboProtocolиспользовать для взаимодействия с запросами на стороне сервера иInjvmProtocolИспользуется для взаимодействия внутренних запросов. Если приложение вызывает себя для предоставления услуг Dubbo, оно не будет выполнять сетевые вызовы и напрямую выполнять внутренние методы.

Здесь мы в основном анализируемDubboProtocolвнутренняя логика.

DubboProtocol#destroyИсходный код:

public void destroy() {
    // 关闭 Server
    for (String key : new ArrayList<String>(serverMap.keySet())) {
        ExchangeServer server = serverMap.remove(key);
        if (server != null) {
            try {
                if (logger.isInfoEnabled()) {
                    logger.info("Close dubbo server: " + server.getLocalAddress());
                }
                server.close(ConfigUtils.getServerShutdownTimeout());
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }
    // 关闭 Client
    for (String key : new ArrayList<String>(referenceClientMap.keySet())) {
        ExchangeClient client = referenceClientMap.remove(key);
        if (client != null) {
            try {
                if (logger.isInfoEnabled()) {
                    logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
                }
                client.close(ConfigUtils.getServerShutdownTimeout());
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

    for (String key : new ArrayList<String>(ghostClientMap.keySet())) {
        ExchangeClient client = ghostClientMap.remove(key);
        if (client != null) {
            try {
                if (logger.isInfoEnabled()) {
                    logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
                }
                client.close(ConfigUtils.getServerShutdownTimeout());
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }
    stubServiceMethodsMap.clear();
    super.destroy();
}

Dubbo по умолчанию использует Netty в качестве базовой коммуникационной среды, которая делится наServerиClient.Serverдля приема других потребителейClientсделанный запрос.

Приведенный выше исходный код сначала закрываетсяServer, перестаньте получать новые запросы, а затем закройтеClient. Это снижает вероятность того, что служба будет вызываться потребителями.

3.4, закрыть сервер

сначала позвонитHeaderExchangeServer#close, исходный код выглядит следующим образом:

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)) {
	   // 发送 READ_ONLY 事件
            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);
}

private void doClose() {
    if (!closed.compareAndSet(false, true)) {
        return;
    }
    stopHeartbeatTimer();
    try {
        scheduled.shutdown();
    } catch (Throwable t) {
        logger.warn(t.getMessage(), t);
    }
}

Здесь будут отправлены потребителю услугиREAD_ONLYсобытие. После того, как потребитель его примет, он активно исключает этот узел и отправляет запрос другим обычным узлам. Это еще больше снижает влияние задержек с уведомлением реестра.

Затем будет отключено обнаружение пульса, а также отключена базовая коммуникационная структура NettyServer. здесь будет называтьсяNettyServer#closeметод, который на самом делеAbstractServerпонял здесь.

AbstractServer#closeИсходный код выглядит следующим образом:

public void close(int timeout) {
    ExecutorUtil.gracefulShutdown(executor, timeout);
    close();
}

Здесь пул бизнес-потоков сначала закрывается.Этот процесс максимально завершит выполнение задач в пуле потоков, затем закроет пул потоков и, наконец, закроет базовый сервер связи Netty.

По умолчанию Dubbo отправляет запросы/пульсы в пул бизнес-потоков для обработки.

Закрытие сервера, изящно ожидающее закрытия пула потоков, решает вторую проблему, упомянутую выше: если поставщик услуг закрыт, запрос службы был получен и должен быть обработан, прежде чем служба сможет быть отключена.

Процесс отключения поставщика услуг Dubbo показан на рисунке:

ps: Для удобства отладки исходников прикрепите сервер, чтобы закрыть ссылку вызова.

DubboProtocol#destroy
    ->HeaderExchangeServer#close
        ->AbstractServer#close
            ->NettyServer#doClose                

3.5 Закрыть клиент

Способ закрытия Клиента примерно такой же, как и у Сервера.Здесь мы в основном вводим логику обработки выданных запросов.Код находится вHeaderExchangeChannel#close.

// graceful close
public void close(int timeout) {
    if (closed) {
        return;
    }
    closed = true;
    if (timeout > 0) {
        long start = System.currentTimeMillis();
	// 等待发送的请求响应信息
        while (DefaultFuture.hasFuture(channel)
                && System.currentTimeMillis() - start < timeout) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                logger.warn(e.getMessage(), e);
            }
        }
    }
    close();
}

При закрытии Клиента, если остались информационные запросы, на которые не получен ответ, он будет ждать определенный период времени, пока не будет подтверждено, что на все запросы получены ответы, или время ожидания не превысит период тайм-аута.

ps: запросы Dubbo будут временно сохраненыDefaultFutureКарта, так что просто оцените карту, чтобы узнать, получил ли запрос ответ.

Благодаря этому мы решаем третью проблему: если потребитель сервиса закрыт, отправленный запрос на сервис должен дождаться возврата ответа.

Общий процесс корректного завершения работы Dubbo показан на рисунке.

Dubbogracefulshutdown.jpg

ps: цепочка вызовов закрытия клиента выглядит так:

DubboProtocol#close
    ->ReferenceCountExchangeClient#close
        ->HeaderExchangeChannel#close
            ->AbstractClient#close

4. 2.7.Х

Dubbo обычно используется с инфраструктурой Spring, и процесс завершения работы версии 2.5.X может привести к сбою корректного завершения работы. Это связано с тем, что соответствующее событие ShutdownHook также будет инициировано при закрытии среды Spring для отмены регистрации связанного компонента. В этом процессе, если Spring первым выполнит отключение, связанные bean-компоненты не будут зарегистрированы. В настоящее время bean-компонент в Spring упоминается в событии выключения Dubbo, что вызовет исключение во время процесса выключения и приведет к сбою корректного выключения.

Чтобы решить эту проблему, Dubbo начал рефакторинг этой части логики в версии 2.6.X и непрерывно выполнял итерации до версии 2.7.X.

добавлена ​​новая версияShutdownHookListener, наследующий от SpringApplicationListenerИнтерфейс для прослушивания событий, связанных с Spring. здесьShutdownHookListenerПросто послушайте событие завершения работы Spring, когда Spring начнет выключаться, оно сработает.ShutdownHookListenerвнутренняя логика.


public class SpringExtensionFactory implements ExtensionFactory {
    private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);

    private static final Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();
    private static final ApplicationListener SHUTDOWN_HOOK_LISTENER = new ShutdownHookListener();

    public static void addApplicationContext(ApplicationContext context) {
        CONTEXTS.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            // 注册 ShutdownHook
            ((ConfigurableApplicationContext) context).registerShutdownHook();
            // 取消 AbstractConfig 注册的 ShutdownHook 事件
            DubboShutdownHook.getDubboShutdownHook().unregister();
        }
        BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);
    }
    // 继承 ApplicationListener,这个监听器将会监听容器关闭事件
    private static class ShutdownHookListener implements ApplicationListener {
        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            if (event instanceof ContextClosedEvent) {
                DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook();
                shutdownHook.doDestroy();
            }
        }
    }
}

Когда среда Spring начнет инициализироваться, она запуститSpringExtensionFactoryлогика, после этого выйдет из системыAbstractConfigрегистрShutdownHook, затем увеличитьShutdownHookListener. Это прекрасно решает вышеупомянутую проблему «двойного крючка».

5. Наконец

Мягкое завершение работы кажется несложным, но в конструкции много деталей, и если есть проблема с реализацией одного пункта, плавное завершение работы не удастся. Если вы также реализуете корректное завершение работы, вы можете обратиться к логике реализации Dubbo.

Рекомендуется серия статей Dubbo

1. Если кто-то спросит вас, как работает реестр в Dubbo, дайте ему эту статью
2. Не знаете, как реализовать динамическое обнаружение сервисов? Приходите посмотреть, как это делает Даббо
3. Структура данных Dubbo Zk
4. Происхождение Dubbo, рассказ о механизме расширения Spring XML Schema

справочная статья

1,Настоятельно рекомендуется прочитать статью Кирито: статья, рассказывающая об элегантном отключении Dubbo.

Добро пожаловать, чтобы обратить внимание на мой официальный аккаунт: программа для общения, ежедневный толчок галантерейных товаров. Если вам интересен мой рекомендуемый контент, вы также можете подписаться на мой блог:studyidea.cn

其他平台.png