天天看點

如何設計運維友好的伺服器端系統

作者:韓偉,個人公衆号:韓大(ID:handa1740168), 以技術提升開發效率

外網事故,一直以來都是網際網路企業力圖盡量避免的,也是伺服器端程式員最重視的問題之一。然而,如果我們統計一下各種外網事故的原因,會發現一個結論:70%的外網事故原因,都是存在于運維領域的,隻有30%左右的事故原因,是由于程式本身的BUG導緻。這裡說的“運維領域的原因”,包括了由于配置錯誤、現網操作失誤、網絡或其他硬體環境變化、硬體故障等等。

在流行“運維”“開發”分離的時代,似乎這些都是運維的鍋,但是這鍋背了幾十年,也沒有什麼本質上的進步。說明僅僅試圖用“厘清責任”這種純管理手段,是解決不了這個技術問題的。

舉個例子,當你需要重新部署一個有幾百個程序,分為十幾不同類型的服務的一個系統時候,你可能要小心翼翼的處理幾十上百項配置以及操作指令。這些配置和指令既有大批相似的,也有一些是需要仔細甄别異同的。加上這些配置之中,還有很多互相的關聯,你必須了解的非常清楚,加上這些配置和指令的執行順序也是有嚴格要求的。部署這一切,就像在操作一個有幾百個按鈕的機器面闆。

最要命的是,如果你搞錯了其中任何一項,都可能讓系統馬上,或者在将來某個不可預料的時間裡,造成“外網事故”。這導緻了老闆就會在半夜三點把你從被窩裡扯到電腦前處理這個爛攤子。當然,我說的這個情況并不是每個項目都會發生,但是我們确實是在很多項目中,都不同程度的陷入過類似的陷阱。不知道我們是不是因為都被apache那複雜的httpd.conf所征服過,是以很多程式員都非常熱愛配置檔案。

“一切都要可配置!”不但成了我們的口号,還成了無數複雜配置項和詭異的工具指令。——但這些東西,在實際的業務運作中,卻切切實實的成為了無數顆定時炸彈。

如何設計運維友好的伺服器端系統

程式員都愛配置檔案

自動化測試現在已經成為開發的标準流程之一。特别是靈活開發方式興起之後,最重要的實踐之一就是自動化測試。我們知道,一般我們用來做測試的環境,往往和真實環境不一樣。比如我們做功能測試的時候,運作被測試程式的記憶體和硬碟可能比真正的運作環境要小的多。如果我們的程式因為這些硬體或者其他的諸如IP位址之類的軟體差異,都要配置一番才能運作,那麼我們的每次測試,都有可能需要手工的去操作一下,這樣的測試就不能說是“自動化”了。加上手工操作的錯誤,更是可能讓測試結果嚴重錯漏。

測試工作除了環境上的差異可能導緻運維操作,本身測試也需要多套環境的,比如很多系統都有多個分支在同時開發,又或者有内部功能測試、外部邀請使用者測試、公共測試等多個測試環境。假設我們的軟體,每次部署都要手工進行一大堆的操作,那麼面對多個環境,頻繁的釋出新版本來做測試,部署的工作肯定是非常繁重的,而這些繁重的工作本來是可以盡量的避免。

如何設計運維友好的伺服器端系統

美好的CI閉環往往毀于複雜的部署流程

快速開發一直是現代軟體企業追求的目标。由于需求、市場日新月異,軟體産品和應用系統也被迫着每天在更新功能。說到伺服器端軟體,我們往往需要在開發的過程中,配合其他很多程式一起開發調試,最典型的就是和用戶端軟體互動。

假設每個參與項目的程式員,都集中連接配接到一個開發伺服器上進行調試,必定産生各種互相影響的事情。而且這種跨機的開發環境,往往也隻有一些指令行界面,比不上圖形界面的IDE軟體使用效率高。假設我們開發的程式,特别是伺服器端軟體,能直接在開發的工作機上運作和調試,那麼除了能響應更快以外,還可以友善的在多個程式同時運作的時候進行調試,這對于常常因為“聯調”而頭疼的工程師來說,是一個非常有效的提升工作效率的措施。

