2005年,我開始和朋友們開始拉活兒做網站,當時第一個網站是在linux上用jsp搭建的,到后來逐步的引入了多種框架,如webwork、hibernate等。在到后來,進入公司,開始用c/c++,做分布式計算和存儲。(到那時才解開了我的一個疑惑:C語言除了用來寫HelloWorld,還能干嘛?^_^)。
總而言之,網站根據不同的需求,不同的請求壓力,不同的業務模型,需要不同的架構來給予支持。我從我的一些經歷和感受出發,大體上總結了一下的一些階段。詳情容我慢慢道來。
【第一階段:搭建屬于自己的網站】
我們最先開始的網站可能是長成這個樣子的:

拿Java做例子,我們可能會引入struts、spring、hibernate等框架,用來做URL分流,C、V、M隔離,數據的ORM等。這樣,我們的系統中,數據訪問層可以抽取出很多公用的類,業務邏輯層也可以抽取出很多公用的業務類,同一個業務邏輯可以對應多個展示頁面,可復用性得到極大的增強。
不過,從性能上看,引入框架后,效率并不見得比第一種架構高,有可能還有降低。因為框架可能會大量引入“反射”的機制,來創建對應的業務對象;同時,也可能增加額外的框架邏輯,來增強隔離性。從而使得整體服務能力下降。幸好,在這個階段,業務請求量不大,性能不是我們太care的事情。J
【第三階段:降低磁盤壓力 】
可能隨著業務的持續發展,或者是網站關注度逐步提升(也有可能是搜索引擎的爬蟲關注度逐步提升。我之前有一個網站,每天有超過1/3的訪問量,就是各種爬蟲貢獻的),我們的請求量逐步變大,這個時候,往往出現瓶頸的就是磁盤性能。在linux下,用vmstat、iostat等命令,可以看到磁盤的bi、bo、wait、util等值持續高位運行。怎么辦呢?
其實,在我們剛剛踏進大學校門的時候,第一門計算機課程——《計算機導論》里面就給出了解決方案。依稀記得下面這個圖:

在我們的存儲體系里面,磁盤一般是機械的(現在Flash、SSD等開始逐步大規模使用了),讀取速度最慢,而內存訪問速度較快(讀取一個字節約10μs,速度較磁盤能高幾百倍),速度最快的是CPU的cache。不過價格和存儲空間卻遞減。
話題切換回來,當我們的磁盤出現性能瓶頸的時候,我們這個時候,就要考慮其他的存儲介質,那么我們是用cpu cache還是內存呢,或是其他形態的磁盤?綜合性價比來看,到這個階段,我個人還是推薦使用內存。現在內存真是白菜價,而且容量持續增長(我現在就看到64G內存的機器[截止2012-4-3])。
但是問題來了,磁盤是持久化存儲的,斷電后。數據不會丟失,而內存卻是易失性存儲介質,斷電后內容會丟失。因此,內存只能用來保存臨時性數據,持久性數據還是需要放到磁盤等持久化介質上。因此,內存可以有多種設計,其中最常見的就是cache(其他的設計方式會在后面提及)。這種數據結構通常利用LRU算法(現在還有結合隊列、集合等多種數據結構,以及排序等多種算法的cache),用于記錄一段時間的臨時性數據,在必要的時候可以淘汰或定期刪除,以保證數據的有效性。cache通常以Key-Value形式來存儲數據(也有Key-SubKey-Value,或者是Key-List,以及Key-Set等形式的)。因為數據存放在內存,所以訪問速度會提高上百倍,并且極大的減少磁盤IO壓力。
Cache有多種架構設計,最常見的就是穿透式和旁路式。穿透式通常是程序本身使用對應的cache代碼庫,將cache編譯進程序,通過函數直接訪問。旁路式則是以服務的方式提供查詢和更新。在此階段,我們通常使用旁路式cache,這種cache往往利用開源的服務程序直接搭建就可以使用(如MemCache)。旁路式結構如下圖:

請求來臨的時候,我們的程序先從cache里面取數據,如果數據存在并且有效,就直接返回結果;如果數據不存在,則從數據庫里面獲取,經過邏輯處理后,先寫入到cache,然后再返回給用戶數據。這樣,我們下次再訪問的時候,就可以從cache中獲取數據。
Cache引入以后,最重要的就是調整內存的大小,以保證有足夠的命中率。根據經驗,好的內存設置,可以極大的提升命中率,從而提升服務的響應速度。如果原來IO有瓶頸的網站,經過引入內存cache以后,性能提升10倍應該是沒有問題的。
不過,有一個問題就是:cache依賴。如果cache出問題(比如掛了,或是命中率下降),那就杯具了L。這個時候,服務就會直接將大的壓力壓向數據庫,造成服務響應慢,或者是直接500。
另外,服務如果重新啟動時,也會出現慢啟動,即:給cache充數據的階段。對于這種情況,可以采取回放日志,或是從數據庫抽取最新數據等方式,在服務啟動前,提前將一部分數據放入到cache中,保證有一定命中率。

