Java 技術(shù)棧中間件優(yōu)雅停機(jī)方案設(shè)計(jì)與實(shí)現(xiàn)全景圖(上)

本系列 Netty 源碼解析文章基于 4.1.56.Final 版本

本文概要

在上篇文章 我為 Netty 貢獻(xiàn)源碼 | 且看 Netty 如何應(yīng)對(duì) TCP 連接的正常關(guān)閉,異常關(guān)閉,半關(guān)閉場(chǎng)景 中筆者為大家詳細(xì)介紹了 Netty 在處理連接關(guān)閉時(shí)的完整過程筹燕,并詳細(xì)介紹了 Netty 如何應(yīng)對(duì) TCP 連接在關(guān)閉時(shí)會(huì)遇到的各種場(chǎng)景唧席。

在連接關(guān)閉之后,接下來就輪到 Netty 的謝幕時(shí)刻了酪惭,本文筆者會(huì)為大家詳盡 Java 技術(shù)棧中間件中關(guān)于優(yōu)雅停機(jī)方案的詳細(xì)設(shè)計(jì)和實(shí)現(xiàn)。

筆者會(huì)從日常開發(fā)工作中常見的版本發(fā)布,服務(wù)上下線的場(chǎng)景聊起浪册,引出服務(wù)優(yōu)雅啟停的需求,并從這個(gè)需求出發(fā)岗照,一步一步帶大家探究各個(gè)中間件里的優(yōu)雅停機(jī)的相關(guān)設(shè)計(jì)村象。

熟悉筆者文風(fēng)的讀者朋友應(yīng)該知道,筆者肯定不會(huì)只是簡(jiǎn)單的介紹攒至,要么不講厚者,要講就要把整個(gè)技術(shù)體系的前世今生給大家講清楚,講明白迫吐。

基于這目的库菲,筆者會(huì)先從支持優(yōu)雅停機(jī)的底層技術(shù)基石--內(nèi)核中的信號(hào)量開始聊起。

image.png

從內(nèi)核層我們接著會(huì)聊到 JVM 層志膀,在 JVM 層一探優(yōu)雅停機(jī)底層的技術(shù)玄機(jī)熙宇。

image.png

隨后我們會(huì)從 JVM 層一路奔襲到 Spring 然后到 Dubbo。在這個(gè)過程中溉浙,筆者還會(huì)帶大家一起 Shooting Dubbo 在優(yōu)雅停機(jī)下的一個(gè) Bug奇颠,并為大家詳細(xì)介紹修復(fù)過程。

image.png

最后由 Dubbo 層的優(yōu)雅停機(jī)放航,引出我們的主角--Netty 優(yōu)雅停機(jī)的設(shè)計(jì)與實(shí)現(xiàn):

Reactor優(yōu)雅關(guān)閉總流程.png

下面我們來正式開始本文的內(nèi)容~~

本文概要.png

1. Java 進(jìn)程的優(yōu)雅啟停

在我們的日常開發(fā)工作中烈拒,業(yè)務(wù)需求的迭代和優(yōu)化伴隨圍繞著我們整個(gè)開發(fā)周期,當(dāng)我們加班加點(diǎn)完成了業(yè)務(wù)需求的開發(fā),然后又歷經(jīng)各種艱難險(xiǎn)阻通過了測(cè)試的驗(yàn)證荆几,最后經(jīng)過和產(chǎn)品經(jīng)理的各種糾纏相愛相殺之后吓妆,終于到了最最激動(dòng)人心的時(shí)刻程序要部署上線了。

上線時(shí)的情緒波動(dòng).png

那么在程序部署上線的過程中勢(shì)必會(huì)涉及到線上服務(wù)的關(guān)閉和重啟吨铸,關(guān)于對(duì)線上服務(wù)的啟停這里面有很多的講究行拢,萬萬不能簡(jiǎn)單粗暴的進(jìn)行關(guān)閉和重啟,因?yàn)榇藭r(shí)線上服務(wù)可能承載著生產(chǎn)的流量诞吱,可能正在進(jìn)行重要的業(yè)務(wù)處理流程舟奠。

比如:用戶正在購(gòu)買商品,錢已經(jīng)付了房维,恰好這時(shí)趕上程序上線沼瘫,如果我們這時(shí)簡(jiǎn)單粗暴的對(duì)服務(wù)進(jìn)行關(guān)閉,重啟咙俩,可能就會(huì)導(dǎo)致用戶付了錢耿戚,但是訂單未創(chuàng)建或者商品未出現(xiàn)在用戶的購(gòu)物清單中,給用戶造成了實(shí)質(zhì)的損失阿趁,這是非常嚴(yán)重的后果膜蛔。

為了保證能在程序上線的過程中做到業(yè)務(wù)無損,所以線上服務(wù)的優(yōu)雅關(guān)閉優(yōu)雅啟動(dòng)顯得就非常非常重要了脖阵。

保持優(yōu)雅很重要.png

1.1 優(yōu)雅啟動(dòng)

在 Java 程序的運(yùn)行過程中皂股,程序的運(yùn)行速度一般會(huì)隨著程序的運(yùn)行慢慢的提高,所以從線上表現(xiàn)上來看 Java 程序在運(yùn)行一段時(shí)間后往往會(huì)比程序剛啟動(dòng)的時(shí)候會(huì)快很多命黔。

這是因?yàn)?Java 程序在運(yùn)行過程中屑墨,JVM 會(huì)不斷收集到程序運(yùn)行時(shí)的動(dòng)態(tài)數(shù)據(jù),這樣可以將高頻執(zhí)行代碼通過即時(shí)編譯成機(jī)器碼纷铣,隨后程序運(yùn)行就直接執(zhí)行機(jī)器碼卵史,運(yùn)行速度完全不輸 C 或者 C++ 程序。

同時(shí)在程序執(zhí)行過程中搜立,用到的類會(huì)被加載到 JVM 中緩存以躯,這樣當(dāng)程序再次使用到的時(shí)候不會(huì)觸發(fā)臨時(shí)加載,影響程序執(zhí)行性能啄踊。

我們可以將以上幾點(diǎn)當(dāng)做 JVM 帶給我們的性能紅利忧设,而當(dāng)應(yīng)用程序重新啟動(dòng)之后,這些性能紅利也就消失了颠通,如果我們讓新啟動(dòng)的程序繼續(xù)承擔(dān)之前的流量規(guī)模址晕,那么就會(huì)導(dǎo)致程序在剛啟動(dòng)的時(shí)候在沒有這些性能紅利的加持下直接進(jìn)入高負(fù)荷的運(yùn)轉(zhuǎn)狀態(tài),這就可能導(dǎo)致線上請(qǐng)求大面積超時(shí)顿锰,對(duì)業(yè)務(wù)造成影響谨垃。

所以說優(yōu)雅地啟動(dòng)一個(gè)程序是非常重要的启搂,優(yōu)雅啟動(dòng)的核心思想就是讓程序在剛啟動(dòng)的時(shí)候不要承擔(dān)太大的流量,讓程序在低負(fù)荷的狀態(tài)下運(yùn)行一段時(shí)間刘陶,使其提升到最佳的運(yùn)行狀態(tài)時(shí)胳赌,在逐步的讓程序承擔(dān)更大的流量處理。

下面我們就來看下常用于優(yōu)雅啟動(dòng)場(chǎng)景的兩個(gè)技術(shù)方案:

1.1.1 啟動(dòng)預(yù)熱

啟動(dòng)預(yù)熱就是讓剛剛上線的應(yīng)用程序不要一下就承擔(dān)之前的全部流量匙隔,而是在一個(gè)時(shí)間窗口內(nèi)慢慢的將流量打到剛上線的應(yīng)用程序上疑苫,目的是讓 JVM 先緩慢的收集程序運(yùn)行時(shí)的一些動(dòng)態(tài)數(shù)據(jù),將高頻代碼即時(shí)編譯為機(jī)器碼纷责。

這個(gè)技術(shù)方案在眾多 RPC 框架的實(shí)現(xiàn)中我們都可以看到,服務(wù)調(diào)用方會(huì)從注冊(cè)中心拿到所有服務(wù)提供方的地址,然后從這些地址中通過特定的負(fù)載均衡算法從中選取一個(gè)服務(wù)提供方的發(fā)送請(qǐng)求夭织。

為了能夠使剛剛上線的服務(wù)提供方有時(shí)間去預(yù)熱题禀,所以我們就要從源頭上控制服務(wù)調(diào)用方發(fā)送的流量秀仲,服務(wù)調(diào)用方在發(fā)起 RPC 調(diào)用時(shí)應(yīng)該盡量少的去負(fù)載均衡到剛剛啟動(dòng)的服務(wù)提供方實(shí)例沛励。

那么服務(wù)調(diào)用方如何才能判斷哪些是剛剛啟動(dòng)的服務(wù)提供方實(shí)例呢?

服務(wù)提供方在啟動(dòng)成功后會(huì)向注冊(cè)中心注冊(cè)自己的服務(wù)信息螟凭,我們可以將服務(wù)提供方的真實(shí)啟動(dòng)時(shí)間包含在服務(wù)信息中一起向注冊(cè)中心注冊(cè),這樣注冊(cè)中心就會(huì)通知服務(wù)調(diào)用方有新的服務(wù)提供方實(shí)例上線并告知其啟動(dòng)時(shí)間土辩。

服務(wù)調(diào)用方可以根據(jù)這個(gè)啟動(dòng)時(shí)間邓厕,慢慢的將負(fù)載權(quán)重增加到這個(gè)剛啟動(dòng)的服務(wù)提供方實(shí)例上。這樣就可以解決服務(wù)提供方冷啟動(dòng)的問題更扁,調(diào)用方通過在一個(gè)時(shí)間窗口內(nèi)將請(qǐng)求慢慢的打到提供方實(shí)例上溃列,這樣就可以讓剛剛啟動(dòng)的提供方實(shí)例有時(shí)間去預(yù)熱听隐,達(dá)到平滑上線的效果拳喻。

1.1.2 延遲暴露

啟動(dòng)預(yù)熱更多的是從服務(wù)調(diào)用方的角度通過降低剛剛啟動(dòng)的服務(wù)提供方實(shí)例的負(fù)載均衡權(quán)重來實(shí)現(xiàn)優(yōu)雅啟動(dòng)陋葡。

而延遲暴露則是從服務(wù)提供方的角度捌归,延遲暴露服務(wù)時(shí)間,利用延遲的這段時(shí)間岭粤,服務(wù)提供方可以預(yù)先加載依賴的一些資源惜索,比如:緩存數(shù)據(jù),spring 容器中的 bean 剃浇。等到這些資源全部加載完畢就位之后巾兆,我們?cè)趯⒎?wù)提供方實(shí)例暴露出去猎物。這樣可以有效降低啟動(dòng)前期請(qǐng)求處理出錯(cuò)的概率。

比如我們可以在 dubbo 應(yīng)用中可以配置服務(wù)的延遲暴露時(shí)間:

//延遲5秒暴露服務(wù)
<dubbo:service delay="5000" /> 

1.2 優(yōu)雅關(guān)閉

優(yōu)雅關(guān)閉需要考慮的問題和處理的場(chǎng)景要比優(yōu)雅啟動(dòng)要復(fù)雜的多角塑,因?yàn)橐粋€(gè)正常在線上運(yùn)行的服務(wù)程序正在承擔(dān)著生產(chǎn)的流量蔫磨,同時(shí)也正在進(jìn)行業(yè)務(wù)流程的處理。

