Dubbo源碼學習--優雅停機原理及在SpringBoot中遇到的問題
相關文章:
前言
主要是前一陣子換了工作,第一個任務就是解決目前團隊在 Dubbo 停機時產生的問題,同時最近又看了一下 Dubbo 的源碼,想重新寫一下 Dubbo 相關的文章。
優雅停機原理
對於一個 java 應用,如果想在關閉應用時,執行一些釋放資源的操作一般是通過注冊一個 ShutDownHook ,當關閉應用時,不是調用 kill -9 命令來直接終止應用,而是通過調用 kill -15 命令來觸發這個 ShutDownHook 進行停機前的釋放資源操作。
對於 Dubbo 來說,需要停機前執行的操作包括兩部分:
- 對於服務的提供者,需要通知注冊中心來把自己在服務列表中摘除掉。
- 根據所配置的協議,關閉協議的端口和連接。
而何為優雅停機呢?就是在集群環境下,有一個應用停機,並不會出現異常。下面來看一下 Dubbo 是怎么做的。
注冊ShutDownHook
Duubo 在 AbstractConfig 的靜態構造函數中注冊了 JVM 的 ShutDownHook,而 ShutdownHook 主要是調用 ProtocolConfig.destroyAll() ,源碼如下:
static {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
if (logger.isInfoEnabled()) {
logger.info("Run shutdown hook now.");
}
ProtocolConfig.destroyAll();
}
}, "DubboShutdownHook"));
}
ProtocolConfig.destroyAll()
先看一下 ProtocolConfig.destroyAll() 源碼:
public static void destroyAll() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
AbstractRegistryFactory.destroyAll(); //1.
// Wait for registry notification
try {
Thread.sleep(ConfigUtils.getServerShutdownTimeout()); //2.
} catch (InterruptedException e) {
logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
}
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
for (String protocolName : loader.getLoadedExtensions()) {
try {
Protocol protocol = loader.getLoadedExtension(protocolName);
if (protocol != null) {
protocol.destroy(); //3.
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
ProtocolConfig.destroyAll() 有三個比較重要的操作:
- 在1這個點調用AbstractRegistryFactory.destroyAll(),其內部會對每個注冊中心進行 destroy 操作,進而把注冊到注冊中心的服務取消注冊。
- 2這個點是最近 Dubbo 版本新增的操作,用來增強 Dubbo 的優雅停機,在老版本的 Dubbo 其邏輯是直接摘除服務列表,關閉暴露的連接,因為服務取消注冊,注冊中心是異步的通知消費者變更其存放在自己內存中的提供者列表。因為是異步操作,當調用量比較大的應用時消費者會拿到已經關閉連接點的提供者進行調用,這時候就會產生大量的錯誤,而2這個點就是通過Sleep 來延遲關閉協議暴露的連接。
- 因為 Dubbo 的擴展機制 ,loader.getLoadedExtensions() 會獲取到已使用的所有協議,遍歷調用 destroy 方法來關閉其打開的端口和連接。
而在第3步會在 Exchange 層 對所有打開的連接進行判斷其有沒有正在執行的請求,如果有會自旋 Sleep 直到設置的 ServerShutdownTimeout 時間或者已經沒有正在執行的請求了才會關閉連接,源碼如下:
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)) {
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); //正在的關閉連接
}
在 SpringBoot 應用中存在的問題
簡單的描述一下問題:就是在應用停機時,瞬間會產生大量的報錯,比如拿到的數據庫連接已經關閉等問題。 其實一看就知道是在停機時還存在正在處理的請求,而這些請求所需要的資源被 Spring 容器所關閉導致的。原來在SpringBoot 啟動時會在 refreshContext 操作也注冊一個 ShotdownHook 來關閉Spring容器。
private void refreshContext(ConfigurableApplicationContext context) {
this.refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException var3) {
}
}
}
而要解決這個問題就需要取消掉這個 ShutDownHook ,然后再 Dubbo 優雅停機執行后關閉 Spring 容器。具體的修改如下:
- 在啟動Main方法中,修改SpringBoot 啟動代碼,取消注冊ShutDownHook。
public static void main(String[] args) {
SpringApplication app = new SpringApplication(XxxApplication.class);
app.setRegisterShutdownHook(false);
app.run(args);
}
- 注冊一個Bean 來讓 Dubbo 關閉后關閉Spring容器。
public class SpringShutdownHook {
private static final Logger logger = LoggerFactory.getLogger(SpringShutdownHook.class);
@Autowired
private ConfigurableApplicationContext configurableApplicationContext;
public SpringShutdownHook() {
}
@PostConstruct
public void registerShutdownHook() {
logger.info("[SpringShutdownHook] Register ShutdownHook....");
Thread shutdownHook = new Thread() {
public void run() {
try {
int timeOut = ConfigUtils.getServerShutdownTimeout();
logger.info("[SpringShutdownHook] Application need sleep {} seconds to wait Dubbo shutdown", (double)timeOut / 1000.0D);
Thread.sleep((long)timeOut);
this.configurableApplicationContext.close();
logger.info("[SpringShutdownHook] ApplicationContext closed, Application shutdown");
} catch (InterruptedException var2) {
SpringShutdownHook.logger.error(var2.getMessage(), var2);
}
}
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
}