淺析分佈式系統的架構及常用方案(多臺服務器的配合)

搞懂分佈式技術開篇:淺析分佈式系統的架構及常用方案

導讀

我們常常會聽說,某個互聯網應用的服務器端系統多麼牛逼,比如QQ、微信、淘寶。那麼,一個互聯網應用的服務器端系統,到底牛逼在什麼地方?爲什麼海量的用戶訪問,會讓一個服務器端系統變得更復雜?本文就是想從最基本的地方開始,探尋服務器端系統技術的基礎概念。

高吞吐、高併發、低延遲和負載均衡(大量用戶訪問同一個互聯網業務)

高吞吐(多臺服務器實現)

高吞吐,意味着你的系統,可以同時承載大量的用戶使用。這裏關注的整個系統能同時服務的用戶數。這個吞吐量肯定是不可能用單臺服務器解決的,因此需要多臺服務器協作,才能達到所需要的吞吐量。而在多臺服務器的協作中,如何纔能有效的利用這些服務器,不致於其中某一部分服務器成爲瓶頸,從而影響整個系統的處理能力,這就是一個分佈式系統,在架構上需要仔細權衡的問題。

高併發

高併發是高吞吐的一個延伸需求。當我們在承載海量用戶的時候,我們當然希望每個服務器都能盡其所能的工作,而不要出現無謂的消耗和等待的情況。然而,軟件系統並不是簡單的設計,就能對同時處理多個任務,做到“儘量多”的處理。很多時候,我們的程序會因爲要選擇處理哪個任務,而導致額外的消耗。這也是分佈式系統解決的問題

低延遲

低延遲對於人數稀少的服務來說不算什麼問題。然而,如果我們需要在大量用戶訪問的時候,也能很快的返回計算結果,這就要困難的多。因爲除了大量用戶訪問可能造成請求在排隊外,還有可能因爲排隊的長度太長,導致內存耗盡、帶寬佔滿等空間性的問題。如果因爲排隊失敗而採取重試的策略,則整個延遲會變的更高。所以分佈式系統會採用很多請求分揀和分發的做法,儘快的讓更多的服務器來出來用戶的請求。但是,由於一個數量龐大的分佈式系統,必然需要把用戶的請求經過多次的分發,整個延遲可能會因爲這些分發和轉交的操作,變得更高,所以分佈式系統除了分發請求外,還要儘量想辦法減少分發的層次數,以便讓請求能儘快的得到處理。

負載均衡(服務器部署的地方不同—通信方式,同時來的數據—負載均衡)

由於互聯網業務的用戶來自全世界,因此在物理空間上可能來自各種不同延遲的網絡和線路,
所以要有效的應對這種用戶來源的複雜性,就需要把多個服務器部署在不同的空間來提供服務
同時,我們也需要讓同時發生的請求,有效的讓多個不同服務器承載。所謂的負載均衡,就是分佈式系統與生俱來需要完成的功課。

分佈式系統提高承載量的基本手段(分層模型、併發模型)

分層模型(路由、代理)

在這裏插入圖片描述
使用多態服務器來協同完成計算任務,最簡單的思路就是,讓每個服務器都能完成全部的請求,然後把請求隨機的發給任何一個服務器處理。
最早期的互聯網應用中,DNS輪詢就是這樣的做法:當用戶輸入一個域名試圖訪問某個網站,這個域名會被解釋成多個IP地址中的一個,隨後這個網站的訪問請求,就被髮往對應IP的服務器了,這樣多個服務器(多個IP地址)就能一起解決處理大量的用戶請求。

(登錄信息)
然而,單純的請求隨機轉發,並不能解決一切問題。比如我們很多互聯網業務,都是需要用戶登錄的。在登錄某一個服務器後,用戶會發起多個請求,如果我們把這些請求隨機的轉發到不同的服務器上,那麼用戶登錄的狀態就會丟失,造成一些請求處理失敗。簡單的依靠一層服務轉發是不夠的,所以我們會增加一批服務器,這些服務器會根據用戶的Cookie,或者用戶的登錄憑據,來再次轉發給後面具體處理業務的服務器。

