分散式進階(十七)分散式設計介紹
分散式設計介紹
前言
分散式設計與開發在IDF05(Intel Developer Forum 2005)上,Intel執行長Craig Barrett就取消4GHz晶片計劃一事,半開玩笑當眾單膝下跪致歉,給廣大軟體開發者一個明顯的訊號,單純依靠垂直提升硬體效能來提高系統性能的時代已結束,分散式開發的時代實際上早已悄悄地成為了時代的主流,吵得很熱的雲端計算實際上只是包裝在分散式之外的商業概念,很多開發者(包括我)都想加入研究雲端計算這個潮流,在google上通過“雲端計算”這個關鍵詞來查詢資料,查到的都是些概念性或商業性的宣傳資料,其實真正需要深入的還是那個早以被人熟知的概念------
分散式可繁也可以簡,最簡單的分散式就是大家最常用的,在負載均衡伺服器後加一堆web伺服器,然後在上面搞一個快取伺服器來儲存臨時狀態,後面共享一個數據庫,其實一致性Hash演算法本身比較簡單,不過可以根據實際情況有很多改進的版本,其目的無非是兩點:
1.節點變動後其他節點受影響儘可能小
2.節點變動後資料重新分配儘可能均衡
實現這個演算法就技術本身來說沒多少難度和工作量,需要做的是建立起你所設計的對映關係,無需藉助什麼框架或工具,sourceforge上倒是有個專案libconhash,可以參考一下。
以上兩個演算法在我看來就算從不涉及演算法的開發人員也需要了解的,演算法其實就是一個策略,而在分散式環境常常需要我們設計一個策略來解決很多無法通過單純的技術搞定的難題,學習這些演算法可以提供我們一些思路。分散式環境中大多數服務是允許部分失敗,也允許資料不一致,但有些最基礎的服務是需要高可靠性,高一致性的,這些服務是其他分散式服務運轉的基礎,比如naming service
高可用性 高一致性 高效能
對於這種有些挑戰CAP原則的服務該如何設計,是一個挑戰,也是一個不錯的研究課題,Apache的ZooKeeper也許給了我們一個不錯的答案。ZooKeeper是一個分散式的,開放原始碼的分散式應用程式協調服務,它暴露了一個簡單的原語集,分散式應用程式可以基於它實現同步服務,配置維護和命名服務等。關於ZooKeeper更多資訊可以參見官方文件。
ZooKeeper的基本使用
搭一個分散式的ZooKeeper環境比較簡單,基本步驟如下:
1)在各伺服器安裝ZooKeeper
tar -xzf zookeeper-3.2.2.tar.gz
2)配置叢集環境
分別在各伺服器的zookeeper安裝目錄下建立名為zoo.cfg的配置檔案,內容填寫如下:
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
dataDir=/home/admin/zookeeper-3.2.2/data
# the port at which the clients will connect
clientPort=2181
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
其中zoo1和zoo2分別對應叢集中各伺服器的機器名或ip,server.1和server.2中1和2分別對應各伺服器的zookeeper id,id的設定方法為在dataDir配置的目錄下建立名為myid的文
件,並把id作為其檔案內容即可,在本例中就分為設定為1和2。其他配置具體含義可見官方文件。
3)啟動叢集環境分別在各伺服器下執行zookeeper啟動指令碼/home/admin/zookeeper-3.2.2/bin/zkServer.sh start
4)應用zookeeper
應用zookeeper可以在是shell中執行命令,也可以在java或c中呼叫程式介面。
在shell中執行命令,可執行以下命令:
bin/zkCli.sh -server 10.20.147.35:2181
其中10.20.147.35為叢集中任一臺機器的ip或機器名。執行後可進入zookeeper的操作面板,具體如何操作可見官方文件
在java中通過呼叫程式介面來應用zookeeper較為複雜一點,需要了解watch和callback等概念,不過試驗最簡單的CURD倒不需要這些,只需要使用ZooKeeper這個類即可,具體測試程式碼如下:
Public static void main(String[] args) {
try {
ZooKeeper zk = new ZooKeeper("10.20.147.35:2181", 30000, null);
String name = zk.create("/company", "alibaba".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
Stat stat = new Stat();
System.out.println(new String(zk.getData(name, null, stat)));
zk.setData(name, "taobao".getBytes(), stat.getVersion(), null, null);
System.out.println(new String(zk.getData(name, null, stat)));
stat = zk.exists(name, null);
zk.delete(name, stat.getVersion(), null, null);
System.out.println(new String(zk.getData(name, null, stat)));
} catch (Exception e) {
e.printStackTrace();
}
}
以上程式碼比較簡單,檢視一下zooKeeper的api doc就知道如何使用了。
ZooKeeper的實現機理
ZooKeeper的實現機理是我看過的開源框架中最複雜的,它解決的是分散式環境中的一致性問題,這個場景也決定了其實現的複雜性。看了兩三天的原始碼還是有些摸不著頭腦,有些超出了我的能力,不過通過看文件和其他高人寫的文章大致清楚它的原理和基本結構。
1)ZooKeeper的基本原理
ZooKeeper是以Fast Paxos演算法為基礎的,在前一篇blog中大致介紹了一下paxos,而沒有提到的是paxos存在活鎖的問題,也就是當有多個proposer交錯提交時,有可能互相排斥導致沒有一個proposer能提交成功,而Fast Paxos作了一些優化,通過選舉產生一個leader,只有leader才能提交propose,具體演算法可見Fast Paxos。因此要想弄懂ZooKeeper首先得對Fast Paxos有所瞭解。
2)ZooKeeper的基本運轉流程
ZooKeeper主要存在以下兩個流程:
選舉Leader
同步資料
選舉Leader過程中演算法有很多,但要達到的選舉標準是一致的:
1.Leader要具有最高的zxid
2.叢集中大多數的機器得到響應並follow選出的Leader同步資料這個流程是ZooKeeper的精髓所在,並且就是Fast Paxos演算法的具體實現。一個牛人畫了一個ZooKeeper資料流動圖,比較直觀地描述了ZooKeeper是如何同步資料的。
以上兩個核心流程我暫時還不能悟透其中的精髓,這也和我還沒有完全理解Fast Paxos演算法有關,有待後續深入學習。
ZooKeeper的應用領域
Tim在blog中提到了Paxos所能應用的幾個主要場景,包括database replication、naming service、config配置管理、access control list等等,這也是ZooKeeper可以應用的幾個主要場景。此外,ZooKeeper官方文件中提到了幾個更為基礎的分散式應用,這也算是ZooKeeper的妙用吧。
1)分散式Barrier
Barrier是一種控制和協調多個任務觸發次序的機制,簡單說來就是搞個閘門把欲執行的任務給攔住,等所有任務都處於可以執行的狀態時,才放開閘門。
在單機上JDK提供了CyclicBarrier這個類來實現這個機制,但在分散式環境中JDK就無能為力了。在分散式裡實現Barrer需要高一致性做保障,因此ZooKeeper可以派上用場,所採取的方案就是用一個Node作為Barrer的實體,需要被Barrer的任務通過呼叫exists()檢測這個Node的存在,當需要開啟Barrier的時候,刪掉這個Node,ZooKeeper的watch機制會通知到各個任務可以開始執行。
2)分散式Queue
與Barrier類似,分散式環境中實現Queue也需要高一致性做保障,ZooKeeper提供了一個種簡單的方式,ZooKeeper通過一個Node來維護Queue的實體,用其children來儲存Queue的內容,並且ZooKeeper的create方法中提供了順序遞增的模式,會自動地在name後面加上一個遞增的數字來插入新元素。可以用其children來構建一個queue的資料結構,offer的時候使用create,take的時候按照children的順序刪除第一個即可。ZooKeeper保障了各個server上資料是一致的,因此也就實現了一個分散式Queue。take和offer的例項程式碼如下所示:
/**
* Removes the head of the queue and returns it, blocks until it succeeds.
* @return The former head of the queue
* @throws NoSuchElementException
* @throws KeeperException
* @throws InterruptedException
*/
Public byte[] take() throws KeeperException, InterruptedException {
TreeMap<Long,String> orderedChildren;
// Same as for element. Should refactor this.
while(true){
LatchChildWatcher childWatcher = new LatchChildWatcher();
try{
orderedChildren = orderedChildren(childWatcher);
}catch(KeeperException.NoNodeException e){
zookeeper.create(dir, new byte[0], acl, CreateMode.PERSISTENT);
continue;
}
if(orderedChildren.size() == 0){
childWatcher.await();
continue;
}
for(String headNode : orderedChildren.values()){
String path = dir +"/"+headNode;
try{
byte[] data = zookeeper.getData(path, false, null);
zookeeper.delete(path, -1);
return data;
}catch(KeeperException.NoNodeException e){
// Another client deleted the node first.
}
}
}
}
/**
* Inserts data into queue.
* @param data
* @return true if data was successfully added
*/
public boolean offer(byte[] data) throws KeeperException, InterruptedException{
for(;;){
try{
zookeeper.create(dir+"/"+prefix,data,acl,CreateMode.PERSISTENT_SEQUENTIAL);
return true;
}catch(KeeperException.NoNodeException e){
zookeeper.create(dir, newbyte[0], acl, CreateMode.PERSISTENT);
}
}
}
3)分散式lock
利用ZooKeeper實現分散式lock,主要是通過一個Node來代表一個Lock,當一個client去拿鎖的時候,會在這個Node下建立一個自增序列的child,然後通過getChildren()方式來
check建立的child是不是最靠前的,如果是則拿到鎖,否則就呼叫exist()來check第二靠前的child,並加上watch來監視。當拿到鎖的child執行完後歸還鎖,歸還鎖僅僅需要刪除
自己建立的child,這時watch機制會通知到所有沒有拿到鎖的client,這些child就會根據前面所講的拿鎖規則來競爭鎖。
一個大型系統裡各個環節中最容易出效能和可用性問題的往往是資料庫,因此分散式設計與開發的一個重要領域就是如何讓資料層具有可擴充套件性,資料庫的擴充套件分為Scale Up 和Scale Out,而Scale Up說白了是通過升級伺服器配置來完成,因此不在分散式設計的考慮之內。Scale Out是通過增加機器的方式來提升處理能力,一般需要考慮以下兩個問題:
1.資料拆分
2.資料庫高可用架構
資料拆分是最先會被想到的,原理很簡單,當一個表的資料達到無法處理的時候,就需要把它拆成多個表,說起來簡單,真正在專案裡運用的時候有很多點是需要深入研究的,一般分為:切分策略與應用程式端的整合策略。
切分策略
切分策略一般分為垂直切分、橫向切分和兩者的混搭。
1)垂直切分
垂直切分就是要把表按模組劃分到不同資料庫中,這種拆分在大型網站的演變過程中是很常見的。當一個網站還在很小的時候,只有小量的人來開發和維護,各模組和表都在一起,當網站不斷豐富和壯大的時候,也會變成多個子系統來支撐,這時就有按模組和功能把表劃分出來的需求。其實,相對於垂直切分更進一步的是服務化改造,說得簡單就是要把原來強耦合的系統拆分成多個弱耦合的服務,通過服務間的呼叫來滿足業務需求看,因此表拆出來後要通過服務的形式暴露出去,而不是直接呼叫不同模組的表,淘寶在架構不斷演變過程,最重要的一環就是服務化改造,把使用者、交易、店鋪、寶貝這些核心的概念抽取成獨立的服務,也非常有利於進行區域性的優化和治理,保障核心模組的穩定性。這樣一種拆分方式也是有代價的:表關聯無法在資料庫層面做單表大資料量依然存在效能瓶頸事務保證比較複雜應用端的複雜性增加上面這些問題是顯而易見的,處理這些的關鍵在於如何解除不同模組間的耦合性,這說是技術問題,其實更是業務的設計問題,只有在業務上是鬆耦合的,才可能在技術設計上隔離開來。沒有耦合性,也就不存在表關聯和事務的需求。另外,大資料瓶頸問題可以參見下面要講的水平切分。
2)水平切分
上面談到垂直切分只是把表按模組劃分到不同資料庫,但沒有解決單表大資料量的問題,而水平切分就是要把一個表按照某種規則把資料劃分到不同表或資料庫裡。例如像計費系統,通過按時間來劃分表就比較合適,因為系統都是處理某一時間段的資料。而像SaaS(software as a service)應用,通過按使用者維度來劃分資料比較合適,因為使用者與使用者之間的隔離的,一般不存在處理多個使用者資料的情況。水平切分沒有破壞表之間的聯絡,完全可以把有關係的表放在一個庫裡,這樣就不影響應用端的業務需求,並且這樣的切分能從根本上解決大資料量的問題。它的問題也是很明顯的:當切分規則複雜時,增加了應用端呼叫的難度資料維護難度比較大,當拆分規則有變化時,需要對資料進行遷移。對於第一個問題,可以參考後面要講的如何整合應用端和資料庫端。對於第二個問題可以參考一致性hash的演算法,通過某些對映策略來降低資料維護的成本。
布式設計與開發(二)------幾種必須瞭解的分散式演算法
3)垂直與水平聯合切分
由上面可知垂直切分能更清晰化模組劃分,區分治理,水平切分能解決大資料量效能瓶頸問題,因此常常就會把兩者結合使用,這在大型網站裡是種常見的策略,這可以結合兩者的優點,當然缺點就是比較複雜,成本較高,不太適合小型網站,下面是結合前面兩個例子的情況:與應用程式端的整合策略資料切出來還只是第一步,關鍵在於應用端如何方便地存取資料,不能因為資料拆分導致應用端存取資料錯誤或者異常複雜。按照從前往後一般說來有以下三種策略:
應用端做資料庫路由
在應用端和伺服器端加一個代理伺服器做路由
資料庫端自行做路由
1)應用端做資料庫路由
應用端做資料庫路由實現起來比較簡單,也就是在資料庫呼叫的點通過工具包的處理,給每次呼叫資料庫加上路由資訊,也就是分析每次呼叫,路由到正確的庫。這種方式多多少少沒有對應用端透明,如果路由策略有更改還需要修改應用端,並且這種更改很難做到動態更改。最關鍵的是應用端的連線池設計會比較複雜,池裡的連線就不是無狀態了,不利於管理和擴充套件。
2)在應用端和伺服器端加一個代理伺服器做路由
通過代理伺服器來做伺服器做路由可以對客戶端遮蔽後端資料庫拆分細節,增強了拆分規則的可維護性,一般而言proxy需要提供以下features:
對客戶端和資料庫服務端的連線管理和安全認證
資料庫請求路由可配置性
對呼叫命令和SQL的解析
呼叫結果的過濾和合並
現在有些開源框架提供了類似功能,比如ameoba,在以前博文設計與開發應用伺服器(一)------常見模式中介紹過ameoba的大致結構,在構建高效能web之路------mysql讀寫分離實戰介紹過如何實戰ameoba,有興趣的朋友可以參考一下。
3)資料庫端自行做路由
例如MySQL就提供了MySQL Proxy的代理產品可以在資料庫端做路由。這種方式的最大問題就是拆分規則配置的靈活性不好,不一定能滿足應用端的多種劃分需求。
以上介紹了些資料拆分的策略和相關支撐策略,隨後會研究一下前面談到的資料庫高可用架構。