通過這樣的拆分后,我們就邁出了多機的第一步。雖然看起來比較簡單和容易,但是這也是非常具有里程碑意義的。這樣的優化,可能會提升20-30%左右的一個CPU idle。能夠使得我們的網站能夠經受更大的壓力。
【第五階段:邏輯程序的多機化】
當我們的訪問量持續增加的時候,我們承受這成長的快樂和痛苦。流量刷刷往上漲,高興!但是呢,服務器叫苦不絕,我們的程序已經快到不能服務的邊緣了。怎么辦?
“快使用分布式,哼哼哈嘿”J
這個時候,我們就需要針對CPU瓶頸,將我們的程序分別放在多臺服務器上,讓他們同時提供服務,將用戶請求分攤到多個提供服務的機器。
好,如果提供這樣的服務,我們會遇到什么樣的問題?怎么樣來解決?
我們一個個的來分析:
一、WebServer怎么樣來分流用戶請求?That’s a good question!
在考慮這個問題的時候,我們常見的WebServer早已給我們想好了解決方案。現在主流的WebServer幾乎都提供一個叫“Load Balance”的功能,翻譯過來就是負載均衡。我們可以在WebServer上配置一組機器列表,當請求來臨的時候,WebServer會根據一定的規則選取某一臺機器,將請求轉發到對應的邏輯處理程序上。
有同學馬上就會問了,“一定的規則”是怎么樣的規則?他能解決什么樣的問題?如果機器宕了怎么辦?……
哈哈,這里的一定規則就是“Load Balance”。負載均衡其實是分布式計算和存儲中最基礎的算法,他的好壞,直接決定了服務的穩定性。我曾經設計和開發了一個負載均衡算法(現在正在大規模使用),有一次就因為一個很小的case,導致服務大面積出現問題。
負載均衡要解決的就是,上游程序如何在我們提供的一堆機器列表中,找到合適的機器來提供下游的服務。因此,我們可以將負載均衡分成兩個方向來看:第一,根據怎樣的規則來選機器;第二,符合規則的機器中,哪些是能提供服務的。對于第一個問題,我們通常使用隨機、輪詢、一致Hash等算法;對于第二個問題,我們要使用心跳、服務響應判定等方法檢測機器的健康狀態。關于負載均衡,要談的話點其實很多,我之前也寫過專門的一篇文章來介紹,后續有空了,我再詳細的描述。
總之,分流的問題,我們可以通過負載均衡來比較輕松的解決了。
二、用戶的session如何來同步?That’s a good question,TOO!
雖然HTTP協議是一個無狀態的服務協議,但是,用戶的基本信息是要求能夠保證的。比如:登錄信息。原來在單機的時候,我們可以很簡單的使用類似setSession(“user”, ”XXX”)的函數來解決。當使用多機的時候,該怎么樣來解決呢?
其實這個問題也是當年困擾我很久的一個問題。如果用setSession,用戶在某一臺機器上登錄了,當下次請求來的時候,到其他機器了,就變成未登錄了。Oh,My God!
Ok,讓我們一個個的來看:
1、一臺機器登錄,其他機器不知道;
2、用戶請求可能到多臺機器。
對于第一個問題,如果我們在一臺機器的登錄信息讓其他機器知道,不就OK了嘛。或者,大家都在一臺機器上登錄,不就可以了嘛。 對于第二個問題,如果我們讓同一個用戶的請求,只落在同一臺機器上,不就OK了嘛。因此,我們可以提出三種解決方案:
1、提供session同步機制;
2、提供統一session服務;
3、將同一用戶分流到同一機器。
嗯,這三種方式,你會選哪個呢?如果是我,我就選最后一個。因為我是一個懶蟲,我會選最簡單的一個,我信奉的一個原則既是:簡單粗暴有效!哈哈。在WebServer層使用一致Hash算法,按session_id進行分流(如果WebServer沒有提供該功能,可以簡單寫一個擴展,或者干脆在WebServer后面做一個代理即可)。但是這種方案有一個致命的問題,當一臺機器宕機了以后,該機器上的所有用戶的session信息即會丟失,即使是做了磁盤備份,也會有一段時間出現session失效。
好,那看看第一種方案。其實現在有一些框架已經提供了這樣的服務機制。比如Tomcat就提供session同步機制。利用自有的協議,將一臺機器上的session數據同步到其他的機器上。這樣就有一個問題,我需要在所有的機器上配置需要同步的機器,機器的耦合度瞬間就增加了,煩啊!而且,如果session量比較大的話,同步的實效性還是一個問題。
那再來看看第二種方案,提供統一session服務。這個就是單獨再寫一個邏輯程序,來管理session,并且以網絡服務的方式提供查詢和更新。對于這樣的一個階段的服務來講,顯得重了一些。因為,我們如果這樣做,又會面臨一堆其他的問題,比如:這個服務是否存在單點(一臺服務器,如果宕機服務就停止),使用什么樣的協議來進行交互等等。這些問題在我們這個階段都還得不到解決。所以,看起來這個方案也不是很完美。
好吧,三種方案選其一,如果是你,你會選哪一種呢?或者還有更好的方案?如果我沒錢沒實力(傳說中的“屌絲”,哈哈),我就可能犧牲一下服務的穩定性,采用代價最低的。
三、數據訪問同步問題。
當多個請求同時到達,并且競爭同一資源的時候(比如:秒殺,或是定火車票),我們怎么來解決呢?
這個時候,因為我們用到了單機數據庫,可以很好的利用數據庫的“鎖”功能來解決這個問題。一般的數據庫都提供事務的功能。事務的級別分多種,比如可重復讀、串行化等,根據不同的業務需求,可能會選擇不同的事務級別。我們可以在需要競爭的資源上加上鎖,用于同步資源的請求。但是,這個東東也不是萬能的,鎖會極大的影響效率,所以盡量的減少鎖的使用,并且已經使用鎖的地方盡量的優化,并檢查是否可能出現死鎖。
cache也有對應的解決方案,比如延遲刪除或者凍結時間等技術,就是讓資源在一段時間處于不可讀狀態,用戶直接從數據庫查詢,這樣保證數據的有效性。
好了,上述三個問題,應該涵蓋了我們在這個階段遇到的大部分問題。那么,我們現在可以把整體的架構圖畫出來看看。

樣的結構,足夠我們撐一段時間了,并且因為邏輯程序的無狀態性,可以通過增加機器來擴展。而接下來我們要面對的,就是提交增長和查詢量增加帶來的存儲性能的瓶頸。
【第六階段:寫分離,提升IO性能】
好了,到現在這個階段,我們的單機數據庫可能已經逐步成為瓶頸,數據庫出現比較嚴重的讀寫沖突(即:多個線程或進程因為讀寫需要,爭搶磁盤,使得磁盤的磁頭不斷變換磁道或盤片,造成讀寫都很緩慢)。
那我們針對這樣的問題,看看有哪些方法來解決。
一、減少讀取量。我們所有的問題來源就是因為讀寫量增加,所以看起來這個是最直接最根源的解決辦法。不過,用戶有那么大請求量我們怎么可能減少呢?其實,對于越后端的系統,這是越可能的事情。我們可以在每一層都減少一部分往后傳輸的請求。具體到數據庫的話,我們可以考慮通過增加cache命中率,減少數據庫壓力。增加cache命中率有很多中方法,比如對業務訪問模式進行優化、多級cache模式、增加內存容量等等。業務模式的修改不是太好通用,因此這里我們考慮如何通過增加內存容量來解決問題。
對于單機,現在通用的cache服務一般都可以配置內存大小,這個只需要很簡單的配置即可。另外,我們也可以考慮多機cache的方案,通過增加機器來擴充內存容量。因此,我們就引入了分布式cache,現在常用的cache(如:memcache),都帶有這樣的功能,支持多機cache服務,可以通過負載均衡算法,將請求分散到多臺不同的機器上,從而擴充內存容量。
這里要強調一點。在我們選擇均衡算法的時候,是有考慮的。這個時候,常常選賊一致Hash算法,將某一系列ID分配到固定的機器,這樣的話,能放的KV對基本等于所有機器相加。否則,如果不做這樣的分配,所有機器內存里面的內容會有大量重復,內存并沒有很好的利用。另外,因為采用一致Hash,即使一臺機器宕掉,也會比較均勻的分散到其他機器,不會造成瞬間其他機器cache大量失效或不命中的問題。
二、減少寫入量。要減少用戶的提交,這個看起來是不太現實的。確實,我們要減少寫入的量似乎是很難的一件事。不過也不是完全不可能。這里我們會提到一個思想:合并寫入。就是將有可能的寫入在內存里進行合并,到一定時間或是一定條件后,再一起寫入。其實,在mysql等存儲引擎內部,都是有這樣的機制的。打個比方,比如有一個邏輯是修改用戶購買物品的數量,每次用戶購買物品后,計數都加一。如果我們現在不是每次都去實時寫磁盤,而是到一定的時間或一定次數后,再寫入,這樣就可以減少大量的寫入操作。但是,這里需要考慮,如果服務器宕掉以后,內存數據的恢復問題(這一部分會在后面來描述)。因此,如果想簡單的使用數據合并,最好是針對數據重要性不是很強的業務,即使丟掉一部分數據,也沒有關系。
三、多機承擔請求,分散壓力。如果我們能將原來單機的服務,擴充成多機,這樣我們就能很好的將處理能力在一定限度內很好的擴展。那怎么來做呢?其實有多種方法,我們常用的有數據同步和數據訂閱。
數據同步,我們將所有的更新數據發送到一臺固定的數據服務器上,由數據服務程序處理后,通過日志等方式,同步到其他機器的數據服務程序上。如下圖:

這種結構的好處就是,我們的數據基本能保證最終一致性(即:數據可能在短暫時間內出現不一致,但最后的數據能達到一致),而且結構比較簡單,擴展性較好。另外,如果我們需要實時數據,我們可以通過查詢Master就行。但是,問題也比較明顯,如果負責處理和分發的機器掛掉了,我們就需要考慮單點備份和切換方案。
數據訂閱,我們也可以通過這樣的方式來解決數據多機更新的問題。這種模式既是在存儲邏輯和數據系統前,增加一個叫做Message Queue(消息隊列,簡稱MQ)的東東,前端業務邏輯將數據直接提交到MQ,MQ將數據做排隊等操作,各個存儲系統訂閱自己想要的數據,然后讓MQ推送或自己拉取需要的數據。

MQ不帶任何業務處理邏輯,他的作用就是數據轉發,將數據轉發給需要的系統。其他系統拿到數據后,自行處理。
這樣的結構,好處是擴展比較方便,數據分發效率很高。但是問題也比較明顯,因為處理邏輯分散在各個機器,所以數據的一致性難以得到保證。另外,因為這種模式看起來就是一個異步提交的模式,如果想得到同步的更新結果,要做很多附加的工作,成本很高且耦合度很大。還有,需要考慮MQ的單點備份和切換問題。
因為現在數據庫(如Mysql)基本帶有數據同步功能,因此我們在這個階段比較推薦數據同步的方法。至于第二種方式,其實是很好的一種思想,后續我們會有著重的提及。那再來看我們的架構,就應該演變成這樣的結構。

到目前這個階段,我們基本上就實現了從單機到多機的轉變。數據的多機化,必然帶來的問題:一致性!這個是否有解決方案?這個時候我們需要引入一個著名的理論:CAP原理。
CAP原理包含了三個要素:一致性(Consistency)、可用性(Availability)、分區容忍性(Partition tolerance)。三個要素中,最多只能保證兩個要素同時滿足,不能三者兼顧。架構設計時,要根據業務需要進行取舍。比如,我們為了保證可用性和分區容忍性,可能會舍去一致性。
我們將數據分成多機,提高了系統的可用性,因此,一致性的保證很難做到強一致性。有可能做到最終一致性。這也是分布式引入以后的煩惱。
這樣的一個系統,也是后續我們分布式架構的一個雛形,雖然比較粗糙,但是他還是比較簡單實用,對于一般中型網站,已經能很好的解決問題。
【第七階段:拆分】
到上面一個階段,我們初步接觸到了邏輯、存儲等的多機模式。這樣的結構,對于邏輯不是特別復雜的網站,足以撐起千萬級的壓力。所以大多數網站,只要能夠用好上面的結構就可以很好的應對服務壓力了。只不過還有很多細節的工作需要精細化,比如:多機的運維、穩定性的監控、日志的管理、請求的分析與挖掘等。
如果流量持續增長,或者是業務持續的擴展,上述的架構可能又將面臨挑戰。比如,多人開發常常出現版本沖突;對于數據庫的更新量變大;一個表里面的記錄數已經超過千萬甚至過億等等。
怎么解決呢?還記得我們之前介紹過一個CAP理論嘛?三要素里面有一個東東叫:分區容忍性(Partition tolerance)。其實,這個就是我們接下來解決問題的基礎:切分!
一、從數據流向來看,切分包括:請求的切分、邏輯的切分、數據的切分。
數據的切分:將不同的數據放到不同的庫中,將原來的單一的一個庫,切分成多個庫。
邏輯的切分:將不同的業務邏輯拆分成多份代碼,用不同的代碼管理路徑來管理(如svn目錄)。
請求的切分:將不同的邏輯請求分流到不同的機器上。比如:圖片請求、視頻請求、注冊請求等。
二、從數據組織來看,切分包括:水平切分、垂直切分。
數據庫的變大通常是朝著兩個方向來進行的,一個是功能增加,導致表結構橫向擴展;一個是提交數據持續增多,導致數據庫表里的數據量持續縱向增加。

數據量變大以后,單機性能會下降很明顯,因此我們需要在合適的時候對數據進行切分(這個我沒有太深入的研究過相關數據庫的最合適的切分點,只是從經驗上來講,單表的字段數控制在20個以內,記錄數控制在5千萬以內會比較好些)。
垂直切分和水平切分,其實是挺糾結的兩個詞。我之前對這兩個詞經常搞混。后來自己畫了個圖,就很直接明了了。
水平切分:

水平切分就是因為記錄數太多了,需要橫著來一刀,將原來一張表里面的數據存入到多張表中,用于減少單張表里的數據量。
垂直切分:

垂直切分就是因為業務邏輯需要的字段太多,需要豎著來一刀,將原來放在一張表里的所有字段,拆分成多張表,通過某一個Key來做關聯(如關系數據庫中的外鍵),從而避免大表的產生。
好了,有了上述的基礎以后,我們再來看實際問題如何來解決。
假設,現在我們有一個博客網站,這個網站擁有多個功能,如:圖片、博客、用戶信息等的插查刪改操作。而現在博客數據膨脹比較厲害。
首先,我們從數據流向來看,用戶訪問博客、圖片、用戶信息等這幾個邏輯沒有直接的耦合,對應的業務邏輯關聯也很少。
因此,我們第一步從入口上就可以把三者分開。最簡單的方式就是通過域名來切分,比如:img.XXX.com、blog.XXX.com、user.XXX.com。然后通過不同的WebServer來接收這些請求。
第二步,我們的業務邏輯代碼,很明顯可以將這些邏輯分開(從部署上分開)。一部分專門處理圖片的請求,如ImageUploadAction/ImageDisplayAction/ImageDeleteAction,一部分專門處理博客請求,如:BlogDisplayAction/BlogDeleteAction,一部分專門處理用戶相關請求,如:UserModifyAction/UserDisplayAction等等。
第三步,從數據庫存儲上,將三者剝離開。簡單的就是分成三個不同的庫。
這樣,從數據流向上,我們就按不同的功能,將請求進行了拆分。
其次,從數據存儲上來看,由于博客數據量增長比較快,我們可以將博客的數據進行水平的拆分。拆分方法很多,比如常用的:
1、按區間拆分。假定我們用blog_id作為Key,那么我們可以每1千萬,做一次切分。比如[1,1kw)、[1kw,2kw)等等。這樣做的好處就是可以不斷的增長。但訪問可能會因為博客新舊的原因,集中到最新的幾個庫或表中。另外,要根據數據的增長動態的建表。
2、按取模拆分。比如我們預估我們的blog_id最多不超過10億,如果每張表里面我們預估存入1千萬的數據,那么我們就需要100張表(或庫)。我們就可以按照blog_id % 100 這樣來做切分。這樣做的好處是簡單,表在一開始就全部建立好了。且每個表或者庫的訪問都比較均勻。但問題就是,如果數據持續擴張,超出預期,那么擴展性就成為最主要的問題。
3、其他還有一些衍生的方式,比如按Hash值切分等等,大多大同小異。
這樣一來,我們通過訪問模式、數據組織等多個維度的拆分以后,我們單機能夠提供服務的能力就變的比較強悍了。具體的架構如下圖。

