如何在高併發環境中灰度升級Nginx?


大咖專欄



陶輝nginx

NGINX頂級專家web



現任職於杭州智鏈達數據有限公司CTO。著有《深刻理解Nginx》一書,在極客時間上開設有一門包含150多節課程的視頻課《Nginx核心知識100講》,並在阿里、騰訊、思科等大廠工做中對Nginx有深刻的實踐。
算法

2019年Nginx發佈了6個stable版本以及12個mainline版本,這些發佈要麼修改了重要的漏洞,要麼新增了頗有用的特性。若是你不能及時升級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子進程間的負載,以下圖所示:


那麼,既然master與worker能夠綁定同一端口,那麼升級新版本nginx時,也由如今的老master進程啓動(子進程默認是父進程的副本,但經過exec函數能夠載入新版本的nginx程序,下文會詳細介紹),這樣,新master進程就是老master進程的子進程,能夠共享老版本nginx已經打開的、包括端口在內的各種資源。至此,兩個版本的nginx皆在運行中,只要老版本的nginx中止創建新鏈接,內核天然只會將新的TCP鏈接交給新版本的nginx處理,等到老版本nginx處理完現存的客戶請求後可令其退出,這就完成了平滑升級。

那麼,到底怎樣通知nginx升級呢?下面咱們來看詳細的操做步驟。


Nginx的平滑升級步驟是什麼?
爲了通知運行中的Nginx進程執行升級,咱們必須使用一種進程間通信的方案。在Linux中,通知進程的最簡便方法是信號,Nginx便選擇了這一方案。因爲熱升級涉及到複雜的回滾操做,必須對新老master進程獨立的發送信號,所以Nginx決定由管理員經過命令行中的kill命令發送信號,完成熱升級或者回滾。
咱們先來看熱升級的步驟。升級前,建議你先將老的binary二進制文件後(即/usr/local/nginx/sbin/nginx文件)備份到另外一個位置,爲後續可能的回滾作準備。接着,你須要把新版本的nginx二進制文件覆蓋老文件,這樣,運行中的master進程生成子進程後才能載入新版本的nginx。注意,雖然你覆蓋了老nginx,但並不會影響運行中的老nginx進程。

接着,你能夠用ps命令找到master進程的pid,並經過kill命令向它發送USR2信號,這樣master進程就會生成新的子進程,同時用exec函數載入新版本的nginx二進制文件,並將進程更名爲nginx: master process。固然,新的master也會依據nginx.conf中的內容,再次啓動新worker子進程提供服務,這些父子進程的關係以下圖所示:

此時,老版本的nginx已經中止監聽80端口,你能夠經過netstat命令看到,如今只有新版本的nginx進程會監聽80端口了,從此新創建的TCP鏈接都會由新版本進程處理:



那麼,如何讓老版本的nginx進程在處理完現存TCP鏈接後退出呢?很簡單,使用nginx的優雅退出功能便可,具體經過kill向老master進程發送WINCH或者QUIT信號便可:

當老版本的master、worker進程都退出後,根據Linux內核的規則,pid爲1的系統守護進程將成爲新master的父進程(目前的守護進程爲systemd,其演進流程參見酷殼上的這篇文章)。

所以,平滑升級Nginx一般會經歷3個階段:

  1. 僅老nginx進程在運行,此時先備份nginx binary文件,再把新版本的nginx覆蓋原位置,最後經過kill發送USR2信號。
  2. 新老nginx進程同時並存,此時須要經過信號命令老master進程優雅退出。
  3. 當處理完全部請求後,老的nginx進程退出,此時平滑升級完畢。


在新老nginx並存時,若是向老master進程發送了QUIT信號,那麼在它的worker子進程退出後,老master進程也會自行退出。這時若是須要重新版本回滾到老版本,就得從新執行一次「升級」。還有一種更簡單的回滾方法,向老master進程發送WINCH信號,這樣老worker進程所有退出後,老master進程仍然存在

因爲老master進程是由老版本的nginx二進制文件啓動,這樣回滾很容易,只要將它的worker進程從新拉起,便可向用戶提供舊版本服務,同時要求新版本的Nginx進行優雅退出便可。

這就是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信號。

在升級過程當中新版本的nginx啓動後,nginx.pid中只會存放新master進程的id,而老master進程的id則會改放在nginx.pid.oldbin文件中。

當老版本的master進程優雅退出後,nginx.pid.oldbin文件會被自動刪除。這些細節能夠協助分析熱升級時遇到的問題。

再來看nginx是如何優雅退出的,即worker進程怎樣斷定全部TCP鏈接都處理完了。當master進程收到QUIT或者WINCH信號後,會向全部worker子進程發送QUIT信號。而worker進程收到QUIT信號後,會作如下4件事:

  1. 設置worker_shutdown_timeout定時器,由於有些應用協議nginx並不解析,也就無從判斷什麼時候會結束。好比,使用stream模塊作四層負載均衡,或者用做七層的websocket反向代理時,nginx都沒法判斷什麼時候該關閉鏈接。所以,舊版本的nginx進程會長時間存在。設置定時器後,worker進程會在worker_shutdown_timeout秒後強行退出。固然,一般狀況下不須要配置worker_shutdown_timeout,由於老worker進程長時間存在並不會影響新nginx的業務

  2. 關閉監聽着的全部端口;

  3. 關閉全部空閒的TCP鏈接;

  4. 設置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配置文件時也使用了優雅退出這一功能,以下圖所示:


小結

本文介紹了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熱升級方案類似嗎?具體是怎樣實現的?歡迎你在帖子下方留言,與我一塊兒探討更好的熱部署實現方案。


活動推薦




  你還不能錯過:







你還不能錯過:



你還不能錯過:






URL是如何關聯location配置塊的?
如何configure定製出屬於你的Nginx?
HTTP請求是如何關聯Nginx server{}塊的?
問答、課件及錄像地址:NGINX從入門到精通進階系列培訓
 F5收購NginX




點擊下方閱讀原文,加入NGINX開源社區!

   


本文分享自微信公衆號 - NGINX開源社區(gh_0d2551f1bdb6)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索