|
約定:本文只考慮 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 服務器機群”的描述。
在《分布式系統的工程化開發方法》中談到了 Zurg 的功能需求:
到了這一境界,日常的管理運維工作已經不再需要反復執行 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。
(待續,下一篇我打算談談分布式系統中心跳協議的設計。)
it知識庫:分布式系統部署、監控與進程管理的幾重境界,轉載需保留來源!
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。