[轉]【長文乾貨】淺析分佈式系統

[轉]【長文乾貨】淺析分佈式系統html

WeTest導讀程序員

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

承載量是分佈式系統存在的緣由

當一個互聯網業務得到大衆歡迎的時候,最顯著碰到的技術問題,就是服務器很是繁忙。當天天有1000萬個用戶訪問你的網站時,不管你使用什麼樣的服務器硬件,都不可能只用一臺機器就承載的了。所以,在互聯網程序員解決服務器端問題的時候,必需要考慮如何使用多臺服務器,爲同一種互聯網應用提供服務,這就是所謂「分佈式系統」的來源。數據庫

 

然而,大量用戶訪問同一個互聯網業務,所形成的問題並不簡單。從表面上看,要能知足不少用戶來自互聯網的請求,最基本的需求就是所謂性能需求:用戶反應網頁打開很慢,或者網遊中的動做很卡等等。而這些對於「服務速度」的要求,實際上包含的部分倒是如下幾個:高吞吐、高併發、低延遲和負載均衡。編程

 

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

 

高併發是高吞吐的一個延伸需求。當咱們在承載海量用戶的時候,咱們固然但願每一個服務器都能盡其所能的工做,而不要出現無謂的消耗和等待的狀況。然而,軟件系統並非簡單的設計,就能對同時處理多個任務,作到「儘可能多」的處理。不少時候,咱們的程序會由於要選擇處理哪一個任務,而致使額外的消耗。這也是分佈式系統解決的問題。安全

 

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


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

 

因爲分佈式系統,幾乎是解決互聯網業務承載量問題,的最基本方法,因此做爲一個服務器端程序員,掌握分佈式系統技術就變得異常重要了。然而,分佈式系統的問題,並不是是學會用幾個框架和使用幾個庫,就能輕易解決的,由於當一個程序在一個電腦上運行,變成了又無數個電腦上同時協同運行,在開發、運維上都會帶來很大的差異。網絡


分佈式系統提升承載量的基本手段

分層模型(路由、代理)

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

 

然而,單純的請求隨機轉發,並不能解決一切問題。好比咱們不少互聯網業務,都是須要用戶登陸的。在登陸某一個服務器後,用戶會發起多個請求,若是咱們把這些請求隨機的轉發到不一樣的服務器上,那麼用戶登陸的狀態就會丟失,形成一些請求處理失敗。簡單的依靠一層服務轉發是不夠的,因此咱們會增長一批服務器,這些服務器會根據用戶的Cookie,或者用戶的登陸憑據,來再次轉發給後面具體處理業務的服務器。

 

除了登陸的需求外,咱們還發現,不少數據是須要數據庫來處理的,而咱們的這些數據每每都只能集中到一個數據庫中,不然在查詢的時候就會丟失其餘服務器上存放的數據結果。因此每每咱們還會把數據庫單獨出來成爲一批專用的服務器。

 

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


又例如,當咱們在使用網上論壇(BBS)系統的時候,咱們發的文章,不可能只寫入一個數據庫裏,由於太多人的閱讀請求會拖死這個數據庫。咱們經常會按論壇板塊來寫入不一樣的數據庫,又或者是同時寫入多個數據庫。這樣把文章數據分別存放到不一樣的服務器上,才能應對大量的操做請求。然而,用戶在讀取文章的時候,就須要有一個專門的程序,去查找具體文章在哪個服務器上,這時候咱們就要架設一個專門的代理層,把全部的文章請求先轉交給它,由它按照咱們預設的存儲計劃,去找對應的數據庫獲取數據。

 

根據上面的例子來看,分佈式系統雖然具備三層典型的結構,可是實際上每每不止有三層,而是根據業務需求,會設計成多個層次的。爲了把請求轉交給正確的進程處理,咱們而設計不少專門用於轉發請求的進程和服務器。這些進程咱們經常以Proxy或者Router來命名,一個多層結構經常會具有各類各樣的Proxy進程。這些代理進程,不少時候都是經過TCP來鏈接先後兩端。然而,TCP雖然簡單,可是卻會有故障後不容易恢復的問題。並且TCP的網絡編程,也是有點複雜的。——因此,人們設計出更好進程間通信機制:消息隊列。


