1. 程式人生 > >服務端技術進階(四)一篇文讀懂分散式系統本質:高吞吐、高可用、可擴充套件

服務端技術進階(四)一篇文讀懂分散式系統本質:高吞吐、高可用、可擴充套件

服務端技術進階( 四)一篇文讀懂分散式系統本質:高吞吐、高可用、可擴充套件

承載量是分散式系統存在的原因

  當一個網際網路業務獲得大眾歡迎的時候,最顯著碰到的技術問題,就是伺服器非常繁忙。當每天有1000萬個使用者訪問你的網站時,無論你使用什麼樣的伺服器硬體,都不可能只用一臺機器就承載的了。因此,在網際網路程式設計師解決伺服器端問題的時候,必須要考慮如何使用多臺伺服器,為同一種網際網路應用提供服務,這就是所謂“分散式系統”的來源。
  然而,大量使用者訪問同一個網際網路業務,所造成的問題並不簡單。從表面上看,要能滿足很多使用者來自網際網路的請求,最基本的需求就是所謂效能需求:使用者反應網頁開啟很慢,或者網遊中的動作很卡等等。而這些對於“服務速度”的要求,實際上包含的部分卻是以下幾個:高吞吐

高併發低延遲負載均衡
  高吞吐意味著你的系統可以同時承載大量的使用使用者。這裡關注的整個系統能同時服務的使用者數。這個吞吐量肯定是不可能用單臺伺服器解決的,因此需要多臺伺服器協作,才能達到所需要的吞吐量。而在多臺伺服器的協作中,如何才能有效的利用這些伺服器,不致於其中某一部分伺服器成為瓶頸,從而影響整個系統的處理能力,這就是一個分散式系統在架構上需要仔細權衡的問題。
  高併發是高吞吐的一個延伸需求。當我們在承載海量使用者的時候,我們當然希望每個伺服器都能盡其所能的工作,而不要出現無謂的消耗和等待的情況。然而,軟體系統並不是簡單的設計就能對同時處理多個任務,做到“儘量多”的處理。很多時候,我們的程式會因為要選擇處理哪個任務,而導致額外的消耗。這也是分散式系統解決的問題。
  低延遲對於人數稀少的服務來說不算什麼問題。然而,如果我們需要在大量使用者訪問的時候,也能很快的返回計算結果,這就要困難的多。因為除了大量使用者訪問可能造成請求在排隊外,還有可能因為排隊的長度太長,導致記憶體耗盡、頻寬佔滿等空間性的問題
。如果因為排隊失敗而採取重試的策略,則整個延遲會變的更高。所以分散式系統會採用很多請求分揀和分發的做法,儘快的讓更多的伺服器來處理使用者的請求。但是,由於一個數量龐大的分散式系統,必然需要把使用者的請求經過多次的分發,整個延遲可能會因為這些分發和轉交的操作,變得更高,所以分散式系統除了分發請求外,還要儘量想辦法減少分發的層次數,以便讓請求能儘快的得到處理。
  由於網際網路業務的使用者來自全世界,因此在物理空間上可能來自各種不同延遲的網路和線路,在時間上也可能來自不同的時區,所以要有效的應對這種使用者來源的複雜性,就需要把多個伺服器部署在不同的空間來提供服務。同時,我們也需要讓同時發生的請求,有效的讓多個不同伺服器承載。所謂的負載均衡,就是分散式系統與生俱來需要完成的功課。
  由於分散式系統幾乎是解決網際網路業務承載量問題的最基本方法,所以作為一個伺服器端程式設計師,掌握分散式系統技術就變得異常重要了。然而,分散式系統的問題並非是學會用幾個框架和使用幾個庫就能輕易解決的。因為當一個程式在一個電腦上執行,變成了在無數個電腦上同時協同執行,在開發、運維上都會帶來很大的差別。

分散式系統提高承載量的基本手段