上述結構看似比較完美,但是在實際的使用中可能會遇到以下幾個問題:
1、業務關聯問題。多個Service之間不可能沒有任何關聯,如果出現關聯,怎么辦?特別是如果是提交的信息要修改多個業務的數據的時候,這個會比較頭疼。
2、服務運維問題。這樣拆分以后,隨著機器數量的膨脹,對于機器的管理將會變的愈發的困難。這個問題直接會影響到整體架構的設計。面向運維的設計是架構設計中必須要考慮的重要因素。
3、還有一個問題是我們WebServer始終是單機的,如果出現宕機等問題,那影響將是致命的。這個我們還沒有解決。
這些問題都會在接下來的部分詳細來解決。
【第八階段:WebServer多機化】
上面說了這么多,我們的業務都基本上運轉在只有一個WebServer的條件下。如果出現宕機,所有服務就停掉了;如果壓力大了,單機不能承載了,怎么辦?
說到這個話題,我們需要來回顧一下在大學時學習的關于網絡的基本知識。^_^
拋開復雜的網絡,我們簡化我們的模型。我們的電腦通過光纖直接連入互聯網。當我們在瀏覽器地址欄里面輸入http://www.XXX.com時,到我們的瀏覽器展現出頁面為止,中間出現了怎么樣的數據變化?(注意:為了不那么麻煩,我簡化了很多東西,比如:NAT、CDN、數據包切片、TCP超時重傳等等)

上面的圖我們應該比較熟悉,同時也應該比較清晰的表達了我們簡化后,從輸入網址到頁面展現的一個過程。中間有兩個東西我們比較關注,也是解決我們WebServer多機化的關鍵。
1、DNS服務是否能幫我們解決多機化?
2、www.XXX.com服務器的WebServer如何多機化?
首先,如果DNS解析能夠根據我們的請求來區分,對于同一域名,將不同的用戶請求,綁定到不同的ip上,這樣,我們就(友情提示:word統計此處已經達到10000字)能部署多個WebServer,對應不同的ip,剩下的無非就是多申請幾個ip地址而已。
當我們網站比較小的時候,我們都是在代理商處購買域名并由代理商的服務域名解析服務器幫我們做域名解析。但是,對于許多大型的網站,都需要對類似于www.XXX.com、blog.XXX.com、img.XXX.com等在XXX.com根下的所有服務的進行域名解析,這樣便于對服務進行控制和管理。而域名的解析往往有專門的策略來處理,比如根據IP地域、根據不同請求IP的運營商等返回不同的服務器IP地址。(大家可能以前也有過這樣的經驗:在不同的地方,ping幾個大的網站,看到的ip是不一樣的)。

DNS策略分析和處理服務是對請求IP進行分析和判斷的系統,判斷請求來自哪個地域、哪個運營商,然后根據內部的一些庫的判斷,決定應該返回哪個WebServer的IP。這樣,就能盡量保證用戶以最快的速度訪問到對應的服務。
但是,如果我們有大量的WebServer,那每個Server都要有一個IP,另外,我們要增加一個新機器,又要申請一個IP地址,好像很麻煩,且不可接受。怎么辦呢?
第二點,我們需要考慮對于服務器的WebServer的多機化方式。
我們為什么要WebServer多機化?原因就是因為單機的處理性能不行了,我們要提升處理能力。
那WebServer要做哪些事情?Hold住大量用戶請求連接;根據URL將請求分流到不同邏輯處理的服務器上;有可能還有一些防攻擊策略等。其實這些都是消耗CPU的。
如果我們在WebServer前端增加一層,什么邏輯都不處理,就是利用一定的負載均衡策略將數據包轉發給WebServer(比如:工作在IP層,而非TCP層)。那這一層的處理能力跟WebServer比是否是要強悍很多?!這樣的話,這一層后面就可以掛載很多的WebServer,而無需增加外網IP。我們暫且叫這一層叫VS(Virtual Server)。這一層服務要求穩定性較高,且處理邏輯要極為簡單,同時最好工作在網絡模型中較低的層次上。

這樣的話,我們就只需要幾個這樣的VS服務器組,就可以組建大量的WebServer集群。當一個群組出現問題,直接可以通過改變IP綁定,就可以切換到其他服務器組上。
現在這樣的VS實現有多種。有靠硬件方式實現的,也有靠軟件方式實現的。硬件方式實現的話,成本較高,但穩定性和效率較好。軟件方式實現的,則成本較低,但穩定性和效率較硬件方式要低一些。
現在用的比較多的有開源的LVS(Linux Virtual Server),是由我國的一個博士寫的,NB!以及根據LVS改寫后的一些變種。
另外還有F5 Networks公司出的收費的F5-BIG-IP-GTM等。(注:這個確實沒用過,以前在網上看過,寫到此處記不清,在百度上搜的。如有錯誤,敬請雅正)。
好了,通過上述的方式,我們基本實現了WebServer的多機化。
【第九階段:邏輯關聯和層次劃分】
在第七階段的時候,我們提到了幾個問題,其中有一個就是業務關聯問題。當我們將業務拆分以后,多個業務之間沒有了耦合(或者是極弱的耦合),能夠獨立的運轉。這個看起來是多么美妙的事情。但是實際情況真是如此嘛?
這樣的業務還真是存在的。比如我們有兩個業務blog和image。blog可以上傳和展示圖片。那image.XXX.com就提供兩個HTTP服務,一個是上傳的,一個是顯示的。這樣,blog業務就可以通過簡單的URL耦合來實現了圖片的這些功能。
但真是所有的情況都是如此的嘛?
我們再看一個例子。比如blog和用戶相關的業務。用戶可以在blog登錄、注銷等,blog需要實時判斷某一個用戶是否登錄等。登錄和注銷兩個操作似乎可以通過類似account.XXX.com提供的login和logout這樣的URL接口實現。但是每次頁面瀏覽要判斷用戶是否已經登錄了,出于安全性等多方面的考慮,就不好通過URL來提供這樣的服務。
那看起來,我們在第七階段提出的按業務切分的理想情況,在實施的時候,并不是那樣的完美。在實際的運行中,耦合是不可避免的。
有了耦合,我的第一反應基本上就是看看是否能夠借助設計模式來解決這些的問題。其實呢,設計模式早已經給我們比較好的解決方案(但絕對不是完美的解決方案。俗話說的:沒有最好,只有更好!)。在這篇文章的最初已經提到過了,為了增強網站代碼的可重用性,我們引入了一些框架,比如:struts、spring、hibernate等等。其實這些框架,基本上是圍繞著MVC的原則來設計的。struts、webwork等框架,將視圖和邏輯控制分離;spring負責組織業務邏輯的數據;hibernate很好的做了數據訪問層的工作,實現了ORM。
那現在我們采用多機分布式的時候,是否可以借鑒這些思想呢?其實也是可以的。
我們來分析一下我們的業務。
其實我們的業務大多可以分為兩類:
一、與實際的產品相關的業務,比如:blog、news等等。這些業務之間的耦合度不是很高,往往可以通過提供HTTP的接口即可實現業務需要的互通。因此,從這個層面上來看,是可以基本做到業務垂直拆分的。
二、基礎服務,比如:用戶帳號管理、消息通知等等。這些服務往往被多個業務所依賴。他們需要提供更通用的、更安全的、更穩定的接口和服務。但是,關于基礎業務的理解和劃分,是沒有一個特定的規則的。比如,image圖片服務,他有可能剛開始是一個業務服務,到一定階段以后,多個系統需要對他有強的依賴,自然也就成為了一個基礎服務。
所以,從上面的描述,我們可以發現服務的類型,并不是固定的。要很好的解決服務耦合的問題,也并沒有一個十分完美的解決方案。我們能做的就是盡量降低耦合,通過某種方式,能夠很好的達到耦合和可維護性的平衡。
總的來看,MVC模式其實給我們提供了一個比較好的方案。
我們把系統從兩個維度上進行劃分:垂直(業務)和水平(邏輯)。