儘管經過各類Proxy或者Router進程能組建出強大的分佈式系統,可是其管理的複雜性也是很是高的。因此人們在分層模式的基礎上,想出了更多的方法,來讓這種分層模式的程序變得更簡單高效的方法。

 

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

當咱們在編寫服務器端程序是,咱們會明確的知道,大部分的程序,都是會處理同時到達的多個請求的。所以咱們不能好像HelloWorld那麼簡單的,從一個簡單的輸入計算出輸出來。由於咱們會同時得到不少個輸入,須要返回不少個輸出。在這些處理的過程當中,每每咱們還會碰到須要「等待」或「阻塞」的狀況,好比咱們的程序要等待數據庫處理結果,等待向另一個進程請求結果等等……若是咱們把請求一個挨着一個的處理,那麼這些空閒的等待時間將白白浪費,形成用戶的響應延時增長,以及總體系統的吞吐量極度降低。

 

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

 

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

 

異步回調基於非阻塞的I/O操做(網絡和文件),這樣咱們就不用在調用讀寫函數的時候「卡」在那一句函數調用,而是馬上返回「有無數據」的結果。而Linux的epoll技術,則利用底層內核的機制,讓咱們能夠快速的「查找」到有數據能夠讀寫的鏈接\文件。因爲每一個操做都是非阻塞的,因此咱們的程序能夠只用一個進程,就處理大量併發的請求。由於只有一個進程,因此全部的數據處理,其順序都是固定的,不可能出現多線程中,兩個函數的語句交錯執行的狀況,所以也不須要各類「鎖」。從這個角度看,異步非阻塞的技術,是大大簡化了開發的過程。因爲只有一個線程,也不須要有線程切換之類的開銷,因此異步非阻塞成爲不少對吞吐量、併發有較高要求的系統首選。


int epoll_create(int size);//建立一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 

緩衝技術

在互聯網服務中,大部分的用戶交互,都是須要馬上返回結果的,因此對於延遲有必定的要求。而相似網絡遊戲之類服務,延遲更是要求縮短到幾十毫秒之內。因此爲了下降延遲,緩衝是互聯網服務中最多見的技術之一。

 

早期的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的時候,如何讓數據庫存放更多的數據,承載更多的鏈接,不少團隊都是絞盡腦汁。甚至於有不少業務,主要的數據存儲方式是文件,數據庫反而變成是輔助的設施了。


然而,當NoSQL興起,你們忽然發現,其實不少互聯網業務,其數據格式是如此的簡單,不少時候根部不須要關係型數據庫那種複雜的表格。對於索引的要求每每也只是根據主索引搜索。而更復雜的全文搜索,自己數據庫也作不到。因此如今至關多的高併發的互聯網業務,首選NoSQL來作存儲設施。最先的NoSQL數據庫有MangoDB等,如今最流行的彷佛就是Redis了。甚至有些團隊,把Redis也當成緩衝系統的一部分,實際上也是承認Redis的性能優點。

 

NoSQL除了更快、承載量更大之外,更重要的特色是,這種數據存儲方式,只能按照一條索引來檢索和寫入。這樣的需求約束,帶來了分佈上的好處,咱們能夠按這條主索引,來定義數據存放的進程(服務器)。這樣一個數據庫的數據,就能很方便的存放在不一樣的服務器上。在分佈式系統的必然趨勢下,數據存儲層終於也找到了分佈的方法。


分佈式系統在可管理性上形成的問題

分佈式系統並非簡單的把一堆服務器一塊兒運行起來就能知足需求的。對比單機或少許服務器的集羣,有一些特別須要解決的問題等待着咱們。

 

硬件故障率

所謂分佈式系統,確定就不是隻有一臺服務器。假設一臺服務器的平均故障時間是1%,那麼當你有100臺服務器的時候,那就幾乎總有一臺是在故障的。雖然這個比方不必定很準確,可是,當你的系統所涉及的硬件愈來愈多,硬件的故障也會從偶然事件變成一個必然事件。通常咱們在寫功能代碼的時候,是不會考慮到硬件故障的時候應該怎麼辦的。而若是在編寫分佈式系統的時候,就必定須要面對這個問題了。不然,極可能只有一臺服務器出故障,整個數百臺服務器的集羣都工做不正常了。