分層模型(路由、代理)

  使用多臺伺服器來協同完成計算任務,最簡單的思路就是讓每個伺服器都能完成全部的請求,然後把請求隨機的發給任何一個伺服器處理。最早期的網際網路應用中,DNS輪詢就是這樣的做法:當用戶輸入一個域名試圖訪問某個網站,這個域名會被解釋成多個IP地址中的一個,隨後這個網站的訪問請求就被髮往對應IP的伺服器了,這樣多個伺服器(多個IP地址)就能一起解決處理大量的使用者請求。
  然而,單純的請求隨機轉發,並不能解決一切問題。比如我們很多網際網路業務,都是需要使用者登入的。在登入某一個伺服器後,使用者會發起多個請求,如果我們把這些請求隨機的轉發到不同的伺服器上,那麼使用者登入的狀態就會丟失,造成一些請求處理失敗。簡單的依靠一層服務轉發是不夠的,所以我們會增加一批伺服器,這些伺服器會根據使用者的Cookie,或者使用者的登入憑據,來再次轉發給後面具體處理業務的伺服器。
  除了登入的需求外,我們還發現,很多資料是需要資料庫來處理的,而我們的這些資料往往都只能集中到一個數據庫中,否則在查詢的時候就會丟失其他伺服器上存放的資料結果。所以往往我們還會把資料庫單獨出來成為一批專用的伺服器。
  至此,我們就會發現,一個典型的三層結構出現了:接入、邏輯、儲存。然而,這種三層結果並不就能包醫百病。例如,當我們需要讓使用者線上互動(網遊就是典型),那麼分割在不同邏輯伺服器上的線上狀態資料,是無法知道對方的,這樣我們就需要專門做一個類似互動伺服器的專門系統,讓使用者登入的時候也同時記錄一份資料到它那裡,表明某個使用者登入在某個伺服器上,而所有的互動操作要先經過這個互動伺服器才能正確地把訊息轉發到目標使用者的伺服器上。
這裡寫圖片描述
  又例如,當我們在使用網上論壇(BBS)系統的時候,我們發的文章不可能只寫入一個數據庫裡,因為太多人的閱讀請求會拖死這個資料庫。我們常常會按論壇板塊來寫入不同的資料庫,又或者是同時寫入多個數據庫。這樣把文章資料分別存放到不同的伺服器上,才能應對大量的操作請求。然而,使用者在讀取文章的時候,就需要有一個專門的程式,去查詢具體文章在哪一個伺服器上,這時候我們就要架設一個專門的代理層,把所有的文章請求先轉交給它,由它按照我們預設的儲存計劃,去找對應的資料庫獲取資料。
  根據上面的例子來看,分散式系統雖然具有三層典型的結構,但是實際上往往不止有三層,而是根據業務需求,會設計成多個層次的。為了把請求轉交給正確的程序處理,我們設計很多專門用於轉發請求的程序和伺服器。這些程序我們常常以Proxy或者Router來命名,一個多層結構常常會具備各種各樣的Proxy程序。這些代理程序,很多時候都是通過TCP來連線前後兩端。然而,TCP雖然簡單,但是卻存在發生故障後不易恢復的問題。而且TCP的網路程式設計也是有點複雜的。——所以,人們設計出更好程序間通訊機制:訊息佇列。
這裡寫圖片描述
  儘管通過各種Proxy或者Router程序能組建出強大的分散式系統,但是其管理的複雜性也是非常高的。所以人們在分層模式的基礎上想出了更多的方法,來讓這種分層模式的程式變得更簡單高效。

併發模型(多執行緒、非同步)

  當我們在編寫伺服器端程式時會明確地知道大部分的程式都是會處理同時到達的多個請求的。因此我們不能好像Hello World那麼簡單的從一個簡單的輸入計算出輸出來。因為我們會同時獲得很多個輸入,需要返回很多個輸出。在這些處理的過程中,往往我們還會碰到需要“等待”或“阻塞”的情況,比如我們的程式要等待資料庫處理結果,等待向另外一個程序請求結果等等……如果我們把請求一個挨著一個的處理,那麼這些空閒的等待時間將白白浪費,造成使用者的響應延時增加,以及整體系統的吞吐量極度下降。
  所以在如何同時處理多個請求的問題上,業界有2個典型的方案。一種是多執行緒,一種是非同步。在早期的系統中,多執行緒或多程序是最常用的技術。這種技術的程式碼編寫起來比較簡單,因為每個執行緒中的程式碼都肯定是按先後順序執行的。但是由於同時執行著多個執行緒,所以你無法保障多個執行緒之間的程式碼的先後順序。這對於需要處理同一個資料的邏輯來說,是一個非常嚴重的問題,最簡單的例子就是顯示某個新聞的閱讀量。兩個++操作同時執行,有可能結果只加了1,而不是2。所以多執行緒下,我們常常要加很多資料的鎖,而這些鎖又反過來可能導致執行緒的死鎖。
  因此非同步回撥模型在隨後比多執行緒更加流行,除了多執行緒的死鎖問題外,非同步還能解決多執行緒下,執行緒反覆切換導致不必要開銷的問題:每個執行緒都需要一個獨立的棧空間,在多執行緒並行執行的時候,這些棧的資料可能需要來回的拷貝,這額外消耗了CPU。同時由於每個執行緒都需要佔用棧空間,所以在大量執行緒存在的時候,記憶體的消耗也是巨大的。而非同步回撥模型則能很好的解決這些問題,不過非同步回撥更像是“手工版”的並行處理,需要開發者自己去實現如何“並行”的問題。
  非同步回撥基於非阻塞的I/O操作(網路和檔案),這樣我們就不用在呼叫讀寫函式的時候“卡”在那一句函式呼叫,而是立刻返回“有無資料”的結果。而Linux的epoll技術,則利用底層核心的機制,讓我們可以快速的“查詢”到有資料可以讀寫的連線\檔案。由於每個操作都是非阻塞的,所以我們的程式可以只用一個程序就可以處理大量併發的請求。因為只有一個程序,所以所有的資料處理,其順序都是固定的,不可能出現多執行緒中,兩個函式的語句交錯執行的情況,因此也不需要各種“鎖”。從這個角度看,非同步非阻塞的技術大大簡化了開發的過程。由於只有一個執行緒,也不需要有執行緒切換之類的開銷,所以非同步非阻塞成為很多對吞吐量、併發有較高要求的系統首選。