要對(duì)這樣的一個(gè)服務(wù)程序進(jìn)行優(yōu)雅關(guān)閉保證業(yè)務(wù)無損還是非常有挑戰(zhàn)的圃伶,一個(gè)好的關(guān)閉流程堤如,可以確保我們業(yè)務(wù)實(shí)現(xiàn)平滑的上下線,避免上線之后增加很多不必要的額外運(yùn)維工作留攒。

下面我們就來討論下具體應(yīng)該從哪幾個(gè)角度著手考慮實(shí)現(xiàn)優(yōu)雅關(guān)閉:

1.2.1 切走流量

image.png

第一步肯定是要將程序承擔(dān)的現(xiàn)有流量全部切走煤惩,告訴服務(wù)調(diào)用方,我要進(jìn)行關(guān)閉了炼邀,請(qǐng)不要在給我發(fā)送請(qǐng)求魄揉。那么如果進(jìn)行切流呢?拭宁?

在 RPC 的場(chǎng)景中洛退,服務(wù)調(diào)用方通過服務(wù)發(fā)現(xiàn)的方式從注冊(cè)中心中動(dòng)態(tài)感知服務(wù)提供者的上下線變化。在服務(wù)提供方關(guān)閉之前杰标,首先就要自己從注冊(cè)中心中取消注冊(cè)兵怯,隨后注冊(cè)中心會(huì)通知服務(wù)調(diào)用方,有服務(wù)提供者實(shí)例下線腔剂,請(qǐng)將其從本地緩存列表中剔除媒区。這樣就可以使得服務(wù)調(diào)用方之后的 RPC 調(diào)用不在請(qǐng)求到下線的服務(wù)提供方實(shí)例上。

但是這里會(huì)有一個(gè)問題掸犬,就是通常我們的注冊(cè)中心都是 AP 類型的袜漩,它只會(huì)保證最終一致性,并不會(huì)保證實(shí)時(shí)一致性湾碎,基于這個(gè)原因宙攻,服務(wù)調(diào)用方感知到服務(wù)提供者下線的事件可能是延后的,那么在這個(gè)延遲時(shí)間內(nèi)介褥,服務(wù)調(diào)用方極有可能會(huì)向正在下線的服務(wù)發(fā)起 RPC 請(qǐng)求座掘。

因?yàn)榉?wù)提供方已經(jīng)開始進(jìn)入關(guān)閉流程,那么很多對(duì)象在這時(shí)可能已經(jīng)被銷毀了柔滔,這時(shí)如果在收到請(qǐng)求過來溢陪,肯定是無法處理的,甚至可能還會(huì)拋出一個(gè)莫名其妙的異常出來睛廊,對(duì)業(yè)務(wù)造成一定的影響形真。

那么既然這個(gè)問題是由于注冊(cè)中心可能存在的延遲通知引起的,那么我們就很自然的想到了讓準(zhǔn)備下線的服務(wù)提供方主動(dòng)去通知它的服務(wù)調(diào)用方喉前。

這種服務(wù)提供方主動(dòng)通知在加上注冊(cè)中心被動(dòng)通知的兩個(gè)方案結(jié)合在一起應(yīng)該就能確保萬無一失了吧没酣。

事實(shí)上王财,在大部分場(chǎng)景下這個(gè)方案是可行的,但是還有一種極端的情況需要應(yīng)對(duì)裕便,就是當(dāng)服務(wù)提供方通知調(diào)用方自己下線的網(wǎng)絡(luò)請(qǐng)求在到達(dá)服務(wù)調(diào)用方之前的很極限的一個(gè)時(shí)間內(nèi)绒净,服務(wù)調(diào)用者向正在下線的服務(wù)提供方發(fā)起了 RPC 請(qǐng)求,這種極端的情況偿衰,就需要服務(wù)提供方和調(diào)用方一起配合來應(yīng)對(duì)了挂疆。

首先服務(wù)提供方在準(zhǔn)備關(guān)閉的時(shí)候,就把自己設(shè)置為正在關(guān)閉狀態(tài)下翎,在這個(gè)狀態(tài)下不會(huì)接受任何請(qǐng)求缤言,如果這時(shí)遇到了上邊這種極端情況下的請(qǐng)求,那么就拋出一個(gè) CloseException (這個(gè)異常是提供方和調(diào)用方提前約定好的)视事,調(diào)用方收到這個(gè) CloseException 胆萧,則將該服務(wù)提供方的節(jié)點(diǎn)剔除鸳玩,并從剩余節(jié)點(diǎn)中通過負(fù)載均衡選取一個(gè)節(jié)點(diǎn)進(jìn)行重試运吓,通過讓這個(gè)請(qǐng)求快速失敗從而保證業(yè)務(wù)無損。

這三種方案結(jié)合在一起菲嘴,筆者認(rèn)為就是一個(gè)比較完美的切流方案了虏辫。

1.2.2 盡量保證業(yè)務(wù)無損

當(dāng)把流量全部切走后蚌吸,可能此時(shí)將要關(guān)閉的服務(wù)程序中還有正在處理的部分業(yè)務(wù)請(qǐng)求,那么我們就必須得等到這些業(yè)務(wù)處理請(qǐng)求全部處理完畢砌庄,并將業(yè)務(wù)結(jié)果響應(yīng)給客戶端后羹唠,在對(duì)服務(wù)進(jìn)行關(guān)閉。

當(dāng)然為了保證關(guān)閉流程的可控娄昆,我們需要引入關(guān)閉超時(shí)時(shí)間限制佩微,當(dāng)剩下的業(yè)務(wù)請(qǐng)求處理超時(shí),那么就強(qiáng)制關(guān)閉稿黄。

為了保證關(guān)閉流程的可控喊衫,我們只能做到盡可能的保證業(yè)務(wù)無損而不是百分之百保證跌造。所以在程序上線之后杆怕,我們應(yīng)該對(duì)業(yè)務(wù)異常數(shù)據(jù)進(jìn)行監(jiān)控并及時(shí)修復(fù)。


通過以上介紹的優(yōu)雅關(guān)閉方案我們知道壳贪,當(dāng)我們將要優(yōu)雅關(guān)閉一個(gè)應(yīng)用程序時(shí)陵珍,我們需要做好以下兩項(xiàng)工作:

  1. 我們首先要做的就是將當(dāng)前將要關(guān)閉的應(yīng)用程序上承載的生產(chǎn)流量全部切走,保證不會(huì)有新的流量打到將要關(guān)閉的應(yīng)用程序?qū)嵗稀?/p>

  2. 當(dāng)所有的生產(chǎn)流量切走之后违施,我們還需要保證當(dāng)前將要關(guān)閉的應(yīng)用程序?qū)嵗谔幚淼臉I(yè)務(wù)請(qǐng)求要使其處理完畢互纯,并將業(yè)務(wù)處理結(jié)果響應(yīng)給客戶端。以保證業(yè)務(wù)無損磕蒲。當(dāng)然為了使關(guān)閉流程變得可控留潦,我們需要引入關(guān)閉超時(shí)時(shí)間只盹。

以上兩項(xiàng)工作就是我們?cè)趹?yīng)用程序?qū)⒁魂P(guān)閉時(shí)需要做的,那么問題是我們?nèi)绾尾拍苤缿?yīng)用程序要被關(guān)閉呢兔院?換句話說殖卑,我們?cè)趹?yīng)用程序里怎么才能感知到程序進(jìn)程的關(guān)閉事件從而觸發(fā)上述兩項(xiàng)優(yōu)雅關(guān)閉的操作執(zhí)行呢?

既然我們有這樣的需求坊萝,那么操作系統(tǒng)內(nèi)核肯定會(huì)給我們提供這樣的機(jī)制孵稽,事實(shí)上我們可以通過捕獲操作系統(tǒng)給進(jìn)程發(fā)送的信號(hào)來獲取關(guān)閉進(jìn)程通知,并在相應(yīng)信號(hào)回調(diào)中觸發(fā)優(yōu)雅關(guān)閉的操作十偶。

接下來讓我們來看一下操作系統(tǒng)內(nèi)核提供的信號(hào)機(jī)制:

2. 內(nèi)核信號(hào)機(jī)制

信號(hào)是操作系統(tǒng)內(nèi)核為我們提供用于在進(jìn)程間通信的機(jī)制菩鲜,內(nèi)核可以利用信號(hào)來通知進(jìn)程,當(dāng)前系統(tǒng)所發(fā)生的的事件(包括關(guān)閉進(jìn)程事件)惦积。

信號(hào)在內(nèi)核中并沒有用特別復(fù)雜的數(shù)據(jù)結(jié)構(gòu)來表示接校,只是用一個(gè)代號(hào)一樣的數(shù)字來標(biāo)識(shí)不同的信號(hào)。Linux 提供了幾十種信號(hào)狮崩,分別代表不同的意義馅笙。信號(hào)之間依靠它們的值來區(qū)分

信號(hào)可以在任何時(shí)候發(fā)送給進(jìn)程,進(jìn)程需要為這個(gè)信號(hào)配置信號(hào)處理函數(shù)厉亏。當(dāng)某個(gè)信號(hào)發(fā)生的時(shí)候董习,就默認(rèn)執(zhí)行對(duì)應(yīng)的信號(hào)處理函數(shù)就可以了。這就相當(dāng)于一個(gè)操作系統(tǒng)的應(yīng)急手冊(cè)爱只,事先定義好遇到什么情況皿淋,做什么事情,提前準(zhǔn)備好恬试,出了事情照著做就可以了窝趣。

內(nèi)核發(fā)出的信號(hào)就代表當(dāng)前系統(tǒng)遇到了某種情況,我們需要應(yīng)對(duì)的步驟就封裝在對(duì)應(yīng)信號(hào)的回調(diào)函數(shù)中训柴。

信號(hào)機(jī)制引入的目的就在于:

  • 讓應(yīng)用進(jìn)程知道當(dāng)前已經(jīng)發(fā)生了某個(gè)特定的事件(比如進(jìn)程的關(guān)閉事件)哑舒。

  • 強(qiáng)制進(jìn)程執(zhí)行我們事先設(shè)定好的信號(hào)處理函數(shù)(比如封裝優(yōu)雅關(guān)閉邏輯)。

通常來說程序一旦啟動(dòng)就會(huì)一直運(yùn)行下去幻馁,除非遇到 OOM 或者我們需要重新發(fā)布程序時(shí)會(huì)在運(yùn)維腳本中調(diào)用 kill 命令關(guān)閉程序洗鸵。Kill 命令從字面意思上來說是殺死進(jìn)程,但是其本質(zhì)是向進(jìn)程發(fā)送信號(hào)仗嗦,從而關(guān)閉進(jìn)程膘滨。

下面我們使用 kill -l 命令查看下 kill 命令可以向進(jìn)程發(fā)送哪些信號(hào):

# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

