03 分佈式 00 分佈式:分佈式系統入門

分佈式系統技術概要

現在互聯網應用,尤其是大型互聯網公司的應用已經發展爲大規模或超大規模的分佈式的,集羣化的應用。而中小規模的分佈式應用也已廣泛出現在各個領域。未來,隨着雲計算向社會生活的方方面面去滲透,分佈式應用將更加地普及。所以,任何一個要從事服務器端應用開發的人員,都有具備對分佈式應用的基本認識。

本文將簡要介紹分佈式應用的各基本領域的相關技術。這些技術在一個分佈式應用中都會有或多或少的設計,即便暫時沒有涉及到,設計人員也要有所考慮,保證系統有進一步發展的空間。

1. 集羣管理

**關鍵字:**Apache Zookeeper、Paxos 算法、Etcd、Raft、Apache Curator

在一個分佈式系統中,存在着一些和系統運行,以及重要業務緊密相關的數據,如節點相關的數據、應用服務和數據服務相關的數據等,這些數據對集羣的正常運行至關重要。

  • **服務器節點相關數據:**服務器的地址、狀態
  • **服務相關數據:**服務的IP、端口、版本、協議、狀態、主備節點信息
  • **數據庫相關數據:**路由規則、分庫分表規則

這些重要的數據在分佈式系統中存在着多份拷貝,以保證高可用性。但這產生了另外一個問題,就是如何保證這些數據的一致性。因爲這些數據是如此重要,不一致的數據會產生嚴重甚至致命的錯誤。在一個小規模的分佈式系統中,因爲可以用一兩臺服務器去做集羣管理,所以數據的一致性容易實現。但是對於一個大規模的分佈式系統,一兩臺集羣配置管理服務器無法支撐整個集羣所帶來的大量併發讀寫操作,所以要使用幾臺、十幾臺,甚至更多的服務器去支撐這些請求。此時,就需要一個保持這些服務器中集羣配置數據的一致性的方案了。

這衆多方案中,Paxos 算法算是最佳方案之一。關於 Paxos 算法的內容,不在這裏詳述了。簡單描述就是集羣中各節點相互以提議的方式通信(對一項數據的修改),提議中帶有不斷增加的 ID 號,節點永遠同意當前 ID 號最大的提議,並拒絕其它提議。當有半數以上節點同意一項提議之後,這個提議便被整個節點所接受並採納。

1.1. Apache Zookeeper

Paxos 算法的語言表述看上去不難,但是其中的技術難點並不少。好在現在已經有了很多的解決方案,其中最爲著名的便是 Apache Zookeeper。Zookeeper 不僅可以用來存儲配置數據,還可以用來實現集羣 Master 選舉、分佈式鎖等場景。Apache Curator 是 Zookeeper 的客戶端,可以簡化對 Zookeeper 的使用,實現各式的場景。

Zookeeper 是一個分佈式的服務管理框架。Zookeeper 的典型的應用場景包括配置文件的管理、集羣管理、分佈式鎖、Leader 選舉、隊列管理等。Zookeeper 可工作在集羣模式下,zoo.cfg 中記錄着集羣中所有 Zookeeper 服務器的地址,每個服務器有自己唯一的 ID。同時,每個服務器在自己的 dataDir 目錄下還要有一個 myid 文件,以標示自己的 ID。在 Zookeeper 中,數據以樹狀的結構存儲,類似於 LDAP 數據庫。

現在類似 Zookeeper 的項目還有使用 go 語言實現的 Etcd。

1.2. 參考:

2. 遠程調用

關鍵字: NIO、Netty、epoll、Thrift、Protobuf

分佈式系統中,模塊間的調用通常需要用遠程調用來實現。而且隨着微服務架構模式的流行,使用遠程調用的比例會越來越高。其實遠程調用這種方式很早以前就出現了,早年的技術有諸如 COBRA、EJB、SOAP 等,但這些技術存在着用法複雜、性能差等缺點。這些缺點限制着遠程調用的普及。這些年,隨着異步 IO 技術、序列化技術的發展進步,以及像 Zookeeper 這樣的集羣管理服務的出現普及,妨礙遠程調用普及的技術障礙逐漸被打破。