除了服務器本身的內存、硬盤等故障,服務器之間的網絡線路故障更加常見。並且這種故障還有多是偶發的,或者是會自動恢復的。面對這種問題,若是隻是簡單的把「出現故障」的機器剔除出去,那仍是不夠的。由於網絡可能過一下子就又恢復了,而你的集羣可能由於這一下的臨時故障,丟失了過半的處理能力。

 

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

 

資源利用率優化

在分佈式系統的集羣,包含了不少個服務器,當這樣一個集羣的硬件承載能力到達極限的時候,最天然的想法就是增長更多的硬件。然而,一個軟件系統不是那麼容易就能夠經過「增長」硬件來提升承載性能的。由於軟件在多個服務器上的工做,是須要有複雜細緻的協調工做。在對一個集羣擴容的時候,咱們每每會要停掉整個集羣的服務,而後修改各類配置,最後才能從新啓動一個加入了新的服務器的集羣。

因爲在每一個服務器的內存裏,均可能會有一些用戶使用的數據,因此若是冒然在運行的時候,就試圖修改集羣中提供服務的配置,極可能會形成內存數據的丟失和錯誤。所以,運行時擴容在對無狀態的服務上,是比較容易的,好比增長一些Web服務器。但若是是在有狀態的服務上,好比網絡遊戲,幾乎是不可能進行簡單的運行時擴容的。

 

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

 

因爲分佈式集羣中的擴容、縮容,以及但願儘可能能在線操做,這致使了很是複雜的技術問題須要處理,好比集羣中互相關聯的配置如何正確高效的修改、如何對有狀態的進程進行操做、如何在擴容縮容的過程當中保證集羣中節點之間通訊的正常。做爲服務器端程序員,會須要花費大量的經歷,來對多個進程的集羣狀態變化,形成的一系列問題進行專門的開發。

 

軟件服務內容更新

如今都流行用敏捷開發模式中的「迭代」,來表示一個服務不斷的更新程序,知足新的需求,修正BUG。若是咱們僅僅管理一臺服務器,那麼更新這一臺服務器上的程序,是很是簡單的:只要把軟件包拷貝過去,而後修改下配置就好。可是若是你要對成百上千的服務器去作一樣的操做,就不可能每臺服務器登陸上去處理。

 

服務器端的程序批量安裝部署工具,是每一個分佈式系統開發者都須要的。然而,咱們的安裝工做除了拷貝二進制文件和配置文件外,還會有不少其餘的操做。好比打開防火牆、創建共享內存文件、修改數據庫表結構、改寫一些數據文件等等……甚至有一些還要在服務器上安裝新的軟件。



若是咱們在開發服務器端程序的時候,就考慮到軟件更新、版本升級的問題,那麼咱們對於配置文件、命令行參數、系統變量的使用,就會預先作必定的規劃,這能讓安裝部署的工具運行更快,可靠性更高。

 

除了安裝部署的過程,還有一個重要的問題,就是不一樣版本間數據的問題。咱們在升級版本的時候,舊版本程序生成的一些持久化數據,通常都是舊的數據格式的;而咱們升級版本中若是涉及修改了數據格式,好比數據表結果,那麼這些舊格式的數據,都要轉換改寫成新版本的數據格式才行。這致使了咱們在設計數據結構的時候,就要考慮清楚這些表格的結構,是用最簡單直接的表達方式,來讓未來的修改更簡單;仍是一早就預計到修改的範圍,專門預設一些字段,或者使用其餘形式存放數據。

 

除了持久化數據之外,若是存在客戶端程序(如受擊APP),這些客戶端程序的升級每每不能和服務器同步,若是升級的內容包含了通訊協議的修改,這就形成了咱們必須爲不一樣的版本部署不一樣的服務器端系統的問題。爲了不同時維護多套服務器,咱們在軟件開發的時候,每每傾向於所謂「版本兼容」的協議定義方式。而怎樣設計的協議纔能有很好的兼容性,又是服務器端程序須要仔細考慮的問題。

 

數據統計和決策

通常來講,分佈式系統的日誌數據,都是被集中到一塊兒,而後統一進行統計的。然而,當集羣的規模到必定程度的時候,這些日誌的數據量會變得很是恐怖。不少時候,統計一天的日誌量,要消耗計算機運行一天以上的時間。因此,日誌統計這項工做,也變成一門很是專業的活動。

 