筆者這里提取幾個(gè)常見的信號(hào)來簡(jiǎn)要說明下:

  • SIGINT:信號(hào)代號(hào)為 2 。比如我們?cè)诮K端以非后臺(tái)模式運(yùn)行一個(gè)進(jìn)程實(shí)例時(shí)稀拐,要想關(guān)閉它火邓,我們可以通過 Ctrl+C 來關(guān)閉這個(gè)前臺(tái)程序。這個(gè) Ctrl+C 向進(jìn)程發(fā)送的正是 SIGINT 信號(hào)。

  • SIGQUIT:信號(hào)代號(hào)為 3 铲咨。比如我們使用 Ctrl+\ 來關(guān)閉一個(gè)前臺(tái)進(jìn)程躲胳,此時(shí)會(huì)向進(jìn)程發(fā)送 SIGQUIT 信號(hào),與 SIGINT 信號(hào)不同的是纤勒,通過 SIGQUIT 信號(hào)終止的進(jìn)程會(huì)在退出時(shí)泛鸟,通過 Core Dump 將當(dāng)前進(jìn)程的運(yùn)行狀態(tài)保存在 core dump 文件里面,方便后續(xù)查看踊东。

  • SIGKILL:信號(hào)代號(hào)為 9 北滥。通過 kill -9 pid 命令結(jié)束進(jìn)程是非常非常危險(xiǎn)的動(dòng)作,我們應(yīng)該堅(jiān)決制止這種關(guān)閉進(jìn)程的行為闸翅,因?yàn)?SIGKILL 信號(hào)是不能被進(jìn)程捕獲和忽略的再芋,只能執(zhí)行內(nèi)核定義的默認(rèn)操作直接關(guān)閉進(jìn)程。而我們的優(yōu)雅關(guān)閉操作是需要通過捕獲操作系統(tǒng)信號(hào)坚冀,從而可以在對(duì)應(yīng)的信號(hào)處理函數(shù)中執(zhí)行優(yōu)雅關(guān)閉的動(dòng)作济赎。由于 SIGKILL 信號(hào)不能被捕獲,所以優(yōu)雅關(guān)閉也就無法實(shí)現(xiàn)〖悄常現(xiàn)在大家就趕快檢查下自己公司生產(chǎn)環(huán)境的運(yùn)維腳本是否是通過 kill -9 pid 命令來結(jié)束進(jìn)程的司训,一定要避免用這種方式,因?yàn)檫@種方式是極其無情并且略帶殘忍的關(guān)閉進(jìn)程行為液南。

image.png
  • SIGSTOP :信號(hào)代號(hào)為 19 壳猜。該信號(hào)和 SIGKILL 信號(hào)一樣都是無法被應(yīng)用程序忽略和捕獲的。向進(jìn)程發(fā)送 SIGSTOP 信號(hào)也是無法實(shí)現(xiàn)優(yōu)雅關(guān)閉的滑凉。 通過 Ctrl+Z 來關(guān)閉一個(gè)前臺(tái)進(jìn)程,發(fā)送的信號(hào)就是 SIGSTOP 信號(hào)。

  • SIGTERM:信號(hào)代號(hào)為 15 。我們通常會(huì)使用 kill 命令來關(guān)閉一個(gè)后臺(tái)運(yùn)行的進(jìn)程玫荣,kill 命令發(fā)送的默認(rèn)信號(hào)就是 SIGTERM 资柔,該信號(hào)也是本文要討論的優(yōu)雅關(guān)閉的基礎(chǔ)羹与,我們通常會(huì)使用 kill pid 或者 kill -15 pid 來向后臺(tái)進(jìn)程發(fā)送 SIGTERM 信號(hào)用以實(shí)現(xiàn)進(jìn)程的優(yōu)雅關(guān)閉往踢。大家如果發(fā)現(xiàn)自己公司生產(chǎn)環(huán)境的運(yùn)維腳本中使用的是 kill -9 pid 命令來結(jié)束進(jìn)程利职,那么就要馬上換成 kill pid 命令讯私。

以上列舉的都是我們常用的一些信號(hào),大家也可以通過 man 7 signal 命令查看每種信號(hào)對(duì)應(yīng)的含義:

Signal     Value     Action   Comment
──────────────────────────────────────────────────────────────────────
SIGHUP        1       Term    Hangup detected on controlling terminal
                              or death of controlling process
SIGINT        2       Term    Interrupt from keyboard
SIGQUIT       3       Core    Quit from keyboard
SIGILL        4       Core    Illegal Instruction


SIGABRT       6       Core    Abort signal from abort(3)
SIGFPE        8       Core    Floating point exception
SIGKILL       9       Term    Kill signal
SIGSEGV      11       Core    Invalid memory reference
SIGPIPE      13       Term    Broken pipe: write to pipe with no
                              readers
SIGALRM      14       Term    Timer signal from alarm(2)
SIGTERM      15       Term    Termination signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
……

而應(yīng)用進(jìn)程對(duì)于信號(hào)的處理一般分為以下三種方式:

  • 內(nèi)核定義的默認(rèn)操作: 系統(tǒng)內(nèi)核對(duì)每種信號(hào)都規(guī)定了默認(rèn)操作碎税,比如上面列表 Action 列中的 Term 杯道,就是終止進(jìn)程的意思霜医。前邊介紹的 SIGINT 信號(hào)和 SIGTERM 信號(hào)的默認(rèn)操作就是 Term 医男。Core 的意思是 Core Dump 丰辣,即終止進(jìn)程后會(huì)通過 Core Dump 將當(dāng)前進(jìn)程的運(yùn)行狀態(tài)保存在文件里面琐凭,方便我們事后進(jìn)行分析問題在哪里牙躺。前邊介紹的 SIGQUIT 信號(hào)默認(rèn)操作就是 Core 。

  • 捕獲信號(hào):應(yīng)用程序可以利用內(nèi)核提供的系統(tǒng)調(diào)用來捕獲信號(hào),并將優(yōu)雅關(guān)閉的步驟封裝在對(duì)應(yīng)信號(hào)的處理函數(shù)中。當(dāng)向進(jìn)程發(fā)送關(guān)閉信號(hào) SIGTERM 的時(shí)候圆兵,在進(jìn)程內(nèi)我們可以通過捕獲 SIGTERM 信號(hào),隨即就會(huì)執(zhí)行我們自定義的信號(hào)處理函數(shù)危队。我們從而可以在信號(hào)處理函數(shù)中執(zhí)行進(jìn)程優(yōu)雅關(guān)閉的邏輯。

  • 忽略信號(hào):當(dāng)我們不希望處理某些信號(hào)的時(shí)候,就可以忽略該信號(hào),不做任何處理,但是前邊介紹的 SIGKILL 信號(hào)和 SIGSTOP 是無法被捕獲和忽略的倦西,內(nèi)核會(huì)直接執(zhí)行這兩個(gè)信號(hào)定義的默認(rèn)操作直接關(guān)閉進(jìn)程耻矮。

當(dāng)我們不希望信號(hào)執(zhí)行內(nèi)核定義的默認(rèn)操作時(shí)踱承,我們就需要在進(jìn)程內(nèi)捕獲信號(hào)倡缠,并注冊(cè)信號(hào)的回調(diào)函數(shù)來執(zhí)行我們自定義的信號(hào)處理邏輯。

比如我們?cè)诒疚闹幸懻摰膬?yōu)雅關(guān)閉場(chǎng)景茎活,當(dāng)進(jìn)程接收到 SIGTERM 信號(hào)時(shí)昙沦,為了實(shí)現(xiàn)進(jìn)程的優(yōu)雅關(guān)閉,我們并不希望進(jìn)程執(zhí)行 SIGTERM 信號(hào)的默認(rèn)操作直接關(guān)閉進(jìn)程,所以我們要在進(jìn)程中捕獲 SIGTERM 信號(hào),并將優(yōu)雅關(guān)閉的操作步驟封裝在對(duì)應(yīng)的信號(hào)處理函數(shù)中促王。

2.1 如何捕獲信號(hào)

在介紹完了內(nèi)核信號(hào)的分類以及進(jìn)程對(duì)于信號(hào)處理的三種方式之后,下面我們來看下如何來捕獲內(nèi)核信號(hào)吕世,并在對(duì)應(yīng)信號(hào)回調(diào)函數(shù)中自定義我們的處理邏輯蒸甜。

內(nèi)核提供了 sigaction 系統(tǒng)調(diào)用,來供我們捕獲信號(hào)以及與相應(yīng)的信號(hào)處理函數(shù)綁定起來。

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
  • int signum:表示我們想要在進(jìn)程中捕獲的信號(hào)撬呢,比如本文中我們要實(shí)現(xiàn)優(yōu)雅關(guān)閉就需要在進(jìn)程中捕獲 SIGTERM 信號(hào),對(duì)應(yīng)的 signum = 15 掉冶。

  • struct sigaction *act:內(nèi)核中會(huì)用一個(gè) sigaction 結(jié)構(gòu)體來封裝我們自定義的信號(hào)處理邏輯透硝。

  • struct sigaction *oldact:這里是為了兼容老的信號(hào)處理函數(shù),了解一下就可以了,和本文主線無關(guān)肖揣。

sigaction 結(jié)構(gòu)體用來封裝信號(hào)對(duì)應(yīng)的處理函數(shù),以及更加精細(xì)化控制信號(hào)處理的信息浮入。

struct sigaction {
  __sighandler_t sa_handler;
  unsigned long sa_flags;
        .......
  sigset_t sa_mask; 
};
  • __sighandler_t sa_handler:其實(shí)本質(zhì)上是一個(gè)函數(shù)指針龙优,用來保存我們?yōu)樾盘?hào)注冊(cè)的信號(hào)處理函數(shù),優(yōu)雅關(guān)閉的邏輯就封裝在這里事秀。

  • long sa_flags:為了更加精細(xì)化的控制信號(hào)處理邏輯彤断,這個(gè)字段保存了一些控制信號(hào)處理行為的選項(xiàng)集合野舶。常見的選項(xiàng)有:

    • SA_ONESHOT:意思是我們注冊(cè)的信號(hào)處理函數(shù),僅僅只起一次作用宰衙。響應(yīng)完一次后筒愚,就設(shè)置回默認(rèn)行為。

    • SA_NOMASK:表示信號(hào)處理函數(shù)在執(zhí)行的過程中會(huì)被中斷菩浙。比如我們進(jìn)程捕獲到一個(gè)感興趣的信號(hào)巢掺,隨后會(huì)執(zhí)行注冊(cè)的信號(hào)處理函數(shù),但是此時(shí)進(jìn)程又收到其他的信號(hào)或者和上次相同的信號(hào)劲蜻,此時(shí)正在執(zhí)行的信號(hào)處理函數(shù)會(huì)被中斷陆淀,從而轉(zhuǎn)去執(zhí)行最新到來的信號(hào)處理函數(shù)。如果連續(xù)產(chǎn)生多個(gè)相同的信號(hào)先嬉,那么我們的信號(hào)處理函數(shù)就要做好同步轧苫,冪等等措施

    • SA_INTERRUPT:當(dāng)進(jìn)程正在執(zhí)行一個(gè)非常耗時(shí)的系統(tǒng)調(diào)用時(shí)疫蔓,如果此時(shí)進(jìn)程接收到了信號(hào)含懊,那么這個(gè)系統(tǒng)調(diào)用將會(huì)被信號(hào)中斷,進(jìn)程轉(zhuǎn)去執(zhí)行相應(yīng)的信號(hào)處理函數(shù)衅胀。那么當(dāng)信號(hào)處理函數(shù)執(zhí)行完時(shí)岔乔,如果這里設(shè)置了 SA_INTERRUPT ,那么系統(tǒng)調(diào)用將不會(huì)繼續(xù)執(zhí)行并且會(huì)返回一個(gè) -EINTR 常量滚躯,告訴調(diào)用方雏门,這個(gè)系統(tǒng)調(diào)用被信號(hào)中斷了,怎么處理你看著辦吧掸掏。

    • SA_RESTART:當(dāng)系統(tǒng)調(diào)用被信號(hào)中斷后茁影,相應(yīng)的信號(hào)處理函數(shù)執(zhí)行完畢后,如果這里設(shè)置了 SA_RESTART 系統(tǒng)調(diào)用將會(huì)被自動(dòng)重新啟動(dòng)丧凤。

  • sigset_t sa_mask:這個(gè)字段主要指定在信號(hào)處理函數(shù)正在運(yùn)行的過程中募闲,如果連續(xù)產(chǎn)生多個(gè)信號(hào),需要屏蔽哪些信號(hào)愿待。也就是說當(dāng)進(jìn)程收到屏蔽的信號(hào)時(shí)浩螺,正在進(jìn)行的信號(hào)處理函數(shù)不會(huì)被中斷。