這裡寫圖片描述

緩衝技術

  在網際網路服務中,大部分的使用者互動都是需要立刻返回結果的,所以對於延遲有一定的要求。而類似網路遊戲之類服務,延遲更是要求縮短到幾十毫秒以內。所以為了降低延遲,緩衝是網際網路服務中最常見的技術之一。
  早期的WEB系統中,如果每個HTTP請求的處理,都去資料庫(MySQL)讀寫一次,那麼資料庫很快就會因為連線數佔滿而停止響應。因為一般的資料庫支援的連線數都只有幾百,而WEB應用的併發請求輕鬆能到幾千。這也是很多設計不良的網站在訪問數量驟增就卡死的最直接原因。為了儘量減少對資料庫的連線和訪問,人們設計了很多緩衝系統——把從資料庫中查詢的結果存放到更快的設施上,如果沒有相關聯的修改,就直接從這裡讀。
  最典型的WEB應用緩衝系統是Memcache。由於PHP本身的執行緒結構是不帶狀態的。早期PHP本身甚至連操作“堆”記憶體的方法都沒有,所以那些持久的狀態就一定要存放到另外一個程序裡。而Memcache就是一個簡單可靠的存放臨時狀態的開源軟體。很多PHP應用現在的處理邏輯都是先從資料庫讀取資料,然後寫入Memcache;當下次請求來的時候,先嚐試從Memcache裡面讀取資料,這樣就有可能大大減少對資料庫的訪問。
這裡寫圖片描述
  然而Memcache本身是一個獨立的伺服器程序,這個程序自身並不帶特別的叢集功能。也就是說這些Memcache程序並不能直接組建成一個統一的叢集。如果一個Memcache不夠用,我們就要手工用程式碼去分配,哪些資料應該去哪個Memcache程序。——這對於真正的大型分散式網站來說,管理一個這樣的緩衝系統,是一個很繁瑣的工作。
  因此人們開始考慮設計一些更高效的緩衝系統:從效能上來說,Memcache的每筆請求都要經過網路傳輸才能去拉取記憶體中的資料。這無疑是有一點浪費的,因為請求者本身的記憶體,也是可以存放資料的。——這就是促成了很多利用請求方記憶體的緩衝演算法和技術,其中最簡單的就是使用LRU演算法,把資料放在一個雜湊表結構的堆記憶體中。
  而Memcache不具備叢集功能也是一個痛點。於是很多人開始設計,如何讓資料快取分佈到不同的機器上。最簡單的思路是所謂讀寫分離,也就是快取每次寫,都寫到多個緩衝程序上記錄,而讀則可以隨機讀任何一個程序。在業務資料有明顯的讀寫不平衡差距上,效果是非常好的。
  然而,並不是所有的業務都能簡單的用讀寫分離來解決問題,比如一些線上互動的網際網路業務,比如社群、遊戲。這些業務的資料讀寫頻率並沒很大的差異,而且也要求很低的延遲。因此人們又再想辦法,把本地記憶體和遠端程序的記憶體快取結合起來使用,讓資料具備兩級快取。同時,一個數據不再同時存在所有的快取程序上,而是按一定規律分佈在多個程序上。——這種分佈規律使用的演算法,最流行的就是所謂“一致性雜湊”。這種演算法的好處是,當某一個程序失效掛掉,不需要把整個叢集中所有的快取資料重新修改一次位置。你可以想象一下,如果我們的資料快取分佈,是用簡單的以資料ID對程序數取模,那麼一旦程序數變化,每個資料存放的程序位置都可能變化,這對於伺服器的故障容忍是不利的。
  Orcale公司旗下有一款叫Coherence的產品,是在快取系統上設計比較好的。這個產品是一個商業產品,支援利用本地記憶體快取和遠端程序快取協作。叢集程序是完全自管理的,還支援在資料快取所在程序進行使用者自定義的計算(處理器功能),這就不僅僅是快取了,還是一個分散式的計算系統。