除了登錄的需求外,我們還發現,很多數據是需要數據庫來處理的,而我們的這些數據往往都只能集中到一個數據庫中,否則在查詢的時候就會丟失其他服務器上存放的數據結果。所以往往我們還會把數據庫單獨出來成爲一批專用的服務器。

至此,我們就會發現,一個典型的三層結構出現了:接入、邏輯、存儲。然而,這種三層結果,並不就能包醫百病。例如,當我們需要讓用戶在線互動(網遊就是典型)
,那麼分割在不同邏輯服務器上的在線狀態數據,是無法知道對方的,這樣我們就需要專門做一個類似互動服務器的專門系統,讓用戶登錄的時候,也同時記錄一份數據到它那裏,表明某個用戶登錄在某個服務器上,而所有的互動操作,要先經過這個互動服務器,才能正確的把消息轉發到目標用戶的服務器上。

併發模型(多線程、異步)

當我們在編寫服務器端程序是,我們會明確的知道,大部分的程序,都是會處理同時到達的多個請求的。因此我們不能好像HelloWorld那麼簡單的,從一個簡單的輸入計算出輸出來。因爲我們會同時獲得很多個輸入,需要返回很多個輸出。在這些處理的過程中,往往我們還會碰到需要“等待”或“阻塞”的情況,比如我們的程序要等待數據庫處理結果,等待向另外一個進程請求結果等等……如果我們把請求一個挨着一個的處理,那麼這些空閒的等待時間將白白浪費,造成用戶的響應延時增加,以及整體系統的吞吐量極度下降。

所以在如何同時處理多個請求的問題上,業界有2個典型的方案。一種是多線程,一種是異步。在早期的系統中,多線程或多進程是最常用的技術。這種技術的代碼編寫起來比較簡單,因爲每個線程中的代碼都肯定是按先後順序執行的。但是由於同時運行着多個線程,所以你無法保障多個線程之間的代碼的先後順序。這對於需要處理同一個數據的邏輯來說,是一個非常嚴重的問題,最簡單的例子就是顯示某個新聞的閱讀量。兩個++操作同時運行,有可能結果只加了1,而不是2。所以多線程下,我們常常要加很多數據的鎖,而這些鎖又反過來可能導致線程的死鎖。

因此異步回調模型在隨後比多線程更加流行,除了多線程的死鎖問題外,異步還能解決多線程下,線程反覆切換導致不必要的開銷的問題:每個線程都需要一個獨立的棧空間,在多線程並行運行的時候,這些棧的數據可能需要來回的拷貝,這額外消耗了CPU。同時由於每個線程都需要佔用棧空間,所以在大量線程存在的時候,內存的消耗也是巨大的。而異步回調模型則能很好的解決這些問題,不過異步回調更像是“手工版”的並行處理,需要開發者自己去實現如何“並行”的問題。

硬件故障率

除了服務器自己的內存、硬盤等故障,服務器之間的網絡線路故障更加常見。而且這種故障還有可能是偶發的,或者是會自動恢復的。面對這種問題,如果只是簡單的把“出現故障”的機器剔除出去,那還是不夠的。因爲網絡可能過一會兒就又恢復了,而你的集羣可能因爲這一下的臨時故障,丟失了過半的處理能力。

如何讓分佈式系統,在各種可能隨時出現故障的情況下,儘量的自動維護和維持對外服務,成爲了編寫程序就要考慮的問題。由於要考慮到這種故障的情況,所以我們在設計架構的時候,
也要有意識的預設一些冗餘、自我維護的功能。這些都不是產品上的業務需求
完全就是技術上的功能需求。能否在這方面提出對的需求,然後正確的實現,是服務器端程序員最重要的職責之一。

資源利用率(集羣系統的擴充和縮容)

擴容