屏蔽并不意味著信號(hào)一定丟失呼盆,而是暫存年扩,這樣可以使相同信號(hào)的處理函數(shù),在進(jìn)程連續(xù)接收到多個(gè)相同的信號(hào)時(shí)访圃,可以一個(gè)一個(gè)的處理厨幻。

最終通過 sigaction 函數(shù)會(huì)調(diào)用到底層的系統(tǒng)調(diào)用 rt_sigaction 函數(shù),在
rt_sigaction 中會(huì)將上邊介紹的用戶態(tài) struct sigaction 結(jié)構(gòu)拷貝為內(nèi)核態(tài)的
k_sigaction ,然后調(diào)用 do_sigaction 函數(shù)况脆。

最后在 do_sigaction 函數(shù)中將用戶要在進(jìn)程中捕獲的信號(hào)以及相應(yīng)的信號(hào)處理函數(shù)設(shè)置到進(jìn)程描述符 task_struct 結(jié)構(gòu)里盛末。

進(jìn)程中的信號(hào)結(jié)構(gòu).png

進(jìn)程在內(nèi)核中的數(shù)據(jù)結(jié)構(gòu) task_struct 中有一個(gè) struct sighand_struct 結(jié)構(gòu)的屬性 sighand 嗡贺,struct sighand_struct 結(jié)構(gòu)中包含一個(gè) k_sigaction 類型的數(shù)組 action[] ,這個(gè)數(shù)組保存的就是進(jìn)程中需要捕獲的信號(hào)以及對(duì)應(yīng)的信號(hào)處理函數(shù)在內(nèi)核中的結(jié)構(gòu)體 k_sigaction 捎琐,數(shù)組下標(biāo)為進(jìn)程需要捕獲的信號(hào)技掏。

#include <signal.h>

static void sig_handler(int signum) {

    if (signum == SIGTERM) {

        .....執(zhí)行優(yōu)雅關(guān)閉邏輯....

    }

}

int main (Void) {

    struct sigaction sa_usr; //定義sigaction結(jié)構(gòu)體
    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = sig_handler;   //設(shè)置信號(hào)處理函數(shù)

    sigaction(SIGTERM, &sa_usr, NULL);//進(jìn)程捕獲信號(hào)雁比,注冊(cè)信號(hào)處理函數(shù)
        
        ,,,,,,,,,,,,
}

我們可以通過如上簡(jiǎn)單的示例代碼,將 SIGTERM 信號(hào)及其對(duì)應(yīng)的自定義信號(hào)處理函數(shù)注冊(cè)到進(jìn)程中申尼,當(dāng)我們執(zhí)行 kill -15 pid 命令之后,進(jìn)程就會(huì)捕獲到 SIGTERM 信號(hào)馋袜,隨后就可以執(zhí)行優(yōu)雅關(guān)閉步驟了。

3. JVM 中的 ShutdownHook

在《2. 內(nèi)核信號(hào)機(jī)制》小節(jié)中為大家介紹的內(nèi)容是操作系統(tǒng)內(nèi)核為我們實(shí)現(xiàn)進(jìn)程的優(yōu)雅關(guān)閉提供的最底層系統(tǒng)級(jí)別的支持機(jī)制友瘤,在內(nèi)核的強(qiáng)力支持下笛求,那么本文的主題 Java 進(jìn)程的優(yōu)雅關(guān)閉就很容易實(shí)現(xiàn)了离唐。

我們要想實(shí)現(xiàn) Java 進(jìn)程的優(yōu)雅關(guān)閉功能庵朝,只需要在進(jìn)程啟動(dòng)的時(shí)候?qū)?yōu)雅關(guān)閉的操作封裝在一個(gè) Thread 中笔链,隨后將這個(gè) Thread 注冊(cè)到 JVM 的 ShutdownHook 中就好了鉴扫,當(dāng) JVM 進(jìn)程接收到 kill -15 信號(hào)時(shí)依沮,就會(huì)執(zhí)行我們注冊(cè)的 ShutdownHook 關(guān)閉鉤子,進(jìn)而執(zhí)行我們定義的優(yōu)雅關(guān)閉步驟。

        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run() {
                .....執(zhí)行優(yōu)雅關(guān)閉步驟.....
            }
        });

3.1 導(dǎo)致 JVM 退出的幾種情況

  1. JVM 進(jìn)程中最后一個(gè)非守護(hù)線程退出言沐。

  2. 在程序代碼中主動(dòng)調(diào)用 java.lang.System#exit(int status) 方法汹押,會(huì)導(dǎo)致 JVM 進(jìn)程的退出并觸發(fā) ShutdownHook 的調(diào)用。參數(shù) int status 如果是非零值起便,則表示本次關(guān)閉是在一個(gè)非正常情況下的關(guān)閉行為棚贾。比如:進(jìn)程發(fā)生 OOM 異常或者其他運(yùn)行時(shí)異常榆综。

public static void main(String[] args) {
        try {

           ......進(jìn)程啟動(dòng)main函數(shù).......

        } catch (RuntimeException e) {
            logger.error(e.getMessage(), e);
            // JVM 進(jìn)程主動(dòng)關(guān)閉觸發(fā)調(diào)用 shutdownHook
            System.exit(1);
        }
}
  1. 當(dāng) JVM 進(jìn)程接收到第二小節(jié)《2.內(nèi)核信號(hào)機(jī)制》介紹的那些關(guān)閉信號(hào)時(shí)妙痹, JVM 進(jìn)程會(huì)被關(guān)閉。由于 SIGKILL 信號(hào)和 SIGSTOP 信號(hào)不能夠被進(jìn)程捕獲和忽略鼻疮,這兩個(gè)信號(hào)會(huì)直接粗暴地關(guān)閉 JVM 進(jìn)程细诸,所以一般我們會(huì)發(fā)送 SIGTERM 信號(hào),JVM 進(jìn)程通過捕獲 SIGTERM 信號(hào)陋守,從而可以執(zhí)行我們定義的 ShutdownHook 完成優(yōu)雅關(guān)閉的操作震贵。

  2. Native Method 執(zhí)行過程中發(fā)生錯(cuò)誤,比如試圖訪問一個(gè)不存在的內(nèi)存水评,這樣也會(huì)導(dǎo)致 JVM 強(qiáng)制關(guān)閉猩系,ShutdownHook 也不會(huì)運(yùn)行。

3.2 使用 ShutdownHook 的注意事項(xiàng)

  1. ShutdownHook 其實(shí)本質(zhì)上是一個(gè)已經(jīng)被初始化但是未啟動(dòng)的 Thread 中燥,這些通過 Runtime.getRuntime().addShutdownHook 方法注冊(cè)的 ShutdownHooks 寇甸,在 JVM 進(jìn)程關(guān)閉的時(shí)候會(huì)被啟動(dòng)并發(fā)執(zhí)行,但是并不會(huì)保證執(zhí)行順序疗涉。

所以在編寫 ShutdownHook 中的邏輯時(shí)拿霉,我們應(yīng)該確保程序的線程安全性扰楼,并盡可能避免死鎖渐裂。最好是一個(gè) JVM 進(jìn)程只注冊(cè)一個(gè) ShutdownHook 筒扒。

  1. 如果我們通過 java.lang.Runtime#runFinalizersOnExit(boolean value) 開啟了 finalization-on-exit 蝇恶,那么當(dāng)所有 ShutdownHook 運(yùn)行完畢之后列另,JVM 在關(guān)閉之前將會(huì)繼續(xù)調(diào)用所有未被調(diào)用的 finalizers 方法滞谢。默認(rèn) finalization-on-exit 選項(xiàng)是關(guān)閉的髓迎。

注意:當(dāng) JVM 開始關(guān)閉并執(zhí)行上述關(guān)閉操作的時(shí)候硅瞧,守護(hù)線程是會(huì)繼續(xù)運(yùn)行的偏瓤,如果用戶使用 java.lang.System#exit(int status) 方法主動(dòng)發(fā)起 JVM 關(guān)閉杀怠,那么關(guān)閉期間非守護(hù)線程也是會(huì)繼續(xù)運(yùn)行的。

  1. 一旦 JVM 進(jìn)程開始關(guān)閉厅克,一般情況下這個(gè)過程是不可以被中斷的赔退,除非操作系統(tǒng)強(qiáng)制中斷或者用戶通過調(diào)用 java.lang.Runtime#halt(int status) 來強(qiáng)制關(guān)閉证舟。
   public void halt(int status) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkExit(status);
        }
        Shutdown.halt(status);
    }

java.lang.Runtime#halt(int status) 方法是用來強(qiáng)制關(guān)閉正在運(yùn)行的 JVM 進(jìn)程的硕旗,它會(huì)導(dǎo)致我們注冊(cè)的 ShutdownHook 不會(huì)被運(yùn)行和執(zhí)行,如果此時(shí) JVM 正在執(zhí)行 ShutdownHook 褪储,當(dāng)調(diào)用該方法后卵渴,JVM 進(jìn)程將會(huì)被強(qiáng)制關(guān)閉,并不會(huì)等待 ShutdownHook 執(zhí)行完畢鲤竹。

  1. 當(dāng) JVM 關(guān)閉流程開始的時(shí)候浪读,就不能在向其注冊(cè) ShutdownHook 或者取消注冊(cè)之前已經(jīng)注冊(cè)好的 ShutdownHook 了,否則將會(huì)拋出 IllegalStateException異常辛藻。

  2. ShutdownHook 中的程序應(yīng)該盡快的完成優(yōu)雅關(guān)閉邏輯碘橘,因?yàn)楫?dāng)用戶調(diào)用 System#exit 方法的時(shí)候是希望 JVM 在保證業(yè)務(wù)無損的情況下盡快完成關(guān)閉動(dòng)作。這里并不適合做一些需要長(zhǎng)時(shí)間運(yùn)行的任務(wù)或者和用戶交互的操作吱肌。

如果是因?yàn)槲锢頇C(jī)關(guān)閉從而導(dǎo)致的 JVM 關(guān)閉痘拆,那么操作系統(tǒng)只會(huì)允許 JVM 限定的時(shí)間內(nèi)盡快的關(guān)閉,超過限定時(shí)間操作系統(tǒng)將會(huì)強(qiáng)制關(guān)閉 JVM 氮墨。

  1. ShutdownHook 中可能也會(huì)拋出異常纺蛆,而 ShutdownHook 對(duì)于 JVM 來說本質(zhì)上是一個(gè) Thread 吐葵,那么對(duì)于 ShutdownHook 中未捕獲的異常,JVM 的處理方法和其他普通的線程一樣桥氏,都是通過調(diào)用 ThreadGroup#uncaughtException 方法來處理温峭。此方法的默認(rèn)實(shí)現(xiàn)是將異常的堆棧跟蹤打印到 System#err 并終止異常的 ShutdownHook 線程。