我們做了這樣的劃分以后,似乎看起來沒有實質性的改變。但是,我們可以明確了我們設計的原則,并強化了代碼的可復用性。另外,最關鍵的是,服務之間的依賴和耦合關系,有了明確的地方來做。同時,我們還可以將業務內部的結構進行拆分,更好的增強復用。
數據訪問和組織層、數據存儲層,這兩個位于下游的層次,應該是屬于系統內部的層次,原則上是最好不要對外開放接口的,否則,系統間的耦合就會非常的大。并且可維護性會非常的難。而邏輯控制和視圖層,實際上是提供對外(對用戶或者是外系統)最好的訪問的入口。當然,這個入口可以是HTTP協議的,也可以是非HTTP協議的。
比如,對于account服務,可以提供基于HTTPS對用戶開放的login和logout服務,也提供基于XXX Protocol數據交換的協議的給內部的get_session服務。從簡單的設計上來看,只是根據服務不同,提供不同的數據交換格式、以及不同的安全控制。這樣也是秉承了一個高內聚低耦合的原則。
這里還有幾個及其重要的問題沒有詳細的提及:系統內外的數據傳輸協議、接口API、服務訪問定位。這幾個問題實際上還跟運維問題緊密相關,都會放到后面來詳細討論。
【第十階段:數據存儲優化】
在前面的階段中,我們都使用數據庫作為默認的存儲引擎,很少談論關于關于數據存儲的話題。但是,數據的存儲卻是我們現在眾多大型網站面臨的最核心的問題。現在眾多網絡巨頭紛紛推出自己的“高端”存儲引擎,也吸引了眾多的眼球。比如:google的BigTable、facebook的cassandra、以及開源的Hadoop等等。國內眾多IT巨頭也紛紛推出自己的“云”存儲引擎。
其實這些存儲引擎用的一些關鍵技術有許多的共性,比如:Meta信息管理、分片、冗余備份、數據自動恢復等。因為之前我也做過一些工作和研究,但是不是特別深入,不敢在此指手畫腳、高談闊論。相關的資料網上比比皆是,大家有興趣有search吧。^_^
關系型數據庫常用的有幾個,比如:MySQL、PostgreSQL、SQLServer、DB2等,當然還有NB的Oracle(唉,作為DB科班出生的屌絲,沒有真正意義上使用過這樣的高帥富數據庫,慚愧啊)。
互聯網使用頻率最高的應該要算是MySQL。最重要的是開源;其次是他提供的一些特性,比如:多種存儲引擎、主從同步機制等,使用起來非常的方便;再次,就是一個單詞:LAMP,幾乎成為搭建網站的必備利器;還有,較高服務的穩定性。
關系型數據使得建立網站變得及其輕松,幾乎是個網站都會有一個數據庫。試想一下,如果沒有這種通用的關系型數據庫,我們的生活會是怎么樣的?
關系型數據庫在95%的場景下是工作的非常好的。而且只要配置得當、數據切分合理、架構設計符合要求,性能上是絕對能夠承受業務的需求。現在很多大型網站的后臺,幾乎都是數據庫作為標準的存儲引擎。
另外,最近炒的比較熱的一個概念就是NoSQL。說起來,其實就是放棄關系型數據庫中許多的特性,比如:事務、外鍵等等。簡化設計,將視線更關注于存儲本身。比較有名的,比如:BDB、MongoDB、Redis等等。這些存儲引擎提供更為直接的Key-Value存儲,以滿足互聯網高效快速的業務需求。其實,從另外一個角度來看,關系型數據庫(比如MySQL),如果不使用那么多的關系型數據庫特性,也可以簡化成KV模式,提升效率。
不過,有些時候,為了節省機器資源,提升存儲引擎的效率,就不得不開發針對業務需要的專用存儲引擎。這些存儲引擎的效率,往往較關系數據庫效率高10-100倍。比如,當一個圖片服務,存儲的圖片量從1億到10億,甚至100億;現在流行的微薄,假如發布總量達到10億或者100億。這樣級別的數據量,如果用數據庫來存儲固然可以,但是有可能需要耗費相當多的機器,且維護成本和代價不小。
其實分析我們通常的業務,我們對數據的操作無非就是四個:查、插、刪、改。對應數據庫的操作就是select、insert、delete、update。那自己設計的存儲引擎無非就是對這四個操作中的某幾個做針對性的優化,讓其中幾個根據不同的業務特點,使其變的更加的高效。比如:對于微薄而言,可能就需要插入和查詢具有很高的效率,而刪除和修改的需求不是那么高。這個時候,就可以犧牲一部分刪除和修改的性能,而重點放在插入和查詢上。
在業務上,我們的提交通常可以看作在某一個維度上是有序的。比如,每一個微薄或者博客可能都會有一個id,這些id可能是按照序列遞增、或是時間遞增等。而查詢的時候,則是按照另外一個維度的順序組織的,比如:按關注的人組織微薄的信息。這樣就造成了一個沖突,就是提交的組織順序和查詢的組織順序不一致。
再來看看我們的磁盤。我們現在常規磁盤還是機械方式運轉的:有盤片、有磁頭等等。當需要寫入或者讀取的時候,磁頭定位到不同的盤片的不同扇區上,然后找到或修改對應的信息。如果信息分散在不同的盤片、扇區上,那么磁頭尋道的時間就會比較長。
反過來,我們再來看看我們的業務和磁盤的組織。A、如果我們按照寫入有序的方式存儲數據,那么磁盤會以很高的效率,將數據連續的寫入到盤片中,無需多次尋道。那么讀取的時候,可能就會出現按照另外的維度來組織數據,這樣就有可能需要在多個地方來讀取。從而造成我們磁盤來回尋道定位,使得查詢效率低下。B、如果我們按照某一種查詢維度來存儲數據(因為同一業務往往有多種查詢模式),那讀取的時候,就讓磁盤順序讀取即可。但帶來的麻煩就是,寫入的時候有可能需要反復的尋道定位,將提交的數據一條條寫入。這樣就會給寫入帶來麻煩。
其實矛盾的主體就是:僵化的磁盤存儲方式不能滿足網站日益增長的提交和查詢需求!
是否有解決方案呢?那必須有!
要解決這個問題,可以從兩方面入手。
1、改變現有的磁盤存儲方式。隨著硬件快速的發展,磁盤本身的效率得到了極大提升。磁盤的轉速,盤片的個數等都大幅增加,本身尋道的速度提升很快。加上緩存等的加強,效率提升還是很明顯。另外,Flash Disk、Solid State Disk等新技術的引入,改變了原來的隨機讀取的低效(沒有數據,根據經驗,Flash或SSD的效率可以達到10-100倍普通硬盤的隨機訪問的效率)。
2、根據不同的業務,有效的組織數據。比如微薄(我沒有寫過微薄,但是做過微薄類似的東東),因為讀取業務組織的維度是按人,而提交組織的維度則是時間。所以,我們可以將某個人一段時間提交的數據,在內存里面進行合并,然后再一次性的刷入到磁盤之中。這樣,某個人一段時間發布的數據就在磁盤上連續存儲。當讀取的時候,原來需要多次讀取的數據,現在可以一次性的讀取出來。
第一點提到的東東現在也逐步的開始普及,其實他給我們的改變是比較大的。不用花太多的精力和時間,去精心設計和優化一個系統,而只需要花一些錢就能使得性能大幅提升,而且這樣的成本還在降低。
但是,資源永遠是不夠的。多年前,當內存還是64K的時候,我們暢想如果內存有個32M該多美妙啊。但是,隨著數據的膨脹,即使現在64G內存,也很快就不能滿足我們的需求。
所以,在一些特殊的應用下面,我們還是需要更多的關注第二個點。
對于存儲優化,一直是一個持久的話題,也有很多成熟的方案。我這里可能提幾個點。
【階段性小結】
經過了上述的架構擴展和優化以后,我們的系統無論是從前端接入,還是后端存儲都較最初的階段有了質的變化。這樣的架構足以支撐起10億級別的流量和10億級別的數據量。我們具體的來看一下整體的架構。

