一個創業公司起步時極可能就兩臺機器,一臺 Web 服務器,一臺數據庫服務器,在一個應用系統中集成了全部的功能模塊。但隨着業務的發展和流量的增加,單應用已不能知足業務需求,分佈式成爲必由之路。前端
背景算法
一個網站的技術架構早期不少是 LAMP(Linux + Apache + MySQL + PHP),隨着業務擴展和流量增加,該架構下的系統很快達到瓶頸,即使嘗試一些高端服務器(如:IOE),除了價格昂貴以外,也阻擋不了瓶頸的到來。分佈式改形成爲必由之路。
什麼是分佈式改造?數據庫
所謂分佈式改造,就是儘可能讓系統無狀態化,或者讓有狀態的信息封裝在必定範圍內,以避免限制應用的橫向擴展。簡單來講,就是當一個應用的少數服務器宕機後,不影響總體業務的穩定性。
實現應用的分佈式改造須要解決好哪些問題?後端
- 應用須要微服務化,即將大量粗粒度的應用邏輯拆小作服務化改造
- 必須創建分佈式服務框架。必須具有分佈式配置系統、分佈式 RPC 框架、異步消息系統、分佈式數據層、分佈式文件系統、服務的發現、註冊和管理。
- 必須解決狀態一致性問題
分佈式架構與傳統單機架構的最大區別在於,分佈式架構能夠解決擴展問題:橫向擴展和縱向擴展。緩存
什麼是橫向擴展?性能優化
橫向擴展,主要解決應用架構上的容量問題。簡單說,假如一臺機器部署的 WEB 應用能夠支持 1億 PV 量,當我想支持 10億 PV 量的請求時,能夠支持橫向擴展機器數量。
什麼是縱向擴展?服務器
縱向擴展,主要解決業務的擴展問題。隨着業務的擴展,業務的複雜程度也不斷提升,架構上也要能根據功能的劃分進行縱向層次的劃分。好比,Web/API 層只作頁面邏輯或展現數據的封裝,服務層作業務邏輯的封裝等。業務邏輯層還能夠劃分紅更多的層次,以支持更細的業務的組合。
一個典型的分佈式網站架構如圖1.1所示:網絡
它將用戶的請求經過負載均衡隨機分配給一臺Web機器,Web機器再經過遠程調用請求服務層。可是數據層通常都是有狀態的,而數據要作到分佈式化,就必須保證數據的一致性。要保證數據的一致性,通常都須要對最細粒度的數據作單寫控制,所以要記錄數據的狀態、作好數據的訪問控制等
一個有狀態的分佈式架構如圖1.2所示:數據結構
分佈式集羣中通常都有一個Master負責管理集羣中全部機器的狀態和數據訪問的規制等,爲了保證高可用Master也有備份,Master一般會把訪問的路由規則推給實際的請求發起端,這樣Client就能夠直接和實際要訪問的節點通訊了,避免中間再通過一層代理
還有一種分佈式架構是非Master-Slave模式而是Leader選舉機制,即分佈式集羣中沒有單獨的Master角色,每一個節點功能都是同樣的,可是在集羣的初始化時會選取一個Leader 承擔Master的功能。一旦該Leader失效,集羣會從新選擇一個Leader。
這種方式的好處是不用單獨考慮Master的節點的可用性,可是也會增長集羣維護的複雜度:架構
須要分佈式中間件
從前面典型的分佈式架構上能夠看出,要搭建一個分佈式應用系統必需要有支持分佈式架構的框架。例如首先要有一個統一的負載均衡系統(LB/LVS)幫助平均分配外部請求的流量,將這些流量分配到後端的多臺機器上,這類設備通常都是工做在第四層,只作鏈路選擇而不作應用層解析;應用層的負載均衡能夠經過HA來實現,例如能夠根據請求的URL或者用戶的Cookie 精準地調度流量。請求到達服務層,就須要解決服務之間的系統調用了。這時,須要在服務層構建一個典型的分佈式系統,包括同步調度的分佈式RPC框架、異步調度的分佈式消息框架和解決靜態配置信息的分佈式配置框架。這三個分佈式框架就像人體的骨骼和經絡,把整個服務層鏈接起來。咱們會在後面詳細介紹這三個典型的分佈式框架(分佈式框架的開源產品有不少,例如Dubbo、RocketMQ等)。
服務化和分佈式化
咱們在網站升級中通常會接觸到兩個概念:
- 服務化改造;
- 分佈式化改造。
它們是一回事嗎?
服務化改造更可能是從業務架構的角度出發,目的是將業務作更細粒度的功能拆分,使業務邏輯更加清晰、邊界更加清楚且易於維護;服務化的另外一個好處是收斂業務邏輯,經過接口標準化提供統一的訪問方式。
分佈式化更可能是從系統架構層面的角度出發,更可能是看請求的訪問路徑,即一個請求必須先訪問什麼再訪問什麼、一次訪問要通過哪些步驟才能最終有結果等……所以,這是兩個不一樣層面的工做。
分佈式配置框架能夠說是其餘分佈式框架的基礎,由於在分佈式系統中要作到全部機器節點都徹底對等幾乎是不可能的,必然有某些機器或者集羣存在某些差別,但同時又要保證程序代碼是一份,因此解決這些差別的惟一辦法就是差別化配置——配置框架就承擔着這些個性化的定製功能:它把差別性封裝到配置框架的後臺中,使集羣中的每臺機器節點的代碼看起來都是一致的,只是某些配置數據有差別。
配置框架的原理很是簡單,即向一個控制檯服務端同步最新的一個K/V集合、JSON/XML或者任意一個文件,配置信息能夠是在內存中的也能夠是持久化的文件。
它的最大難點在於集羣管理機器數量的能力和配置下發的延時率。
如圖1.3所示,分佈式配置框架通常有如下兩種管理方式:
拉取模式
拉取模式就是Client集羣主動向 ConfigServer機器詢問配置信息是否有更新,若是有則拉取最新的信息更新本身。這種模式對配置集羣來講比較簡單,不須要知道Client的狀態也不須要管理它們,只需處理Client的請求就好了,是典型的C/s模式。缺點是ConfigServer 無法主動及時更新配置信息,必須等Client請求時再更新。通常Client都會設置一個定時更新週期,一般是幾秒鐘。
推送模式
這種模式是當ConfigServer感知到配置信息變化時主動把信息推送給每一個Client。它須要ConfigServer感知到每一個Client的存在,須要保持和它們之間的心跳,這使得ConfigServer的管理難度增長了,當Client的數據很大時,ConfigServer 有可能會成爲瓶頸。
在實際的應用場景中,通常而言,若是對配置下發延時率比較敏感並且Client數據不是太大(千級別)時,推薦使用推送模式,像ZooKeeper就是這種框架(咱們在後面的小節會介紹幾種分佈式管理的場景);而當Client 數據在萬級以上時推薦使用拉取模式。不過,無論運用哪一種模式咱們都要考慮如下兩個問題:
分佈式配置框架是最簡單的管理Client機器及相應配置下發功能的框架,所以,它也很容易發展成帶有這兩種屬性的其餘的工具平臺,如名字服務、開關係統等。
應用系統要作分佈式改造,必須先要有分佈式RPC框架,不然將事倍功半,爲何呢?這就像蓋樓同樣,若是沒有先搭好骨架的話,極可能就是給本身「埋坑」。要作好分佈式RPC框架須要實現服務的註冊、服務發現、服務調度和負載均衡、統一的SDK封裝。當前Java環境中分佈式RPC框架如Dubbo、HSF都是比較成熟的框架,可是其餘語言像PHP、C++還很少。
一個一般意義上的RPC框架通常包含如圖1.4所示的結構:
服務註冊
服務要能被發現,必須先註冊。服務的註冊對Java程序來講很是簡單,只須要在應用啓動時調用RPC框架直接向服務端註冊就行,通常須要傳遞類名、方法名、參數類型以及版本號。因爲語言上的一些限制,用PHP來作服務的註冊和發現相比之下要困難不少。因爲PHP不像Java那樣很容易地支持長鏈接,因此在PHP文件啓動時很難像Java程序那樣在初始化函數裏完成註冊。對PHP來講目前有兩個辦法:第一個辦法是寫一個FPM的擴展,在第一次FPM初始化時完成服務的註冊。可是若是一個FPM部署了多個服務模塊,那麼如何區分也是一個難題;第二個辦法是在PHP模塊裏手動配置一個要註冊的服務名錄列表,在PHP打包發佈時進行註冊。
服務註冊後,服務發佈者所在的機器須要和註冊中心保持心跳以維持本身處於一直能夠提供服務的狀態,不然註冊中心就會踢掉機器。對PHP來講,維持RPC鏈接是個麻煩事,因此通常會在本機另外再起一個proxy agent或者直接發送HTTP請求,可是這樣作比較消耗性能。
服務發現
服務發佈方註冊服務後,服務調用方就要可以發現服務。服務發現(如圖1.5所示)最重要的一個目標就是要把服務和提供服務的對應機器解耦,而不是經過機器的IP尋找服務。
服務調用方只須要關心服務名,不用關心該服務由誰提供、在哪兒提供、是否可用……服務發現的組件會把這些信息封裝好。
對Java語言來講。服務發現就是把對應的服務提供者當前可以存活的機器列表推給服務調用者的機器的內存,真正發起調用時隨機選取一個IP就能夠發起RPC調用。對PHP來講,如何把服務發佈者的機器列表推給調用方也很麻煩,目前的解決方案更傾向於在本機的Agent中完成地址列表的更新,而後真正調用時再查詢Agent更新的本地文件,並從本地文件中查找相應的服務。
服務調度和負載均衡
服務註冊和發現完成後,就要處理服務的調度和負載均衡了(如圖1.6所示)。服務調用有兩個關鍵點:一是要摘除故障節點,二是負載均衡。
- 摘除故障節點。這對Java來講比較容易處理:一旦有機器下線後,很容易更新地址列表。但對PHP來講就只能按期從Agent中拉取最新的地址列表,作不到像Java那樣實時。
- 負載均衡。負載均衡須要將調用方的請求平均分佈到不一樣的服務提供者的機器上,一個最簡單的算法就是隨機選取,作得複雜一點能夠給每一個提供者IP設置一個權重,而後根據權重選取。
統一的 SDK 封裝
服務框架須要提供一個統一的客戶端和服務端的標準接口規範,這樣能夠減小業務開發的重複工做量,例如SDK會統一封裝通訊協議、失敗重試以及封裝一些隱式參數傳遞(trace信息)。Java經過提供一個統一的jar包,封裝了服務發佈和調用的接口,業務層只要作些簡單的配置就能方便地調用服務,例如Java通常都會配置一個Spring的Bean。
對其餘語言來講,運用IDL規範是個好選擇。在thrift的基礎上,修改code-gen,生成struct(class)的read和write,生成Client和server插件框架,並基於thrift的lib提供Binary Protocol、TCP Transport。
一個分佈式應用系統中,除了RPC調用以外,還須要在應用之間傳遞一些消息數據,這時分佈式消息中間件就成爲必需品。消息中間件主要用於異步和一個Provider多個Consumer的場景中。消息可分爲實時消息和延時消息。
實時消息
實時消息就是當消息發送後,接受者能實時消費的消息(如圖1.7所示),不少開源的消息中間件如開源的RocketMQ,Apache Kafka等都是比較成熟的消息中間件。
異步解耦的好處體如今多個方面:能夠分開調用者和被調用者的處理邏輯,下降系統耦合,解決處理語言之間的差別、數據結構之間的差別以及生產消息和消費者的速度差別(削峯填谷)。
經過中間的消息隊列服務,能夠作不少事情,可是要保證如下兩點:
一個消息被多個訂閱者消費是典型的一種應用場景(如圖1.9所示),多消費端很是適合用在單一事情觸發的場景中。例如當一個訂單產生時,下游會對這個訂單作不少額外的處理,而消息的生產者對這些消息的消費者根本不會關心,很是適合用在一個大型的異構系統中。
在此場景中咱們會遇到下面這些典型問題:
延時消息
除了實時消息外,延時消息用得也比較普遍。典型的例子好比一張電影票的訂單產生後,用戶在15分鐘後仍然沒有付款,那麼系統會要求在15分鐘後取消該訂單,釋放座位(如圖1.10所示)。這種場景很是適合用延時消息隊列來處理。
延時消息在技術實現上比實時消息隊列要更難,由於它須要增長一個觸發事件,而這個觸發事情有時候不必定是時間觸發事件,還有多是其餘消息事件觸發,這樣致使它所承擔的業務邏輯會更重,架構也會更復雜。
延時消息的核心是須要有一個延時事情的觸發器,此外還必須解決消息的持久化存儲問題,其餘方面和實時消息隊列差很少。整體來講,全部的消息隊列都必需要解決最終一致性、高性能和高可靠性問題。
幾種常見的消息中間件
在 http://queues.io/ 上幾乎列出了當前大部分開源的消息隊列,每一個產品各有特色,適用於不一樣應用場景,適合的纔是最好的。
分佈式數據層主要解決數據的分庫分表、主備切換以及讀寫分離等問題,統一封裝數據庫的訪問細節,如創建鏈接中的用戶名和密碼、鏈接數、數據類型的轉換等信息。
分庫分表
分佈式數據層最重要的功能是對數據作分庫分表處理(如圖1.11所示),尤爲對互聯網公司來講,數據量的不斷增加要求切分數據是至關日常的任務。
當一條或者一批SQL提交給分佈式數據層,並根據某些標識進行規制運算後,應該將這些SQL分發給規則被命中的機器去執行並返回結果。這裏最重要的是數據分片的規制要對開發透明,即寫SQL的同窗不用關心他要請求的數據到底在哪臺機器上,當咱們改變數據分片規制時,只須要修改路由規則而無須修改SQL。因此很顯然該分佈式數據層須要解析用戶的SQL,而且有可能會重寫SQL(例如修改表名或者增長一些 HINT 等信息)
主備讀寫分離
數據庫的讀寫分離是常見的操做,如圖1.12所示。因爲數據庫資源很是寶貴,爲了保證數據庫的高可用通常都會設置一主多備的架構;爲了充分利用數據庫資源,都會進行讀寫分離,即寫主庫讀從庫。在同機房的場景下,數據庫主從複製的延遲很是低,對應用層沒有什麼影響。
在原理上主從的讀寫分離比較簡單,就是拆開用戶的寫請求和讀請求,並分別路由到不一樣的DataSource上。在這種場景下要注意讀寫一致性的問題。在某些場景如雙11搶單時,用戶下完單當即查詢下單是否成功,若是查詢從庫延時會比較大,用戶極可能看不到下單成功界面從而重複下單,要有保障機制避免此類問題發生。
有些應用須要讀寫文件數據時,若是隻寫本機的話那麼就會和本機綁定,這樣這個應用就成爲有狀態的應用,那麼就很難方便地對這個應用進行遷移,水平擴展也變得困難。
不只是文件數據,一些緩存數據也存在相似問題。如今不少的分佈式緩存系統Redis、Memcache等就是用於解決數據的分佈式存儲問題的。
當前開源的分佈式文件系統不少,像開源的TFS、FastDFS、GFS等,它們主要解決的是數據的高可用性和高性能問題,下面咱們介紹一個分佈式文件系統Seaweedfs的設計,它的設計比較巧妙,頗有啓發性(如圖1.13所示):
Seaweedfs 文件系統有3個很是重要的組成部分,分別是Master、VolumeServer和Volume:
假設一個Volume有30GB的容量,被分配一個惟一的32bit的VolumelD;每一個VolumeServer 維護多個Volume和每一個Volume剩餘可寫的存儲空間,並上報給Master;Master維護整個集羣中當前可寫的Volume列表,一旦Volume寫滿就標識爲只讀。若是整個集羣中沒有可寫的Volume,那麼整個集羣都將不可寫,只能經過擴容增長新的VolumeServer。
Client 要上傳一個文件首先須要向Master申請一個fid,Master會從當前可寫的Volume列表中隨機選擇一個。和大多數分佈式文件系統同樣,這個fid就是表示文件在集羣中的存儲地址,最重要的是fid的前32bit,表明的是VolumeID,表示該文件具體存儲在哪一個Volume中並返回此fid應該存儲的VolumeServer的機器地址。
典型的一個上傳文件請求如curl-F"file=@/tmp/test.jpg""192.168.0.1:9333/submit",返回{"fid":"3,01f83e45ff","fileName":"test.jpg","fileUrl":"192.168.0.1:8081/3,01f83e45ff",
"size":12315}。Client拿到fileUrl再將文件實際上傳到192.168.0.1:8081的VolumeServer中。
文件的多副本也是在Master上管理的,巧妙的地方在於文件的多副本不是以單個用戶的文件爲單位而是以Volume爲單位進行管理。例如上面的「3,01f83e45ff」這個fid,它是存在3的Volume中,那麼這個3Volume會有一個副本,即在其餘的VolumeServer上也有一個3的Volume,那麼當test.jpg上傳到192.168.0.1:8081時,它會查詢Master這個Volume的副本在哪臺機器上,而後由這臺機器把文件copy到對應的機器上,再返回結果給用戶。目前仍是採用強一致性來保證多副本的一致性,若是某個副本上傳失敗則返回用戶失敗的結果。
擴容比較簡單,當集羣中全部的Volume都寫滿時,再增長一些VolumeServer並增長若干Volume,Master會收集新增的VolumeServer中空閒的Volume並加入到可寫的Volume列表中。
若是有機器掛掉的話,因爲Volume是有備份Volume的,因此只要存在一份Volume,Master都會保證可以返給用戶正確的請求。這裏須要注意的是Volume的多備份管理並無主次的概念,每次Master都會在可用的多備份中隨機選擇一個返回。假設3這個Volume的副本分別在192.168.0.2:8081和192.168.0.3:8081上,那麼若是192.168.0.3:8081機器「宕」掉,那麼這個3對應的Volume就會被設置爲只讀,實際上192.168.0.3:8081上全部對應的Volume都會被設置爲只讀——只要某個Volume的副本數減小,都會禁止再寫。
綜上,這個文件系統的設計思路能夠總結成如下兩點:
到目前爲止,該文件系統0.7的版本還不是太完善,表如今沒有一個很完善的Client程序來處理Master到VolumeServer之間的跳轉,通常須要本身寫。可是它最吸引人的地方就是Volume(集裝箱式的設計),這個設計比較簡單和獨立,尤爲是管理比較方便。固然,大部分分佈式系統的設計也都有類似之處。
解決好跨應用的鏈接和數據訪問後,咱們的應用也要作好相應的改造,如應用分層的設計、接口服務化拆分等。
應用分層設計
應用分層設計頗有必要。例如最起碼要把對數據庫的訪問統一抽象出來造成數據層,而不是直接在代碼裏寫SQL——這會使重構應用和水平拆分數據庫很是困難。
咱們一般從垂直方向劃分應用,分紅服務層、業務邏輯層和數據層,每一層儘可能作到解耦:上層依賴下層,而下層不要反向依賴上層。
應用分層最核心的目的是每一個層都會封裝一些信息、完成一些特定的功能需求,層與層之間經過接口交互,並且交互的數據是清晰和固定的,作到隔離和交互。能夠從如下兩個方向判斷分層是否合理。
- 若是我要增長一些新需求或者修改某些需求時,是否能清楚地知道要到哪一個層去完成,換句話說,這些分層的職責是否清晰
- 若是每一個層對個人接口不變,那麼每一個層內部的修改是否會致使其餘層也發生修改,即每一個層是否作到了收斂
分層設計中最怕的就是在接口中設計一些超級數據結構,如傳遞一個對象,而後把這個對象一直傳遞下去,並且每一個層均可能修改這個對象。這種作法致使兩個問題:一是一旦該對象更改,全部層都要隨之更改;二是沒法知道該對象的數據在哪一個層被修改,在排查問題時會比較複雜。所以,在設計層接口時要儘可能使用原生數據類型如String、Integer和Long等。
微服務化
微服務化,是從水平劃分的角度儘可能把服務分得更細,每一個業務只負責一個功能單元,這樣能夠把這些微服務組合成更大的功能模塊。也就是有目的地拆小應用,造成單一職責從而提高系統可維護性、擴展性和開發效率。圖1.14所示是基於Spring Boot構建的一個典型的微服務架構,它按照不一樣功能將大的會員服務和商品服務拆成更小原子的服務,將重要穩定的服務獨立出來,以避免常常更新的服務發佈影響這些重要穩定的服務。
在大型分佈式互聯網系統中,Session問題是典型的分佈式化過程當中會遇到的難題。由於Session數據必須在服務端的機器中共享,並要保證狀態的一致性。該問題在《深刻分析Java Web技術內幕(修訂版)》的第10章中有詳細的介紹。
ZooKeeper是一個分佈式的,開放源碼的分佈式應用程序協調服務,它是一個爲分佈式應用提供一致性服務的軟件,所提供的功能包括:配置維護、域名服務、分佈式同步、組服務等。下面咱們介紹一下典型的分佈式環境下遇到的一些典型問題的解決辦法。
集羣管理(Group Membership)
ZooKeeper 可以很容易地實現集羣管理的功能,如圖1.15所示。若是多臺Server組成一個服務集羣,那麼必須有一個「總管」知道當前集羣中每臺機器的服務狀態,一旦有機器不能提供服務,就必須知會集羣中的其餘集羣,並從新分配服務策略。一樣,當集羣的服務能力增長時,就會增長一臺或多臺Server,這些也必須讓「總管」知道。
ZooKeeper不只可以維護當前集羣中機器的服務狀態,並且可以選出一個「總管」,讓「總管」來管理集羣——這就是ZooKeeper的另外一個功能Leader Election。它的實現方式是在ZooKeeper 上建立一個EPHEMERAL類型的目錄節點,而後每一個Server在它們建立目錄節點的父目錄節點上調用getChildren(String path,Boolean watch)方法並設置watch爲true。因爲是EPHEMERAL目錄節點,當建立它的Server死去時,這個目錄節點也隨之被刪除,因此Children將會變化;這時getChildren上的Watch將會被調用,通知其餘Server某臺Server已死了。新增Server也是一樣的原理。
那麼,ZooKeeper如何實現Leader Election,也就是選出一個Master Server呢?
和前面的同樣,每臺Server建立一個EPHEMERAL目錄節點,不一樣的是它仍是一個SEQUENTIAL目錄節點,因此它是個EPHEMERAL_SEQUENTIAL目錄節點。之因此它是EPHEMERAL SEQUENTIAL目錄節點,是由於咱們能夠給每臺Server編號—咱們能夠選擇當前最小編號的Server爲Master,假如這個最小編號的Server死去,因爲它是EPHEMERAL節點,死去的Server對應的節點也被刪除,因此在當前的節點列表中又出現一個最小編號的節點,咱們就選擇這個節點爲當前Master。這樣就實現了動態選擇Master,避免傳統上單Master容易出現的單點故障問題。
共享鎖
在同一個進程中,共享鎖很容易實現,可是在跨進程或者不一樣Server的狀況下就很差實現了。然而ZooKeeper能很容易地實現這個功能,它的實現方式也是經過得到鎖的Server建立一個EPHEMERAL_SEQUENTIAL 目錄節點,再經過調用getChildren方法,查詢當前的目錄節點列表中最小的目錄節點是不是本身建立的目錄節點,若是是本身建立的,那麼它就得到了這個鎖;若是不是,那麼它就調用exists(String path,Boolean watch)方法,並監控ZooKeeper上目錄節點列表的變化,直到使本身建立的節點是列表中最小編號的目錄節點,從而得到鎖。釋放鎖很簡單,只要刪除前面它本身所建立的目錄節點便可,如圖1.16所示。
用ZooKeeper 實現同步隊列的實現思路以下:
咱們用圖1.17的流程圖來直觀地展現該過程:
用ZooKeeper實現FIFO隊列的思路以下:
在特定的目錄下建立SEQUENTIAL類型的子目錄/queue_i,這樣就能保證全部成員加入隊列時都是有編號的;出隊列時經過getChildren()方法返回當前全部隊列中的元素,再消費其中最小的一個,這樣就能保證FIFO。
分佈式消息通道普遍應用在不少公司,尤爲是在移動App和服務端須要上傳、推送大量的數據和消息時。好比打車App天天要上傳大量的位置信息,服務端也有不少訂單要及時推送給司機;此外,因爲司機是在高速移動過程當中,因此網絡鏈接的穩定性也不是很好——這類場景給消息通道的高可用設計帶來很大的挑戰。
如圖1.18所示是一個典型的移動App的消息通道的設計架構圖,這種設計比較適合上傳數據量大,而且高速移動致使網絡不太穩定的鏈路。
鏈路1是Client和整個服務端的長鏈接鏈路,通常採用私有協議的TCP請求。若是是第一次請求還會經過2作連接認證,認證經過後會把該Client和接入集羣的某個服務器作個K/V對,並記錄到路由表裏一—這能夠方便下發消息時找到該連接。
通過鏈路4,上行消息處理集羣會將TCP請求轉成普通的HTTP請求,再調用後端業務執行具體的業務邏輯,或者只是上傳一個數據而已,不作任何響應。若是業務有數據須要下發,會通過鏈路6,把消息推送到消息下發處理集羣,由它把消息推送給Client。
消息下發集羣會查詢連接路由表,肯定當前Client的連接在哪臺機器上,再經過該服務器把消息推送下去。這裏常見的問題是當前Client的網絡不可達,致使消息沒法推送。在這種狀況下,消息下發處理集羣會保持該消息,並定時嘗試再推送;若是Client 從新創建鏈接,鏈接的服務器也會隨之變化,那麼消息下發集羣會去查詢連接路由表再從新鏈接新的K/V對。
鏈路9是爲了處理Client端的一些同步請求而設計的。例如Client須要發送一個HTTP請求而且指望能返回結果,這時Client中的業務層可能直接請求HTTP,再通過Client中的網絡模塊轉成私有TCP協議,在上行長鏈請求集羣轉成HTTP請求,調用後端業務並將HTTP的response轉成消息發送到消息下發處理集羣,異步下發給Client,到達Client 再轉成業務的HTTP response。這種設計的主要考慮是當HTTP響應返回時,若是長鏈已經斷掉,該響應就無法再推送回去。所以,這種上行同步請求而下行異步推送是一種更高可用的設計。
從總體架構上看,只有接入集羣是有狀態的,其餘集羣都是無狀態的,這也保證了集羣的擴展性。若是接入點在全國有多個點,而且這些點與服務端有專線網絡服務,接入集羣還能夠作到就近接入。
當前的分佈式集羣管理中一般有兩種設計思路:
兩種思路各有優缺點:
Master 節點是固定集中式的,管理着全部其餘節點,統一指揮、統一調度,全部信息的一致性都由它控制,不容易出錯,是一種典型的集權式管理。
- 一旦Master掛了,整個集羣就容易崩潰
- 因爲它控制了全部的信息,因此也容易成爲性能瓶頸
圖示以下:
- 每一個節點的功能都是同樣的,因此每一個節點都有能力成爲 Master 節點
- 整個集羣中全部機器的狀態都保持一致
要達到整個集羣中全部機器的狀態都保持一致,須要節點之間充分的信息交換,這會致使:
- 機器之間交互控制的信息過多
- 集羣越大信息越多,管理越複雜,在出現 bug 時不太容易排查
例如,對等集羣管理模式中,最典型就是 Cassandra 的集羣管理。Cassandra 利用了 Gossip 協議(謠言協議)達到集羣中全部機器的狀態都保持一致。
Gossip 協議(謠言協議)是指:一個節點狀態發生變化很快被傳播到機器中的全部節點,因而每一個節點發生相應的變動知識發散:路由器路由表維護所涉及的 RIP 動態路由協議原理。在RIP中,每一個路由器都週期地向其直通的鄰居路由器發送本身徹底的路由表,而且也從本身直通的鄰居路由器接收路由更新信息。由於每一個路由器都是從本身的鄰居路由器瞭解路由信息,所以也將其稱爲「謠言」路由。
圖示以下:
下面咱們以開源的Tair 集羣管理爲例着重介紹Maser/Slaver的一種管理模式(如圖1.21所示),它的設計比較巧妙:
從集羣的架構上能夠看出一般有3個角色:Client、ConfigServer和DataNode,整個集羣經過一個路由信息對照表來管理,以下面路由對照表所示:
Bucket | Node |
---|---|
0 | 192.168.0.1 |
1 | 192.168.0.2 |
2 | 192.168.0.1 |
3 | 192.168.0.2 |
4 | 192.168.0.1 |
5 | 192. 168.0.2 |
Bucket是DataNode上數據管理的基本單位,經過Bucket能夠將用戶的數據劃分紅若干個集合。上表中分紅6個Bucket,那麼全部用戶的數據能夠對6取模,這樣每條數據都會存儲在其中的一個Bucket中,而每一個Bucket也會對應一臺DataNode。只要控制這張列表就能夠控制用戶數據的分佈。
ConfigServer與DataNode保持心跳,並根據DataNode的狀態生成對照表,Client主動向 ConfigServer 請求最新的對照表並緩存。ConfigServer最重要的責任就是維護對照表,但從實際的數據交互角度看,它並非強依賴——正常的數據請求不須要和ConfigServer 交互,即便ConfigServer 掛掉也不會當即影響整個集羣的工做。緣由在於對照表在Client 或者DataNode上都有備份,所以ConfigServer不是傳統意義上的Master 節點,也就不會成爲集羣的瓶頸。
下面介紹一下它們如何解決集羣中的狀態變動:擴容和容災
擴容
假如要擴容一臺機器192.168.0.3,那麼整個集羣的對照表須要從新分配,而從新分配對照表必然也會伴隨着數據在DataNode之間的移動。數據的從新分配必須基於兩個原則:儘量地保持現有的對照關係,均衡地分佈到全部節點上。新的對照表以下表所示:
Bucket | Node |
---|---|
0 | 192.168.0.1 |
1 | 192.168.0.2 |
2 | 192.168.0.1 |
3 | 192.168.0.2 |
4 | 192.168.0.3 |
5 | 192.168.0.3 |
此時只需將4和5Bucket數據移動到新機器上。
容災
容災模式比擴容更復雜一些,除了上面兩個原則之外,還須要考慮數據的備份狀況。假如保存了3份數據,則對照表以下表所示:
Bucket | Node | Node | Node |
---|---|---|---|
0 | 192.168.0.1 | 192.168.0.2 | 192.168.0.3 |
1 | 192.168.0.2 | 192.168.0.3 | 192.168.0.1 |
2 | 192.168.0.1 | 192.168.0.2 | 192.168.0.3 |
3 | 192.168.0.2 | 192.168.0.1 | 192.168.0.3 |
4 | 192.168.0.3 | 192.168.0.1 | 192.168.0.2 |
5 | 192.168.0.3 | 192.168.0.1 | 192.168.0.2 |
第一列的Node做爲主節點,若是主節點掛掉,那麼第二列的備份節點就會升級爲主節點;若是備份節點掛掉則不會受影響,而是再從新分配一個備份節點以保證數據的備份數。
當DataNode節點發生故障時,ConfigServer要從新生成對照表,並把新的對照表同步給全部的DataNode。
Client是如何獲取最新對照表的呢?
每份對照表都有一個版本號,每次Client向DataNode請求數據時,DataNode都會把本身對照表的版本號返回給Client,若是Client發現本身的版本低,則會從ConfigServer拉取最新的對照表。
這種方式會產生一個問題:當對照表發生變動時,Client有可能會更新不及時致使請求失敗。
爲什麼ConfigServer不把對照表主動推送給Client呢?
固然能夠,但這會致使ConfigServer保持對每一個Client的心跳,加劇ConfigServer的負擔,尤爲當Client數量很是大的時候,容易給ConfigServer形成管理瓶頸。
所以,上面的設計實際上是取中考慮,即在Client的數量和DataNode發生故障的機率之間選擇一個。
綜上,集羣管理中最大的困難就是當DataNode數量發生變化、涉及的數據發生遷移時,既要保證數據的一致性,又要保證高可用。
網站的分佈式改造,核心是要解決如下問題:
最後咱們用圖 1.22 、圖 1.23 來總結單應用系統向分佈式系統演進的過程:
單應用集羣架構:
演進後典型的分佈式集羣架構:
說明
本文內容源自於許令波著的《大型網站技術架構演進與性能優化》一書的第一章。