經典的分佈式統計模型,有Google的Map Reduce模型。這種模型既有靈活性,也能利用大量服務器進行統計工做。可是缺點是易用性每每不夠好,由於這些數據的統計和咱們常見的SQL數據表統計有很是大的差別,因此咱們最後仍是經常把數據丟到MySQL裏面去作更細層面的統計。


因爲分佈式系統日誌數量的龐大,以及日誌複雜程度的提升。咱們變得必需要掌握相似Map Reduce技術,才能真正的對分佈式系統進行數據統計。並且咱們還須要想辦法提升統計工做的工做效率。


解決分佈式系統可管理性的基本手段

目錄服務(ZooKeeper)

分佈式系統是一個由不少進程組成的總體,這個總體中每一個成員部分,都會具有一些狀態,好比本身的負責模塊,本身的負載狀況,對某些數據的掌握等等。而這些和其餘進程相關的數據,在故障恢復、擴容縮容的時候變得很是重要。

 

簡單的分佈式系統,能夠經過靜態的配置文件,來記錄這些數據:進程之間的鏈接對應關係,他們的IP地址和端口,等等。然而一個自動化程度高的分佈式系統,必然要求這些狀態數據都是動態保存的。這樣才能讓程序本身去作容災和負載均衡的工做。

 

一些程序員會專門本身編寫一個DIR服務(目錄服務),來記錄集羣中進程的運行狀態。集羣中進程會和這個DIR服務產生自動關聯,這樣在容災、擴容、負載均衡的時候,就能夠自動根據這些DIR服務裏的數據,來調整請求的發送目地,從而達到繞開故障機器、或鏈接到新的服務器的操做。

 

然而,若是咱們只是用一個進程來充當這個工做。那麼這個進程就成爲了這個集羣的「單點」——意思就是,若是這個進程故障了,那麼整個集羣可能都沒法運行的。因此存放集羣狀態的目錄服務,也須要是分佈式的。幸虧咱們有ZooKeeper這個優秀的開源軟件,它正是一個分佈式的目錄服務區。

 

ZooKeeper能夠簡單啓動奇數個進程,來造成一個小的目錄服務集羣。這個集羣會提供給全部其餘進程,進行讀寫其巨大的「配置樹」的能力。這些數據不只僅會存放在一個ZooKeeper進程中,而是會根據一套很是安全的算法,讓多個進程來承載。這讓ZooKeeper成爲一個優秀的分佈式數據保存系統。

 

因爲ZooKeeper的數據存儲結構,是一個相似文件目錄的樹狀系統,因此咱們經常會利用它的功能,把每一個進程都綁定到其中一個「分枝」上,而後經過檢查這些「分支」,來進行服務器請求的轉發,就能簡單的解決請求路由(由誰去作)的問題。另外還能夠在這些「分支」上標記進程的負載的狀態,這樣負載均衡也很容易作了。

 

目錄服務是分佈式系統中最關鍵的組件之一。而ZooKeeper是一個很好的開源軟件,正好是用來完成這個任務。

 

消息隊列服務(ActiveMQ、ZeroMQ、Jgroups)

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

 

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

 

通常的消息隊列服務,都是提供簡單的「投遞」和「收取」兩個接口,可是消息隊列自己的管理方式卻比較複雜,通常來講有兩種。一部分的消息隊列服務,提倡點對點的隊列管理方式:每對通訊節點之間,都有一個單獨的消息隊列。這種作法的好處是不一樣來源的消息,能夠互不影響,不會由於某個隊列的消息過多,擠佔了其餘隊列的消息緩存空間。並且處理消息的程序也能夠本身來定義處理的優先級——先收取、多處理某個隊列,而少處理另一些隊列。

 

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


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

 

無論使用那種消息隊列服務,在一個分佈式服務器端系統中,進程間通信都是必需要解決的問題,因此做爲服務器端程序員,在編寫分佈式系統代碼的時候,使用的最多的就是基於消息隊列驅動的代碼,這也直接致使了EJB3.0把「消息驅動的Bean」加入到規範之中。

 

事務系統

