REST
越來越多的人開始意識到,網站即軟件,而且是一種新型的軟件。
網站開發,完全可以采用軟件開發的模式。但是傳統上,軟件和網絡是兩個不同的領域,很少有交集;軟件開發主要針對單機環境,網絡則主要研究系統之間的通信。
互聯網的興起,使得這兩個領域開始融合,現在我們必須考慮,如何開發在互聯網環境中使用的軟件。
RESTful架構,就是目前最流行的一種互聯網軟件架構。它結構清晰、符合標准、易於理解、擴展方便,所以正得到越來越多網站的采用。
EST這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的。
"本文研究計算機科學兩大前沿----軟件和網絡----的交叉點。長期以來,軟件研究主要關注軟件設計的分類、設計方法的演化,很少客觀地評估不同的設計選擇對系統行為的影響。而相反地,網絡研究主要關注系統之間通信行為的細節、如何改進特定通信機制的表現,常常忽視了一個事實,那就是改變應用程序的互動風格比改變互動協議,對整體表現有更大的影響。我這篇文章的寫作目的,就是想在符合架構原理的前提下,理解和評估以網絡為基礎的應用軟件的架構設計,得到一個功能強、性能好、適宜通信的架構。"
Fielding將他對互聯網軟件的架構原則,定名為REST,即Representational State Transfer的縮寫。我對這個詞組的翻譯是"表現層狀態轉化"。
如果一個架構符合REST原則,就稱它為RESTful架構。
要理解RESTful架構,最好的方法就是去理解Representational State Transfer這個詞組到底是什么意思,它的每一個詞代表了什么涵義。如果你把這個名稱搞懂了,也就不難體會REST是一種什么樣的設計。
資源(Resources)
所謂"資源",就是網絡上的一個實體,或者說是網絡上的一個具體信息。它可以是一段文本、一張圖片、一首歌曲、一種服務,總之就是一個具體的實在。你可以用一個URI(統一資源定位符)指向它,每種資源對應一個特定的URI。要獲取這個資源,訪問它的URI就可以,因此URI就成了每一個資源的地址或獨一無二的識別符。
REST的名稱"表現層狀態轉化"中,省略了主語。"表現層"其實指的是"資源"(Resources)的"表現層"。
表現層(Representation)
"資源"是一種信息實體,它可以有多種外在表現形式。我們把"資源"具體呈現出來的形式,叫做它的"表現層"(Representation)。
比如,文本可以用txt格式表現,也可以用HTML格式、XML格式、JSON格式表現,甚至可以采用二進制格式;圖片可以用JPG格式表現,也可以用PNG格式表現。
URI只代表資源的實體,不代表它的形式。嚴格地說,有些網址最后的".html"后綴名是不必要的,因為這個后綴名表示格式,屬於"表現層"范疇,而URI應該只代表"資源"的位置。它的具體表現形式,應該在HTTP請求的頭信息中用Accept和Content-Type字段指定,這兩個字段才是對"表現層"的描述。
狀態轉化(State Transfer)
訪問一個網站,就代表了客戶端和服務器的一個互動過程。在這個過程中,勢必涉及到數據和狀態的變化。
互聯網通信協議HTTP協議,是一個無狀態協議。這意味着,所有的狀態都保存在服務器端。因此,如果客戶端想要操作服務器,必須通過某種手段,讓服務器端發生"狀態轉化"(State Transfer)。
客戶端用到的手段,只能是HTTP協議。具體來說,就是HTTP協議里面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT用來更新資源,DELETE用來刪除資源。
綜合上面的解釋,我們總結一下什么是RESTful架構:
- 每一個URI代表一種資源;
- 客戶端和服務器之間,傳遞這種資源的某種表現層;
- 客戶端通過四個HTTP動詞,對服務器端資源進行操作,實現"表現層狀態轉化"。
RESTful API more
必須有一種統一的機制,方便不同的前端設備與后端進行通信。這導致API構架的流行,甚至出現"API First"的設計思想。RESTful API是目前比較成熟的一套互聯網應用程序的API設計理論。
HTTP
REST本身並沒有創造新的技術、組件或服務,而隱藏在RESTful背后的理念就是使用Web的現有特征和能力, 更好地使用現有Web標准中的一些准則和約束。雖然REST本身受Web技術的影響很深, 但是理論上REST架構風格並不是綁定在HTTP上,只不過目前HTTP是唯一與REST相關的實例。 所以上面描述的REST也是通過HTTP實現的REST。
RPC
RPC(Remote Procedure Call),即遠程過程調用,是一個分布式系統間通信的必備技術。RPC 最核心要解決的問題就是在分布式系統間,如何執行另外一個地址空間上的函數、方法,就仿佛在本地調用一樣。
下面依次展開每個部分。
傳輸(Transport)
TCP 協議是 RPC 的 基石,一般來說通信是建立在 TCP 協議之上的,而且 RPC 往往需要可靠的通信,因此不采用 UDP。
RPC 傳輸的 message 也就是 TCP body 中的數據,這個 message 也同樣可以包含 header+body。body 也經常叫做 payload。
TCP 協議棧存在端口的概念,端口是進程獲取數據的渠道。
I/O 模型(I/O Model)
做一個高性能 /scalable 的 RPC,需要能夠滿足:
- 服務端盡可能多的處理並發請求
- 同時盡可能短的處理完畢。
Socket I/O 可以看做是二者之間的橋梁,如何更好地協調二者,去滿足前面說的兩點要求,有一些模式(pattern)是可以應用的。RPC 框架可選擇的 I/O 模型嚴格意義上有 5 種,這里不討論基於 信號驅動 的 I/O(Signal Driven I/O)。它們分別是:
- 傳統的阻塞 I/O(Blocking I/O)
- 非阻塞 I/O(Non-blocking I/O)
- I/O 多路復用(I/O multiplexing)
- 異步 I/O(Asynchronous I/O)
這里不細說每種I/O模型。這里舉一個形象的例子,讀者就可以領會這四種I/O的區別,就用銀行辦業務 這個生活的場景描述。
傳統的阻塞 I/O模型
一個櫃員服務所有客戶,可見當客戶填寫單據的時候也就是發生網絡I/O的時候,櫃員(也就是寶貴的線程或者進程)就會被阻塞,白白浪費了 CPU 資源,無法服務后面的請求。
如果一個櫃員不夠,那么就並發處理,對應采用線程池或者多進程方案,一個客戶對應一個櫃員,這明顯加大了並發度,在並發不高的情況下性能夠用,但是仍然存在櫃員被 I/O 阻塞的可能。
I/O 多路復用
存在一個大堂經理,相當於代理,它來負責所有的客戶,只有當客戶寫好單據后,才把客戶分配一個櫃員處理,可以想象櫃員不用阻塞在 I/O 讀寫上,這樣櫃員效率會非常高,這也就是 I/O 多路復用的精髓。
異步 I/O
完全不存在大堂經理,銀行有一個天然的“高級的分配機器”,櫃員注冊自己負責的業務類型,例如 I/O 可讀,那么由這個“高級的機器”負責I/O讀,當可讀時候,通過回調機制,把客戶已經填寫完畢的單據主動交給櫃員,回調其函數完成操作。
重點說下高性能,並且工業界普遍使用的方案,也就是后兩種。
I/O 多路復用
基於內核,建立在epoll或者kqueue上實現,I/O多路復用最大的優勢是用戶可以在一個線程內同時處理多個Socket的I/O請求。通過一個線程監聽全部的TCP連接,有任何事件發生就通知用戶態處理即可。
異步 I/O
這里重點說下同步 I/O 和異步I/O,理論上前三種模型都叫做同步I/O,同步是指用戶線程發起I/O請求后需要等待或者輪詢內核I/O完成后再繼續,而異步是指用戶線程發起I/O請求直接退出,當內核I/O操作完成后會通知用戶線程來調用其回調函數。
I/O 多路復用往往對應 Reactor 模式,異步 I/O 往往對應 Proactor。
Reactor 一般使用epoll+事件驅動的經典模式,通過分治的手段,把耗時的網絡連接、安全認證、編碼等工作交給專門的線程池或者進程去完成,然后再去調用真正的核心業務邏輯層,這在 *nix 系統中被廣泛使用。
著名的 Redis、Nginx、Node.js 的 Socket I/O 都用的這個,而 Java 的 NIO 框架 Netty 也是,Spark 2.0 RPC 所依賴的同樣采用了 Reactor 模式。
Proactor在*nix中沒有很好的實現,但是在Windows上大放異彩(例如 IOCP 模型)。
說個具體的例子,Thrift 作為一個融合了 序列化+RPC 的框架,提供了很多種 Server 的構建選項。
在Reactor中實現讀:
- 注冊讀就緒事件和相應的事件處理器
- 事件分離器等待事件
- 事件到來,激活分離器,分離器調用事件對應的處理器。
- 事件處理器完成實際的讀操作,處理讀到的數據,注冊新的事件,然后返還控制權。
在Proactor中實現讀:
- 處理器發起異步讀操作(注意:操作系統必須支持異步IO)。在這種情況下,處理器無視IO就緒事件,它關注的是完成事件。
- 事件分離器等待操作完成事件
- 在分離器等待過程中,操作系統利用並行的內核線程執行實際的讀操作,並將結果數據存入用戶自定義緩沖區,最后通知事件分離器讀操作完成。
- 事件分離器呼喚處理器。
- 事件處理器處理用戶自定義緩沖區中的數據,然后啟動一個新的異步操作,並將控制權返回事件分離器。
可以看出,兩個模式的相同點,都是對某個IO事件的事件通知(即告訴某個模塊,這個IO操作可以進行或已經完成)。在結構上,兩者也有相同點:demultiplexor負責提交IO操作(異步)、查詢設備是否可操作(同步),然后當條件滿足時,就回調handler;不同點在於,異步情況下(Proactor),當回調handler時,表示IO操作已經完成;同步情況下(Reactor),回調handler時,表示IO設備可以進行某個操作(can read or can write)。
Schema 和序列化(Schema & Data Serialization)
序列化和反序列化,是做對象到二進制數據的轉換。
程序是可以理解對象的,對象一般含有schema或者結構,基於這些語義來做特定的業務邏輯處理。
考察一個序列化框架一般會關注以下幾點:
- Encoding format。是 human readable 還是 binary。
- Schema declaration。也叫作契約聲明,基於 IDL,比如 Protocol Buffers/Thrift,還是自描述的,比如 JSON、XML。另外還需要看是否是強類型的。
- 語言平台的中立性。比如 Java 的 Native Serialization 就只能自己玩,而 Protocol Buffers 可以跨各種語言和平台。
- 新老契約的兼容性。比如 IDL 加了一個字段,老數據是否還可以反序列化成功。
- 和壓縮算法的契合度。跑benchmark和實際應用都會結合各種壓縮算法,例如 gzip、snappy。
- 性能。這是最重要的,序列化、反序列化的時間,序列化后數據的字節大小是考察重點。
序列化方式非常多,常見的有 Protocol Buffers, Avro,Thrift,XML,JSON,MessagePack,Kyro,Hessian,Protostuff,Java Native Serialize,FST。
協議結構(Wire Protocol)
TCP 只是 binary stream 通道,是binary數據的可靠搬用工,它不懂 RPC 里面包裝的是什么。而在一個通道上傳輸 message,勢必涉及 message 的識別。
舉個例子,正如下圖中的例子,ABC+DEF+GHI 分 3 個 message,也就是分 3 個 Frame 發送出去,而接收端分四次收到 4 個 Frame。
Socket I/O 的工作完成得很好,可靠地傳輸過去,這是 TCP 協議保證的,但是接收到的是 4 個 Frame,不是原本發送的 3 個 message 對應的 3 個 Frame。
這種情況叫做發生了 TCP 粘包和半包 現象,AB、H、I 的情況叫做半包,CDEFG的情況叫做粘包。雖然順序是對的,但是分組完全和之前對應不上。
這時候應用層如何做語義級別的 message 識別是個問題,只有做好了協議的結構,才能把一整個數據片段做序列化或者反序列化處理。
比如:memcache的換行符、http中的固定長度頭。
可靠性(Reliability)
RPC 框架不光要處理 Network I/O、序列化、協議棧。還有很多不確定性問題要處理,這里的不確定性就是由 網絡的不可靠 帶來的麻煩。
例如如何保持長連接心跳?網絡閃斷怎么辦?重連、重傳?連接超時?這些都非常的細碎和麻煩,所以說開發好一個穩定的 RPC 類庫是一個非常系統和細心的工程。
但是好在工業界有一群人就致力於提供平台似的解決方案,例如 Java 中的 Netty,它是一個強大的異步、事件驅動的網絡 I/O 庫,使用 I/O 多路復用的模型,做好了上述的麻煩處理。
它是面向對象設計模式的集大成者,使用方只需要會使用 Netty 的各種類,進行擴展、組合、插拔,就可以完成一個高性能、可靠的 RPC 框架。
著名的 gRPC Java 版本、Twitter 的 Finagle 框架、阿里巴巴的 Dubbo、新浪微博的 Motan、Spark 2.0 RPC 的網絡層(可以參考 kraps-rpc:https://github.com/neoremind/kraps-rpc)都采用了這個類庫。
易用性(Ease of use)
RPC 是需要讓上層寫業務邏輯來實現功能的,如何優雅地啟停一個 server,注入 endpoint,客戶端怎么連,重試調用,超時控制,同步異步調用,SDK 是否需要交換等等,都決定了基於 RPC 構建服務,甚至 SOA 的工程效率與生產力高低。這里不做展開,看各種 RPC 的文檔就知道他們的易用性如何了。
工業界的 RPC 框架一覽
國內
- Dubbo。來自阿里巴巴 http://dubbo.I/O/
- Motan。新浪微博自用 https://github.com/weibocom/motan
- Dubbox。當當基於dubbo的https://github.com/dangdangdotcom/dubbox
- rpcx。基於 Golang 的 https://github.com/smallnest/rpcx
- Navi & Navi-pbrpc。作者開源的 https://github.com/neoremind/navi https://github.com/neoremind/navi-pbrpc
國外
- Thrift from facebook https://thrift.apache.org
- Avro from hadoop https://avro.apache.org
- Finagle by twitter https://twitter.github.I/O/finagle
- gRPC by Google http://www.grpc.I/O (Google inside use Stuppy)
- Hessian from cuacho http://hessian.caucho.com
- Coral Service inside amazon (not open sourced)
上述列出來的都是現在互聯網企業常用的解決方案,暫時不考慮傳統的 SOAP,XML-RPC 等。這些是有網絡資料的,實際上很多公司內部都會針對自己的業務場景,以及和公司內的平台相融合(比如監控平台等),自研一套框架,但是殊途同歸,都逃不掉剛剛上面所列舉的 RPC 的要考慮的各個部分。
既然有 HTTP 請求,為什么還要用 RPC 調用
http好比普通話,rpc好比團伙內部黑話。講普通話,好處就是誰都聽得懂,誰都會講。
講黑話,好處是可以更精簡、更加保密、更加可定制,壞處就是要求“說”黑話的那一方(client端)也要懂,而且一旦大家都說一種黑話了,換黑話就困難了。
言歸正傳
這個問題其實是有理解誤區的,首先 http 和 rpc 並不是一個並行概念。rpc是遠端過程調用,其調用協議通常包含傳輸協議和編碼協議。
傳輸協議包含: 如著名的 [gRPC](grpc / grpc.io) 使用的 http2 協議,也有如dubbo一類的自定義報文的tcp協議。編碼協議包含: 如基於文本編碼的 xml json,也有二進制編碼的 protobuf binpack 等。
因此問題應該是:為什么要使用自定義 tcp 協議的 rpc 做后端進程通信?
要解決這個問題就應該搞清楚 http 使用的 tcp 協議,和我們自定義的 tcp 協議在報文上的區別。首先要否認一點 http 協議相較於自定義tcp報文協議,增加的開銷在於連接的建立與斷開。
http協議是支持連接池復用的,也就是建立一定數量的連接不斷開,並不會頻繁的創建和銷毀連接。另外要說的是http也可以使用protobuf這種二進制編碼協議對內容進行編碼,因此二者最大的區別還是在傳輸協議上。
通用定義的http1.1協議的tcp報文包含太多廢信息,一個POST協議的格式大致如下:
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
<html>
<body>Hello World</body>
</html>
即使編碼協議也就是body是使用二進制編碼協議,報文元數據也就是header頭的鍵值對卻用了文本編碼,非常占字節數。如上圖所使用的報文中有效字節數僅僅占約 30%,也就是70%的時間用於傳輸元數據廢編碼。當然實際情況下報文內容可能會比這個長,但是報頭所占的比例也是非常可觀的。
自定義tcp協議可以極大地簡化傳輸頭內容。
所謂的效率優勢是針對http1.1協議來講的,http2.0協議已經優化編碼效率問題,像grpc這種rpc庫使用的就是http2.0協議。這么來說吧http容器的性能測試單位通常是kqps,自定義tpc協議則通常是以10kqps到100kqps為基准簡單來說成熟的rpc庫相對http容器,
更多的是封裝了“服務發現”,"錯誤重試"一類面向服務的高級特性。可以這么理解,rpc框架是面向服務的更高級的封裝。如果把一個http server容器上封裝一層服務發現和函數代理調用,那它就已經可以做一個rpc框架了。
附:thrift和http共存的一個示例