使用 HTTP + JSON 的方式同樣可以實現模塊之間的遠程調用,但這種方式通常用來實現 Public API。在系統內部,遠程調用要求更快的速度,更小的延遲,還有還有異步調用的需求,所以 HTTP + JSON 通常無法滿足這樣的要求。遠程調用有兩個重要的技術點,一個是 IO 技術、一個是序列化技術。另外,遠程調用還引出來另兩個問題:1. 服務註冊、發現、路由的問題。這個問題的需要結合例如 Zookeeper 服務去解決;2. 如何簡化遠程調用的使用,使其如同本地調用一樣簡單。這個問題需要結合 AOP 之類的技術。這兩個問題的具體解決不在本節討論範圍之內。

2.1. IO

(這裏只說 Socket IO)常見的 IO 模型有阻塞 IO、非阻塞 IO 和異步 IO。阻塞 IO 指的是如果一個線程要在 Socket 連接上進行某種 IO 操作時(讀或寫數據),當沒有操作不可執行時(沒有數據可讀或無法寫數據),執行操作的線程便會被掛起,操作便會被阻塞,直到操作可以執行。這種方式的好處是業務代碼編寫起來很簡單,缺點是資源利用率不高。因爲一個連接必須有一個線程去處理。當有大量連接時,便會消耗大量的線程。這個缺點放在服務器端開發領域就顯得非常嚴重了。

非阻塞 IO 實現了線程的多路複用,一個線程被用來可以處理多個連接;異步 IO 則是由操作系統來實現 IO 的讀寫操作。在數據 ready 之後,通知業務線程處理。

上面只是對阻塞 IO 和非阻塞 IO 的一個籠統的介紹。從具體的技術來看,Linux 通過 epoll 技術提供了對非阻塞 IO 的支持。epoll 是 Linux 內核的一個系統調用,最早在 2.5.44 版中被加入。epoll 的意思是 event poll。簡單來說就是當有一個 IO 事件發生時,Linux 內核便會通知用戶。使用方式是在創建 epoll 句柄之後,用戶在其上不斷地循環以獲取新的事件(當有事件發生時)。這些事件是來自多個連接的,從而實現了線程的多路複用。

在 Java 1.4 中,也引入了 NIO 的支持 (java.nio.*)。在 Java NIO API 中,用戶的程序可以將一個連接 (SelectableChannel.register(Selector sel, int ops)) 註冊到一個 Selector 上(一個 Selector 可以有多個連接註冊)。註冊之後,用戶的程序便可以通過不斷地循環調用 Selector.selectedKeys() 方法獲得這個連接上的事件並進行處理(通常會使用另外的線程去處理事件,即 Reactor 模型)

雖然 Java 爲 NIO 開發提供了良好的 API 支持(從 1.7 開始還支持了 AIO),但是 IO 開發依舊有很高的複雜性,且 Java NIO 類庫的是 JDK 中 bug 較多的部分。故不推薦普通開發者直接基於 JDK 開發網絡 IO 功能,而是建議使用 Netty 進行開發。關於 Netty 這裏就不做介紹了。

2.2. 序列化技術

序列化技術是遠程調用的通信協議中的重要一部分,它定義了編程語言中的數據結構和數據傳輸協議中的數據結構之間如何相互轉化。序列化技術的性能的好壞會影響到對遠程調用性能的好壞在序列化方面。序列化技術性能的好壞主要包含兩方面的含義:一個是序列化時佔用的資源(CPU、內存、所需時間);另一個是序列化之後數據的大小。SOAP WebService 和 REST WebService 通常會把數據序列化成 XML 格式或者 JSON 格式。這兩種格式因爲都是文本格式,所以有着良好的可讀性,但是對於需要頻繁使用的遠程調用來說,它們的體積偏大。所以邊有了性能更好的序列化解決方案,被大家所熟知的有 Protocol Buffers 和 Apache Arvo。此外,Apache Thrift 的序列化的性能也很好,但是 Thrift 無法被當做一個單獨的序列化技術被使用,而是一個完整的遠程調用解決方案。其序列化部分不太容易被剝離出來,沒有完整的 API 被開放使用。這裏列出了常見的序列化技術的性能比較

2.3. Apache Thrift

Thrift 由 Facebook 貢獻,它是一個高性能、跨語言的 RPC 服務框架,適合用來實現內部服務的 RPC 調用。Thrift 採用通過 IDL 接口描述語言定義並生成服務接口,再結合其提供的服務端和客戶端調用棧實現 RPC 功能。