在分佈式的系統中,事務是最難解決的技術問題之一。因爲一個處理可能分佈在不一樣的處理進程上,任何一個進程均可能出現故障,而這個故障問題則須要致使一次回滾。這種回滾大部分又涉及多個其餘的進程。這是一個擴散性的多進程通信問題。要在分佈式系統上解決事務問題,必須具有兩個核心工具:一個是穩定的狀態存儲系統;另一個是方即可靠的廣播系統。


事務中任何一步的狀態,都必須在整個集羣中可見,而且還要有容災的能力。這個需求,通常仍是由集羣的「目錄服務」來承擔。若是咱們的目錄服務足夠健壯,那麼咱們能夠把每步事務的處理狀態,都同步寫到目錄服務上去。ZooKeeper再次在這個地方能發揮重要的做用。

 

若是事務發生了中斷,須要回滾,那麼這個過程會涉及到多個已經執行過的步驟。也許這個回滾只須要在入口處回滾便可(加入那裏有保存回滾所需的數據),也可能須要在各個處理節點上回滾。若是是後者,那麼就須要集羣中出現異常的節點,向其餘全部相關的節點廣播一個「回滾!事務ID是XXXX」這樣的消息。這個廣播的底層通常會由消息隊列服務來承載,而相似Jgroups這樣的軟件,直接提供了廣播服務。

 

雖然如今咱們在討論事務系統,但實際上分佈式系統常常所需的「分佈式鎖」功能,也是這個系統能夠同時完成的。所謂的「分佈式鎖」,也就是一種能讓各個節點先檢查後執行的限制條件。若是咱們有高效而單子操做的目錄服務,那麼這個鎖狀態實際上就是一種「單步事務」的狀態記錄,而回滾操做則默認是「暫停操做,稍後再試」。這種「鎖」的方式,比事務的處理更簡單,所以可靠性更高,因此如今愈來愈多的開發人員,願意使用這種「鎖」服務,而不是去實現一個「事務系統」。


自動部署工具(Docker)

因爲分佈式系統最大的需求,是在運行時(有可能須要中斷服務)來進行服務容量的變動:擴容或者縮容。而在分佈式系統中某些節點故障的時候,也須要新的節點來恢復工做。這些若是仍是像老式的服務器管理方式,經過填表、申報、進機房、裝服務器、部署軟件……這一套作法,那效率確定是不行。

 

在分佈式系統的環境下,咱們通常都是採用「池」的方式來管理服務。咱們預先會申請一批機器,而後在某些機器上運行服務軟件,另一些則做爲備份。顯然咱們這一批服務器不可能只爲某一個業務服務,而是會提供多個不一樣的業務承載。那些備份的服務器,則會成爲多個業務的通用備份「池」。隨着業務需求的變化,一些服務器可能「退出」A服務而「加入」B服務。

 

這種頻繁的服務變化,依賴高度自動的軟件部署工具。咱們的運維人員,應該掌握這開發人員提供的部署工具,而不是厚厚的手冊,來進行這類運維操做。一些比較有經驗的開發團隊,會統一全部的業務底層框架,以期大部分的部署、配置工具,都能用一套通用的系統來進行管理。而開源界,也有相似的嘗試,最廣爲人知的莫過於RPM安裝包格式,然而RPM的打包方式仍是太複雜,不太符合服務器端程序的部署需求。因此後來又出現了Chef爲表明的,可編程的通用部署系統。


然而,當NoSQL興起,你們忽然發現,其實不少互聯網業務,其數據格式是如此的簡單,不少時候根部不須要關係型數據庫那種複雜的表格。對於索引的要求每每也只是根據主索引搜索。而更復雜的全文搜索,自己數據庫也作不到。因此如今至關多的高併發的互聯網業務,首選NoSQL來作存儲設施。最先的NoSQL數據庫有MangoDB等,如今最流行的彷佛就是Redis了。甚至有些團隊,把Redis也當成緩衝系統的一部分,實際上也是承認Redis的性能優點。

 

NoSQL除了更快、承載量更大之外,更重要的特色是,這種數據存儲方式,只能按照一條索引來檢索和寫入。這樣的需求約束,帶來了分佈上的好處,咱們能夠按這條主索引,來定義數據存放的進程(服務器)。這樣一個數據庫的數據,就能很方便的存放在不一樣的服務器上。在分佈式系統的必然趨勢下,數據存儲層終於也找到了分佈的方法。


