無停機服務遷移
一直以來在營運now.in都有一個令人困擾的問題,就是每當伺服器更新或是出現問題時,就得關掉重開,雖然說大部份的伺服器我在設計上都做成重開也沒有關係,但有少部份一但重啟就會造成使用者斷線,參考下面這張伺服器狀態的圖
圖中被切斷的山峰都是伺服器重啟的時候,無疑的這對使用者來說是不好的體驗,對於一般網頁HTTP這類stateless的伺服器來說重啟是無所謂,但串流伺服器就不一樣,因此最近我在思考如何解決這樣的問題,首先想到的是伺服器重開的原因,不外乎有幾種
- 部署新版的程式
- 為了修正BUG
- 記憶體用量過高
- 重新讀取執行環境,例如ulimit -n,也就是檔案數量的限制大小
- 從主機A移到主機B
以Python的伺服器來說,如果只是單單只是為了部署新版的程式,Python有個reload函數可以重讀整個module,因此可以設個管理用的後門用來重載模組,但是這樣做有個問題,就是已經產生的instance還是一樣,其實說穿了等於是重新執行那個module,如果只是簡單的改版還好,但複雜的改版就可能牽扯到太多因素難以透過這樣來更新,而除此之外,很多重開的因素像是記憶體用量過高,或著執行的環境參數改變,這些都無可避免的一定得建新的process,至於從A主機移到B主機,基本上是難以避免的一定得重啟伺服器,除非有某個前端的伺服器保持連線,後端的伺服器進行交移工作才有可能,而主機轉移的狀況其實很少出現,因此這篇文章要探討的是如何辦到同一臺機器內的服務轉移
方法
最大的問題就在於,連線中的socket該如保持住連線,最簡單的想法就是建立新的process,將目前的連線交給新的process,新的process接手完所有的連線開始運作後舊的process就能終止了,當然所謂的連線還包函了服務的狀態,我們在者裡稱之為 CSD (Connection State Descriptor),大致想法如下面圖片所示
首先Process A是正在運行的process,而Manager是負責控制遷移的process
此時Manager啟動了要負責接手的Process B
接著Manager通知A將連線狀態傳送給Process B,B接手後繼續提供服務,並且告知接收完成
在確定B能夠接手服務並且正確執行後,這時Manager就能將A中止,走到這裡服務就算是完整地移交了,中間沒有任何間斷
技術層面問題 – Socket的轉移
這樣的做法理論上看起來確實可行沒錯,但是最主要有技術上的問題得解決,就是如何將socket,或著是file descriptor轉移到另一個process,在我找過相關的資料後知道目前有兩個方法
child process
這個方法主要是因為child process在生成後會繼承parent process的file descriptor,但是缺點就是只限於child process,如果我們想將服務移交給另一個完整的獨立process這個方法就會行不通,因此使用上會有很多限制
sendmsg
另一個方法就是unix domain的socket有提供一個函數叫sendmsg,可以將file descriptor轉移到任何process,這樣一來實用性就比child process高很多,於是我決定採用此方法
簡易的實作
在這樣的轉移架構設計,Manager主要目的是要能夠對外有個統一個process能溝通,如果不需要的話其實兩個process就能達成服務轉移,為了實作簡單因此這個例子只用了兩個process進行轉移
還有另一個問題是Python標準函式庫裡沒有sendmsg,不過幸好有第三方函式庫已經準備好,就叫做sendmsg,安裝也很簡單,只要打
easy_install sendmsg
就可以了,接下來就是兩個範例程式
a.py:
import os import socket import sendmsg import struct s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', 5566)) s.listen(1) conn, addr = s.accept() conn.send('Hello, process %d is serving\n' % os.getpid()) print 'Accept inet connection', conn us = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) us.bind(('mig.sock')) us.listen(1) uconn, addr = us.accept() print 'Accept unix connection', uconn payload = struct.pack('i', conn.fileno()) sendmsg.sendmsg( uconn.fileno(), '', 0, (socket.SOL_SOCKET, sendmsg.SCM_RIGHTS, payload)) print 'Sent socket', conn.fileno() print 'Done.'
b.py
import os import socket import sendmsg import struct us = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) us.connect(('mig.sock')) print 'Make unix connection', us result = sendmsg.recvmsg(us.fileno()) identifier, flags, [(level, type, data)] = result print identifier, flags, [(level, type, data)] fd = struct.unpack('i', data)[0] print 'Get fd', fd conn = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) os.close(fd) conn.send('Hello, process %d is serving\n' % os.getpid()) raw_input()
大致上的流程很簡單,首先a.py接收一個inet的連線,接著就開一個unix socket,等待b.py上線,然後把服務透過sendmsg轉移給b.py
執行結果
由執行結果我們可以看見,原本的連線是由process A提供的,然後轉交給process B,中間沒有任何中斷
有了這樣的技術基礎和想法,接著就剩下的是細節,基本上連線的狀態盡量以抽象的方式提供,如此一來任何程式只要能夠處理連線狀態就能夠接手,甚至可以由Python寫的伺服器轉移到由C/C++寫的伺服器,這些都可以辦到,中間服務都不會有間斷,而且也可以像HTTP伺服器一樣,處理到一定的量自動開新的process轉移給自己,如此一來可以保持低記憶體用量,有相當多的好處,未來我會試著將伺服器以這樣的想法改寫,應該就能做到無停機更新,有興趣的話也可以自己試試看