Введение
существует«ShutdownHook — решение для корректного завершения работы Java»В этой статье мы рассказали о принципе изящного завершения работы в Java. Далее, основываясь на приведенных выше знаниях, мы углубимся в Dubbo, чтобы понять, как Dubbo может добиться корректного завершения работы.
2. Проблемы с изящным завершением работы Dubbo, которые необходимо решить
Чтобы добиться корректного завершения работы, Dubbo необходимо решить несколько проблем:
- Новые запросы больше не могут быть отправлены поставщику услуг Dubbo, который не работает.
- Если поставщик услуг закрыт, запрос на обслуживание получен, и перед переходом в автономный режим необходимо обработать услугу.
- Если потребитель службы закрыт, отправленный запрос службы должен дождаться возврата ответа.
Только решив три вышеупомянутые проблемы, можно свести к минимуму влияние простоя на бизнес и добиться «мягкого» простоя.
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 в основном делится на два этапа:
- Отменить реестр
- выйти из системы
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 показан на рисунке.
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
справочная статья
Добро пожаловать, чтобы обратить внимание на мой официальный аккаунт: программа для общения, ежедневный толчок галантерейных товаров. Если вам интересен мой рекомендуемый контент, вы также можете подписаться на мой блог:studyidea.cn