注意:這里只會(huì)停止異常的 ShutdownHook 字支,但不會(huì)影響其他 ShutdownHook 線程的執(zhí)行更不會(huì)導(dǎo)致 JVM 退出凤藏。

  1. 最后也是非常重要的一點(diǎn)是,當(dāng) JVM 進(jìn)程接收到 SIGKILL 信號(hào)和 SIGSTOP 信號(hào)時(shí)堕伪,是會(huì)強(qiáng)制關(guān)閉揖庄,并不會(huì)執(zhí)行 ShutdownHook 。另外一種導(dǎo)致 JVM 強(qiáng)制關(guān)閉的情況就是 Native Method 執(zhí)行過程中發(fā)生錯(cuò)誤欠雌,比如試圖訪問一個(gè)不存在的內(nèi)存蹄梢,這樣也會(huì)導(dǎo)致 JVM 強(qiáng)制關(guān)閉,ShutdownHook 也不會(huì)運(yùn)行桨昙。

3.3 ShutdownHook 執(zhí)行原理

我們?cè)?JVM 中通過 Runtime.getRuntime().addShutdownHook 添加關(guān)閉鉤子检号,當(dāng) JVM 接收到 SIGTERM 信號(hào)之后,就會(huì)調(diào)用我們注冊(cè)的這些 ShutdownHooks 蛙酪。

本小節(jié)介紹的 ShutdownHook 就類似于我們?cè)诘诙」?jié)《內(nèi)核信號(hào)機(jī)制》中介紹的信號(hào)處理函數(shù)齐苛。

大家這里一定會(huì)有個(gè)疑問,那就是在介紹內(nèi)核信號(hào)機(jī)制小節(jié)中桂塞,我們可以通過系統(tǒng)調(diào)用 sigaction 函數(shù)向內(nèi)核注冊(cè)進(jìn)程要捕獲的信號(hào)以及對(duì)應(yīng)的信號(hào)處理函數(shù)凹蜂。

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

但是在本小節(jié)介紹的 JVM 中,我們只是通過 Runtime.getRuntime().addShutdownHook 注冊(cè)了一個(gè)關(guān)閉鉤子阁危。但是并未注冊(cè) JVM 進(jìn)程所需要捕獲的信號(hào)玛痊。那么 JVM 是怎么捕獲關(guān)閉信號(hào)的呢?

        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run() {
                .....執(zhí)行優(yōu)雅關(guān)閉步驟.....
            }
        });

事實(shí)上狂打,JVM 捕獲操作系統(tǒng)信號(hào)的部分在 JDK 中已經(jīng)幫我們處理好了擂煞,在用戶層我們并不需要關(guān)注捕獲信號(hào)的處理,只需要關(guān)注信號(hào)的處理邏輯即可趴乡。

下面我們就來看一下 JDK 是如何幫助我們將要捕獲的信號(hào)向內(nèi)核注冊(cè)的对省?

當(dāng) JVM 第一個(gè)線程被初始化之后,隨后就會(huì)調(diào)用 System#initializeSystemClass 函數(shù)來初始化 JDK 中的一些系統(tǒng)類晾捏,其中就包括注冊(cè) JVM 進(jìn)程需要捕獲的信號(hào)以及信號(hào)處理函數(shù)蒿涎。

public final class System {

    private static void initializeSystemClass() {

           .......省略.......

            // Setup Java signal handlers for HUP, TERM, and INT (where available).
           Terminator.setup();

           .......省略.......

    }

}

從這里可以看出,JDK 在向 JVM 注冊(cè)需要捕獲的內(nèi)核信號(hào)是在 Terminator 類中進(jìn)行的惦辛。


class Terminator {
    //信號(hào)處理函數(shù)
    private static SignalHandler handler = null;

    static void setup() {
        if (handler != null) return;
        SignalHandler sh = new SignalHandler() {
            public void handle(Signal sig) {
                Shutdown.exit(sig.getNumber() + 0200);
            }
        };
        handler = sh;

        try {
            Signal.handle(new Signal("HUP"), sh);
        } catch (IllegalArgumentException e) {
        }
        try {
            Signal.handle(new Signal("INT"), sh);
        } catch (IllegalArgumentException e) {
        }
        try {
            Signal.handle(new Signal("TERM"), sh);
        } catch (IllegalArgumentException e) {
        }
    }

}

JDK 向我們提供了 sun.misc.Signal#handle(Signal signal, SignalHandler signalHandler) 函數(shù)來實(shí)現(xiàn)在 JVM 進(jìn)程中對(duì)內(nèi)核信號(hào)的捕獲劳秋。底層依賴于我們?cè)诘诙」?jié)介紹的系統(tǒng)調(diào)用 sigaction 。

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

sun.misc.Signal#handle 函數(shù)的參數(shù)含義和系統(tǒng)調(diào)用函數(shù) sigaction 中的參數(shù)含義是一一對(duì)應(yīng)的:

  • Signal signal:表示要捕獲的內(nèi)核信號(hào)。從這里我們可以看出 JVM 主要捕獲三種信號(hào):SIGHUP(1)玻淑,SIGINT(2)嗽冒,SIGTERM(15)。

除了上述的這三種信號(hào)之外岁忘,JVM 如果接收到其他信號(hào)辛慰,會(huì)執(zhí)行系統(tǒng)內(nèi)核默認(rèn)的操作,直接關(guān)閉進(jìn)程干像,并不會(huì)觸發(fā) ShutdownHook 的執(zhí)行。

  • SignalHandler handler:信號(hào)響應(yīng)函數(shù)驰弄。我們看到這里直接調(diào)用了 Shutdown#exit 函數(shù)麻汰。
    SignalHandler sh = new SignalHandler() {
            public void handle(Signal sig) {
                Shutdown.exit(sig.getNumber() + 0200);
            }
        };

我們這里應(yīng)該很容易就會(huì)猜測(cè)出 ShutdownHook 的調(diào)用應(yīng)該就是在 Shutdown#exit 函數(shù)中被觸發(fā)的。

class Shutdown {

    static void exit(int status) {

          ........省略.........

          synchronized (Shutdown.class) {
              // 開始 JVM 關(guān)閉流程戚篙,執(zhí)行 ShutdownHooks
              sequence();
              // 強(qiáng)制關(guān)閉 JVM
              halt(status);
          }

    }

    private static void sequence() {
        synchronized (lock) {
            if (state != HOOKS) return;
        }
        //觸發(fā) ShutdownHooks
        runHooks();
        boolean rfoe;
        synchronized (lock) {
            state = FINALIZERS;
            rfoe = runFinalizersOnExit;
        }
        //如果 runFinalizersOnExit = true
        //開始運(yùn)行所有未被調(diào)用過的 Finalizers
        if (rfoe) runAllFinalizers();
    }
}

Shutdown#sequence 函數(shù)中的邏輯就是我們?cè)凇?.2 使用ShutdownHook的注意事項(xiàng)》小節(jié)中介紹的 JVM 關(guān)閉時(shí)的運(yùn)行邏輯:在這里會(huì)觸發(fā)所有 ShutdownHook 的并發(fā)運(yùn)行五鲫。注意這里并不會(huì)保證運(yùn)行順序。

當(dāng)所有 ShutdownHook 運(yùn)行完畢之后岔擂,如果我們通過 java.lang.Runtime#runFinalizersOnExit(boolean value) 開啟了 finalization-on-exit 選項(xiàng)位喂,JVM 在關(guān)閉之前將會(huì)繼續(xù)調(diào)用所有未被調(diào)用的 finalizers 方法。默認(rèn) finalization-on-exit 選項(xiàng)是關(guān)閉的乱灵。

3.4 ShutdownHook 的執(zhí)行

shutdownhook的運(yùn)行.png

如上圖所示塑崖,在 JDK 的 Shutdown 類中,包含了一個(gè) Runnable[] hooks 數(shù)組痛倚,容量為 10 规婆。JDK 中的 ShutdownHook 是以類型來分類的,數(shù)組 hooks 每一個(gè)槽中存放的是一種特定類型的 ShutdownHook 蝉稳。

而我們通常在程序代碼中通過 Runtime.getRuntime().addShutdownHook 注冊(cè)的是 Application hooks 類型的 ShutdownHook 抒蚜,存放在數(shù)組 hooks 中索引為 1 的槽中。

當(dāng)在 Shutdown#sequence 中觸發(fā) runHooks() 函數(shù)開始運(yùn)行 JVM 中所有類型的 ShutdownHooks 時(shí)耘戚,會(huì)在 runHooks() 函數(shù)中依次遍歷數(shù)組 hooks 中的 Runnable 嗡髓,進(jìn)而開始運(yùn)行 Runnable 中封裝的 ShutdownHooks 。

當(dāng)遍歷到數(shù)組 Hooks 的第二個(gè)槽(索引為 1 )的時(shí)候收津,Application hooks 類型的 ShutdownHook 得以運(yùn)行饿这,也就是我們通過 Runtime.getRuntime().addShutdownHook 注冊(cè)的 ShutdownHook 在這個(gè)時(shí)候開始運(yùn)行起來。


    // The system shutdown hooks are registered with a predefined slot.
    // The list of shutdown hooks is as follows:
    // (0) Console restore hook
    // (1) Application hooks
    // (2) DeleteOnExit hook
    private static final int MAX_SYSTEM_HOOKS = 10;
    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

    /* Run all registered shutdown hooks
     */
    private static void runHooks() {
        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            try {
                Runnable hook;
                synchronized (lock) {
                    // acquire the lock to make sure the hook registered during
                    // shutdown is visible here.
                    currentRunningHook = i;
                    hook = hooks[i];
                }
                if (hook != null) hook.run();
            } catch(Throwable t) {
                if (t instanceof ThreadDeath) {
                    ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }

下面我們就來看一下朋截,JDK 是如果通過 Runtime.getRuntime().addShutdownHook 函數(shù)將我們自定義的 ShutdownHook 注冊(cè)到 Shutdown 類中的數(shù)組 Hooks 里的蛹稍。

3.5 ShutdownHook 的注冊(cè)

public class Runtime {

    public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        //注意 這里注冊(cè)的是 Application 類型的 hooks
        ApplicationShutdownHooks.add(hook);
    }

}

從 JDK 源碼中我們看到在 Runtime 類中的 addShutdownHook 方法里,JDK 會(huì)將我們自定義的 ShutdownHook 封裝在 ApplicationShutdownHooks 類中部服,從這類的命名上看唆姐,它里邊封裝的就是我們?cè)谏闲」?jié)《3.4 ShutdownHook 的執(zhí)行》提到的 Application hooks 類型的 ShutdownHook ,由用戶自定義實(shí)現(xiàn)廓八。

class ApplicationShutdownHooks {
    // 存放用戶自定義的 Application 類型的 hooks
    private static IdentityHashMap<Thread, Thread> hooks;

    static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }

    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }
        // 順序啟動(dòng) shutdownhooks
        for (Thread hook : threads) {
            hook.start();
        }
        // 并發(fā)調(diào)用 shutdownhooks 奉芦,等待所有 hooks 運(yùn)行完畢退出
        for (Thread hook : threads) {
            try {
                hook.join();
            } catch (InterruptedException x) { }
        }
    }
}

ApplicationShutdownHooks 類中也有一個(gè)集合 IdentityHashMap<Thread, Thread> hooks 赵抢,專門用來存放由用戶自定義的 Application hooks 類型的 ShutdownHook 。通過 ApplicationShutdownHooks#add 方法添加進(jìn) hooks 集合中声功。

然后在 runHooks 方法里挨個(gè)啟動(dòng) ShutdownHook 線程烦却,并發(fā)執(zhí)行。注意這里的 runHooks 方法是 ApplicationShutdownHooks 類中的先巴。

