螞蟻變大象:淺談常規網站是如何從小變大的

       2005年,我開始和朋友們開始拉活兒做網站,當時第一個網站是在linux上用jsp搭建的,到後來逐步的引入了多種框架,如webworkhibernate等。在到後來,進入公司,開始用c/c++,做分佈式計算和存儲。(到那時才解開了我的一個疑惑:C語言除了用來寫HelloWorld,還能幹嘛?^_^)。

         總而言之,網站根據不同的需求,不同的請求壓力,不同的業務模型,需要不同的架構來給予支持。我從我的一些經歷和感受出發,大體上總結了一下的一些階段。詳情容我慢慢道來。

         【第一階段 : 搭建屬於自己的網站】

         我們最先開始的網站可能是長成這個樣子的:


         我們用jspaspphp等頁面語言搭建最簡單的業務邏輯,將數據存放在通用關係數據庫中,如果有必要還搭建一個WebServer(如果偷懶的話,jsp直接使用tomcatresin當服務器,連apache都省了)。所有的東西都放在一臺服務器上(實際可能是買一個空間或者託管服務器)。這裏有一個經典的單詞:LAMP

         這個時候,我們的業務可能比較簡單,往往是做一個×××Management Information System;數據量也比較小,通常都是幾千,最多也就幾萬條記錄;壓力也比較小,估計也就每天幾千,最多幾萬次請求。

         這樣做的好處就是簡單,入門快。所有的代碼都寫在Server Page中,看起來就像是HTML中增加了很多怪異的非HTML標籤和代碼。根據不同的邏輯和機器性能,單機性能從幾十到幾千QPS(一般能到100就不錯了,^_^)。

         不過問題也很明顯,就是頁面集成了各種代碼,既要負責處理請求參數,又要處理各種邏輯,還要負責連接數據庫,最後輸出顯示…… 可想而知,代碼的可重用性和可維護性是不太好的。而且,還有一個問題,像JSP這樣的語言,必須在請求時才能編譯,生成class代碼,調試上很難受。

         嗯,於是呢,我們考慮代碼的可重用性和可維護性了。

         【第二階段 : 增強代碼可重用性和可維護性】

         我們引入了很多框架,來幫我們解決重用性問題和可維護性問題。


 

         Java做例子,我們可能會引入strutsspringhibernate等框架,用來做URL分流,CVM隔離,數據的ORM等。這樣,我們的系統中,數據訪問層可以抽取出很多公用的類,業務邏輯層也可以抽取出很多公用的業務類,同一個業務邏輯可以對應多個展示頁面,可複用性得到極大的增強。

         不過,從性能上看,引入框架後,效率並不見得比第一種架構高,有可能還有降低。因爲框架可能會大量引入“反射”的機制,來創建對應的業務對象;同時,也可能增加額外的框架邏輯,來增強隔離性。從而使得整體服務能力下降。幸好,在這個階段,業務請求量不大,性能不是我們太care的事情。J

         【第三階段 :降低磁盤壓力

         可能隨着業務的持續發展,或者是網站關注度逐步提升(也有可能是搜索引擎的爬蟲關注度逐步提升。我之前有一個網站,每天有超過1/3的訪問量,就是各種爬蟲貢獻的),我們的請求量逐步變大,這個時候,往往出現瓶頸的就是磁盤性能。在linux下,用vmstatiostat等命令,可以看到磁盤的bibowaitutil等值持續高位運行。怎麼辦呢?

         其實,在我們剛剛踏進大學校門的時候,第一門計算機課程——《計算機導論》裏面就給出瞭解決方案。依稀記得下面這個圖:

 

 

         在我們的存儲體系裏面,磁盤一般是機械的(現在FlashSSD等開始逐步大規模使用了),讀取速度最慢,而內存訪問速度較快(讀取一個字節約10μs,速度較磁盤能高几百倍),速度最快的是CPUcache。不過價格和存儲空間卻遞減。

         話題切換回來,當我們的磁盤出現性能瓶頸的時候,我們這個時候,就要考慮其他的存儲介質,那麼我們是用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和p_w_picpath。blog可以上傳和展示圖片。那p_w_picpath.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的接口即可實現業務需要的互通。因此,從這個層面上來看,是可以基本做到業務垂直拆分的。

         二、基礎服務,比如:用戶帳號管理、消息通知等等。這些服務往往被多個業務所依賴。他們需要提供更通用的、更安全的、更穩定的接口和服務。但是,關於基礎業務的理解和劃分,是沒有一個特定的規則的。比如,p_w_picpath圖片服務,他有可能剛開始是一個業務服務,到一定階段以後,多個系統需要對他有強的依賴,自然也就成爲了一個基礎服務。

         所以,從上面的描述,我們可以發現服務的類型,並不是固定的。要很好的解決服務耦合的問題,也並沒有一個十分完美的解決方案。我們能做的就是儘量降低耦合,通過某種方式,能夠很好的達到耦合和可維護性的平衡。

         總的來看,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地址。訪問層將請求轉發到對應的邏輯處理組,邏輯處理組從不同的存儲系統裏獲取數據,並返回處理結果。

         通過以上的分析,我們通過原有的一些技術手段,可以做到比較好的自動化運維的方式。不過這種運維方式也不是完全智能的。有些時候也需要人工的參與。最難的一點就是存儲系統的設計和實現。如果要完全的自動化的話,是一件比較難的事情。

         說明:以上的描述是一個比較理想化的模型,要真正實現這種模型,需要很多的輔助手段,並且需要搭建很多基礎設施。可能會遇到我們沒有提到的很多的實際問題,比如:跨機房網絡傳輸延遲、服務間隔離性、網卡帶寬限制、服務的存活監控等等。因此,在具體實施的時候,需要仔細分析和考慮。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章