上述的模型是我個人覺得的一個比較理想的模型。Virtual Server Cluster接收數據包,轉發給Web Server Cluster或者Private Protocol Server Cluster(如果有的話)。然后視圖和邏輯層server負責調用cache或者數據訪問組織層的接口,返回處理后的數據。定制存儲系統、通用存儲系統和數據庫集群,提供基本的數據。
每一個層次通過負載均衡和一定的協議來獲取下一層提供的數據,或者提交數據。在存儲系統內部,通過Meta信息管理、主從同步、消息訂閱等方式,實現數據的同步。
如果我們再要擴大規模,比如:機器數擴展到上千臺、萬臺。對于我們來說,管理機器就成為了機器頭等的大問題。
同時,我們之前還有幾個問題沒有很詳盡的描述,比如:數據傳輸協議、遠程系統調用、系統的異構性等等,這些都是會影響到我們系統可維護性的大問題。
我7年前就開始使用Java,到現在,總算能看懂一些東西了。J2EE我個人覺得確實是一個比較偉大的東東。里面其實早已經提出了一套比較完善的解決大型或者超大型網站的整體解決方案。
比如:
1、JNDI(Java Naming and Directory Interface):描述了如何使用命名和目錄等的規范,使得我們能夠將服務作為資源掛接到命名服務器上,并且通過標準的接口進行訪問。這對我們管理巨大的機器資源和服務提供了很好的方案。
2、RMI(Remote Method Invoke):遠程過程調用。即,通過標準的RMI接口,可以輕松實現跨系統的遠程調用。
3、IDL(Interface Definition Language):接口定義語言。這個本來是CORBA中用來訪問異構系統對象的統一語言,其實也給我們提供了跨系統調用中,對外接口的定義方案。
4、JDBC(Java Database Connectivity):數據庫訪問接口。屏蔽了不同數據庫訪問的實現細節,使得數據庫開發變得輕松。
5、JSP(Java Server Page):實現將視圖和控制層很好分離的方式。
6、JMS(Java Message Service):用于和面向消息的中間件相互通信的應用程序接口。提供了很好的消息推送和訂閱的機制。
以上這些組件其實很好的協助構建了J2EE整體架構。我的很多想法都來源于這些東東。后續會結合這些,詳細來分析諸如資源命名位置服務、數據傳輸協議、異構系統接口定義等解決大規模機器運維問題的方案。
【第十一階段:命名位置服務】
在前面我們不止一次提到了命名位置服務(Naming & Location Service)。在不同的架構或者公司里面,這個名字往往不一樣,比如,在java里面叫JNDI(Java Naming & Directory Interface),在有些地方可能會叫做資源位置系統(Resource Location System)。
總之,不管叫什么名字,我們要知道的就是為什么要有這樣的系統?他能做哪些事情?他有哪些實現方式?等等。
在我們之前的章節中,我們的服務從一臺單機擴展到十臺左右的多機,到成百上千臺機器。我們的服務從單一的一個服務擴展到成百上千的服務。這么多的機器、服務,如果不好好管理,我們就崩潰了。比如,我們的服務A要連接服務B,如果現在采用配置IP的方式,可能需要配置幾十臺機器的IP,如果其中某些機器出現了變更,那所有服務A連接服務B的IP都要改變。如果所有的服務都是這樣,這將是多么痛苦的一件事情 Orz。
如果我們只是簡單的將我們的服務看成是一個個的資源(Resource),這些資源可以是數據庫,可以是cache,可以是我們自己寫的服務。他們都有一個共同的特點,就是在某一個IP上,打開一個PORT,遵循一定的協議,提供服務。
我們先簡單的來構建這樣一個模型。我們這里先抽象一個接口,叫做Interface Resource。這個接口下面有多個實現,比如:DBResource、CacheResource、ImageResource等等。具體類圖如下:

有了這樣的一個層次結構以后,我們為了得到某一個實例,有多種方式,比如:
1、直接生成的方式:
Resource r = new ImageResource();
2、間接生成的方式:
A、比如我們在設計模式中經常使用到的工廠模式:
Resource r = ResourceFactory.get("Image");
B、IoC(Inversion of Control)方式:
Resource r = (Resource)Container.getInstance("ImageResource");
我們打一個不是很完全匹配的比方。我們如果直接在服務中采用IP配置的方式,就類似于直接生成實例一樣,如果實例發生變化,或者要調整生成的對象,就需要修改調用者的代碼。也就是說,如果其他服務的IP發生變化,我們調用者就需要修改配置,重啟程序等等。而且對于如果有很多很多這樣的服務,我們就崩潰了。
那么,我們覺得更好的一種方式呢,就是,如果有一個工廠,或者一個容器,來幫我們管理這一堆的服務IP、端口、協議等等,我們要用的時候,就只需要叫一聲:“給我XXX服務的實例”,那是多么美妙的事情啊!
其實呢,我們的命名位置服務要做的就是這樣的事情。他類似于一個Meta Server,記錄所有服務的IP、port、protocol等基礎信息,以及檢查這些服務的健康狀態,提供給調用者最基礎的信息服務。同時,再結合調用時的負載均衡策略,就可以幫我們提供很好的資源管理方式。
這個服務,提供注冊、注銷、獲取列表等接口。他的存在,就將直接關聯的兩個服務給很好的解耦了。我們看看對比:

在沒有Naming Location Service的時候,我們的服務相互直接依賴,到最后,關聯關系及其復雜,可能完全沒有辦法維護。
如果我們增加Naming Location Service以后,這個狀態就可以得到極大的改善。