但上面說的這些措施,都需要我們的伺服器端程式,能很友善簡單的部署到各種環境上。反過來說,有的伺服器系統結構比較複雜,要啟動很多程序,配對很多配置檔案才能啟動,那麼大家一定會懶得部署很多套,而是都擠到一個環境上做開發。

防止資源洩漏。我們知道,伺服器端程式需要長期運作,特别害怕資源系列,比如記憶體漏洞、檔案句柄漏洞、網絡連接配接相關漏洞等等。是以很多時候,我們願意在伺服器一啟動的時候,就把所有需要的資源,都全部“占用”或“配置設定”好,然後不管後續來了多少個請求,做什麼事情,都完全不需要“配置設定”,這樣就杜絕了一切的“洩漏”。然而,這個方法也導緻程式在運維上的複雜程度大大提高。

首先,我們難以明确的寫死一個程式所運作的硬體資源,而是設計了諸如配置檔案、指令行參數這些東西,來根據運作時的環境,來确定可能使用的硬體資源。比如我們會在配置檔案中,設計一個“網絡協定緩沖區大小”的配置項,根據伺服器的記憶體大小來配置。但是,一個程式中的功能可能是很複雜的,要把所有用到的記憶體、檔案等資源都弄成配置項,這個配置檔案必定也同樣巨複雜無比。

如果我們指望運維人員去了解這些配置檔案,那還不如開發人員自己去做運維,因為開發人員有時候自己都沒想清楚這些資源的合理配置應該是怎樣——原因是太過于依賴這種“預先申請”的方式,而習慣于把這些難處理的問題,都延後來解決了。防止資源漏洞,固然是一個重要的問題,但是僅僅是簡單化的把資源申請都變成配置檔案,卻同樣帶來另外一個災難。

特别要命的是,這種配置檔案災難在多程序協作的系統中,會承幾何倍數的增長。這種運維複雜度,在一個系統剛剛上線的時候,似乎都還可以接受,但是随着系統逐漸變得更大、更複雜,運維工作的難度就好像溫水煮青蛙一樣,慢慢的變得完全不可收拾。有些系統随着3-5年的營運,到後來居然發展到完全沒有人能從零開始部署一套新環境的地步。

如何設計運維友好的伺服器端系統

打一成語

快速診斷故障。現在的商業應用系統,往往都不會是一個很簡單的功能體,而是會包含了大量相關或者不相關的功能。而我們最害怕的問題,就是這些共同運作的功能,在同時處理成千上萬的網絡請求的時候,如果因為某一個部分功能代碼的BUG,導緻整個系統不可用。是以我們往往更希望建立某種隔離體系,比如把不同功能的代碼運作在不同的程序裡。

這樣利用作業系統的工具,就能很快的發現那些出問題的代碼。但如果你真的要把一個系統的多個功能分開到不同的程序中運作,首先會碰到的就是程序間通訊的問題。這個問題是現代分布式系統的核心問題之一,無數的開源軟體項目都在試圖解決這個問題。但不管你是使用開源軟體,還是自己寫代碼解決,這都會讓系統的程序變多。特别是我們很喜歡按功能來劃分代碼和程序,那就是說在運維一個系統的時候,我們需要面對大量“不同種類”的程序。

而我們越是劃分功能細緻,程序的種類就越多,需要操作和運維的程序就複雜。在管理這些程序的時候,除了前面說到的一些性能參數需要配置,還有巨量的程序間關系需要配置。而這些程序間關系,還會随着業務的變更而變化,對于那些沒有具體接觸開發需求的運維人員來說,簡直是噩夢。