這裡寫圖片描述

儲存技術(NoSQL)

  相信CAP理論大家已經耳熟能詳,然而在互聯發展的早期,大家都還在使用MySQL的時候,如何讓資料庫存放更多的資料,承載更多的連線,很多團隊都是絞盡腦汁。甚至於有很多業務主要的資料儲存方式是檔案,資料庫反而變成是輔助的設施了。
  附:CAP原則又稱CAP定理,指的是在一個分散式系統中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分割槽容錯性),三者不可得兼。
這裡寫圖片描述
  然而,當NoSQL興起,大家突然發現,其實很多網際網路業務,其資料格式是如此簡單,很多時候根本不需要關係型資料庫那種複雜的表格。對於索引的要求往往也只是根據主索引搜尋。而更復雜的全文搜尋,本身資料庫也做不到。所以現在相當多的高併發網際網路業務首選NoSQL來做儲存設施。最早的NoSQL資料庫有MongoDB等,現在最流行的似乎就是Redis了。甚至有些團隊把Redis也當成緩衝系統的一部分,實際上也是認可Redis的效能優勢。
  NoSQL除了更快、承載量更大以外,更重要的特點是這種資料儲存方式只能按照一條索引來檢索和寫入。這樣的需求約束帶來了分佈上的好處,我們可以按照這條主索引來定義資料存放的程序(伺服器)。這樣一個數據庫的資料就能很方便的存放在不同的伺服器上。在分散式系統的必然趨勢下,資料儲存層終於也找到了分佈的方法。

布式系統在可管理性上造成的問題

  分散式系統並不是簡單的把一堆伺服器一起執行起來就能滿足需求的。對比單機或少量伺服器的叢集,有一些特別需要解決的問題等待著我們。

硬體故障率

  所謂分散式系統,肯定就不是隻有一臺伺服器。假設一臺伺服器的故障時間是1%,那麼當你有100臺伺服器的時候,那就幾乎總有一臺是在故障狀態的。雖然這個比方不一定很準確,但是,當你的系統所涉及的硬體越來越多,硬體的故障也會從偶然事件變成一個必然事件。一般我們在寫功能程式碼的時候,是不會考慮到硬體故障的時候應該怎麼辦的。而如果在編寫分散式系統的時候,就一定需要面對這個問題了。否則,很可能只有一臺伺服器出故障,整個數百臺伺服器的叢集都工作不正常了。
這裡寫圖片描述
  除了伺服器自己的記憶體、硬碟等故障,伺服器之間的網路線路故障更加常見。而且這種故障還有可能是偶發的,或者是會自動恢復的。面對這種問題,如果只是簡單的把“出現故障”的機器剔除出去,那還是不夠的。因為網路可能過一會兒就又恢復了,而你的叢集可能因為這一下的臨時故障,丟失了過半的處理能力。
  如何讓分散式系統在各種可能隨時出現故障的情況下,儘量的自動維護和維持對外服務,成為了編寫程式時就要考慮的問題。由於要考慮到這種故障的情況,所以我們在設計架構的時候,也要有意識地預設一些冗餘、自我維護的功能。這些都不是產品上的業務需求,完全就是技術上的功能需求。能否在這方面提出對的需求,然後正確的實現,是伺服器端程式設計師最重要的職責之一。

資源利用率優化

  分散式系統的叢集包含了很多伺服器,當這樣一個叢集的硬體承載能力到達極限的時候,最自然的想法就是增加更多的硬體。然而,一個軟體系統並不是通過“增加”硬體就可以輕鬆提高承載效能的。因為軟體在多個伺服器上的工作需要複雜細緻的協調工作。在對一個叢集擴容的時候,我們往往會要停掉整個叢集的服務,然後修改各種配置,最後才能重新啟動一個加入了新的伺服器的叢集。