爲了管理大量的分佈式服務器端進程,咱們確實須要花不少功夫,其優化其部署管理的工做。統一服務器端進程的運行規範,是實現自動化部署管理的基本條件。咱們能夠根據「操做系統」做爲規範,採用Docker技術;也能夠根據「Web應用」做爲規範,採用某些PaaS平臺技術;或者本身定義一些更具體的規範,本身開發完整的分佈式計算平臺。

 

日誌服務(log4j)

服務器端的日誌,一直是一個既重要又容易被忽視的問題。不少團隊在剛開始的時候,僅僅把日誌視爲開發調試、排除BUG的輔助工具。可是很快會發現,在服務運營起來以後,日誌幾乎是服務器端系統,在運行時能夠用來了解程序狀況的惟一有效手段。

 

儘管咱們有各類profile工具,可是這些工具大部分都不適合在正式運營的服務上開啓,由於會嚴重下降其運行性能。因此咱們更多的時候須要根據日誌來分析。儘管日誌從本質上,就是一行行的文本信息,可是因爲其具備很大的靈活性,因此會很受開發和運維人員的重視。

 

日誌自己從概念上,是一個很模糊的東西。你能夠隨便打開一個文件,而後寫入一些信息。可是現代的服務器系統,通常都會對日誌作一些標準化的需求規範:日誌必須是一行一行的,這樣比較方便往後的統計分析;每行日誌文本,都應該有一些統一的頭部,好比日期時間就是基本的需求;日誌的輸出應該是分等級的,好比fatal/error/warning/info/debug/trace等等,程序能夠在運行時調整輸出的等級,以即可以節省日誌打印的消耗;日誌的頭部通常還須要一些相似用戶ID或者IP地址之類的頭信息,用於快速查找定位過濾某一批日誌記錄,或者有一些其餘的用於過濾縮小日誌查看範圍的字段,這叫作染色功能;日誌文件還須要有「回滾」功能,也就是保持固定大小的多個文件,避免長期運行後,把硬盤寫滿。


因爲有上述的各類需求,因此開源界提供了不少遊戲的日誌組件庫,好比大名鼎鼎的log4j,以及成員衆多的log4X家族庫,這些都是應用普遍而飽受好評的工具。

 

不過對比日誌的打印功能,日誌的蒐集和統計功能卻每每比較容易被忽視。做爲分佈式系統的程序員,確定是但願能從一個集中節點,能蒐集統計到整個集羣日誌狀況。而有一些日誌的統計結果,甚至但願能在很短期內反覆獲取,用來監控整個集羣的健康狀況。要作到這一點,就必須有一個分佈式的文件系統,用來存放源源不斷到達的日誌(這些日誌每每經過UDP協議發送過來)。而在這個文件系統上,則須要有一個相似Map Reduce架構的統計系統,這樣才能對海量的日誌信息,進行快速的統計以及報警。有一些開發者會直接使用Hadoop系統,有一些則用Kafka來做爲日誌存儲系統,上面再搭建本身的統計程序。

 

日誌服務是分佈式運維的儀表盤、潛望鏡。若是沒有一個可靠的日誌服務,整個系統的運行情況可能會是失控的。因此不管你的分佈式系統節點是多仍是少,必須花費重要的精力和專門的開發時間,去創建一個對日誌進行自動化統計分析的系統。


分佈式系統在開發效率上形成的問題和解決思路

根據上文所述,分佈式系統在業務需求的功能覺得,還須要增長額外不少非功能的需求。這些非功能需求,每每都是爲了一個多進程系統能穩定可靠運行而去設計和實現的。這些「額外」的工做,通常都會讓你的代碼更加複雜,若是沒有很好的工具,就會讓你的開發效率嚴重降低。

 

微服務框架:EJB、WebService

當咱們在討論服務器端軟件分佈的時候,服務進程之間的通訊就不免了。然而服務進程間的通信,並非簡單的收發消息就能完成的。這裏還涉及了消息的路由、編碼解碼、服務狀態的讀寫等等。若是整個流程都由本身開發,那就太累人了。


因此業界很早就推出了各類分佈式的服務器端開發框架,最著名的就是「EJB」——企業JavaBean。但凡冠以「企業」的技術,每每都是分佈式下所需的部分,而EJB這種技術,也是一種分佈式對象調用的技術。咱們若是須要讓多個進程合做完成任務,則須要把任務分解到多個「類」上,而後這些「類」的對象就會在各個進程容器中存活,從而協做提供服務。這個過程很「面向對象」。每一個對象都是一個「微服務」,能夠提供某些分佈式的功能。

 

