分散式系統:分散式部署,監控與程序管理
約定:本文只考慮 Linux 系統,文中涉及的“服務程式”是以 C++ 或 Java 編寫,編譯成二進位制可執行檔案(binary 或 jar),程式啟動的時候一般會讀取配置檔案(或者以其他方式獲得配置資訊),同一個程式每個服務程序的配置檔案可能略有不同。“伺服器”這個詞有多重含義,為避免混淆,本文以 host 指代伺服器硬體,以“服務端程式/程序”指代伺服器軟體(或者具體說 Web Server 和 Sudoku Solver,這兩個都是服務軟體)。
在進入正題之前,先看一個虛構但典型的例子:Sudoku Solver。(Sudoku Solver 是個均質的無狀態服務,分散式系統中程序的狀態遷移不是本文的主題。)
假設你們公司的分散式系統中有一個專門求解數獨(Sudoku)的服務程式,這個程式是你們團隊開發並維護的。通常 Web Server 會使用這個 Sudoku Solver 提供的服務,使用者通過 web 頁面提交一個 Sudoku 謎題,web server 轉而向 Sudoku Solver 尋求答案。每個 Web Server 會同時跟多個 Sudoku Solver 聯絡,以實現負載均衡。系統的訊息結構大致如下,每個圓角矩形是一個程序,執行在各自的 host 上:
上圖中的 Web Server 請不要簡單理解為 httpd + cgi,它其實泛指一切客戶端,本身可能是個 stateful 的服務程式。
當然,系統不是一開始就是這樣,它經歷了多步演化。
一開始 (a),只有一個 Sudoku Solver,也只有一臺 Web Server,是個簡單的一對一 (1:1) 的使用關係;
隨後 (b),隨著業務量增加,一臺 host 不堪重負,於是又部署了幾臺 Sudoku Solver,變成了一對多 (1:n) 的使用關係;
再後來 (c),一臺 Web Server 撐不住了,於是部署了幾臺 Web Server,形成了我們一開始看到的多對多 (m:n) 的使用關係;
(d) 中的情況留到文末再講。
在分散式系統中部署並執行 Sudoku Solver,需要考慮以下幾個問題:
- Sudoku Solver 如何部署到多臺 host 上執行?是把可執行檔案拷過去嗎?程式用到的庫怎麼辦?配置檔案怎麼辦?
- 如何啟動服務程式 Sudoku Solver ?如果每個 Solver 的配置檔案稍有不同(比如每個 Solver 有自己的 service name),那麼配置檔案是自動生成嗎?
- Sudoku Solver 的 listening port 如何配置?如何保證它不與其他服務程式重複?
- 如果程式 crash,誰來重啟?能否自動重啟?開發/運維人員能否及時收到 alert?
- 如果想主動重啟 Sudoku Solver,要不要登入到那臺 host 上去 kill ?還是能夠遠端控制?
- 如果要升級 Sudoku Solver 程式,如何重新部署?如何(儘量)做到不中斷服務?
- Web Server 如何知道那些 Sudoku Solver 的地址?是不是靜態寫到 Web Server 的配置檔案裡?
- 如果 Sudoku Solver 所在的 host 發生硬體故障,管理人員是否能立刻得知這一狀況?Web Server 能否自動 fail over 到其他 alive 的 Solver 上?
- 部署新的 Sudoku Solver 之後,Web Server 能否自動開始使用新的 Solver 而無需重啟?(重啟 Web Server 似乎不是大問題,這裡我們進一步考慮 client 是個有狀態的服務,應該儘量避免重啟。)
- 程式可否安全地退役?比方說公司不再做求解 Sudoku 的業務,那麼關閉全部 Sudoku Solver 會不會對其他業務造成影響?
這些問題可以大致歸結為幾個方面:部署(含升級)可執行檔案與配置檔案、監控程序狀態、管理服務程序,合起來可稱為運維 operation。
根據公司的規模和技術水平不同,分散式系統的運維分為幾重境界,以下是我對各重境界的簡要描述。
境界1:全手工操作
這個大概是高校實驗室的水平,分散式系統的規模不大,可能十來臺機器上下。分散式系統的實現者為在校學生。
系統完全是手工搭起來,host 的 IP 地址靜態配置。
部署:編譯之後手工把可執行檔案拷貝到各臺機器上,或者放到公用的 NFS 目錄下。配置檔案也手工修改並拷貝到各臺機器上(或者放到每個 Sudoku Solver 自己單獨的 NFS 目錄下)。
管理:手工啟動程序,手工在命令列指定配置檔案的路徑。重啟程序的時候需要登陸到 host 上並 kill 程序。
升級:如果需要升級 Sudoku Solver,則需要手工登陸多臺 hosts,可以拷貝新的可執行檔案覆蓋原來的,並重啟。
配置:Web Server 的配置檔案裡寫上 Sudoku Solver 的 ip:port。如果部署了新的 Sudoku Solver,多半要重啟 Web Server 才能發揮作用。
監控:無。系統不是真實的商業應用,僅僅用作學習研究,發現哪兒不對勁了就登陸到那臺 host 上去看看,手工解決問題。
這個級別可算是“過家家”,系統時零時不靈,可以跑跑測試,發發 paper。
境界2:使用零散的自動化指令碼和第三方元件
這大概是剛起步的公司的水平,系統已經投入商業應用。公司的開發重心放在實現核心業務,新增新功能,暫時還顧不上高效的運維,或許系統的運維任務由開發人員或網管人員兼任。公司已經有了基本的開發流程,程式碼採用中心化的版本管理工具(比如 SVN),有比較正式的 QA sign-off 流程。
公司內網有 DNS,可以把 hostname 解析為 IP 地址,host 的 IP 地址由 DHCP 配置。公司內部的 host 的軟硬體配置比較統一,比如硬體都是 x86-64 平臺,作業系統統一使用 Ubuntu 10.04 LTS,每天機器上安裝的 package 和第三方 library 也是完全一樣的(版本號也相同),這樣任何一個程式在任何一臺 host 上都能啟動,不需要單獨的配置。
假設各臺 host 已經配置好了 ssh authentication key 或者 GSSAPI,不需要手工輸入密碼。如果要在 host1, host2, host3, host4 上執行 md5sum 命令,看一下各臺機器上的 SudokuSolver 可執行檔案的內容是否相同,可以在本機執行:
for h in host1 host2 host3 host4; do ssh $h md5sum /path/to/SudokuSolver/version/bin/sudoku-solver ; done
公司的技術人員有能力配置使用 cron、at、logrotate、rrdtool 等標準的 linux 工具來將部分運維任務自動化。
部署:可執行檔案必須經過 QA 簽署放行才能部署到生產環境(如有必要,QA 要簽署可執行檔案的 md5)。為了可靠性,可能不會把可執行檔案放到 NFS 上(如果 NFS 故障,整個系統就癱瘓了)。有可能採用 rsync 把可執行檔案拷貝到本機目錄(考慮到可執行檔案比較大,估計不適合直接放到版本管理庫裡),並且用 md5sum 檢查拷貝之後的檔案是否與原始檔相同。部署可執行檔案這一步驟應該可以用指令碼自動執行(比方說 ssh $host rsync /path/to/source/on/nfs /path/to/local/copy/)。為了讓 C++ 可執行檔案拷到 host 上就能用,那麼通常採用靜態連結,以避免 .so 版本不同造成故障。
Sudoku Solver 的配置檔案會放到版本管理工具裡,每個 Solver instance 可能有自己的 branch,每次修改都必須入庫。程式啟動的時候用的配置檔案必須從 SVN 裡 check-out,不能手工修改(減少人為錯誤)。
管理:第一次啟動程序的時候,會從 SVN check-out 配置檔案;以後重啟程序的時候可以從本地 working copy 讀取配置檔案(以避免 SVN 伺服器故障對系統造成影響),只在改過配置檔案之後才要求 svn update。服務程序使用 daemon 方式管理 (/sbin/init 或 upright 工具),crash 之後會立刻自動重啟(利用 respawn 功能)。服務程序一般會隨 host 啟動而啟動(放到 /etc/init.d 裡),如果要重啟 hostA 上的服務程序,可以通過 ssh 遠端操作(比如在本機執行 ssh hostA /etc/init.d/sudoku-solver restart )。程序管理是分散的,每臺 host 執行哪些 service 完全由本機是的 /etc/init.d 目錄決定。把一個 service 從一臺 host 遷移到另一臺 host,需要登入到這兩臺 host 上去做一些手工配置。
升級:可執行檔案也有一套版本管理(不一定通過 SVN),釋出新版本的時候嚴禁覆蓋已有的可執行檔案。比方說,現在執行的是
/path/to/SudokuSolver/1.0.0/bin/sudoku-solver
那麼新版本的 Sudoku Solver 會發布到
/path/to/SudokuSolver/1.1.0/bin/sudoku-solver
這麼做的原因是,對於 C++ 服務程式,如果在程式執行的時候覆蓋了原有的可執行檔案,那麼可能會在一段時間之後出現 bus error,程式因 SIGBUS 而 crash。另外,如果程式發生 core dump,那麼驗屍 (post mortem) 的時候必須用“產生 core dump 的可執行檔案”配合 core 檔案。如果覆蓋了原來的可執行檔案,post mortem 無法進行。
配置:Web Server 的配置檔案裡寫上 Sudoku Solver 的 host:port (比 境界1 有所提高,這裡依賴 DNS,通常 DNS 有一主一備,可靠性足夠高)。不過 Web Server 的配置檔案和 Sudoku Solver 的配置檔案是獨立的,如果新增了 Sudoku Solver 或者遷移了 host,除了修改 Sudoku Solver 的配置檔案,還有修改所有用到它的 Web Server 的配置檔案。這在系統規模比較小的時候尚且可行,系統規模一大,這種服務之間的依賴關係會變得隱晦。如果關閉了某個服務程式,可能一不小心造成其他組的某個服務失靈。如孟巖在《通過一個真實故事理解SOA監管》舉的那個例子一樣。
監控:公司會使用一些開源的監控工具(以下以 Monit 為例)來監控每臺 host 的資源使用情況(記憶體、CPU、磁碟空間、網路頻寬等等)。必要的話可以寫一些外掛,使之能監控我們自己寫的服務程式 (Sudoku Solver)。但是這些監控工具通常只是觀察者,它們與程序管理工具是獨立的,只能看,不能動。這些監控工具有自己的配置檔案,這些配置需要與 Sudoku Solver 的配置同步修改。Monit 可以管理程序,但是它判斷服務程序是否能正常工作是通過定時輪詢,不一定能立刻(幾秒鐘)發現問題。
在這個境界,分散式系統已經基本可用了,但也有一些隱患。
配置零散
每個服務程式有自己獨立的配置,但是整個系統沒有全域性的部署配置檔案(比方說哪個服務程式應該執行在哪些 hosts 上)。
服務程式的配置檔案和用到此服務的客戶端程式的配置是獨立的,如果把 Sudoku Solver 遷移到另一臺 host,那麼不僅要修改 Sudoku Solver 的配置,還要修改用到 Sudoku Solver 的 Web Server 的配置,以及監控 Sudoku Solver 的 Monit 的配置。如果忘記修改其中一處,就會造成系統故障。
分散式系統中服務程式的依賴關係是個令人頭疼的問題,“依賴”還好辦(程式的作者知道我這個服務程式會依賴哪些其他服務),“被依賴”則比較棘手(如何才能知道停掉我這個程式會不會讓公司其他系統崩潰?)。這也從一個側面證明使用 TCP 協議作為唯一的 IPC 手段的必要性,如果採用 TCP 通訊,為了查出有哪些程式用到了我的 Sudoku Solver (假設 listening port 是 9981),那麼我只要執行 netstat -tpn |grep 9981 就能找到現在的客戶;或者讓 Sudoku Solver 自己列印 accept(2) log,連續檢查一週或這一個月就能知道有哪些程式用到了 Sudoku Solver。
程序管理分散
如果 hostA 發生硬體故障,如何能快速地用一臺備用伺服器硬體頂替它?能否先把它上面原來執行的 Sudoku Solver 遷移到空閒的 hostB 上,然後通知 Web Server 用 hostB 上的 Sudoku Solver?“通知 Web Server”這一步要不要重啟 Web Server?
境界3:自制機群管理系統,集中化配置
這可能是比較成熟的大公司的水平。
境界 2 中的分散式程序管理已經不能滿足業務靈活性方面的需求,公司開始整合現有的運維工具,開發一套自己的機群管理軟體。我還沒有找到一個開源的符合我的要求的機群管理軟體,以下虛構一套名為 Zurg (名字取自科幻電影《第五元素》,拼寫稍有不同;Zurg 也是《玩具總動員》中的一個反派角色。)的分散式系統管理軟體。
Zurg 的架構很簡單,典型的 master slave 結構,見陳碩在《多執行緒伺服器的適用場合》中對“管理 Linux 伺服器機群”的描述。
到了這一境界,日常的管理運維工作已經不再需要反覆執行 ssh,常見任務都可以通過 Zurg 來完成。
部署:只需要向 master 發一條指令,master 會命令 slaves 從指定的地點 rsync 新的可執行檔案到本地目錄。
程序管理與監控:Zurg 的主要功能就是程序管理和監控,比起一般的開源工具,Zurg 更具備一些優勢。由於 Sudoku Solver 是由 Zurg Slave fork() 而得,那麼當 Sudoku Solver crash 的時候,Zueg Slave 會立刻收到 SIGCHLD,從而能立刻向管理員報告狀態並重啟。這比 munit 的輪詢要迅速得多。(還可以在 fork() 之前做一些手腳,讓 Zueg Slave 能更方便地獲得 Sudoku Solver 的存活狀態。)
為了安全起見,Zurg Slave 在啟動可執行檔案的時候可以驗證其 md5,這樣避免錯誤版本的服務程式執行在生產環境。
Zurg Master 可以提供一個 Web 頁面以供檢視本機群內各個服務程式是否正常執行。並且提供一個介面(可以是 HTTP)讓我們能編寫指令碼來控制 Zurg master。
升級:如果要主動重啟 Sudoku Solver,可以向 Zurg master 發出指令,不需要用 ssh & kill。Zurg 會儲存每臺 host 上服務程序的啟動記錄,以便事後分析。如果用境界 2 中的手動 /etc/init.d 管理方式,需要到每臺機器上收集 log 才知道 Sudoku Solver 什麼時候重啟過。
另外也可以單獨開發 GUI 程式,執行在運維人員的桌面上,重啟多臺 host 上的 Sudoku Solver 只需要點幾下滑鼠。
配置:零散的配置檔案被集中的 Zurg 配置檔案取代。
Zurg 配置檔案會制定哪些 service 會在哪些 host 上執行,Zurg Master 讀取配置檔案,然後命令各個 Zurg Slave 啟動相應的服務程式。比方說配置檔案指定 Sudoku Solver 執行在 host1、host2、host3 上,那麼 Zurg 會通知在 host1、host2、host3 上的 Zurg Slave 啟動 Sudoku Solver。(當然,每臺 host 上的 Zurg Slave 需要由 /etc/init.d 啟動,其他的服務程式都由它負責啟動。)
更重要的是,服務程式之間的依賴關係在 Zurg 配置檔案裡直接體現出來。比方說,在 Zurg 配置檔案裡指明 Web Server 依賴 Sudoku Solver,Web Server 的配置檔案由 Zurg master 生成(可能會用到模板引擎,讀入一個 Web Server 的配置模板),其中出現的 Sudoku Solver 的 host:port 由 Zurg master 自動填上,這樣如果把 Sudoku Solver 從 hostA 遷移到 hostB,只需要改一處地方(Zurg 的配置),而 Sudoku Solver 和 Web Solver 的配置都由 Zurg master 自動生成。這樣大大降低了犯錯誤的機會。
到了這一境界,分散式系統日常管理已經基本成熟,但在容錯與負載均衡方面有較大的提升空間。
目前最大在障礙是 DNS,它限制了快速 Failover。比方說,如果 hostA 發生硬體故障,Zurg Master 固然可以在 hostB 上立刻啟動 Sudoku Solver,但是如何通知 Web Server 到 hostB 上享用服務呢?修改 DNS entry 的話(把 hostA 的域名解析到 hostB 的 IP),可能要好幾分鐘才能完成更新,因為 DNS 沒有推送機制。
如果思路受限制於 host:port,那麼會採取一些看似高階,實則笨拙的高可用 (high availability) 解決方案。比方說在核心裡做做手腳,設法讓兩臺機器共享同一個 IP,然後通過專門的心跳連線來控制哪臺 host 對外提供服務,哪臺是備用機。如果那臺“主機”發生故障,可以快速(幾秒鐘)切換到備用機,因為 hostname 和 IP 地址是相同的,客戶端不用重新配置或重啟,只要重新連線 TCP 就能完成 failover。如果在錯誤的道路上走得更遠一點,可能還會設法把 TCP 連線一同遷移到備用機,這樣客戶端都不需要斷開並重連。
Load balance 也受限於 DNS。
如果發現現有的 4 個 Sudoku Solver 不堪重負,又部署了 4 臺 Sudoku Solver,如何通知各個 Web Server 把新的 Sudoku Solver 加到連線池裡?
有一些 ad hoc 的手段,比方說每個 Web Server 有一個管理介面,可以透過這個介面向它動態地增減 Sudoku Solver 的地址。藉助這個管理介面,我們也可以做一些計劃中的聯機遷移。比方說要主動把某個 Sudoku Solver 從 hostA 遷移到 hostB,我們可以先在 hostB 上啟動 Sudoku Solver,然後透過 Web Server 的管理介面把 hostB:9981 新增到 Web Server 的連線池中,再把 hostA:9981 從連線池中刪掉,最後停掉 hostA 上的 Sudoku Solver。這對計劃中的 Sudoku Solver 升級是可行的,能做到避免中斷 Web Server 服務。對於 failover,這種做法似乎稍顯不夠方便,因為要讓 Zurg Master 理解 Web Server 的管理介面,會給系統帶來迴圈依賴。(正常情況下,Zurg Master 不應該知道/訪問它管理的服務程式的介面細節,這樣 Sudoku Solver 升級的時候不用升級 Zurg Master。)
這種做法要求 Web Server 在開發的時候留下適當的維修探查通道,見陳碩《構建易於維護的分散式程式》中的推薦做法。
另外一種 ad hoc 的手段,每個 Sudoku Solver 在啟動的時候自己主動往某個資料庫表裡 insert 或 update 本程式的 host:port。Web Server 的配置裡寫的不是 host:port,而是一條 SELECT 語句,用於找出它依賴的 Sudoku Solver 的 host:port,Web Server 還可以通過資料庫觸發器來及時獲知 Sudoku Solver address list 的變化。這樣增加或減少 Sudoku Server 的話,Web Server 幾乎可以立刻應對,也不需要透過管理介面來手工增減 Sudoku Solver 地址。資料庫在這裡扮演了 naming service 的角色,它的可用性直接影響了整個系統的可用性。
境界 3 是黎明前的黑暗,只要統一引入 naming service,拋開 DNS,容錯和負載均衡的問題迎刃而解。
境界4:機群管理與 naming service 結合
這是業內領先的公司的水平。
前面分析到,使用 Zurg 機群管理軟體能大大簡化分散式系統的日常運維,但是它也有很大的缺陷——不能實現快速 failover。如果系統規模大到一定程度,機器出故障的頻率會顯著增加,這時候自動化的快速 failover 是必備的,否則運維人員疲於奔命救火。
實現簡單而快速的 failover 不需要特殊的程式設計技巧,也不需要對 kernel 動手腳,只要拋棄傳統的 DNS 觀念,擺脫 host:port 的束縛,採用為分散式系統特製的 naming service 代替 DNS 即可。
naming service 的功能是把一個 service_name 解析成 list of ip:port。比方說,查詢 "sudoku_solver",返回 host1:9981、host2:9981、host3:9981。
naming service 與 DNS 最大的不同在於它能把新的地址資訊推送給客戶端。比方說,Web Server 訂閱了 "sudoku_solver",每當 sudoku_solver 發生變化,Web Server 就會立刻收到更新。Web Server 不需要輪詢,而是等候通知。
naming service 誰負責更新?
在境界 2 中,Sudoku Solver 會自己主動去 naming server 註冊。到了境界 3,由於 Sudoku Solver 是有 Zurg 負責啟動,那麼 Zurg 知道 Sudoku Solver 執行在哪些 hosts 上,它會主動更新 naming service,不需要 Sudoku Solver 自己動手。
naming service 的可用性(availability)和一致性如何保證?
毫無疑問,一旦採用這種方案,naming service 是系統正常運轉的關鍵,它的可用性決定了系統的可用性。naming service 絕對不能只 run 在一臺伺服器上,為了可靠性,應該用一組(通常是 5 臺)伺服器同時提供服務,當然,這需要解決一致性問題。目前實現高可用 naming service 的公認辦法是 Paxos 演算法,也有了一些開源的實現(ZooKeeper、KeySpace、Doozer)。
對程式設計的影響?
如果公司的網路庫在設計的時候就考慮了 naming service,那麼對程式設計來說是透明的。配置檔案裡寫的不再是 host:port,而是 service_name,交給網路庫去解析成 ip:port 地址列表。
為什麼 muduo 網路庫沒有封裝 DNS 解析?
一方面因為 gethostbyname() 和 getaddrinfo() 做 DNS 解析是阻塞的,我一時沒有時間寫一個非阻塞的 DNS 庫;另一方面,因為在大規模分散式系統中 DNS 的作用不大,我寧願花時間實現一個 naming service,並且為它編寫 name resolve library。
在境界 3 中,每個專案組有自己的 hosts,只執行本專案中的服務程式,每個服務程式的 TCP 埠可以靜態分配(比如 Sudoku Solver 固定使用 9981 埠),不擔心埠衝突。如果公司規模繼續擴大,遲早會把 16-bit 的 port 名稱空間用完,這時候給新專案分配埠號將成為問題。
到了境界 4,這一限制將被打破,服務程式可以 run 在公司內任何一臺 host 上,也不用擔心埠衝突,因為 Zurg 會選擇當前 host 的空閒埠來啟動 Sudoku Solver,並且把選中的埠儲存在 naming service 中。這樣一來,TCP port 也實現了動態配置,Web Server 完全能自動適應 run 在不同 port 的 Sudoku Solver。
@轉自 陳碩 (giantchen_AT_gmail)