Thrift IDL 定義示例(來自 Thrift 官網,完整示例

service Calculator extends shared.SharedService {
 
  /**
   * A method definition looks like C code. It has a return type, arguments,
   * and optionally a list of exceptions that it may throw. Note that argument
   * lists and exception lists are specified using the exact same syntax as
   * field lists in struct or exception definitions.
   */
 
   void ping(),
 
   i32 add(1:i32 num1, 2:i32 num2),
 
   i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
 
   /**
    * This method has a oneway modifier. That means the client only makes
    * a request and does not listen for any response at all. Oneway methods
    * must be void.
    */
   oneway void zip()
}

Thrift 提供了工具,根據 IDL 文件,爲各種編程語言(C++, Java, Python, PHP, Ruby 等)生成相應的接口和數據結構。Thrift 不僅提供了傳統的 Request/Response 方式的接口調用,也有單向的調用方式(用關鍵字 oneway 修飾)。

Thrift 的序列化部分和整個框架結合緊密,並沒有直接提供序列化和反序列化的接口,所以不容易和其它傳輸協議配合使用。

示例與解釋

這裏提供了一個 Thrift 的簡單使用的示例。其中除了 ThriftClientThriftServer 和 CalculatorHandler 三個類,剩下的類都是從 *.thrift 文件,即 Thrift 的 IDL 文件生成的。Thrift 的 IDL 支持 namespace(即包空間)、繼承等語法。

以 Java 爲例,Thrift IDL 中的 service 將生成接口、服務器端棧和客戶端棧,這三部分又都有同步和異步兩種類型。即一個 IDL 文件將生成6個內部類。客戶端通過這個調用棧,在配置了傳輸協議、地址信息和序列化協議之後,就可以調用服務器端了。

服務器端的實現也不復雜。當然開發人員需要實現相應的業務類,這個業務類要實現至少一種由 IDL 生成的接口,同步接口或異步接口,也可兩者都實現。

基於 IDL 進行開發是使用 Thrift 這樣 RPC 框架的一種方式。這種方式對於新開發的、需要被遠程訪問的服務、並且有多重語言的客戶端的場景來說是很合適的。但是對於已有的業務方法,如果要讓其可以被遠程訪問的話,那這種方式就顯得不方便了。所以 Facebook 有提供了另一個項目 —— Swift(不是蘋果的 Swift)。這個項目可以通過在 Java 代碼上添加 Annotation,使得普通的 Java 方法調用轉變成 Thrift 的遠程調用。這種方式類似於 JAX-RS 或其它許多 REST 框架所提供的功能。這種方式對主要使用 Java 或其它一些 JVM 語言,如 Scala 和 Groovy 開發的項目來說是很合適的。使用了 Thrift 的遠程調用的同時,還降低了引入 IDL 所導致的複雜度的提高和可讀性的下降。Thrift Swift 示例

2.4. 其它技術介紹

Protocol Buffers

Protobuf 是一個高性能序列化解決方案,有完善的文檔,可以和例如 HTTP 這樣的協議搭配使用。

Apache Avro

Apache Avro 是 Apache Hadoop 的一個子項目。它提供了兩種序列化格式:JSON和二進制格式。JSON格式有良好的可讀性,而二進制格式在性能上和 Protobuf 不相上下。

2.5. 參考

3. 消息中間件

**關鍵字:**Kafka、RabbitMQ 在分佈式系統中,消息中間件的重要性越來越明顯。消息中間件可以解耦模塊、提供異步調用功能、消息持久化、消息削峯。已有的如 Apache ActiveMQ 無法滿足新的需要,於是出現瞭如 RabbitMQ、Apache Kafka 等新型的消息中間件產品。

3.1 Apache Kafka

Apache Kafka 充分利用了機械磁盤順序讀寫速度快的特點,在接受消息之後同步地寫入到磁盤中,保證數據可靠性的同時,也保證了非常快的速度。每個 Kafka 集羣上都有多個 Topic,Topic 相當於一個 category,消費者可以訂閱一個或多個 Topic。每個 Topic 由多個 Partition 組成。消息被順序的添加到 Partition 中,每條消息有一個唯一的、有序的 ID,這個 ID 被稱爲 Offset。Consumer 需要維護自己消費到的消息的位置 (Offset)。

Apache Kafka 不同於傳統的消息中間件,它採用“拉”消息模式,而不是傳統的“推”消息模式。即客戶端需要主動從消息中間件獲取消息,好處是客戶端可以更好地控制請求量。

Queue 模式和 Topic 模式

傳統消息隊列服務中有隊列模式和發佈訂閱模式兩種模式,前者一條消息只會被一個消費者消費;後者一條消息會發布給所有的訂閱這個 Topic 的消費者。在 Kafka 中,這兩種模式是使用一種方式 —— 消費者組來實現的。在同一個消費者組中的不同消費者不會受到相同的消息。如果想實現發佈訂閱模式,消費者必須處於不同的消費者組中。

<font color="red">Kafka 集羣</font>

3.2 RabbitMQ

RabbitMQ 是一個使用 Erlang 開發的 AMQP (Advanced Message Queue Protocol) 實現。現在 RabbitMQ 是由 VMware 旗下的 SpringSource 負責開發。AMQP 是一個語言無關的消息隊列協議。在 RabbitMQ 中,有三個概念:Exchange、Queue 和 Route key。Exchange 用來標示生產者,Queue 用來標示消費者,而 Route key 用來關聯這兩者。RabbitMQ 中這種方式提供了更靈活的應用模式。

4. 分佈式文件系統

4.1 塊存儲與對象存儲

塊存儲是將一塊裸盤提供給客戶使用,但是這塊裸盤可能是來自一塊物理硬盤,也有可能是多塊,或是來自不同服務器上的硬盤。對象存儲提供了更高級的接口,通過這些接口可以讀寫文件以及相關的元數據。其中的元數據包含了文件每一個塊的存儲信息。通過文件元數據,文件可以被並行地操作。

4.2 分佈式文件系統的高可用

爲了保證數據的安全,分佈式文件系統通常會將文件複製爲三份。這三份數據會位於不同的服務器上,對應要求更高的系統,比如公有云存儲。其中的一份數據會放置在另一個機房中,以保證即便整個機房出現故障,整個文件系統仍是可用的。

4.3 Ceph

Ceph 目前是 OpenStack 的一個組件,提供了塊存儲和對象存儲的功能。

4.4 GridFS

GridFS 是 MongoDB 的一部分。用來存儲超過 BSON 大小限制(16MB)的文件。GridFS 將文件分成一個個部分,分開存儲。

4.5 FastDFS

FastDFS 是一個輕量的分佈式文件系統,適合存儲中小文件(對象存儲)。FastDFS 的跟蹤服務器不負責記錄文件的元信息。文件的具體存儲位置等信息包含在返回給用戶的 File ID 中。

5. 分佈式內存

內存是新的硬盤,硬盤是新的磁帶 -- Jim Gray

5.1 Hazelcast

Hazelcast 是一個面向 Java 的分佈式內存解決方案,提供了豐富的功能特性。實現了諸如分佈式 Java 集合類、分佈式鎖、分佈式 ExecutorService 實現等等。但現實往往是殘酷的,Hazelcast 在實際應用中存在大量的缺陷。詳見 “hazelcast的坑爹事”

5.2 Memcached

Memcached 是老牌的“分佈式”緩存解決方案。分佈式之所以加引號,是因爲 Memcached 服務器端本身並不支持分佈式,服務器端每個節點之間並不會相互通信。分佈式的支持需要客戶端來實現。早期的內存分佈式是通過節點之間複製來實現的,但這種方式卻限制了可伸縮性。這也是因爲諸如 Terrecotta 這樣的內存分佈式解決方案沒有成爲主流的原因。

5.3 Redis

5.4 Gemfire

6. 分佈式數據庫

6.1 關係型數據庫

在大規模的分佈式應用中,單庫或者簡單的讀寫分離已經無法滿足要求,因此必須對數據庫進行水平和垂直的劃分和分庫分表。在對數據庫進行分庫分表之後,應用對數據庫的訪問便不再是一件簡單的事情了。應用在進行一次數據庫操作時,其所對應的數據庫的地址和表名必須通過某種邏輯運算才能得到。例如,ID從1到1,000,000的User數據是數據庫1的User_1表中,ID從1,000,001到2,000,000的User數據在數據庫1的User_2表中,而其它的User數據又會在不同的數據庫的不同的表中。同時,還要考慮主從數據庫,讀寫分離的問題。這樣的數據庫使用方式會使數據操作變得極爲複雜,也會增加數據遷移,增容擴容時的難度。

對於這樣複雜的問題,靠應用自己解決顯然是不合適的。所以各家分佈式應用的使用大戶——互聯網廠商,都自己實現了相應的解決方案。這些解決方案可分爲中間間方式和框架方式,前者作爲數據庫訪問的代理,使得分佈式的數據庫對應用是透明的。後者作爲一個框架嵌入到應用中,也能起到類似的作用。這兩種方式各有優劣,分別適合不同的場合。

搜狗 Compass,阿里 TDDL、Cobar

6.2 NoSQL

大部分 NoSQL 雖然對分佈式的支持是友好的,但這並不意味着使用這些 NoSQL 數據庫就可以輕輕鬆鬆地實現一個集羣。例如著名的 Key/Value 數據庫 Redis。它 3.0 之前一直沒有官方的集羣方案,所以各個大規模使用 Redis 都需要自己實現分佈式方案,例如 Twitter 的 Twemproxy、豌豆莢的 Codis 等等。

在實現數據的分佈式解決方案的時候,有一個算法是最常被使用的 —— 一致性哈希算法,這裏只是簡單提一下,不做進一步介紹。

7. 虛擬化技術

**關鍵字:**OpenStack、Docker、容器技術 虛擬化技術是提高硬件利用率的重要手段。虛擬化技術是實現雲計算的重要技術。虛擬化技術的最底層是各種硬件的虛擬化,如 CPU 虛擬化、內存虛擬化、存儲虛擬化、網絡虛擬化等等。然後再基於這些技術,構建出各種虛擬機技術。然後各個廠商又基於虛擬機技術和其它虛擬化技術構建出 IaaS、PaaS 和 SaaS 等平臺和軟件產品。

7.1 OpenStack

OpenStack 這個開源項目包含了一系列用於 IaaS 平臺搭建的組件的合稱。這些組件包含用於網絡虛擬化的 Neutron、提供存儲虛擬化的 Ceph 和 Swift、以及提供例如鏡像管理、控制面板等功能的諸多組件。OpenStack 本身並不提供虛擬化技術,而是通過支持諸多現有的虛擬化技術,例如 KVM,並在此之上提供一系列構建 IaaS 解決方案的技術。OpenStack 中的組件可以靈活搭配使用,並且因爲開源的原因,使用者可以對其進行自定義或二次開發。但是也是因爲這個原因,任何廠商想要成功使用 OpenStack 必須有一個強大的技術團隊做後盾。這也是目前 OpenStack 技術發展遇到的最大困難。

7.2 Docker

嚴格說來 Docker 並不是一個虛擬化技術,但是因爲 Docker 能夠提供給使用者一種輕量化的虛擬機的使用體驗,所以也將 Docker 列在這裏。Docker 是一個容器技術,它通過 Linux 內核的支持,使不同的進程可以相互隔離並做到資源的限制,從而實現了部分的虛擬機資源隔離的需要。Docker 相比較虛擬機的優勢在於輕量和系統資源使用效率接近於實體機。因爲現在隨着需求的發展和技術的進步,服務器端應用向着一種輕量化和越來越分佈式的方向發展。虛擬機這樣重量級的技術對於小而多的應用來說便不再合適,這也是 Docker 這樣的容器技術近些年迅速發展並呈現火熱狀態的重要原因。

Docker 和前面提到的 OpenStack 是兩個不同層面的技術,兩者並不衝突。現在 OpenStack 和 Docker 社區正在緊密合作(容器不會取代OpenStack,但二者如何深度整合?)。

8. 分佈式系統之負載均衡

8.1 HAProxy

HAProxy 是一個高性能的 TCP 和 HTTP 反向代理代理和負載均衡服務器。可用反向代理和負載均衡還有 Nginx。Niginx 更偏向於 HTTP 協議。另外 Varnish 和 Squid 可以作爲前端的代理,但是它們更偏重緩存功能

更上一層

9. 服務編排:註冊、發現和路由

**結合技術:**配置管理、遠程調用等

有些類似早年的 JNDI。即一個應用去遠程訪問另外一個應用時,只需知道它所要訪問的應用的名稱、版本等信息,即可調用成功。不需要考慮它所要調用的應用的具體地址。

10. 雲操作系統

**結合技術:**虛擬機、容器技術、網絡虛擬化、配置管理、消息隊列

Apache Mesos、Google Berg、騰訊 Gaia、百度 Matrix

總結

就像上面所提到的,上面的這些技術之間都是你中有我,我中有你的關係,或者有着相類似的設計思想。掌握它們,基本不去使用,也會對你設計開發能力的提高大有裨益。

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