背景
MOSN在熱升級上面,也曾做過自己的探索;業界雖然有Nginx Envoy 也都實現了熱升級方式,那麼這裡有什麼異同呢?
Nginx: 透過Fork的方式直接繼承父程式的監聽資訊和連結資訊等,僅僅只用於重啟
Envoy: Envoy對埠監聽(Listener)進行了遷移,但是對建立的連線(connection)則透過命令的方式,進行主動斷開重連
MOSN: 鑑於低版本Bolt 等協議,不支援主動斷連,且是長連線,導致MOSN在進行熱升級的時候,不僅僅進行了埠監聽的遷移,還有connection的遷移,保證了熱升級過程中連結不中斷,客戶端服務端無感的升級體驗
升級流程
觸發情況
這裡贅述一下,MOSN的熱升級方式,是透過一個Operator實現的
- Operator在Pod中增加新的MOSN容器
- New Mosn啟動時候,觀測到有Old Mosn存在,開啟熱升級的邏輯
- New Mosn啟動成功後,Old Mosn進行退出
- Operator銷燬Old Mosn的Container
至此,一個完整的流程熱升級流程結束
整體設計
MOSN熱升級中核心的資料
- 配置資料(這樣New Mosn才知道代理的是什麼應用,有哪些配置資訊等等)
- 監聽埠
- 連線
MOSN熱升級的互動流程設計如上,接下來對各個模組遷移邏輯進行一下跟蹤
原始碼解析
程式碼邏輯僅保留核心流程
socket監聽情況
func (stm *StageManager) Run() {
// 1: parser params
stm.runParamsParsedStage()
// 2: init
stm.runInitStage()
// 3: pre start
stm.runPreStartStage()
// 4: run
stm.runStartStage()
// 5: after start
stm.runAfterStartStage()
stm.SetState(Running)
}
MOSN啟動分為上述幾個流程,其中 熱升級邏輯主要分佈在 InitStage
和 StartStage
InitStage: 遷移配置資訊 和 遷移Listener
StartStage: 建立 reconfig.sock 的監聽 和 遷移 connection
在MOSN中有4個socket監聽
reconfig.sock: 由 old mosn 監聽,用於 new mosn 感知 old mosn存在
listen.sock: 由new mosn 監聽,用於old mosn傳遞 listener資料給new mosn
conn.sock: 由 new mosn監聽,用於 old mosn 傳遞 connection 的 fd和connection讀取到的資料 (沒有則不傳)給 new mosn
mosnconfig.sock: 由 new mosn 監聽,用於 old mosn 傳遞配置資訊給 new mosn
Old Mosn 遷移主流程
func ReconfigureHandler() error {
// dump lastest config, and stop DumpConfigHandler()
configmanager.DumpLock()
configmanager.DumpConfig()
// if reconfigure failed, enable DumpConfigHandler()
defer configmanager.DumpUnlock()
// transfer listen fd
var listenSockConn net.Conn
var err error
var n int
var buf [1]byte
if listenSockConn, err = sendInheritListeners(); err != nil {
return err
}
if enableInheritOldMosnconfig {
if err = SendInheritConfig(); err != nil {
listenSockConn.Close()
log.DefaultLogger.Alertf(types.ErrorKeyReconfigure, "[old mosn] [SendInheritConfig] new mosn start failed")
return err
}
}
// Wait new mosn parse configuration
listenSockConn.SetReadDeadline(time.Now().Add(10 * time.Minute))
n, err = listenSockConn.Read(buf[:])
if n != 1 {
log.DefaultLogger.Alertf(types.ErrorKeyReconfigure, "[old mosn] [read ack] new mosn start failed")
return err
}
// ack new mosn
if _, err := listenSockConn.Write([]byte{0}); err != nil {
log.DefaultLogger.Alertf(types.ErrorKeyReconfigure, "[old mosn] [write ack] new mosn start failed")
return err
}
// stop other services
store.StopService()
// Wait for new mosn start
time.Sleep(3 * time.Second)
// Stop accepting new connections & graceful close the existing connections if they supports graceful close.
shutdownServers()
// Wait for all connections to be finished
WaitConnectionsDone(GracefulTimeout)
log.DefaultLogger.Infof("[server] [reconfigure] process %d gracefully shutdown", os.Getpid())
// will stop the current old mosn in stage manager
return nil
}
- old mosn 首先將 listener 遷移到new mosn
- 然後 遷移 配置資訊,這一步是可選的
- 等待 new mosn ack完成後
- 呼叫 WaitConnectionsDone 將 connection close掉,遷移 connection
配置遷移
配置遷移是用過 mosnconfig.sock 來完成的,new mosn 監聽,old mosn 連線上去並傳輸資料
new mosn監聽是在 GetInheritConfig
這個方法中實現的
func GetInheritConfig() (*v2.MOSNConfig, error) {
......
l, err := net.Listen("unix", types.TransferMosnconfigDomainSocket)
......
defer l.Close()
ul := l.(*net.UnixListener)
ul.SetDeadline(time.Now().Add(time.Second * 10))
uc, err := ul.AcceptUnix()
......
defer uc.Close()
log.StartLogger.Infof("[server] Get GetInheritConfig Accept")
configData := make([]byte, 0)
buf := make([]byte, 1024)
for {
n, err := uc.Read(buf)
configData = append(configData, buf[:n]...)
.......
}
// log.StartLogger.Infof("[server] inherit mosn config data: %v", string(configData))
oldConfig := &v2.MOSNConfig{}
err = json.Unmarshal(configData, oldConfig)
if err != nil {
return nil, err
}
return oldConfig, nil
}
New mosn 建立好socket監聽後,卡在 Accept 函式中,等待 old mosn 建立連結
然後 接收old mosn 傳遞過來的資料即可
old mosn是在 SendInheritConfig
中與 new mosn建立連結並傳遞配置資訊的
func SendInheritConfig() error {
var unixConn net.Conn
var err error
// retry 10 time
for i := 0; i < 10; i++ {
unixConn, err = net.DialTimeout("unix", types.TransferMosnconfigDomainSocket, 1*time.Second)
......
}
......
configData, err := configmanager.InheritMosnconfig()
......
uc := unixConn.(*net.UnixConn)
defer uc.Close()
n, err := uc.Write(configData)
......
return nil
}
至此,配置資料傳遞也就完成了
監聽埠遷移
listener遷移是用過 listen.sock 來完成的,new mosn 監聽,old mosn 連線上去並傳輸資料,跟配置傳輸的邏輯差不多
new mosn監聽是在 GetInheritListeners
這個方法中實現的,並獲取所有的listener
func GetInheritListeners() ([]net.Listener, []net.PacketConn, net.Conn, error) {
l, err := net.Listen("unix", types.TransferListenDomainSocket)
......
defer l.Close()
ul := l.(*net.UnixListener)
ul.SetDeadline(time.Now().Add(time.Second * 10))
// 這裡卡主,等待 old mosn連線
uc, err := ul.AcceptUnix()
buf := make([]byte, 1)
oob := make([]byte, 1024)
// 接收 old mosn傳遞過來的資料
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
scms, err := unix.ParseSocketControlMessage(oob[0:oobn])
// 解析出來fd
gotFds, err := unix.ParseUnixRights(&scms[0])
var listeners []net.Listener
var packetConn []net.PacketConn
for i := 0; i < len(gotFds); i++ {
fd := uintptr(gotFds[i])
file := os.NewFile(fd, "")
.....
defer file.Close()
// 透過fd 恢復 listener,本質是對fd的監聽
fileListener, err := net.FileListener(file)
if err != nil {
pc, err := net.FilePacketConn(file)
if err == nil {
packetConn = append(packetConn, pc)
} else {
log.StartLogger.Errorf("[server] recover listener from fd %d failed: %s", fd, err)
return nil, nil, nil, err
}
} else {
// for tcp or unix listener
listeners = append(listeners, fileListener)
}
}
return listeners, packetConn, uc, nil
}
透過上述方法,new mosn建立 socket監聽,並等待 old mosn的連線,old mosn連線上後,等待 old mosn 傳遞 所有listener 的fd,然後 new mosn 進行恢復即可
但是 同一個 fd,有兩個監聽,不就亂了嗎,所以 當 old mosn 傳遞過來fd後,會主動 stop accept,不再進行監聽
new mosn 的listener 傳遞邏輯在sendInheritListeners
裡面
func sendInheritListeners() (net.Conn, error) {
// 列出來所有的 listener,返回格式 os.File
lf := ListListenersFile()
......
lsf, lerr := admin.ListServiceListenersFile()
......
var files []*os.File
files = append(files, lf...)
files = append(files, lsf...)
......
fds := make([]int, len(files))
for i, f := range files {
// 獲取 file 的 fd
fds[i] = int(f.Fd())
defer f.Close()
}
var unixConn net.Conn
var err error
// retry 10 time
for i := 0; i < 10; i++ {
unixConn, err = net.DialTimeout("unix", types.TransferListenDomainSocket, 1*time.Second)
.......
}
......
uc := unixConn.(*net.UnixConn)
buf := make([]byte, 1)
// 將 fd 轉成 socket message
rights := syscall.UnixRights(fds...)
n, oobn, err := uc.WriteMsgUnix(buf, rights, nil)
......
return uc, nil
}
Old mosn 透過 ListListenersFile 將所有的listener羅列出來,這個主要得益於MOSN良好的設計模式;mosn維護了一個全域性的servers,而server的結構如下
type server struct {
serverName string
stopChan chan struct{}
handler types.ConnectionHandler
}
type ConnectionHandler interface {
......
// ListListenersFD reports all listeners' fd
ListListenersFile(lctx context.Context) []*os.File
}
每個server對自己的listener描述清晰
繼續回到 old mosn傳遞listener的過程
- old mosn 羅列出來所有的listener,並獲取file
- old mosn 獲取所有 file的fd,並將fd透過UnixRights轉成 socket message,供傳遞
- new mosn 接收到 socket message,轉成fd,並透過檔案建立listener
然後 old mosn 會關閉掉所有的listener,停止accept 新的連結
func shutdownServers() {
for _, server := range servers {
server.Shutdown()
}
}
func (ch *connHandler) GracefulStopListeners() error {
var failed bool
listeners := ch.listeners
wg := sync.WaitGroup{}
wg.Add(len(listeners))
for _, l := range listeners {
al := l
log.DefaultLogger.Infof("graceful shutdown listener %v", al.listener.Name())
// Shutdown listener in parallel
utils.GoWithRecover(func() {
defer wg.Done()
if err := al.listener.Shutdown(); err != nil {
log.DefaultLogger.Errorf("failed to shutdown listener %v: %v", al.listener.Name(), err)
failed = true
}
}, nil)
}
wg.Wait()
return nil
}
func (l *listener) Shutdown() error {
changed, err := l.stopAccept()
if changed {
l.cb.OnShutdown()
}
return err
}
至此,只有new mosn 能建立新的連結,old mosn不再建立新的連結了
listener傳遞完成後,新的連結都建立到 new mosn上去了,剩餘的就是存量長連結了
長連結遷移
長連結遷移是用過 conn.sock 來完成的,同上,也是由new mosn 監聽,old mosn 連線上去並傳輸資料;不過,這裡並沒有傳遞配置和listener那樣簡單,需要考慮很多邊際問題
在這裡,連結分為兩部分
- client/server -> mosn的連結
- mosn -> client/server 的連結
我們只需要考慮 client/server -> mosn的連結
的情況即可,mosn -> client/server 的連結
這種情況,由於mosn是主動連線方,斷開並不會對下游造成任何影響
先來看下 長連結遷移的流程
- Client 傳送請求到 MOSN
- MOSN 透過 domain socket(conn.sock) 把 TCP1 的 FD 和連線的狀態資料傳送給 New MOSN
- New MOSN 接受 FD 和請求資料建立新的 Conection 結構,然後把 Connection id 傳給 MOSN,New MOSN 此時就擁有了TCP1 的一個複製。Old MOSN 停止讀取 TCP1 的請求,New MOSN 開始讀取 TCP1的請求,TCP1的遷移就完成了
- New MOSN 透過 LB 選取一個新的 Server,建立 TCP3 連線,轉發請求到 Server
- Server 回覆響應到 New MOSN
- New MOSN 透過 MOSN 傳遞來的 TCP1 的複製,回覆響應到 Client
接下來看下程式碼
注: 前面 WaitConnectionsDone
已經將 connection.stopChan close掉了
連結遷移是在 startReadLoop
中完成的
func (c *connection) startReadLoop() {
var transferTime time.Time
for {
......
select {
case <-c.stopChan:
// 首先設定 transfer 時間
if transferTime.IsZero() {
if c.transferCallbacks != nil && c.transferCallbacks() {
randTime := time.Duration(rand.Intn(int(TransferTimeout.Nanoseconds())))
transferTime = time.Now().Add(TransferTimeout).Add(randTime)
log.DefaultLogger.Infof("[network] [read loop] transferTime: Wait %d Second", (TransferTimeout+randTime)/1e9)
} else {
// set a long time, not transfer connection, wait mosn exit.
transferTime = time.Now().Add(10 * TransferTimeout)
log.DefaultLogger.Infof("[network] [read loop] not support transfer connection, Connection = %d, Local Address = %+v, Remote Address = %+v",
c.id, c.rawConnection.LocalAddr(), c.RemoteAddr())
}
} else {
if transferTime.Before(time.Now()) {
c.transfer()
return
}
}
default:
}
.......
}
- 在 old mosn 呼叫
WaitConnectionsDone
將 connection.stopChan close之後,在startReadLoop
迴圈中 首先設定以下 隨機 transfer 時間 - 在 transfer時間到了之後,開始 遷移 連結
func (c *connection) transfer() {
c.notifyTransfer()
id, _ := transferRead(c)
c.transferWrite(id)
}
func transferRead(c *connection) (uint64, error) {
......
unixConn, err := net.Dial("unix", types.TransferConnDomainSocket)
......
defer unixConn.Close()
file, tlsConn, err := transferGetFile(c)
......
uc := unixConn.(*net.UnixConn)
// send type and TCP FD
err = transferSendType(uc, file)
......
// send header + buffer + TLS
err = transferReadSendData(uc, tlsConn, c.readBuffer)
......
// recv ID
id := transferRecvID(uc)
log.DefaultLogger.Infof("[network] [transfer] [read] TransferRead NewConn Id = %d, oldId = %d, %p, addrass = %s", id, c.id, c, c.RemoteAddr().String())
return id, nil
}
- 每一個connection遷移的時候,會首先構建一個 unix connection 用於 old mosn 和 new mosn互動
- 首先將 connection 的 fd 透過 scoket傳遞給new mosn
- 然後 將 connection tls 和 讀取的buf資料再傳遞給 new mosn 處理
- 最後 記錄下來 new mosn 根據 fd 建立的 新的connection的id
這裡mosn構造了一個簡單的socket協議,用於 傳遞 connection 的tls和buf資料
/**
* transfer read protocol
* header (8 bytes) + (readBuffer data) + TLS
*
* 0 4 8
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | data length | TLS length |
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | data |
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | TLS |
* +-----+-----+-----+-----+-----+-----+-----+-----+
*
接下來繼續看下,new mosn 收到 connection後的處理, new mosn 處理是在 transferHandler
中完成的
func transferHandler(c net.Conn, handler types.ConnectionHandler, transferMap *sync.Map) {
......
uc, ok := c.(*net.UnixConn)
......
// recv type
conn, err := transferRecvType(uc)
......
if conn != nil {
// transfer read
// recv header + buffer
dataBuf, tlsBuf, err := transferReadRecvData(uc)
......
connection := transferNewConn(conn, dataBuf, tlsBuf, handler, transferMap)
if connection != nil {
transferSendID(uc, connection.id)
} else {
transferSendID(uc, transferErr)
}
}
......
}
new mosn 收到connection遷移請求後,根據傳遞過來的fd,首先轉換成connection,然後 根據 tls 資料,構建為 tls conn,這些完成後,根據 conn的監聽資訊和handler型別,找到對應的listener,並將這個connection加進去,然後就可以開始處理 傳遞過來的buf資料了
最後,new mosn 將新的connection的id,傳給old mosn,用於傳遞 寫請求
至此,old mosn connection的 readLoop 也退出了,不再讀取新的資料,資料也都由new mosn來讀取了
接下來就是寫請求了
old mosn 如果繼續往連線裡面寫資料,可能會和new mosn衝突,導致資料操作,所以 old mosn的寫請求,是直接轉給 new mosn來處理的
Old mosn 遷移寫請求:
func transferWrite(c *connection, id uint64) error {
......
unixConn, err := net.Dial("unix", types.TransferConnDomainSocket)
......
defer unixConn.Close()
uc := unixConn.(*net.UnixConn)
err = transferSendType(uc, nil)
......
// build net.Buffers to IoBuffer
buf := transferBuildIoBuffer(c)
// send header + buffer
err = transferWriteSendData(uc, int(id), buf)
......
return nil
}
Mosn 也為寫請求,構建了一個簡單的socket協議
* transfer write protocol
* header (8 bytes) + (writeBuffer data)
*
* 0 4 8
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | data length | connection ID |
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | data |
* +-----+-----+-----+-----+-----+-----+-----+-----+
*
這裡會將,遷移 讀請求時獲取到的 connection id 記錄下來,並傳遞給 new mosn,讓 new mosn 根據 id 找到對應的連結
new mosn 接收寫請求處理邏輯,也是在 transferHandler
中完成的:
func transferHandler(c net.Conn, handler types.ConnectionHandler, transferMap *sync.Map) {
......
// transfer write
// recv header + buffer
id, buf, err := transferWriteRecvData(uc)
......
connection := transferFindConnection(transferMap, uint64(id))
......
err = transferWriteBuffer(connection, buf)
......
}
New mosn 從 old mosn的socket請求中,解析出來 connection id 和 buf 資料
根據 id 找到 new mosn中的 connection,然後將buf寫入即完成
至此,讀寫請求都遷移完成,整個長連線的遷移也就完成了
總結
MOSN對熱升級的處理,是做到極致的,深度是遠遠高於市場上其他產品的;同時,深度也往往伴隨著風險,做到連結遷移層面,可能也並不是MOSN的本意,而是歷史原因的驅動
在原始碼邏輯層面,個人也有一點簡單的看法
- 整個熱升級模組,更像是函式驅動,而缺少設計,從 conn.sock listen.sock 等多個sock檔案就可以看出,如果設計好點的話,完全可以透過構建socket協議,而規避掉多個socket檔案,同時,邏輯也會更清晰簡潔一點,而非散落在各個方法裡面
- 有些邊界性問題還是沒有處理的很好;例如,listener遷移的時候,如果有listener close掉了,但是配置還存在MOSN中,這裡就會panic了;eg: reconfig.sock 在處理最後會刪除,如果走到了這一步,然後回滾了,後續也無法進行熱升級
最後,MOSN還是很優秀的,respect!!!