這裡寫圖片描述
  由於在每個伺服器的記憶體裡,都可能會有一些使用者使用的資料,所以如果冒然在執行的時候,就試圖修改叢集中提供服務的配置,很可能會造成記憶體資料的丟失和錯誤。因此,執行時擴容在對無狀態的服務是比較容易的,比如增加一些Web伺服器。但如果是在有狀態的服務上,比如網路遊戲,幾乎是不可能進行簡單執行時擴容的。
  分散式叢集除了擴容,還有縮容的需求。當用戶人數下降,伺服器硬體資源出現空閒的時候,我們往往需要這些空閒的資源能利用起來,放到另外一些新的服務叢集裡去。縮容和叢集中有故障需要容災有一定類似之處,區別是縮容的時間點和目標是可預期的。
  由於分散式叢集中的擴容、縮容,以及希望儘量能線上操作,這導致了非常複雜的技術問題需要處理,比如叢集中互相關聯的配置如何正確高效的修改、如何對有狀態的程序進行操作、如何在擴容縮容的過程中保證叢集中節點之間通訊的正常。作為伺服器端程式設計師,會需要花費大量的精力來應對多個程序的叢集狀態變化造成的一系列問題。

軟體服務內容更新

  現在都流行用敏捷開發模式中的“迭代”,來表示一個服務不斷的更新程式,滿足新的需求,修正BUG。如果我們僅僅管理一臺伺服器,那麼更新這一臺伺服器上的程式是非常簡單的:只要把軟體包拷貝過去,然後修改下配置就好。但是如果你要對成百上千的伺服器去做同樣的操作,就不可能每臺伺服器登入上去處理。
這裡寫圖片描述
  伺服器端的批量程式安裝部署工具,是每個分散式系統開發者都需要的。然而,我們的安裝工作除了拷貝二進位制檔案和配置檔案外,還會有很多其他的操作需要完成。比如開啟防火牆、建立共享記憶體檔案、修改資料庫表結構、改寫一些資料檔案等等……甚至有一些還要在伺服器上安裝新的軟體。
  如果我們在開發伺服器端程式的時候,就考慮到軟體更新、版本升級的問題,那麼我們對於配置檔案、命令列引數、系統變數的使用,就會預先做一定的規劃,這能讓安裝部署的工具執行更快,可靠性更高。
  除了安裝部署的過程,還有一個重要的問題,就是不同版本間資料的問題。我們在升級版本的時候,舊版本程式生成的一些持久化資料一般都是舊的資料格式;而我們的升級版本中如果涉及到了修改的資料格式,比如資料表結構,那麼這些舊格式的資料都要轉換改寫成新版本的資料格式才行。這導致了我們在設計資料結構的時候,就要考慮清楚這些表格的結構,是用最簡單直接的表達方式來讓將來的修改更簡單;還是一早就預計到修改的範圍,專門預設一些欄位,或者使用其他形式存放資料。
  除了持久化資料以外,如果存在客戶端程式(如手機APP),這些客戶端程式的升級往往不能和伺服器同步,如果升級的內容包含了通訊協議的修改,這就造成了我們必須為不同的版本部署不同的伺服器端系統的問題。為了避免同時維護多套伺服器,我們在軟體開發的時候,往往傾向於所謂“版本相容”的協議定義方式。而怎樣設計的協議才能有很好的相容性,又是伺服器端程式需要仔細考慮的問題。

資料統計和決策

  一般來說,分散式系統的日誌資料都是被集中到一起,然後統一進行統計的。然而,當叢集的規模達到一定程度的時候,這些日誌的資料量會變得非常恐怖。很多時候,統計一天的日誌量要消耗計算機執行一天以上的時間。所以,日誌統計這項工作,也變成一門非常專業的活動。
  經典的分散式統計模型,有Google的Map Reduce模型。這種模型既有靈活性,也能利用大量伺服器進行統計工作。但是缺點是易用性往往不夠好,因為這些資料的統計和我們常見的SQL資料表統計有非常大的差異,所以我們最後還是常常把資料丟到MySQL裡面去做更細層面的統計。
  由於分散式系統日誌數量的龐大,以及日誌複雜程度的提高。我們變得必須要掌握類似Map Reduce技術,才能真正的對分散式系統進行資料統計。而且我們還需要想辦法提高統計工作的工作效率。

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述