相關(guān)文章:
前言
主要是前一陣子換了工作,第一個任務(wù)就是解決目前團(tuán)隊在 Dubbo 停機(jī)時產(chǎn)生的問題,同時最近又看了一下 Dubbo 的源碼,想重新寫一下 Dubbo 相關(guān)的文章。
優(yōu)雅停機(jī)原理
對于一個 java 應(yīng)用,如果想在關(guān)閉應(yīng)用時,執(zhí)行一些釋放資源的操作一般是通過注冊一個 ShutDownHook ,當(dāng)關(guān)閉應(yīng)用時,不是調(diào)用 kill -9 命令來直接終止應(yīng)用,而是通過調(diào)用 kill -15 命令來觸發(fā)這個 ShutDownHook 進(jìn)行停機(jī)前的釋放資源操作。
對于 Dubbo 來說,需要停機(jī)前執(zhí)行的操作包括兩部分:
- 對于服務(wù)的提供者,需要通知注冊中心來把自己在服務(wù)列表中摘除掉。
- 根據(jù)所配置的協(xié)議,關(guān)閉協(xié)議的端口和連接。
而何為優(yōu)雅停機(jī)呢?就是在集群環(huán)境下,有一個應(yīng)用停機(jī),并不會出現(xiàn)異常。下面來看一下 Dubbo 是怎么做的。
注冊ShutDownHook
Duubo 在 AbstractConfig 的靜態(tài)構(gòu)造函數(shù)中注冊了 JVM 的 ShutDownHook,而 ShutdownHook 主要是調(diào)用 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這個點調(diào)用AbstractRegistryFactory.destroyAll(),其內(nèi)部會對每個注冊中心進(jìn)行 destroy 操作,進(jìn)而把注冊到注冊中心的服務(wù)取消注冊。
- 2這個點是最近 Dubbo 版本新增的操作,用來增強(qiáng) Dubbo 的優(yōu)雅停機(jī),在老版本的 Dubbo 其邏輯是直接摘除服務(wù)列表,關(guān)閉暴露的連接,因為服務(wù)取消注冊,注冊中心是異步的通知消費者變更其存放在自己內(nèi)存中的提供者列表。因為是異步操作,當(dāng)調(diào)用量比較大的應(yīng)用時消費者會拿到已經(jīng)關(guān)閉連接點的提供者進(jìn)行調(diào)用,這時候就會產(chǎn)生大量的錯誤,而2這個點就是通過Sleep 來延遲關(guān)閉協(xié)議暴露的連接。
- 因為 Dubbo 的擴(kuò)展機(jī)制 ,loader.getLoadedExtensions() 會獲取到已使用的所有協(xié)議,遍歷調(diào)用 destroy 方法來關(guān)閉其打開的端口和連接。
而在第3步會在 Exchange 層 對所有打開的連接進(jìn)行判斷其有沒有正在執(zhí)行的請求,如果有會自旋 Sleep 直到設(shè)置的 ServerShutdownTimeout 時間或者已經(jīng)沒有正在執(zhí)行的請求了才會關(guān)閉連接,源碼如下:
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); //正在的關(guān)閉連接
}
在 SpringBoot 應(yīng)用中存在的問題
簡單的描述一下問題:就是在應(yīng)用停機(jī)時,瞬間會產(chǎn)生大量的報錯,比如拿到的數(shù)據(jù)庫連接已經(jīng)關(guān)閉等問題。 其實一看就知道是在停機(jī)時還存在正在處理的請求,而這些請求所需要的資源被 Spring 容器所關(guān)閉導(dǎo)致的。原來在SpringBoot 啟動時會在 refreshContext 操作也注冊一個 ShotdownHook 來關(guān)閉Spring容器。
private void refreshContext(ConfigurableApplicationContext context) {
this.refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException var3) {
}
}
}
而要解決這個問題就需要取消掉這個 ShutDownHook ,然后再 Dubbo 優(yōu)雅停機(jī)執(zhí)行后關(guān)閉 Spring 容器。具體的修改如下:
- 在啟動Main方法中,修改SpringBoot 啟動代碼,取消注冊ShutDownHook。
public static void main(String[] args) {
SpringApplication app = new SpringApplication(XxxApplication.class);
app.setRegisterShutdownHook(false);
app.run(args);
}
- 注冊一個Bean 來讓 Dubbo 關(guān)閉后關(guān)閉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();
SpringShutdownHook.logger.info("[SpringShutdownHook] Application need sleep {} seconds to wait Dubbo shutdown", (double)timeOut / 1000.0D);
Thread.sleep((long)timeOut);
SpringShutdownHook.this.configurableApplicationContext.close();
SpringShutdownHook.logger.info("[SpringShutdownHook] ApplicationContext closed, Application shutdown");
} catch (InterruptedException var2) {
SpringShutdownHook.logger.error(var2.getMessage(), var2);
}
}
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
}