在 ApplicationShutdownHooks 類的靜態(tài)代碼塊 static{.....} 中會(huì)將 runHooks 方法封裝成 Runnable 添加進(jìn) Shutdown 類中的 hooks 數(shù)組中其爵。注意這里 Shutdown#add 方法傳遞進(jìn)的索引是 1 。

class ApplicationShutdownHooks {
    /* The set of registered hooks */
    private static IdentityHashMap<Thread, Thread> hooks;

    static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }
}

Shutdownhook的執(zhí)行.png

Shutdown#add 方法的邏輯就很簡(jiǎn)單了:

class Shutdown {

    private static final int MAX_SYSTEM_HOOKS = 10;
    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

    static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
        synchronized (lock) {
            if (hooks[slot] != null)
                throw new InternalError("Shutdown hook at slot " + slot + " already registered");

            if (!registerShutdownInProgress) {
                if (state > RUNNING)
                    throw new IllegalStateException("Shutdown in progress");
            } else {
                if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
                    throw new IllegalStateException("Shutdown in progress");
            }

            hooks[slot] = hook;
        }
    }
}
  • 參數(shù) Runnable hook 就是在 ApplicationShutdownHooks 中的靜態(tài)代碼塊 static{....} 中將 runHooks 方法封裝成的 Runnable伸蚯。

  • 參數(shù) int slot 表示將封裝好的 Runnable 放入 hooks 數(shù)組中的哪個(gè)槽中摩渺。這里我們注冊(cè)的是 Application hooks 類型的 ShutdonwHook ,所以這里的索引為 1 剂邮。

  • 參數(shù) registerShutdownInProgress 表示是否允許在 JVM 關(guān)閉流程開始之后摇幻,繼續(xù)向 JVM 添加 ShutdownHook 。默認(rèn)為 false 表示不允許挥萌。否則將會(huì)拋出 IllegalStateException 異常绰姻。這一點(diǎn)筆者在小節(jié)《3.2 使用ShutdownHook的注意事項(xiàng)》中強(qiáng)調(diào)過。

以上就是 JVM 如何捕獲操作系統(tǒng)內(nèi)核信號(hào)引瀑,如何注冊(cè) ShutdownHook 狂芋,以及何時(shí)觸發(fā) ShutdownHook 的執(zhí)行的一個(gè)全面介紹。

shutdownhook完整觸發(fā)時(shí)機(jī).png

讀到這里大家應(yīng)該徹底明白了為什么不能使用 kill -9 pid 命令來關(guān)閉進(jìn)程了吧伤疙,現(xiàn)在趕快去檢查一下你們公司生產(chǎn)環(huán)境的運(yùn)維腳本吧R铩!


俗話說的好 talk is cheap! show me the code! 徒像,在介紹了這么多關(guān)于優(yōu)雅關(guān)閉的理論方案和原理之后黍特,我想大家現(xiàn)在一定很好奇究竟我們?cè)撊绾螌?shí)現(xiàn)這一套優(yōu)雅關(guān)閉的方案呢?

那么接下來筆者就從一些知名框架源碼實(shí)現(xiàn)角度锯蛀,為大家詳細(xì)闡述一下優(yōu)雅關(guān)閉是如何實(shí)現(xiàn)的灭衷?

image.png

4. Spring 的優(yōu)雅關(guān)閉機(jī)制

前面兩個(gè)小節(jié)中我們從支持優(yōu)雅關(guān)閉最底層的內(nèi)核信號(hào)機(jī)制開始聊起然后到 JVM 進(jìn)程實(shí)現(xiàn)優(yōu)雅關(guān)閉的 ShutdwonHook 原理,經(jīng)過這一系列的介紹旁涤,我們現(xiàn)在對(duì)優(yōu)雅關(guān)閉在內(nèi)核層和 JVM 層的相關(guān)機(jī)制原理有了一定的了解翔曲。

那么在真實(shí) Java 應(yīng)用中,我們到底該如何基于上述機(jī)制實(shí)現(xiàn)一套優(yōu)雅關(guān)閉方案呢劈愚?本小節(jié)我們來從 Spring 源碼中獲取下答案M椤!

在介紹 Spring 優(yōu)雅關(guān)閉機(jī)制源碼實(shí)現(xiàn)之前菌羽,筆者先來帶大家回顧下掠械,在 Spring 的應(yīng)用上下文關(guān)閉的時(shí)候,Spring 究竟給我們提供了哪些關(guān)閉時(shí)的回調(diào)機(jī)制,從而可以讓我們?cè)谶@些回調(diào)中編寫 Java 應(yīng)用的優(yōu)雅關(guān)閉邏輯猾蒂。

4.1 發(fā)布 ContextClosedEvent 事件

在 Spring 上下文開始關(guān)閉的時(shí)候均唉,首先會(huì)發(fā)布 ContextClosedEvent 事件,注意此時(shí) Spring 容器的 Bean 還沒有開始銷毀肚菠,所以我們可以在該事件回調(diào)中執(zhí)行優(yōu)雅關(guān)閉的操作舔箭。

@Component
public class ShutdownListener implements ApplicationListener<ContextClosedEvent> {
       @Override
       public void onApplicationEvent(ContextClosedEvent event) {
                  ........優(yōu)雅關(guān)閉邏輯.....
       }
}

4.2 Spring 容器中的 Bean 銷毀前回調(diào)

當(dāng) Spring 開始銷毀容器中管理的 Bean 之前,會(huì)回調(diào)所有實(shí)現(xiàn) DestructionAwareBeanPostProcessor 接口的 Bean 中的 postProcessBeforeDestruction 方法蚊逢。

@Component
public class DestroyBeanPostProcessor implements DestructionAwareBeanPostProcessor {

    @Override
    public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {

             ........Spring容器中的Bean開始銷毀前回調(diào).......
    }
}

4.3 回調(diào)標(biāo)注 @PreDestroy 注解的方法

@Component
public class Shutdown {
    @PreDestroy
    public void preDestroy() {
        ......釋放資源.......
    }
}

4.4 回調(diào) DisposableBean 接口中的 destroy 方法

@Component
public class Shutdown implements DisposableBean{

    @Override
    public void destroy() throws Exception {
         ......釋放資源......
    }

}

4.5 回調(diào)自定義的銷毀方法

<bean id="Shutdown" class="com.test.netty.Shutdown"  destroy-method="doDestroy"/>
public class Shutdown {

    public void doDestroy() {
        .....自定義銷毀方法....
    }
}

4.6 Spring 優(yōu)雅關(guān)閉機(jī)制的實(shí)現(xiàn)

Spring 相關(guān)應(yīng)用程序本質(zhì)上也是一個(gè) JVM 進(jìn)程层扶,所以 Spring 框架想要實(shí)現(xiàn)優(yōu)雅關(guān)閉機(jī)制也必須依托于我們?cè)诒疚牡谌」?jié)中介紹的 JVM 的 ShutdownHook 機(jī)制。

在 Spring 啟動(dòng)的時(shí)候时捌,需要向 JVM 注冊(cè) ShutdownHook 怒医,當(dāng)我們執(zhí)行 kill - 15 pid 命令時(shí),隨后 Spring 會(huì)在 ShutdownHook 中觸發(fā)上述介紹的五種回調(diào)奢讨。

下面我們來看一下 Spring 中 ShutdownHook 的注冊(cè)邏輯:

4.6.1 Spring 中 ShutdownHook 的注冊(cè)

public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext, DisposableBean {

    @Override
    public void registerShutdownHook() {
        if (this.shutdownHook == null) {
            // No shutdown hook registered yet.
            this.shutdownHook = new Thread() {
                @Override
                public void run() {
                    synchronized (startupShutdownMonitor) {
                        doClose();
                    }
                }
            };
            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
        }
    }
}

在 Spring 啟動(dòng)的時(shí)候,我們需要調(diào)用 AbstractApplicationContext#registerShutdownHook 方法向 JVM 注冊(cè) Spring 的 ShutdownHook 塞茅,從這段源碼中我們看出,Spring 將 doClose() 方法封裝在 ShutdownHook 線程中描沟,而 doClose() 方法里邊就是 Spring 優(yōu)雅關(guān)閉的邏輯。

這里需要強(qiáng)調(diào)的是鞭光,當(dāng)我們?cè)谝粋€(gè)純 Spring 環(huán)境下惰许,Spring 框架是不會(huì)為我們主動(dòng)調(diào)用 registerShutdownHook 方法去向 JVM 注冊(cè) ShutdownHook 的佩伤,我們需要手動(dòng)調(diào)用 registerShutdownHook 方法去注冊(cè)。

public class SpringShutdownHook {

    public static void main(String[] args) throws IOException {
        GenericApplicationContext context = new GenericApplicationContext();
                      ........
        // 注冊(cè) Shutdown Hook
        context.registerShutdownHook();
                      ........
    }
}

而在 SpringBoot 環(huán)境下孤荣,SpringBoot 在啟動(dòng)的時(shí)候會(huì)為我們調(diào)用這個(gè)方法去主動(dòng)注冊(cè) ShutdownHook 邀层。我們不需要手動(dòng)注冊(cè)。

public class SpringApplication {

    public ConfigurableApplicationContext run(String... args) {

                  ...............省略.................

                  ConfigurableApplicationContext context = null;
                  context = createApplicationContext();
                  refreshContext(context);

                  ...............省略.................
    }

    private void refreshContext(ConfigurableApplicationContext context) {
        refresh(context);
        if (this.registerShutdownHook) {
            try {
                context.registerShutdownHook();
            }
            catch (AccessControlException ex) {
                // Not allowed in some environments.
            }
        }
    }

}

4.6.2 Spring 中的優(yōu)雅關(guān)閉邏輯

    protected void doClose() {
        // 更新上下文狀態(tài)
        if (this.active.get() && this.closed.compareAndSet(false, true)) {
            if (logger.isInfoEnabled()) {
                logger.info("Closing " + this);
            }
            // 取消 JMX 托管
            LiveBeansView.unregisterApplicationContext(this);

            try {
                // 發(fā)布 ContextClosedEvent 事件
                publishEvent(new ContextClosedEvent(this));
            }
            catch (Throwable ex) {
                logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
            }

            // 回調(diào) Lifecycle beans,相關(guān) stop 方法
            if (this.lifecycleProcessor != null) {
                try {
                    this.lifecycleProcessor.onClose();
                }
                catch (Throwable ex) {
                    logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
                }
            }

            // 銷毀 bean,觸發(fā)前面介紹的幾種回調(diào)
            destroyBeans();

            // Close the state of this context itself.
            closeBeanFactory();

            // Let subclasses do some final clean-up if they wish...
            onClose();

            // Switch to inactive.
            this.active.set(false);
        }
    }

在這里我們可以看出最終是在 AbstractApplicationContext#doClose 方法中觸發(fā)本小節(jié)開始介紹的五種回調(diào):

  1. 發(fā)布 ContextClosedEvent 事件。注意這里是一個(gè)同步事件臭蚁,也就是說 Spring 的 ShutdownHook 線程在這里發(fā)布完事件之后會(huì)繼續(xù)同步執(zhí)行事件的處理,等到事件處理完畢后系枪,才會(huì)去執(zhí)行后面的 destroyBeans() 方法對(duì) IOC 容器中的 Bean 進(jìn)行銷毀。

所以在 ContextClosedEvent 事件監(jiān)聽類中,可以放心地去做優(yōu)雅關(guān)閉相關(guān)的操作嚎卫,因?yàn)榇藭r(shí) Spring 容器中的 Bean 還沒有被銷毀。

  1. destroyBeans() 方法中依次觸發(fā)剩下的四種回調(diào)奠支。

