伺服器無失真升級技術解析

2023-07-01 21:00:23

宣告:本人原創文章,詳細內容已釋出在我的微信個人技術公眾號---網路技術修煉,公眾號總結普及網路基礎知識,包括基礎原理、網路方案、開發經驗和問題定位案例等,歡迎關注。

概述

軟體工程中持續迭代和更新是必不可少的,在伺服器端軟體更新時,保持服務的連續性是一項關鍵任務。本文將從技術角度解析伺服器端軟體更新過程如何實現不停止服務的重要功能。

在進行熱升級時,程序的程式碼和資料都是非常重要的。為了實現程式碼的更新,同時又不丟失有用的資料,需要採取一些措施。有用的資料包括記憶體中的資料和檔案描述符。對於記憶體中的資料,例如設定資訊,可以通過將其落盤到組態檔中來實現保留。這樣,在升級過程中,新的程序可以讀取組態檔並繼續使用之前的設定。而對於檔案描述符,可以採用一種叫做UNIX域通訊端的機制,在程序之間進行遷移。通過這種方式,新程序可以接管原來程序的檔案描述符,從而保持之前開啟的檔案和網路連線的狀態。在某些情況下,專案可能會選擇不遷移檔案描述符,而是通過讓新舊程序共同處理一段時間的請求來逐步過渡。這樣,新程序可以逐漸接收和處理新的請求,而老程序則繼續處理舊的請求,直到所有請求都由新程序處理完畢。

另外,為了減輕對使用者端的影響,還可以採用一些HTTP協定的特性。例如,在HTTP1中可以使用"Connection: Close"頭部欄位,告知使用者端斷開連線並重新連線。而在HTTP2中,可以使用Goaway幀來類似地通知使用者端斷開連線。這樣一來,使用者端就能夠及時與新程序建立新的連線,以繼續進行請求和響應的處理。

通過這些措施和優化方法,可以實現熱升級過程中程式碼更新和資料保留的目標,並儘可能減少對系統和使用者端的影響。

詳解

通過fork + execve實現無失真升級

典型專案

nginx

nginx為例解析

互動流程

  • 先不停掉老程序,啟動新程序。
  • 老程序繼續處理仍然沒有處理完的請求,但不再接受新請求。
  • 新程序接受新請求。
  • 老程序處理完所有存量請求,關閉所有連線,退出。

訊號支援

官方檔案:http://nginx.org/en/docs/control.html

nginx中master程序為管理程序,woker程序為master程序fork出的子程序,是處理網路的程序。

master程序支援的訊號

TERM,INT

快速退出

QUIT

優雅退出master+worker程序(worker程序處理完存量請求再退出)

KILL

強子終止程序

HUP

使用新的的設定啟動worker程序,並優雅退出老的worker程序

USR1

重新開啟紀錄檔檔案

USR2

升級可執行檔案(即啟動新的master程序)

WINCH

優雅退出woker程序

worker程序支援的訊號:

TERM,INT

快速退出

QUIT

優雅退出(處理完存量請求再退出)

USR1

重新開啟紀錄檔檔案

實驗

  • 更新前程序狀態檢視。
#ps -ef | grep nginx
root      82556      1  0 11:58 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     82562  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82563  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82564  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82565  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82566  82556  0 11:58 ?        00:00:00 nginx: worker process
nginx     82567  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82569  82556  2 11:58 ?        00:00:03 nginx: worker process
nginx     82570  82556 14 11:58 ?        00:00:24 nginx: worker process

#cat /app/nginx/logs/nginx.pid
82556
可以看出nginx.pid記錄的是當前master的程序號。
  • 將舊Nginx二進位制換成新Nginx二進位制(注意備份舊二進位制)。
  • 向master程序傳送USR2訊號。
kill -USR2 `cat /app/nginx/logs/nginx.pid`
    • nginx收到訊號會建立新master並fork出新worker,此時新老共存,都會處理請求。

執行後結果

#ps -ef | grep nginx
root      82556      1  0 11:58 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     82562  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82563  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82564  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82565  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82566  82556  0 11:58 ?        00:00:01 nginx: worker process
nginx     82567  82556  0 11:58 ?        00:00:02 nginx: worker process
nginx     82569  82556  2 11:58 ?        00:00:06 nginx: worker process
nginx     82570  82556 13 11:58 ?        00:00:43 nginx: worker process
root      85710  82556  0 12:04 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     85716  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85717  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85718  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85719  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85720  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85721  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85723  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85724  85710  0 12:04 ?        00:00:00 nginx: worker process