而另一些系統,則走向學習互聯網的基本模型:HTTP。因此就有了各類的WebService框架,從開源的到商業軟件,都有各自的WebService實現。這種模型,把複雜的路由、編解碼等操做,簡化成常見的一次HTTP操做,是一種很是有效的抽象。開發人員開發和部署多個WebService到Web服務器上,就完成了分佈式系統的搭建。


無論咱們是學習EJB仍是WebService,實際上咱們都須要簡化分佈式調用的複雜程度。而分佈式調用的複雜之處,就是由於須要把容災、擴容、負載均衡等功能,融合到跨進程調用裏。因此使用一套通用的代碼,來爲全部的跨進程通信(調用),統一的實現容災、擴容、負載均衡、過載保護、狀態緩存命中等等非功能性需求,能大大簡化整個分佈式系統的複雜性。

 

通常咱們的微服務框架,都會在路由階段,對整個集羣全部節點的狀態進行觀察,如哪些地址上運行了哪些服務的進程,這些服務進程的負載情況如何,是否可用,而後對於有狀態的服務,還會使用相似一致性哈希的算法,去儘可能試圖提升緩存的命中率。當集羣中的節點狀態發生變化的時候,微服務框架下的全部節點,都能儘快的得到這個變化的狀況,重新根據當前狀態,從新規劃之後的服務路由方向,從而實現自動化的路由選擇,避開那些負載太高或者失效的節點。

 

有一些微服務框架,還提供了相似IDL轉換成「骨架」、「樁」代碼的工具,這樣在編寫遠程調用程序的時候,徹底無需編寫那些複雜的網絡相關的代碼,全部的傳輸層、編碼層代碼都自動的編寫好了。這方面EJB、Facebook的Thrift,Google gRPC都具有這種能力。在具有代碼生成能力的框架下,咱們編寫一個分佈式下可用的功能模塊(多是一個函數或者是一個類),就好像編寫一個本地的函數那樣簡單。這絕對是分佈式系統下很是重要的效率提高。



異步編程工具:協程、Futrue、Lamda

在分佈式系統中編程,你不可避免的會碰到大量的「回調」型API。由於分佈式系統涉及很是多的網絡通訊。任何一個業務命令,均可能被分解到多個進程,經過屢次網絡通訊來組合完成。因爲異步非阻塞的編程模型大行其道,因此咱們的代碼也每每動不動就要碰到「回調函數」。然而,回調這種異步編程模型,是一種很是不利於代碼閱讀的編程方法。由於你沒法從頭至尾的閱讀代碼,去了解一個業務任務,是怎樣被逐步的完成的。屬於一個業務任務的代碼,因爲屢次的非阻塞回調,從而被分割成不少個回調函數,在代碼的各處被串接起來。

 

更有甚者,咱們有時候會選擇使用「觀察者模式」,咱們會在一個地方註冊大量的「事件-響應函數」,而後在全部須要回調的地方,都發出一個事件。——這樣的代碼,比單純的註冊回調函數更難理解。由於事件對應的響應函數,一般在發出事件處是沒法找到的。這些函數永遠都會放在另外的一些文件裏,並且有時候這些函數還會在運行時改變。而事件名字自己,也每每是匪夷所思難以理解的,由於當你的程序須要成千上百的事件的時候,起一個容易理解名符其實的名字,幾乎是不可能的。

 

爲了解決回調函數這種對於代碼可讀性的破壞做用,人們發明了不少不一樣的改進方法。其中最著名的是「協程」。咱們之前經常習慣於用多線程來解決問題,因此很是熟悉以同步的方式去寫代碼。協程正是延續了咱們的這一習慣,但不一樣於多線程的是,協程並不會「同時」運行,它只是在須要阻塞的地方,用Yield()切換出去執行其餘協程,而後當阻塞結束後,用Resume()回到剛剛切換的位置繼續往下執行。這至關於咱們能夠把回調函數的內容,接到Yield()調用的後面。這種編寫代碼的方法,很是相似於同步的寫法,讓代碼變得很是易讀。可是惟一的缺點是,Resume()的代碼仍是須要在所謂「主線程」中運行。用戶必須本身從阻塞恢復的時候,去調用Resume()。協程另一個缺點,是須要作棧保存,在切換到其餘協程以後,棧上的臨時變量,也都須要額外佔用空間,這限制了協程代碼的寫法,讓開發者不能用太大的臨時變量。


