![](http://static.javashuo.com/static/loading.gif)
陶輝nginx
NGINX頂級專家web
現任職於杭州智鏈達數據有限公司CTO。著有《深刻理解Nginx》一書,在極客時間上開設有一門包含150多節課程的視頻課《Nginx核心知識100講》,並在阿里、騰訊、思科等大廠工做中對Nginx有深刻的實踐。
算法
十多年前,咱們大能夠升級前在官網上發個公告,聲明某個凌晨不提供服務,那時能夠從容地中止進程、更換程序、重啓服務。然而,當下的用戶卻很難容忍停機升級這種體驗,尤爲對於接入層充當負載均衡的Nginx來講,它的併發鏈接數以百萬計,哪怕只終止Nginx進程1秒鐘,也會致使大量用戶出現業務中斷。瀏覽器
怎樣保證升級高負載的Nginx時,不影響到海量的在線用戶呢?並且,雖然官方Nginx是穩定的,但畢竟Nginx在編譯期能夠定製加入各類C模塊,若是某些模塊在升級後出現異常,就須要將Nginx回滾到舊版本,此時又怎樣保證降級時也不會影響到正常服務的在線用戶?安全
實際上,Nginx的熱升級功能能夠解決上述問題,它容許新老版本灰度地平滑過渡,這受益於Nginx的多進程架構。本文將介紹該如何升級、回滾Nginx,以及Nginx的進程架構是怎樣保障不對用戶產生影響的。理解熱升級後,你也能更透徹的掌握熱加載功能(reload使新配置文件生效),由於熱加載至關於簡化版的熱升級。服務器
►怎樣才能平滑升級程序?微信
最簡單的升級方式,是關閉現有的舊進程後,再基於新程序啓動進程。許多可用性要求不高的場景,就是這麼作的。然而,在多數服務SLA(Service-Level Agreement)高達4個9以上的今天(99.99%意味着服務一年內的總宕機時間不得超過0.876小時),這種簡單粗暴的方式不可取,它對於服務質量影響太大。當舊進程關閉時,操做系統會對進程打開的全部TCP鏈接發送RST復位報文,強行關閉TCP鏈接,接着,全部瀏覽器都會收到ERR_CONNECTION_RESET錯誤。
爲了避免影響現有TCP鏈接,能不能在命令行中先啓動新程序,由升級後的新程序服務後創建的TCP鏈接,而原TCP鏈接在所有天然終止後,再關閉老進程呢?這其實作不到。websocket
這是由於服務器程序不一樣於客戶端,一般它須要監聽80等指定端口,這樣客戶端才能針對明確的80端口創建TCP鏈接,而OSI傳輸層(由Linux內核實現)保證報文能夠到達Nginx進程。所以,兩個徹底不一樣的進程是不能打開同一個端口的,若是咱們在舊進程關閉前,啓動新程序,每每會遇到bind failed( Address already in use)錯誤,致使進程沒法啓動。架構
事實上,上述經過新老進程並存的升級方案,就是平滑升級的最佳解決方案。可是怎樣繞過同一端口不能被兩個進程同時打開的限制呢?其實經過父子進程(參見wiki)就能夠作到,而Nginx的平滑升級也正是這麼作到的。併發
操做系統規定,每個進程都必須由另外一個進程啓動,這兩個進程就稱爲父子進程,其中,子進程自動繼承父進程已經申請到的資源,好比監聽的80端口。在Linux中,子進程是由fork函數建立的,最初它只是父進程的副本。好比在生產環境中啓動Nginx時(即master_process on;),nginx會在綁定80端口後再用fork函數生成worker子進程(注意,nginx會自動將父進程名字改成nginx: master process),這樣,worker進程也能夠經過80端口與客戶端創建TCP鏈接。固然,多個worker進程同時監聽80端口時,系統內核會有一套算法決定某個TCP鏈接由哪一個worker進程處理(能夠參考Linux 3.9內核版本後提供的SO_REUSEPORT選項),均衡多個worker子進程間的負載,以下圖所示:
![](http://static.javashuo.com/static/loading.gif)
那麼,既然master與worker能夠綁定同一端口,那麼升級新版本nginx時,也由如今的老master進程啓動(子進程默認是父進程的副本,但經過exec函數能夠載入新版本的nginx程序,下文會詳細介紹),這樣,新master進程就是老master進程的子進程,能夠共享老版本nginx已經打開的、包括端口在內的各種資源。至此,兩個版本的nginx皆在運行中,只要老版本的nginx中止創建新鏈接,內核天然只會將新的TCP鏈接交給新版本的nginx處理,等到老版本nginx處理完現存的客戶請求後可令其退出,這就完成了平滑升級。
那麼,到底怎樣通知nginx升級呢?下面咱們來看詳細的操做步驟。
接着,你能夠用ps命令找到master進程的pid,並經過kill命令向它發送USR2信號,這樣master進程就會生成新的子進程,同時用exec函數載入新版本的nginx二進制文件,並將進程更名爲nginx: master process。固然,新的master也會依據nginx.conf中的內容,再次啓動新worker子進程提供服務,這些父子進程的關係以下圖所示:
![](http://static.javashuo.com/static/loading.gif)
此時,老版本的nginx已經中止監聽80端口,你能夠經過netstat命令看到,如今只有新版本的nginx進程會監聽80端口了,從此新創建的TCP鏈接都會由新版本進程處理:
![](http://static.javashuo.com/static/loading.gif)
那麼,如何讓老版本的nginx進程在處理完現存TCP鏈接後退出呢?很簡單,使用nginx的優雅退出功能便可,具體經過kill向老master進程發送WINCH或者QUIT信號便可:
![](http://static.javashuo.com/static/loading.gif)
當老版本的master、worker進程都退出後,根據Linux內核的規則,pid爲1的系統守護進程將成爲新master的父進程(目前的守護進程爲systemd,其演進流程參見酷殼上的這篇文章)。
所以,平滑升級Nginx一般會經歷3個階段:
-
僅老nginx進程在運行,此時先備份nginx binary文件,再把新版本的nginx覆蓋原位置,最後經過kill發送USR2信號。 -
新老nginx進程同時並存,此時須要經過信號命令老master進程優雅退出。 當處理完全部請求後,老的nginx進程退出,此時平滑升級完畢。
![](http://static.javashuo.com/static/loading.gif)
在新老nginx並存時,若是向老master進程發送了QUIT信號,那麼在它的worker子進程退出後,老master進程也會自行退出。這時若是須要重新版本回滾到老版本,就得從新執行一次「升級」。還有一種更簡單的回滾方法,向老master進程發送WINCH信號,這樣老worker進程所有退出後,老master進程仍然存在。
![](http://static.javashuo.com/static/loading.gif)
因爲老master進程是由老版本的nginx二進制文件啓動,這樣回滾很容易,只要將它的worker進程從新拉起,便可向用戶提供舊版本服務,同時要求新版本的Nginx進行優雅退出便可。
![](http://static.javashuo.com/static/loading.gif)
這就是Nginx平滑升級和回滾的全過程,這是咱們在大流量生產環境中必須掌握的步驟。
►Nginx是怎樣實現 「平滑」升級的?
最後,咱們結合Nginx的進程架構,從實現層面分析Nginx究竟是如何執行平滑升級的,這樣就能夠快速定位熱升級時可能遇到的問題。
平滑升級涉及兩個關鍵的子功能,一是在收到USR2信號後,啓動新版本Nginx;二是將再也不監聽端口的nginx進程優雅退出。先來看USR2信號的處理。
在Linux中,使用fork函數就能夠生成子進程副本,再用execve函數載入新版本的nginx二進制文件運行,就進入新老版本nginx並存的階段。此時,寫入master進程pid的nginx.pid文件內容會發生變化(瞭解了這一點就清楚找不到nginx.pid文件後,nginx的命令行爲什麼再也不生效)。
因爲nginx支持經過命令行發送信號,好比上文介紹過的熱加載,其實與向master進程發送HUP信號是徹底一致的。但平常咱們更習慣經過更方便的nginx -s reload命令行來完成,reload命令在讀取nginx.pid文件中的進程id後,就會向master進程發送HUP信號。
![](http://static.javashuo.com/static/loading.gif)
在升級過程當中新版本的nginx啓動後,nginx.pid中只會存放新master進程的id,而老master進程的id則會改放在nginx.pid.oldbin文件中。
![](http://static.javashuo.com/static/loading.gif)
當老版本的master進程優雅退出後,nginx.pid.oldbin文件會被自動刪除。這些細節能夠協助分析熱升級時遇到的問題。
再來看nginx是如何優雅退出的,即worker進程怎樣斷定全部TCP鏈接都處理完了。當master進程收到QUIT或者WINCH信號後,會向全部worker子進程發送QUIT信號。而worker進程收到QUIT信號後,會作如下4件事:
設置worker_shutdown_timeout定時器,由於有些應用協議nginx並不解析,也就無從判斷什麼時候會結束。好比,使用stream模塊作四層負載均衡,或者用做七層的websocket反向代理時,nginx都沒法判斷什麼時候該關閉鏈接。所以,舊版本的nginx進程會長時間存在。設置定時器後,worker進程會在worker_shutdown_timeout秒後強行退出。固然,一般狀況下不須要配置worker_shutdown_timeout,由於老worker進程長時間存在並不會影響新nginx的業務。
關閉監聽着的全部端口;
關閉全部空閒的TCP鏈接;
設置ngx_exiting標誌位爲1(協助業務模塊關閉鏈接),等待業務模塊關閉全部的TCP鏈接後,自行退出進程。好比對於HTTP短鏈接請求而言(即HTTP頭部中存在Connection: closed),當nginx發送完響應後就能夠主動關閉TCP鏈接。若是是HTTP長鏈接(即存在Connection: keep-alive頭部),正常狀況下應當由客戶端關閉鏈接,或者鏈接上處理過的請求個數超過了keepalive_request_count才能由nginx關閉鏈接,但在優雅退出這個場景中,nginx能夠在處理完當前http請求後馬上關閉鏈接,以下代碼所示:
if (!ngx_terminate
&& !ngx_exiting //在優雅退出時,ngx_exiting會置爲1
&& r->keepalive
&& clcf->keepalive_timeout > 0)
{
ngx_http_set_keepalive(r); //做爲HTTP長鏈接繼續複用
return;
}複製
worker進程正是按照這樣的優雅退出流程自行關閉的。熱重載新的nginx.conf配置文件時也使用了優雅退出這一功能,以下圖所示:
![](http://static.javashuo.com/static/loading.gif)
►小結
本文介紹了Nginx熱升級的原理、運維操做步驟及架構實現。
平滑升級的前提是同時啓動新老2個版本的Nginx進程,其中老進程服務於正在傳輸數據的TCP鏈接,而新進程處理以後創建的TCP鏈接。因爲新老進程須要同時打開80等監聽端口,這就須要利用父子進程能夠共享資源這一特性,所以,新版本的Nginx必須由老的master進程啓動。
Nginx提供的熱升級功能,須要使用Linux命令行的kill命令發送信號。其中,USR2信號用於命令老master進程啓動新版本的nginx;WINCH信號用於令老master進程優雅的終止worker子進程;HUP信號用於回滾時啓動老worker進程;QUIT信號用於令老master及worker進程優雅地退出。
Nginx爲了提供-s reload等命令行,須要將master進程的pid保存到nginx.pid文件中。須要注意的是,在熱升級中nginx.pid文件的內容會發生變化。
優雅退出是平滑升級的關鍵,它須要業務模塊的支持。好比http模塊一般能夠完美的實現優雅退出,而其餘一些不解析協議內容的模塊就很難作到,此時,nginx提供了優雅退出定時器,限制worker進程在worker_shutdown_timeout秒內必須關閉。這些措施都進一步加強了熱升級的適用性。
最後能不能請你談談,你還使用過哪些其餘支持熱升級的軟件?它們的實現方式與本文介紹的Nginx熱升級方案類似嗎?具體是怎樣實現的?歡迎你在帖子下方留言,與我一塊兒探討更好的熱部署實現方案。
![](http://static.javashuo.com/static/loading.gif)
活動推薦
![](http://static.javashuo.com/static/loading.gif)
本文分享自微信公衆號 - NGINX開源社區(gh_0d2551f1bdb6)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。