#cat /app/nginx/logs/nginx.pid
85710
可以看出nginx.pid已經變成新master程序號

#cat /app/nginx/logs/nginx.pid.oldbin
82556
nginx.pid.oldbin存放老master程序號。
  • 向老master程序傳送WINCH訊號。
kill -WINCH `cat /app/nginx/logs/nginx.pid.oldbin`
    • nginx的老master程序收到訊號會給所有老worker程序傳送訊號,老worker執行優雅退出。
    • 老worker收到優雅退出訊號後不再接收新請求,只處理存量請求,處理完後進程退出。
#ps -ef | grep nginx
root      82556      1  0 11:58 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     82569  82556  1 11:58 ?        00:00:06 nginx: worker process is shutting down
nginx     82570  82556 11 11:58 ?        00:00:43 nginx: worker process is shutting down
root      85710  82556  0 12:04 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     85716  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85717  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85718  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85719  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85720  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85721  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85723  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85724  85710  0 12:04 ?        00:00:00 nginx: worker process

此過程要不停有請求存取到nginx才能看到worker優雅退出過程,一段時間後存量請求全部處理完畢。

#ps -ef | grep nginx
root      82556      1  0 11:58 ?        00:00:00 nginx: master process ./sbin/nginx
root      85710  82556  0 12:04 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     85716  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85717  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85718  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85719  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85720  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85721  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85723  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85724  85710  0 12:04 ?        00:00:00 nginx: worker process
  • 檢查是否回滾
    • 一段時間後,老woker請求全部處理完,就變成了新老master、新worker共存,此時老master並沒有關閉listen sockets,如果新二進位制有問題還有辦法回滾。
    • 回滾方法:
    • 方法1
    • 向老master傳送HUP訊號。
    • 老master收到HUP訊號會建立worker程序。
    • 向新master傳送QUIT訊號。
    • 新master收到QUIT會退出所有新worker和新master程序。
    • 方法2
    • 向新master傳送TERM訊號。
    • nginx新程序收到這個訊號,對應master和worker會退出,同時老master會建立出老worker繼續工作。
  • 如果不需要回滾,向老master傳送QUIT訊號。
kill -QUIT `cat /app/nginx/logs/nginx.pid.oldbin`
    • 老master收到這個訊號會退出。
#ps -ef | grep nginx
root      85710      1  0 12:04 ?        00:00:00 nginx: master process ./sbin/nginx
nginx     85716  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85717  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85718  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85719  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85720  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85721  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85723  85710  0 12:04 ?        00:00:00 nginx: worker process
nginx     85724  85710  0 12:04 ?        00:00:00 nginx: worker process

原始碼

nginx訊號處理常式:ngx_signal_handler

unix domain sockets

典型專案

envoy

mosn

原理概括

linux環境可以使用下面函數在程序間傳遞fd。

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

mosn為例解析

互動流程

ps:下面程式碼均以v1.5.0版本為例。

listen fd遷移

涉及Domain Socket:

reconfig.sock記錄老程序的監聽

listen.sock記錄新程序的監聽

流程:

  • 老程序啟動時候會執行ReconfigureListener函數,這裡面監聽reconfig.sock並通過寫一個位元組(uc.Write([]byte{0}))阻塞,直到有新程序啟動並執行read才會繼續往下執行。
  • 新程序init-->inheritConfig-->IsReconfigure通過uc.Read(buf)觸發老程序執行reconfigure流程。
  • 老程序通過reconfig.sock向新程序傳送fd。

ReconfigureHandler

sendInheritListeners:老程序將已經存在的 fd 通過 listen.sock 傳送給新程序。

shutdownServers:老程序不再接收新連線,並優雅關閉。

WaitConnectionsDone:處理完存量請求後退出。

  • 新程序接收老程序的fd並處理:GetInheritListeners。

長連線遷移

涉及Domain Socket:conn.sock

流程:

  • 新程序啟動一個協程執行TransferServer,將監聽conn.sock。
  • 老程序通過transferRead和transferWrite進入長連結遷移過程。

參考檔案

nginx官方檔案:http://nginx.org/en/docs/control.html

MOSN 平滑升級原理解析:https://mosn.io/docs/products/structure/smooth-upgrade/

MOSN 原始碼解析 - reconfig 機制:https://mosn.io/blog/code/mosn-reconfig-mechanism/

淺談長連線的平滑重啟:https://www.infoq.cn/article/Qfkq8Wk4FtVot46LaVkR?source=app_share

Nginx vs Envoy vs Mosn 平滑升級原理解析:https://ms2008.github.io/2019/12/28/hot-upgrade/