而另一種改善回調函數的寫法,每每叫作Future/Promise模型。這種寫法的基本思路,就是「一次性把全部回調寫到一塊兒」。這是一個很是實用的編程模型,它沒有讓你去完全乾掉回調,而是讓你能夠把回調從分散各處,集中到一個地方。在同一段代碼中,你能夠清晰的看到各個異步的步驟是如何串接、或者並行執行的。

最後說一下lamda模型,這種寫法流行於js語言的普遍應用。因爲在其餘語言中,定一個回調函數是很是費事的:Java語言要設計一個接口而後作一個實現,簡直是五星級的費事程度;C/C++支持函數指針,算是比較簡單,可是也很容易致使代碼看不懂;腳本語言相對好一些,也要定義個函數。而直接在調用回調的地方,寫回調函數的內容,是最方便開發,也比較利於閱讀的。更重要的,lamda通常意味着閉包,也就是說,這種回調函數的調用棧,是被分別保存的,不少須要在異步操做中,須要創建一個相似「會話池」的狀態保存變量,在這裏都是不須要的,而是能夠天然生效的。這一點和協程有殊途同歸之妙。

無論使用哪種異步編程方式,其編碼的複雜度,都是必定比同步調用的代碼高的。因此咱們在編寫分佈式服務器代碼的時候,必定要仔細規劃代碼結構,避免出現隨意添加功能代碼,致使代碼的可讀性被破壞的狀況。不可讀的代碼,就是不可維護的代碼,而大量異步回調的服務器端代碼,是更容易出現這種狀況的。

 

雲服務模型:IaaS/PaaS/SaaS

在複雜的分佈式系統開發和使用過程當中,如何對大量服務器和進程的運維,一直是一個貫穿其中的問題。無論是使用微服務框架、仍是統一的部署工具、日誌監控服務,都是由於大量的服務器,要集中的管理,是很是不容易的。這裏背後的緣由,主要是大量的硬件和網絡,把邏輯上的計算能力,切割成不少小塊。

 

隨着計算機運算能力的提高,出現的虛擬化技術,卻能把被分割的計算單元,更智能的統一塊兒來。其中最多見的就是IaaS技術:當咱們能夠用一個服務器硬件,運行多個虛擬的服務器操做系統的時候,咱們須要維護的硬件數量就會成倍的降低。

 

而PaaS技術的流行,讓咱們能夠爲某一種特定的編程模型,統一的進行系統運行環境的部署維護。而不須要再一臺臺服務器的去裝操做系統、配置運行容器、上傳運行代碼和數據。在沒有統一的PaaS以前,安裝大量的MySQL數據庫,曾經是消耗大量時間和精力的工做。

 

當咱們的業務模型,成熟到能夠抽象爲一些固定的軟件時,咱們的分佈式系統就會變得更加易用。咱們的計算能力再也不是代碼和庫,而是一個個經過網絡提供服務的雲——SaaS,這樣使用者根原本維護、部署的工做都不須要,只要申請一個接口,填上預期的容量額度,就能直接使用了。這不只節省了大量開發對應功能的事件,還等於把大量的運維工做,都交出去給SaaS的維護者——而他們作這樣的維護會更加專業。


在運維模型的進化上,從IaaS到PaaS到SaaS,其應用範圍也許是愈來愈窄,但使用的便利性卻成倍的提升。這也證實了,軟件勞動的工做,也是能夠經過分工,向更專業化、更細分的方向去提升效率。


總結分佈式系統問題的解決路徑


本文轉載自 騰訊雲技術社區——騰雲閣 https://www.qcloud.com/community

針對服務器承載能力的問題,騰訊WeTest運用了沉澱十多年的內部實踐經驗總結,經過基於真實業務場景和用戶行爲進行壓力測試,幫助遊戲開發者發現服務器端的性能瓶頸,進行鍼對性的性能調優,下降服務器採購和維護成本,提升用戶留存和轉化率。

相關文章
相關標籤/搜索