也許有些程式員一開始是在通信企業工作,是以很習慣于按程序劃分功能,按通信層次來組織系統,但是随着業務系統的日益複雜化,這種工作習慣帶來的是大量的麻煩——每周都有可能需要往系統中加入新程序,或者調整某些程序的通信關系。不同的行業需要不同的技術方案,這才是理性的工程師的想法。

如何設計運維友好的伺服器端系統

小貓喜歡畫地為牢,我們的代碼也喜歡用程序邊界作牢

負載均衡。現代的伺服器端系統,基本上都是分布式系統。也就是由多個伺服器、多個程序組合起來提供服務的系統。而為了讓這個系統工作穩定,最常見的措施,就是防止過載。在多個程序中防止某一個過載,就需要負載均衡。

而防止所有同類的所有程序過載,則需要過載保護。分布式系統最常見的配置工作之一,就是要配置每類程序啟動多少個,每個程序的過載保護門檻值。然而,在一個上千個程序,幾百個伺服器的系統中,要準确無誤的填對這些配置,實際上是很難的。特别是這些伺服器并不都如提供商說的那樣是性能一緻的。

假如你需要在叢集中加入一些伺服器,或者修改(搬遷)某些伺服器上的服務,那麼更是危險重重,因為稍有不慎,就可能讓原來能工作的系統出現故障。然而,和業務需求在不停變化一樣,運維環境也是在不停變化,比如搬遷IDC就是最常見的“折騰”。

我們大可以編寫很多運維管理的工具,來試圖“自動化”這些工作,但是,業務需求也是在不停“折騰”的,而在一些“開發、運維分離”的團隊中,開發人員可不太關心運維工具的開發,因為他們已經被市場和業務人員逼得連續加班,隻想功能盡快上線拉倒了。由于需要負載均衡,而産生的大量伺服器端軟體的與我内工作量,由于和叢集中巨大伺服器的數量相關,是以是最直接展現運維和開發伺服器端系統困難的地方。

為了讓伺服器端系統能夠良好的運作,我們顯然應該采取一些開發措施,而不單純的依靠所謂“運維”甚至更不靠譜的“管理”手段來降低失誤和故障。

第一個可供參考的思路,就是“建立具備性能彈性的系統”。是以性能彈性,最簡單的是指,我們的伺服器程序,可以在各種不同的性能環境下運作,而無需複雜的配置檔案或運維操作。這裡除了最簡單的自己檢測機器的IP位址、記憶體大小等自我配置的功能外,更重要的是我們對于資源管理的思路上的改進。由于一個系統要處理的問題可能比較複雜,需要使用到的資源也會很複雜,比如我們需要用記憶體來緩沖沒收完的網絡包,還需要用記憶體來存放使用者的會話資料等等。

如果我們隻是把這樣一塊塊記憶體都提出來配置,就會有一大堆各種記憶體容量的配置。然而,我們完全可以通過建立一個業務抽象,來簡化這種資源模型。比如對于一個線上互動的系統,我們可以把資源管理的機關定義為“會話”——每個會話代表了一次“并發”的服務,每個會話要使用多少資源,是我們可以設計的,然後我們注意管理總的“會話”數量,防止資源洩漏。

當然這種“會話”在不同的業務系統中,其概念和功能可能都不一樣,幸好我們還可以用面向對象的思想,來用類和對象封裝這些會話及其相關資料。這樣我們在性能規劃的時候,就不用在程式到處翻找使用“資源”的地方機器配置,而僅僅抓住一個關鍵變量就可以了。更進一步的是,我們可以對“會話”這類關鍵名額,采用一種“池”的管理政策,把對這種對象的使用,變成需要“申請/歸還”的機制,這樣我們放棄在一開始就“配置設定”大量資源的做法,而是根據實際需要來配置設定資源,而由于“池”的限制,在資源達到上限的時候,拒絕進一步的服務請求,在防止資源漏洞的同時,解決一些過載保護的問題。