這個時候,我們所有的服務都在NLS上注冊,同時向NLS獲取其他服務的信息,所有的信息都匯聚到NLS上管理。
有了這個服務,就好類比成我們生成一個類的時候,采用間接的方式生成。
Service s = (Service) NamingService.getService("Image");
好,有了這樣一個架構以后,我們可能會關注這個NLS如何來實現。
實現這個服務有簡單的方式,也有復雜的方式。關鍵是要考慮以下幾個方面:
1、如何找到這個NLS。NLS是所有服務的入口,他應該是有一個不變的地址來保證我們的服務。因此,我們可以使用我們之前提到過的Virtual Server的方案,通過一個(或多個)固定的域名或者IP來綁定這個服務。
2、可用性(Availability)和數據一致性(Consistency)。因為這個服務是一個最基礎的服務,如果這個服務掛掉了,其他服務就沒有辦法來定位了。那么這個服務的穩定和可靠性就是及其重要的。解決方案有如下幾種:
A、單機實現服務,本地增加備份。我們用單機來實現這樣一個服務,這樣可以保證絕對的數據一致性。同時,每次請求數據后,每個服務本地保留一份備份數據。當這個服務掛掉了,就使用最近的一次備份。這個方案對于大多數情況是足以應付的,而且具備簡單粗暴有效的特點。
B、多機服務,數據同步。采用多機提供服務,信息更新時進行數據同步。這種方式的優點就是服務可以保證7*24小時服務,服務穩定性高。但是,問題就是維護成敗會比上面一種方式高。
3、功能。實現服務名稱到IP、Port、Protocol等信息的一個對應。如果要設計的更通用,比如可以注冊任何信息,就只需要實現Key-Value的通用數據格式。其中,Value部分需要支持更多更豐富的結構,比如List、Set等。
有了這樣的一個系統,我們就可以很方便的擴展我們的服務,并且能很好的規范我們服務的獲取、訪問接口。
【第十二階段:傳輸協議、接口、遠程調用】
這一部分主要談談關于協議、接口和遠程調用相關的內容。本來這一部分應該在之前就有比較詳細的討論,不過我放到后面來,足見其重要性。特別是在系統越來越多的時候,這幾個東東直接決定了我們的開發速度和運維成本。
好,接下來我們一個個的看。
1、傳輸協議
到目前為止,在不同系統之間獲取數據的時候,你是采用那種方式呢?
我們簡單看一個例子:

以上這個可能是我們最(|兩萬字的分隔線|)初學習網絡編程的時候,最常使用的一種C-S交互方式。其實這里面我們已經定義了一種交互協議,只是這種方式顯得比較山寨,沒有規范。擴充性等等都沒有充分考慮。
我們在學習網絡編程的時候,老師就給我們講過,網絡分層的概念,經典的有5層和7層模型。在每一層里面,都有自己的協議。比如:IP協議、TCP協議、HTTP協議等等。這些協議基本上都由兩部分組成:頭+數據。
【頭信息】
我們來看看TCP協議:

(注:以上是我從百度百科上截取的)
頭信息中,一般可能會包含幾個重要的元素:協議標識符、版本號、串號(或是本次交互的id)、數據包長度、數據校驗等信息,可能有些協議還會帶一些其他數據,比如數據發出方名稱、接收方名稱、時間、保留字段等等信息。
這些信息的目的,就是為了清晰的表達,我是怎么樣的一個協議,我有哪些特征,我帶的數據有多大。方便接收方能夠清晰的辨認出來。
【數據部分】
數據部分是為了讓應用層更好的通訊和表達數據。要達到的目標就是簡潔高效、清晰明了。說起來很容易,但是實現起來要考慮的東西就比較多,比如:數據壓縮、字符轉移、二進制數據表達等等。
我們通常有多種格式來作為數據部分的協議。大體上可以分為:
A、二進制流。比如:C里面的結構體、JAVA里面的Serializable,以及像Google的protobuf等。將內存里面實體的數據,按字節序列化到緩沖區。這種方式的好處就是數據非常緊湊,幾乎沒有什么浪費。但是,問題也比較明顯。雙方必須很清楚協議,面向的語言基本上是要求一樣的,很難做兼容,跨平臺差。且擴展性比較差。另外,還有網絡大小端字節序(Big-Endian、Little-Endian)的問題需要考慮。
B、文本傳輸協議。就是以字符串的方式來組織信息。常見的有XML、JSON等等。這種方式的好處就是擴展性強,跨平臺兼容能力好,接口標準且規范。問題就是傳輸量比較大,需要考慮做壓縮或者優化。
XML方式:

JSON方式:

因此,我們可以按照我們實際的需求,來定制我們想要的數據格式,從而達到高效和易于表達和擴展的效果。
經過上述分析,我們基本上對傳輸協議有了一個比較大致的了解。有了傳輸協議之后,我們的跨機器間的數據交互,才顯得比較規范和具有擴展性。如果我們還不想自己來定義協議,我們可以用現有的協議進行組合,比如:HTTP+XML、HTTPS+JSON等等。只要在一個平臺上,大家都遵守這樣的規范,后續開發起來就變得輕松容易。
2、接口(或者API)
接口是我們的服務對外表達的窗口。接口的好壞直接決定了我們服務的可表達性。因此,接口是一個承上啟下的作用。對外,很好的表達提供的服務名稱、參數、功能、返回的數據等;對內,能夠自動生成描述所對應的代碼函數框架,讓開發者編寫實現。
我們描述我們接口的方式有很多,可以利用描述性語言來表達。比如:XML、JAVA里提供的IDL(Interface Definition Language)等等。我們把這種描述接口的語言統一稱為IDL(Interface Definition Language)。

我們可以自己開發一些工具,將IDL進行翻譯,轉換成方便閱讀的HTML格式、DOC、CHM等等。方便其他開發者查閱。
同時,另外一方面,我們可以將IDL轉換成我們的接口代碼,讓服務接口的開發者和調用方的開發者按規范和標準來實現。

對于下層代碼如何來實現數據的解析、函數的調用、參數的傳遞、數據的轉換和壓縮、數據的交換等等工作,則由工具來生成。對上完全屏蔽。詳細的內容,我們將在接下來的遠程調用中來分析。
3、遠程調用
我們最初寫代碼的時候,就被教授了函數的概念。我們可以將一些公用的代碼,或者實現一定含義或邏輯的代碼,做成一個函數,方便重復的使用。
最開始,這些函數往往在同一個文件里,我們只要先申明,即可使用。
后來,我們開始使用庫里面的函數,或是將函數封裝成一個個的庫(比如C里面的靜態庫.a或者動態庫.so,或者是Java里面的.jar)。
以上對于函數的調用,以及函數自身的處理都是在本地。假如,當我們單機不能滿足需求的時候,我們就需要將函數的處理放到其他機器上,讓機器做到并行的計算。這個時候,我們就需要遠程的函數調用,或者叫做遠程過程調用(Remote Procedure Call 或者 Remote Method Invoke,簡稱RPC或者RMI)。
我們在這之前講過幾個東東:負載均衡、命名位置服務、協議、接口。其實前面講這幾個東東都是為了給遠程過程調用做鋪墊。RPC都是建立在以上部分的基礎上。
還是按照我們之前分析問題的思路:為什么要這個東東?這個東西解決什么問題?如何實現?有哪些問題?等等來分析RPC吧。
RPC的目的,就是使得從不同服務上獲取數據如同本地調用一樣方便和自然。讓程序調用者不需要了解網絡細節,不用了解協議細節,不用了解服務的機器狀態細節等等。如果沒有RPC,其實也是可以的,就是我們寫程序的時候難受點而已,哈哈。
接下來,我們看看如何來實現。

以上是整個的一個大體靜態邏輯。最先編寫調用的IDL,完成后由工具生成接口說明文檔(doc);同時,生成客戶端調用代碼(stub,我們叫做存根);另外,需要生成server端接口框架(skeleton),接口開發者實現具體的代碼邏輯。