在對一個集羣擴容的時候,我們往往會要停掉整個集羣的服務,然後修改各種配置,最後才能重新啓動一個加入了新的服務器的集羣。 由於在每個服務器的內存裏,都可能會有一些用戶使用的數據,所以如果冒然在運行的時候,就試圖修改集羣中提供服務的配置,很可能會造成內存數據的丟失和錯誤。因此,運行時擴容在對無狀態的服務上,是比較容易的,比如增加一些Web服務器。但如果是在有狀態的服務上,比如網絡遊戲,幾乎是不可能進行簡單的運行時擴容的。

縮容

分佈式集羣除了擴容,還有縮容的需求。當用戶人數下降,服務器硬件資源出現空閒的時候,我們往往需要這些空閒的資源能利用起來,放到另外一些新的服務集羣裏去。縮容和集羣中有故障需要容災有一定類似之處,區別是縮容的時間點和目標是可預期的。

消息隊列服務(兩個進程間的通信摒棄TCP和UDP,而使用消息隊列機制)

在一個基於分佈式的遊戲服務器系統中,不同的服務器之間,哪種通信方式是不可行的

兩個進程間如果要跨機器通訊,我們幾乎都會用TCP/UDP這些協議。但是直接使用網絡API去編寫跨進程通訊,是一件非常麻煩的事情。除了要編寫大量的底層socket代碼外,我們還要處理諸如:如何找到要交互數據的進程,如何保障數據包的完整性不至於丟失,如果通訊的對方進程掛掉了,或者進程需要重啓應該怎樣等等這一系列問題。這些問題包含了容災擴容、負載均衡等一系列的需求。

爲了解決分佈式系統進程間通訊的問題,人們總結出了一個有效的模型,就是“消息隊列”模型。消息隊列模型,就是把進程間的交互,抽象成對一個個消息的處理,而對於這些消息,我們都有一些“隊列”,也就是管道,來對消息進行暫存。每個進程都可以訪問一個或者多個隊列,從裏面讀取消息(消費)或寫入消息(生產)。由於有一個緩存的管道,我們可以放心的對進程狀態進行變化。當進程起來的時候,它會自動去消費消息就可以了。而消息本身的路由,也是由存放的隊列決定的,這樣就把複雜的路由問題,變成了如何管理靜態的隊列的問題。

一般的消息隊列服務,都是提供簡單的“投遞”和“收取”兩個接口,但是消息隊列本身的管理方式卻比較複雜,一般來說有兩種。一部分的消息隊列服務,提倡點對點的隊列管理方式:每對通信節點之間,都有一個單獨的消息隊列。這種做法的好處是不同來源的消息,可以互不影響,不會因爲某個隊列的消息過多,擠佔了其他隊列的消息緩存空間。而且處理消息的程序也可以自己來定義處理的優先級——先收取、多處理某個隊列,而少處理另外一些隊列。

但是這種點對點的消息隊列,會隨着集羣的增長而增加大量的隊列,這對於內存佔用和運維管理都是一個複雜的事情。因此更高級的消息隊列服務,開始可以讓不同的隊列共享內存空間,而消息隊列的地址信息、建立和刪除,都採用自動化的手段。——這些自動化往往需要依賴上文所述的“目錄服務”,來登記隊列的ID對應的物理IP和端口等信息。比如很多開發者使用ZooKeeper來充當消息隊列服務的中央節點;而類似Jgropus這類軟件,則自己維護一個集羣狀態來存放各節點今昔。

另外一種消息隊列,則類似一個公共的郵箱。一個消息隊列服務就是一個進程,任何使用者都可以投遞或收取這個進程中的消息。這樣對於消息隊列的使用更簡便,運維管理也比較方便。不過這種用法下,任何一個消息從發出到處理,最少進過兩次進程間通信,其延遲是相對比較高的。並且由於沒有預定的投遞、收取約束,所以也比較容易出BUG。

不管使用那種消息隊列服務,在一個分佈式服務器端系統中,進程間通訊都是必須要解決的問題,所以作爲服務器端程序員,在編寫分佈式系統代碼的時候,使用的最多的就是基於消息隊列驅動的代碼,這也直接導致了EJB3.0把“消息驅動的Bean”加入到規範之中。

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