最后結(jié)合前邊小節(jié)中介紹的內(nèi)容迈螟,總結(jié) Spring 的整個(gè)優(yōu)雅關(guān)閉流程如下圖所示:

Spring優(yōu)雅關(guān)閉機(jī)制.png

5. Dubbo 的優(yōu)雅關(guān)閉

本小節(jié)優(yōu)雅關(guān)閉部分源碼基于 apache dubbo 2.7.7 版本季春,該版本中的優(yōu)雅關(guān)閉是有 Bug 的载弄,下面讓我們一起來 Shooting Bug !

在前邊幾個(gè)小節(jié)的內(nèi)容中惫叛,我們從內(nèi)核提供的底層技術(shù)支持開始聊到了 JVM 的 ShutdonwHook ,然后又從 JVM 聊到了 Spring 框架的優(yōu)雅關(guān)閉機(jī)制。

在了解了這些內(nèi)容之后,本小節(jié)我們就來看下 dubbo 中的優(yōu)雅關(guān)閉實(shí)現(xiàn),由于現(xiàn)在幾乎所有 Java 應(yīng)用都會(huì)采用 Spring 作為開發(fā)框架葫笼,所以 dubbo 一般是集成在 Spring 框架中供我們使用的,它的優(yōu)雅關(guān)閉和 Spring 有著緊密的聯(lián)系洋丐。

5.1 Dubbo 在 Spring 環(huán)境下的優(yōu)雅關(guān)閉

在本文第四小節(jié)《4. Spring的優(yōu)雅關(guān)閉機(jī)制》的介紹中,我們知道在 Spring 的優(yōu)雅關(guān)閉流程中郭宝,Spring 的 ShutdownHook 線程會(huì)首先發(fā)布 ContextClosedEvent 事件,該事件是一個(gè)同步事件衔统,ShutdownHook 線程發(fā)布完該事件緊接著就會(huì)同步執(zhí)行該事件的監(jiān)聽器,當(dāng)在事件監(jiān)聽器中處理完 ContextClosedEvent 事件之后,在回過頭來執(zhí)行 destroyBeans() 方法并依次觸發(fā)剩下的四種回調(diào)來銷毀 IOC 容器中的 Bean 迷郑。

Spring優(yōu)雅關(guān)閉流程.png

由于在處理 ContextClosedEvent 事件的時(shí)候畦攘,Dubbo 所依賴的一些關(guān)鍵 bean 這時(shí)還沒有被銷毀叹螟,所以 dubbo 定義了一個(gè) DubboBootstrapApplicationListener 用來監(jiān)聽 ContextClosedEvent 事件,并在 onContextClosedEvent 事件處理方法中調(diào)用 dubboBootstrap.stop() 方法開啟 dubbo 的優(yōu)雅關(guān)閉流程良价。

public class DubboBootstrapApplicationListener extends OneTimeExecutionApplicationContextEventListener
        implements Ordered {

    @Override
    public void onApplicationContextEvent(ApplicationContextEvent event) {
        // 這里是 Spring 的同步事件,publishEvent 和處理 Event 是在同一個(gè)線程中
        if (event instanceof ContextRefreshedEvent) {
            onContextRefreshedEvent((ContextRefreshedEvent) event);
        } else if (event instanceof ContextClosedEvent) {
            onContextClosedEvent((ContextClosedEvent) event);
        }
    }

    private void onContextClosedEvent(ContextClosedEvent event) {
        // spring 在 shutdownhook 中會(huì)先觸發(fā) ContextClosedEvent ,然后在銷毀 spring beans
        // 所以這里 dubbo 開始優(yōu)雅關(guān)閉時(shí)泌射,依賴的 spring beans 并未銷毀
        dubboBootstrap.stop();
    }

}

當(dāng)服務(wù)提供者 ServiceBean 和服務(wù)消費(fèi)者 ReferenceBean 被初始化時(shí),會(huì)將 DubboBootstrapApplicationListener 注冊(cè)到 Spring 容器中。并開始監(jiān)聽 ContextClosedEvent 事件和 ContextRefreshedEvent 事件拒秘。

public class ServiceClassPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware,
        ResourceLoaderAware, BeanClassLoaderAware {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        // @since 2.7.5 注冊(cè)spring啟動(dòng) 關(guān)閉事件的listener
        //在事件回調(diào)中中調(diào)用啟動(dòng)類 DubboBootStrap的start  stop來啟動(dòng) 關(guān)閉dubbo應(yīng)用
        registerBeans(registry, DubboBootstrapApplicationListener.class);
      
                  ........省略.......
    }
}

5.2 Dubbo 優(yōu)雅關(guān)閉流程簡(jiǎn)介

由于本文的主題是介紹優(yōu)雅關(guān)閉的一整條流程主線蔑歌,所以這里筆者只是簡(jiǎn)要介紹 Dubbo 優(yōu)雅關(guān)閉的主流程园匹,相關(guān)細(xì)節(jié)部分筆者會(huì)在后續(xù)的 dubbo 源碼解析系列里為大家詳細(xì)介紹 Dubbo 優(yōu)雅關(guān)閉的細(xì)節(jié)。為了避免本文發(fā)散太多,我們這里還是聚焦于流程主線紊馏。

public class DubboBootstrap extends GenericEventListener {

    public DubboBootstrap stop() throws IllegalStateException {
        destroy();
        return this;
    }

}

這里的核心邏輯其實(shí)就是我們?cè)凇?.2 優(yōu)雅關(guān)閉》小節(jié)中介紹的兩大優(yōu)雅關(guān)閉主題:

  • 從當(dāng)前正在關(guān)閉的應(yīng)用實(shí)例上切走現(xiàn)有生產(chǎn)流量原叮。

  • 保證業(yè)務(wù)無損。

這里大家只需要了解 Dubbo 優(yōu)雅關(guān)閉的主流程即可唯欣,相關(guān)細(xì)節(jié)筆者后續(xù)會(huì)有一篇專門的文章詳細(xì)為大家介紹。

    public void destroy() {
        if (destroyLock.tryLock()) {
            try {
                DubboShutdownHook.destroyAll();

                if (started.compareAndSet(true, false)
                        && destroyed.compareAndSet(false, true)) {

                    //取消注冊(cè)
                    unregisterServiceInstance();
                    //取消元數(shù)據(jù)服務(wù)
                    unexportMetadataService();
                    //停止暴露服務(wù)
                    unexportServices();
                    //取消訂閱服務(wù)
                    unreferServices();
                    //注銷注冊(cè)中心
                    destroyRegistries();
                    //關(guān)閉服務(wù)
                    DubboShutdownHook.destroyProtocols();
                    //銷毀注冊(cè)中心客戶端實(shí)例
                    destroyServiceDiscoveries();
                    //清除應(yīng)用配置類以及相關(guān)應(yīng)用模型
                    clear();
                    //關(guān)閉線程池
                    shutdown();
                    //釋放資源
                    release();
                }
            } finally {
                destroyLock.unlock();
            }
        }
    }

從以上內(nèi)容可以看出,Dubbo 的優(yōu)雅關(guān)閉依托于 Spring ContextClosedEvent 事件的發(fā)布,而 ContextClosedEvent 事件的發(fā)布又依托于 Spring ShutdownHook 的注冊(cè)挡鞍。

dubbo spring環(huán)境優(yōu)雅關(guān)閉.png

從《4.6.1 Spring 中 ShutdownHook 的注冊(cè)》小節(jié)的介紹中我們知道默伍,在 SpringBoot 環(huán)境下炼蹦,SpringBoot 在啟動(dòng)的時(shí)候會(huì)為我們調(diào)用 ApplicationContext#registerShutdownHook 方法去主動(dòng)注冊(cè) ShutdownHook 。我們不需要手動(dòng)注冊(cè)。

而在一個(gè)純 Spring 環(huán)境下,Spring 框架并不會(huì)為我們主動(dòng)調(diào)用 registerShutdownHook 方法去向 JVM 注冊(cè) ShutdownHook 的伪节,我們需要手動(dòng)調(diào)用 registerShutdownHook 方法去注冊(cè)。

所以 Dubbo 這里為了兼容 SpringBoot 環(huán)境和純 Spring 環(huán)境下的優(yōu)雅關(guān)閉,引入了 SpringExtensionFactory類 铐炫,只要在 Spring 環(huán)境下都會(huì)調(diào)用 registerShutdownHook 去向 JVM 注冊(cè) Spring 的 ShutdownHook 。

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

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

    public static void addApplicationContext(ApplicationContext context) {
        CONTEXTS.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            //在spring啟動(dòng)成功之后設(shè)置shutdownHook(兼容非SpringBoot環(huán)境)
            ((ConfigurableApplicationContext) context).registerShutdownHook();
        }
    }

}

當(dāng)服務(wù)提供者 ServiceBean 和服務(wù)消費(fèi)者 ReferenceBean 在初始化完成之后,會(huì)回調(diào) SpringExtensionFactory#addApplicationContext 方法注冊(cè) ShutdownHook 。

public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
        ApplicationContextAware, BeanNameAware, ApplicationEventPublisherAware {

   @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        SpringExtensionFactory.addApplicationContext(applicationContext);
    }

}
public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean,
        ApplicationContextAware, InitializingBean, DisposableBean {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        SpringExtensionFactory.addApplicationContext(applicationContext);
    }

}

以上就是 Dubbo 在 Spring 集成環(huán)境下的優(yōu)雅關(guān)閉全流程,下面我們來看下 Dubbo 在非 Spring 環(huán)境下的優(yōu)雅關(guān)閉流程。

5.3 Dubbo 在非 Spring 環(huán)境下的優(yōu)雅關(guān)閉

在上小節(jié)的介紹中我們知道 Dubbo 在 Spring 環(huán)境下依托 Spring 的 ShutdownHook ,通過監(jiān)聽 ContextClosedEvent 事件嚷兔,從而觸發(fā) Dubbo 的優(yōu)雅關(guān)閉流程竟块。

而到了非 Spring 環(huán)境下前弯,Dubbo 就需要定義自己的 ShutdownHook 询枚,從而引入了 DubboShutdownHook 刷后,直接將優(yōu)雅關(guān)閉流程封裝在自己的 ShutdownHook 中執(zhí)行。

public class DubboBootstrap extends GenericEventListener {

    private DubboBootstrap() {
        configManager = ApplicationModel.getConfigManager();
        environment = ApplicationModel.getEnvironment();

        DubboShutdownHook.getDubboShutdownHook().register();
        ShutdownHookCallbacks.INSTANCE.addCallback(new ShutdownHookCallback() {
            @Override
            public void callback() throws Throwable {
                DubboBootstrap.this.destroy();
            }
        });
    }

}
public class DubboShutdownHook extends Thread {

   public void register() {
        if (registered.compareAndSet(false, true)) {
            DubboShutdownHook dubboShutdownHook = getDubboShutdownHook();
            Runtime.getRuntime().addShutdownHook(dubboShutdownHook);
            dispatch(new DubboShutdownHookRegisteredEvent(dubboShutdownHook));
        }
    }

    @Override
    public void run() {
        if (logger.isInfoEnabled()) {
            logger.info("Run shutdown hook now.");
        }

        callback();
        doDestroy();
    }

   private void callback() {
        callbacks.callback();
    }

}

從源碼中我們看到,當(dāng)我們的 Dubbo 應(yīng)用程序接收到 kill -15 pid 信號(hào)時(shí),JVM 捕獲到 SIGTERM(15) 信號(hào)之后,就會(huì)觸發(fā) DubboShutdownHook 線程運(yùn)行,從而通過 callback() 又回調(diào)了上小節(jié)中介紹的 DubboBootstrap#destroy 方法(dubbo 的整個(gè)優(yōu)雅關(guān)閉邏輯全部封裝在這里)洽沟。

