前面咱們認識了兩個經常使用文本類的 RPC 協議,對於陌生人之間的溝通,用 NBA、CBA 這樣的縮略語,會使得協議約定很是不方便。html
在講 CDN 和 DNS 的時候,咱們講過接入層的設計,對於靜態資源或者動態資源靜態化的部分均可以作緩存。可是對於下單、支付等交易場景,仍是須要調用 API。數據庫
對於微服務的架構,API 須要一個 API 網關統一的管理。API 網關有多種實現方式,用 Nginx 或者 OpenResty 結合 Lua 腳本是經常使用的方式。在上一節講過的 Spring Cloud 體系中,有個組件 Zuul 也是幹這個的。緩存
API 網關用來管理 API,可是 API 的實現通常在一個叫做Controller 層的地方。這一層對外提供 API。因爲是讓陌生人訪問的,咱們能看到目前業界主流的,基本都是 RESTful 的 API,是面向大規模互聯網應用的。網絡
在 Controller 以內,就是我們互聯網應用的業務邏輯實現。上節講 RESTful 的時候,說過業務邏輯的實現最好是無狀態的,從而能夠橫向擴展,可是資源的狀態還須要服務端去維護。資源的狀態不該該維護在業務邏輯層,而是在最底層的持久化層,通常會使用分佈式數據庫和 ElasticSearch。架構
這些服務端的狀態,例如訂單、庫存、商品等,都是重中之重,都須要持久化到硬盤上,數據不能丟,可是因爲硬盤讀寫性能差,於是持久化層每每吞吐量不能達到互聯網應用要求的吞吐量,於是前面要有一層緩存層,使用 Redis 或者 memcached 將請求攔截一道,不能讓全部的請求都進入數據庫「中軍大營」。併發
緩存和持久化層之上通常是基礎服務層,這裏面提供一些原子化的接口。例如,對於用戶、商品、訂單、庫存的增刪查改,將緩存和數據庫對再上層的業務邏輯屏蔽一道。有了這一層,上層業務邏輯看到的都是接口,而不會調用數據庫和緩存。於是對於緩存層的擴容,數據庫的分庫分表,全部的改變,都截止到這一層,這樣有利於未來對於緩存和數據庫的運維。負載均衡
再往上就是組合層。由於基礎服務層只是提供簡單的接口,實現簡單的業務邏輯,而複雜的業務邏輯,好比下單,要扣優惠券,扣減庫存等,就要在組合服務層實現。框架
這樣,Controller 層、組合服務層、基礎服務層就會相互調用,這個調用是在數據中心內部的,量也會比較大,仍是使用 RPC 的機制實現的。運維
因爲服務比較多,須要一個單獨的註冊中心來作服務發現。服務提供方會將本身提供哪些服務註冊到註冊中心中去,同時服務消費方訂閱這個服務,從而能夠對這個服務進行調用。異步
調用的時候有一個問題,這裏的 RPC 調用,應該用二進制仍是文本類?其實文本的最大問題是,佔用字節數目比較多。好比數字 123,其實原本二進制 8 位就夠了,可是若是變成文本,就成了字符串 123。若是是 UTF-8 編碼的話,就是三個字節;若是是 UTF-16,就是六個字節。一樣的信息,要多費好多的空間,傳輸起來也更加佔帶寬,時延也高。
於是對於數據中心內部的相互調用,不少公司選型的時候,仍是但願採用更加省空間和帶寬的二進制的方案。
這裏一個著名的例子就是 Dubbo 服務化框架二進制的 RPC 方式。
Dubbo 會在客戶端的本地啓動一個 Proxy,其實就是客戶端的 Stub,對於遠程的調用都經過這個 Stub 進行封裝。
接下來,Dubbo 會從註冊中心獲取服務端的列表,根據路由規則和負載均衡規則,在多個服務端中選擇一個最合適的服務端進行調用。
調用服務端的時候,首先要進行編碼和序列化,造成 Dubbo 頭和序列化的方法和參數。將編碼好的數據,交給網絡客戶端進行發送,網絡服務端收到消息後,進行解碼。而後將任務分發給某個線程進行處理,在線程中會調用服務端的代碼邏輯,而後返回結果。
這個過程和經典的 RPC 模式何其類似啊!
接下來咱們仍是來看 RPC 的三大問題,其中註冊發現問題已經經過註冊中心解決了。咱們下面就來看協議約定問題。
Dubbo 中默認的 RPC 協議是 Hessian2。爲了保證傳輸的效率,Hessian2 將遠程調用序列化爲二進制進行傳輸,而且能夠進行必定的壓縮。這個時候你可能會疑惑,同爲二進制的序列化協議,Hessian2 和前面的二進制的 RPC 有什麼區別呢?這不繞了一圈又回來了嗎?
Hessian2 是解決了一些問題的。例如,原來要定義一個協議文件,而後經過這個文件生成客戶端和服務端的 Stub,才能進行相互調用,這樣使得修改就會不方便。Hessian2 不須要定義這個協議文件,而是自描述的。什麼是自描述呢?
所謂自描述就是,關於調用哪一個函數,參數是什麼,另外一方不須要拿到某個協議文件、拿到二進制,靠它自己根據 Hessian2 的規則,就能解析出來。
原來有協議文件的場景,有點兒像兩我的事先約定好,0 表示方法 add,而後後面會傳兩個數。服務端把兩個數加起來,這樣一方發送 012,另外一方知道是將 1 和 2 加起來,可是不知道協議文件的,當它收到 012 的時候,徹底不知道表明什麼意思。
而自描述的場景,就像兩我的說的每句話都帶來龍去脈。例如,傳遞的是「函數:add,第一個參數 1,第二個參數 2」。這樣不管誰拿到這個表述,都知道是什麼意思。可是隻不過都是以二進制的形式編碼的。這其實至關於綜合了 XML 和二進制共同優點的一個協議。
Hessian2 是如何作到這一點的呢?這就須要去看 Hessian2 的序列化的語法描述文件。
看起來很複雜,編譯原理裏面是有這樣的語法規則的。
咱們從 Top 看起,下一層是 value,直到造成一棵樹。這裏面的有個思想,爲了防止歧義,每個類型的起始數字都設置成爲獨一無二的。這樣,解析的時候,看到這個數字,就知道後面跟的是什麼了。
這裏仍是以加法爲例子,「add(2,3)」被序列化以後是什麼樣的呢?
H x02 x00 # Hessian 2.0 C # RPC call x03 add # method "add" x92 # two arguments x92 # 2 - argument 1 x93 # 3 - argument 2
這個就叫做自描述。
另外,Hessian2 是面向對象的,能夠傳輸一個對象。
class Car { String color; String model; } out.writeObject(new Car("red", "corvette")); out.writeObject(new Car("green", "civic")); --- C # object definition (#0) x0b example.Car # type is example.Car x92 # two fields x05 color # color field name x05 model # model field name O # object def (long form) x90 # object definition #0 x03 red # color field value x08 corvette # model field value x60 # object def #0 (short form) x05 green # color field value x05 civic # model field value
首先,定義這個類。對於類型的定義也傳過去,於是也是自描述的。類名爲 example.Car,字符長 11 位,於是前面長度爲 0x0b。有兩個成員變量,一個是 color,一個是 model,字符長 5 位,於是前面長度 0x05,。
而後,傳輸的對象引用這個類。因爲類定義在位置 0,於是對象會指向這個位置 0,編碼爲 0x90。後面 red 和 corvette 是兩個成員變量的值,字符長分別爲 3 和 8。
接着又傳輸一個屬於相同類的對象。這時候就不保存對於類的引用了,只保存一個 0x60,表示同上就能夠了。
能夠看出,Hessian2 真的是能壓縮儘可能壓縮,多一個 Byte 都不傳。
接下來,咱們再來看 Dubbo 的 RPC 傳輸問題。前面咱們也說了,基於 Socket 實現一個高性能的服務端,是很複雜的一件事情,在 Dubbo 裏面,使用了 Netty 的網絡傳輸框架。
Netty 是一個非阻塞的基於事件的網絡傳輸框架,在服務端啓動的時候,會監聽一個端口,並註冊如下的事件。
當事件觸發以後,服務端在這些函數中的邏輯,能夠選擇直接在這個函數裏面進行操做,仍是將請求分發到線程池去處理。通常異步的數據讀寫都須要另外的線程池參與,在線程池中會調用真正的服務端業務代碼邏輯,返回結果。
Hessian2 是 Dubbo 默認的 RPC 序列化方式,固然還有其餘選擇。例如,Dubbox 從 Spark 那裏借鑑 Kryo,實現高性能的序列化。
到這裏,咱們說了數據中內心面的相互調用。爲了高性能,你們都願意用二進制,可是爲何後期 Spring Cloud 又興起了呢?這是由於,併發量愈來愈大,已經到了微服務的階段。同原來的 SOA 不一樣,微服務粒度更細,模塊之間的關係更加複雜。
在上面的架構中,若是使用二進制的方式進行序列化,雖然不用協議文件來生成 Stub,可是對於接口的定義,以及傳的對象 DTO,仍是須要共享 JAR。由於只有客戶端和服務端都有這個 JAR,才能成功地序列化和反序列化。
但當關系複雜的時候,JAR 的依賴也變得異常複雜,難以維護,並且若是在 DTO 里加一個字段,雙方的 JAR 沒有匹配好,也會致使序列化不成功,並且還有可能循環依賴。這個時候,通常有兩種選擇。
第一種,創建嚴格的項目管理流程。
第二種,改用 RESTful 的方式。