而且,在某些環境下,我們還可以讓這個“資源池”變得更智能和彈性,比如我們可以在請求壓力接近門檻值上限的時候,不是簡單的拒絕服務,而是開始啟動一些擴容或者報警的工作。又或者我們可以定期的查詢被“申請”的資源的處理情況,如果發現占用時間過久,就可以清理掉這些服務請求,這樣就有了一定的自我恢複服務的彈性。

如果建立了具備“資源彈性”的系統能力,這樣的程序隻需要進行很少的配置就能自我管理和運作。從根本上減輕了運維工作的複雜程度,也降低了環境變化對系統的影響程度。同時抽象良好的功能代碼,在代碼維護和開發上也非常有好處,可謂一舉多得。

如何設計運維友好的伺服器端系統

子彈封裝了火藥、彈頭、底火,是以告别了通馬桶式發射

第二個思路,是“在功能容器下運作”。在某個項目實踐中,我見過某一個系統,他的每個程序,都包含了整個系統的全部功能代碼。通過啟動時的指令行參數,可以指定此程序需要提供什麼功能。這個系統在運維的便利性上,就遠遠比需要配置、部署各種不同功能釋出包的系統來的簡單。而且這個伺服器系統,還可以以單程序全功能的形态,用于開發和自動化測試,在開發效率上有着明顯的優勢。

而在JSP/Servlet技術的使用中,我們往往也是把不同的WebApp部署到不同的Servlet容器(如Tomcat/Resin等)中運作,而不需要完整的配置各種不同的Servlet容器。現在還有一些系統,把主要的業務功能,都用類似python/JS/Lua這類腳本語言來編寫,系統中的程序部署,隻要完成了腳本容器(引擎)後,基本上就是拷貝腳本檔案而已。在容器技術的支援下,我們除了可以簡化部署的工作,還可以獲得一些“熱更新”的好處。

而基于硬體、通路量的運維工作,運維人員可以集中注意力管理好“容器”即可。比如GoogleAppEngine,就是一個高度自動化的Web App容器,使用者甚至完全不需要安裝部署任何軟體,直接上傳一個PHP腳本或者Servlet類檔案,就可以開始提供服務。在容器下運作伺服器系統,還可以利用容器規定的一些通訊規範,做一些自動化運維的事情,比如自動擴容、縮容、容災——容器可以自我發現叢集的運作狀态,加入新的運作資源,剔除有故障(比如通路逾時)的運作資源。這也是所謂SOA概念最常見的實作方式。

從另外一個角度說,如果有了容器支援,我們在配置伺服器程序的時候,是可以簡化對整個叢集中各種關系的配置的,因為隻要告訴容器,怎樣加入一個目标的叢集,其他的事情都可以讓容器去和其他叢集成員協商配置。容器除了提出了統一的功能代碼開發環境限制,還規範了運維工作。這對于需要頻繁變化服務内容,以及不斷改變運作環境的項目來說,是非常有價值的。

在WEB開發領域,容器的概念已經是深入人心了,是以這一類的系統應用比較廣泛,而運維工作也能比較專業順利的開展,但是在諸如網絡遊戲這種沒有“行業标準”的領域,關于功能容器的概念還是沒有被很多人接受,很多人還是在質疑為什麼要給自己套上這個“枷鎖”,卻不知道自由從來都是在限制下行走的。

如何設計運維友好的伺服器端系統

程序需要容器,功能也同樣需要容器,有容器比沒容器好!

最後,我想說說各種運維工具,不管是Chef,還是各種非通用的運維部署系統,如果僅僅以作業系統提供的能力,就想把所有的系統都統一管理起來,是非常困難的。而如果我們在開發的時候,就充分考慮到系統的運維需求,那麼可能隻進行了一些簡單的限制,都能讓運維工作有巨大的改進。我想這也是所謂DevOps流行起來的原因吧。