dubbo 非Spring環(huán)境下優(yōu)雅關(guān)閉流程.png
public class DubboBootstrap extends GenericEventListener {

    public void destroy() {
        if (destroyLock.tryLock()) {
            try {
                DubboShutdownHook.destroyAll();

                if (started.compareAndSet(true, false)
                        && destroyed.compareAndSet(false, true)) {

                    ........取消注冊(cè)......
                  
                    ........取消元數(shù)據(jù)服務(wù)........
                  
                    ........停止暴露服務(wù)........
                 
                    ........取消訂閱服務(wù)........
                 
                    ........注銷注冊(cè)中心........
                 
                    ........關(guān)閉服務(wù)........
                  
                    ........銷毀注冊(cè)中心客戶端實(shí)例........
                 
                    ........清除應(yīng)用配置類以及相關(guān)應(yīng)用模型........
                
                    ........關(guān)閉線程池........
                 
                    ........釋放資源........
                 
                }
            } finally {
                destroyLock.unlock();
            }
        }
    }

}

5.4 啊哈跷车!Bug!

前邊我們?cè)凇?.1 Dubbo在Spring環(huán)境下的優(yōu)雅關(guān)閉》小節(jié)和《5.3 Dubbo在非Spring環(huán)境下的優(yōu)雅關(guān)閉》小節(jié)中介紹的這兩個(gè)環(huán)境的下的優(yōu)雅關(guān)閉方案水援,當(dāng)它們?cè)诟髯缘膱?chǎng)景下運(yùn)行的時(shí)候是沒有任何問題的或渤。

但是當(dāng)這兩種方案結(jié)合在一起運(yùn)行,就出大問題了~~~

還記得筆者在《3.2 使用 ShutdownHook 的注意事項(xiàng)》小節(jié)中特別強(qiáng)調(diào)的一點(diǎn):

  • ShutdownHook 其實(shí)本質(zhì)上是一個(gè)已經(jīng)被初始化但是未啟動(dòng)的 Thread 奔害,這些通過 Runtime.getRuntime().addShutdownHook 方法注冊(cè)的 ShutdownHooks 端考,在 JVM 進(jìn)程關(guān)閉的時(shí)候會(huì)被啟動(dòng)并發(fā)執(zhí)行寻馏,但是并不會(huì)保證執(zhí)行順序

所以在編寫 ShutdownHook 中的邏輯時(shí)核偿,我們應(yīng)該確保程序的線程安全性诚欠,并盡可能避免死鎖。最好是一個(gè) JVM 進(jìn)程只注冊(cè)一個(gè) ShutdownHook 漾岳。

Dubbo在Spring環(huán)境下的優(yōu)雅關(guān)閉Bug.png

那么現(xiàn)在 JVM 中我們注冊(cè)了兩個(gè) ShutdownHook 線程腾么,一個(gè) Spring 的 ShutdownHook 摄杂,另一個(gè)是 Dubbo 的 ShutdonwHook 诈泼。那么這會(huì)引出什么問題呢?

經(jīng)過前邊的內(nèi)容介紹我們知道捅儒,無論是在 Spring 的 ShutdownHook 中觸發(fā)的 ContextClosedEvent 事件還是在 Dubbo 的 ShutdownHook 中執(zhí)行的 CallBack 液样。最終都會(huì)調(diào)用到 DubboBootstrap#destroy 方法執(zhí)行真正的優(yōu)雅關(guān)閉邏輯。

public class DubboBootstrap extends GenericEventListener {

    private final Lock destroyLock = new ReentrantLock();

    public void destroy() {
        if (destroyLock.tryLock()) {
            try {
                DubboShutdownHook.destroyAll();

                if (started.compareAndSet(true, false)
                        && destroyed.compareAndSet(false, true)) {
                    
                        .......dubbo應(yīng)用的優(yōu)雅關(guān)閉.......
                 
                }
            } finally {
                destroyLock.unlock();
            }
        }
    }

}

讓我們來設(shè)想一個(gè)這種的場(chǎng)景:當(dāng) Spring 的 ShutdownHook 線程和 Dubbo 的 ShutdownHook 線程同時(shí)執(zhí)行并且在同一個(gè)時(shí)間點(diǎn)來到 DubboBootstrap#destroy 方法中爭(zhēng)奪 destroyLock 巧还。

  • Dubbo 的 ShutdownHook 線程獲得 destroyLock 進(jìn)入 destroy() 方法體開始執(zhí)行優(yōu)雅關(guān)閉邏輯鞭莽。

  • Spring 的 ShutdownHook 線程沒有獲得 destroyLock,退出 destroy() 方法麸祷。

Dubbo優(yōu)雅關(guān)閉Bug.png

在 Spring 的 ShutdownHook 線程退出 destroy() 方法之后緊接著就會(huì)執(zhí)行 destroyBeans() 方法銷毀 IOC 容器中的 Bean 澎怒,這里邊肯定涉及到一些關(guān)鍵業(yè)務(wù) Bean 的銷毀,比如:數(shù)據(jù)庫(kù)連接池阶牍,以及 Dubbo 相關(guān)的核心 Bean喷面。

于此同時(shí) Dubbo 的 ShutdownHook 線程開始執(zhí)行優(yōu)雅關(guān)閉邏輯,《1.2 優(yōu)雅關(guān)閉》小節(jié)中我們提到走孽,優(yōu)雅關(guān)閉要保證業(yè)務(wù)無損惧辈。所以需要將剩下正在進(jìn)行中的業(yè)務(wù)流程繼續(xù)處理完畢并將業(yè)務(wù)處理結(jié)果響應(yīng)給客戶端。但是這時(shí)依賴的一些業(yè)務(wù)關(guān)鍵 Bean 已經(jīng)被銷毀磕瓷,比如數(shù)據(jù)庫(kù)連接池盒齿,這時(shí)執(zhí)行數(shù)據(jù)庫(kù)操作就會(huì)拋出 CannotGetJdbcConnectionException 。導(dǎo)致優(yōu)雅關(guān)閉失敗,對(duì)業(yè)務(wù)造成了影響县昂。

5.5 Bug 的修復(fù)

該 Bug 最終在 apache dubbo 2.7.15 版本中被修復(fù)

詳情可查看Issue:https://github.com/apache/dubbo/issues/7093

經(jīng)過上小節(jié)的分析,我們知道既然這個(gè) Bug 產(chǎn)生的原因是由于 Spring 的 ShutdownHook 線程和 Dubbo 的 ShutdownHook 線程并發(fā)執(zhí)行所導(dǎo)致的陷舅。

那么當(dāng)我們處于 Spring 環(huán)境下的時(shí)候倒彰,就將 Dubbo 的 ShutdownHook 注銷掉即可。

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

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

    public static void addApplicationContext(ApplicationContext context) {
        CONTEXTS.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            // 注冊(cè) Spring 的 ShutdownHook
            ((ConfigurableApplicationContext) context).registerShutdownHook();
            // 在 Spring 環(huán)境下將 Dubbo 的 ShutdownHook 取消掉
            DubboShutdownHook.getDubboShutdownHook().unregister();
        }
    }
}

而在非 Spring 環(huán)境下莱睁,我們依然保留 Dubbo 的 ShutdownHook 待讳。

public class DubboBootstrap {

    private DubboBootstrap() {
        configManager = ApplicationModel.getConfigManager();
        environment = ApplicationModel.getEnvironment();

        DubboShutdownHook.getDubboShutdownHook().register();
        ShutdownHookCallbacks.INSTANCE.addCallback(DubboBootstrap.this::destroy);
    }

}

以上內(nèi)容就是 Dubbo 的整個(gè)優(yōu)雅關(guān)閉主線流程,以及優(yōu)雅關(guān)閉 Bug 產(chǎn)生的原因和修復(fù)方案仰剿。


在 Dubbo 的優(yōu)雅關(guān)閉流程中最終會(huì)通過 DubboShutdownHook.destroyProtocols() 關(guān)閉底層服務(wù)创淡。

public class DubboBootstrap extends GenericEventListener {

    private final Lock destroyLock = new ReentrantLock();

    public void destroy() {
        if (destroyLock.tryLock()) {
            try {
                DubboShutdownHook.destroyAll();

                if (started.compareAndSet(true, false)
                        && destroyed.compareAndSet(false, true)) {
                    
                        .......dubbo應(yīng)用的優(yōu)雅關(guān)閉.......
                    //關(guān)閉服務(wù)
                    DubboShutdownHook.destroyProtocols();

                        .......dubbo應(yīng)用的優(yōu)雅關(guān)閉.......

                }
            } finally {
                destroyLock.unlock();
            }
        }
    }

}

在 Dubbo 服務(wù)的銷毀過程中,會(huì)通過調(diào)用 server.close 關(guān)閉底層的 Netty 服務(wù)南吮。

public class DubboProtocol extends AbstractProtocol {

   @Override
    public void destroy() {
        for (String key : new ArrayList<>(serverMap.keySet())) {
            ProtocolServer protocolServer = serverMap.remove(key);
            RemotingServer server = protocolServer.getRemotingServer();
            server.close(ConfigurationUtils.getServerShutdownTimeout());
             ...........省略........
        }

         ...........省略........
}

最終觸發(fā) Netty 的優(yōu)雅關(guān)閉琳彩。

public class NettyServer extends AbstractServer implements RemotingServer {

    @Override
    protected void doClose() throws Throwable {
        ..........關(guān)閉底層Channel......
        try {
            if (bootstrap != null) {
                // 關(guān)閉 Netty 的主從 Reactor 線程組
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        } catch (Throwable e) {
            logger.warn(e.getMessage(), e);
        }
        .........清理緩存Channel數(shù)據(jù).......
    }

}

由于字?jǐn)?shù)的限制,在下一篇文章中筆者會(huì)為大家詳細(xì)介紹 Netty 優(yōu)雅關(guān)閉的相關(guān)內(nèi)內(nèi)容~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末部凑,一起剝皮案震驚了整個(gè)濱河市露乏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌涂邀,老刑警劉巖瘟仿,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異比勉,居然都是意外死亡劳较,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門浩聋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來观蜗,“玉大人,你說我怎么就攤上這事赡勘∩┍悖” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵闸与,是天一觀的道長(zhǎng)毙替。 經(jīng)常有香客問我,道長(zhǎng)践樱,這世上最難降的妖魔是什么厂画? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮拷邢,結(jié)果婚禮上袱院,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好忽洛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布腻惠。 她就那樣靜靜地躺著,像睡著了一般欲虚。 火紅的嫁衣襯著肌膚如雪集灌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天复哆,我揣著相機(jī)與錄音欣喧,去河邊找鬼。 笑死梯找,一個(gè)胖子當(dāng)著我的面吹牛唆阿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锈锤,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼驯鳖,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了牙咏?” 一聲冷哼從身側(cè)響起臼隔,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎妄壶,沒想到半個(gè)月后摔握,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丁寄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年氨淌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伊磺。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盛正,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屑埋,到底是詐尸還是另有隱情豪筝,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布摘能,位于F島的核電站续崖,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏团搞。R本人自食惡果不足惜严望,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望逻恐。 院中可真熱鬧像吻,春花似錦峻黍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惭每,卻和暖如春阵面,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背洪鸭。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仑扑,地道東北人览爵。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像镇饮,于是被迫代替她去往敵國(guó)和親蜓竹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容