以上就是客戶端調用的整個邏輯。
【第十三階段:分布式計算和存儲的運維設計與考慮】
以上的部分已經從前到后的將系統架構進行了描述,同時針對我們會遇到的問題進行了分析和處理,提出了一些解決方案,以保證我們的系統在不斷增長的壓力之下,如何的良好運轉。
不過,我們很少描述運維相關的工作,以及設計如何和運維相關聯。系統運維的成敗,直接決定了系統設計的成敗。所以系統的運維問題,是設計中必須考慮的問題。特別是當我們有成千上萬的(tens of thousands)臺機器的時候,運維越發顯得重要。因此,在系統設計初期,就應當把運維問題納入其中來進行綜合的考慮。
如果我們用人來管機器,在幾臺、幾十臺機器的時候是比較可行的。出了問題,人直接上,搞定!不過,當我們有幾百臺、幾千臺、幾萬臺、幾十萬臺機器的時候,我們如果要讓人去搞定,那就未見得可行了。
首先,人是不一定靠譜的。即使再聰明可靠的人,也有犯錯誤的時候。按照一定概率計算,如果我們機器數量變多,那么出錯的絕對數量也是很大的。同時,人和人之間的協作也可能會出現問題。另外,每個人的素質也是不一樣的。
其次,隨著機器數量的膨脹,需要投入更多的人力來管理機器。人的精力是有限的,機器增多以后,需要增加人力來管理機器。這樣的膨脹是難以承受的。
再次,人工恢復速度慢。如果出現了故障,人工來恢復的速度是比較慢的,一般至少是分鐘級別。這對于要提供7×24小時的服務來說,系統穩定運行的指標是存在問題的。同時,隨著機器的增多,機器出問題的概率一定的條件下,絕對數量會變多,這也導致我們的服務會經常處于出錯的情況之下。
還有,如果涉及到多地機房,如何來管理還是一個比較麻煩的事情。
如果我們能轉換思維,在設計系統的時候,如果能有一套自動化管理的模式,借助電腦的計算能力和運算速度,讓機器來管理機器,那我們的工作就輕松了。
比如,我們可以設計一套系統,集成了健康檢查、負載均衡、任務調度、自動數據切片、自動數據恢復等等功能,讓這套系統來管理我們的程序,一旦出現問題,系統可以自動的發現有問題的機器,并自動修復或處理。
當然以上都是比較理想的情況。凡事沒有絕對之說,只是需要盡可能達到一個平衡。
好了,說了這么多的問題,要表述的一點就是:人來管理機器是不靠譜的,我們需要盡量用機器來管理機器!
接下來,我們比較簡單的描述一下一個比較理想的自動化管理模型。
我們將我們的系統層次進行初步的劃分。

我們將系統粗略的劃分為三個層次:訪問接入層、邏輯處理層和數據存儲層。上一次對下一層進行調用,獲取數據,并返回。
因此,如果我們能夠做到,說下一層提供給上一層足夠可靠的服務,我們就可以簡化我們的設計模型了。
好,那如果要提供足夠可靠的服務,方便調用的話,應該如何來做呢?
我們可以針對每一層來看。
首先,看看訪問接入層:

所有的Web Server和 Private Protocol Server將服務注冊到命名和定位服務上。一旦注冊后,NLS會定期去檢查服務的存活,如果服務宕掉,會自動摘除,并發出報警信息,供運維人員查閱。等服務恢復后,再自動注冊。
Virtual Server從NLS獲取服務信息,并利用負載均衡策略去訪問對應的后繼服務。而對外,只看到有一臺Virtual Server。
接下來,我們看看邏輯層:

由于HTTP的無狀態性,我們將邏輯代碼按標準接口寫成一個個的邏輯處理單元,放入到我們的邏輯處理容器之中,進行統一的運行。并通過容器,到NLS中注冊。Virtual Server通過NLS獲取到對應的信息,并通過負載均衡策略將數據轉發到下游。
這里比較關鍵的數據處理單元,實際就是我們要寫的業務邏輯。業務邏輯的編寫,我們需要嚴格按照容器提供的規范(如IDL的標準等),并從容器獲取資源(如存儲服務、日志服務等)。這樣上線也變的簡單,只需要將我們的處理單元發布到對應的容器目錄下,容器就可以自動的加載服務,并在NLS上注冊。如果某一個服務出現異常,就從NLS上將其摘掉。
容器做的工作就相對比較多。需要提供基本的服務注冊功能、服務分發功能等。同時,還需要提供各種資源,如:存儲服務資源(通過NLS、API等,提供存儲層的訪問接口)、日志服務資源等,給處理單元,讓其能夠方便的計算和處理。
總體來說,因為HTTP的無狀態特性,以及不存在數據的存儲,邏輯層要做到同構化是相對比較容易的,并且同構化以后的運維也就非常容易了。
最后,看看數據存儲層。

存儲層是運維設計中最難的一部分。因為根據不同的業務需要,可能提供不同的存儲引擎,而不同的存儲引擎實現的機理和方式可能完全不一樣。比如,為了保證數據的有效性和一致性,有些存儲系統需要使用事務;而有些業務,可能為了追求高效,可能會犧牲一些數據的一致性,而提供快速的KV查詢等等。
因此,數據存儲層的異構性就是運維設計中亟待解決的問題。
一種比較理想的方式,就是讓各個存儲系統,隱藏內部的實現,對外提供簡單的訪問接口。而在系統內部,通過meta server、data assemble server、status manager、message queue等管理單元來管理數據存儲單元。當然,這只是其中一種方式。也可以利用mysql類似的主從級聯方式來管理,這種方式也是可行的。
數據存儲系統的設計,也沒有一個固定的規范(比如:Big Table、Cassandra、Oracle數據庫集群等),所以運維的設計需要在系統中來充分考慮。上述圖示只是提供了一種簡單的設計方案。
好了,有了多個層次的詳細分析以后,上一層次調用下一層次,就直接通過固定的地址進行訪問即可。
不過有一個問題就是,如果我們的Virtual Server是一個單點,出現故障后,該層就不能提供服務。這個是我們不可接受的。怎么辦呢?
要解決這樣的問題,就是利用冗余。我們可以將我們的服務劃分為多個組,分別由多個VS來管理。上層調用下層的時候,通過一定的選擇策略來選擇即可。這樣,如果服務出現問題,我們就能通過冗余策略,將請求冗余到其他組上。
我們來看一個實例:

用戶通過域名訪問我們的服務,DNS通過訪問IP解析,返回對應訪問層的IP地址。訪問層將請求轉發到對應的邏輯處理組,邏輯處理組從不同的存儲系統里獲取數據,并返回處理結果。
通過以上的分析,我們通過原有的一些技術手段,可以做到比較好的自動化運維的方式。不過這種運維方式也不是完全智能的。有些時候也需要人工的參與。最難的一點就是存儲系統的設計和實現。如果要完全的自動化的話,是一件比較難的事情。
說明:以上的描述是一個比較理想化的模型,要真正實現這種模型,需要很多的輔助手段,并且需要搭建很多基礎設施。可能會遇到我們沒有提到的很多的實際問題,比如:跨機房網絡傳輸延遲、服務間隔離性、網卡帶寬限制、服務的存活監控等等。因此,在具體實施的時候,需要仔細分析和考慮。
文章來源:老王的技術博客