愈來愈多的人開始意識到,網站即軟件,並且是一種新型的軟件。html
網站開發,徹底能夠採用軟件開發的模式。可是傳統上,軟件和網絡是兩個不一樣的領域,不多有交集;軟件開發主要針對單機環境,網絡則主要研究系統之間的通訊。前端
互聯網的興起,使得這兩個領域開始融合,如今咱們必須考慮,如何開發在互聯網環境中使用的軟件。git
RESTful架構,就是目前最流行的一種互聯網軟件架構。它結構清晰、符合標準、易於理解、擴展方便,因此正獲得愈來愈多網站的採用。github
EST這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的。算法
"本文研究計算機科學兩大前沿----軟件和網絡----的交叉點。長期以來,軟件研究主要關注軟件設計的分類、設計方法的演化,不多客觀地評估不一樣的設計選擇對系統行爲的影響。而相反地,網絡研究主要關注系統之間通訊行爲的細節、如何改進特定通訊機制的表現,經常忽視了一個事實,那就是改變應用程序的互動風格比改變互動協議,對總體表現有更大的影響。我這篇文章的寫做目的,就是想在符合架構原理的前提下,理解和評估以網絡爲基礎的應用軟件的架構設計,獲得一個功能強、性能好、適宜通訊的架構。"apache
Fielding將他對互聯網軟件的架構原則,定名爲REST,即Representational State Transfer的縮寫。我對這個詞組的翻譯是"表現層狀態轉化"。json
若是一個架構符合REST原則,就稱它爲RESTful架構。後端
要理解RESTful架構,最好的方法就是去理解Representational State Transfer這個詞組究竟是什麼意思,它的每個詞表明瞭什麼涵義。若是你把這個名稱搞懂了,也就不難體會REST是一種什麼樣的設計。設計模式
資源(Resources)
所謂"資源",就是網絡上的一個實體,或者說是網絡上的一個具體信息。它能夠是一段文本、一張圖片、一首歌曲、一種服務,總之就是一個具體的實在。你能夠用一個URI(統一資源定位符)指向它,每種資源對應一個特定的URI。要獲取這個資源,訪問它的URI就能夠,所以URI就成了每個資源的地址或獨一無二的識別符。
REST的名稱"表現層狀態轉化"中,省略了主語。"表現層"其實指的是"資源"(Resources)的"表現層"。api
表現層(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架構:
RESTful API more
必須有一種統一的機制,方便不一樣的前端設備與後端進行通訊。這致使API構架的流行,甚至出現"API First"的設計思想。RESTful API是目前比較成熟的一套互聯網應用程序的API設計理論。
REST自己並無創造新的技術、組件或服務,而隱藏在RESTful背後的理念就是使用Web的現有特徵和能力, 更好地使用現有Web標準中的一些準則和約束。雖然REST自己受Web技術的影響很深, 可是理論上REST架構風格並非綁定在HTTP上,只不過目前HTTP是惟一與REST相關的實例。 因此上面描述的REST也是經過HTTP實現的REST。
RPC(Remote Procedure Call),即遠程過程調用,是一個分佈式系統間通訊的必備技術。RPC 最核心要解決的問題就是在分佈式系統間,如何執行另一個地址空間上的函數、方法,就彷彿在本地調用同樣。
下面依次展開每一個部分。
TCP 協議是 RPC 的 基石,通常來講通訊是創建在 TCP 協議之上的,並且 RPC 每每須要可靠的通訊,所以不採用 UDP。
RPC 傳輸的 message 也就是 TCP body 中的數據,這個 message 也一樣能夠包含 header+body。body 也常常叫作 payload。
TCP 協議棧存在端口的概念,端口是進程獲取數據的渠道。
作一個高性能 /scalable 的 RPC,須要可以知足:
Socket I/O 能夠看作是兩者之間的橋樑,如何更好地協調兩者,去知足前面說的兩點要求,有一些模式(pattern)是能夠應用的。RPC 框架可選擇的 I/O 模型嚴格意義上有 5 種,這裏不討論基於 信號驅動 的 I/O(Signal Driven 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或者結構,基於這些語義來作特定的業務邏輯處理。
考察一個序列化框架通常會關注如下幾點:
序列化方式很是多,常見的有 Protocol Buffers, Avro,Thrift,XML,JSON,MessagePack,Kyro,Hessian,Protostuff,Java Native Serialize,FST。
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中的固定長度頭。
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)都採用了這個類庫。
RPC 是須要讓上層寫業務邏輯來實現功能的,如何優雅地啓停一個 server,注入 endpoint,客戶端怎麼連,重試調用,超時控制,同步異步調用,SDK 是否須要交換等等,都決定了基於 RPC 構建服務,甚至 SOA 的工程效率與生產力高低。這裏不作展開,看各類 RPC 的文檔就知道他們的易用性如何了。
國內
國外
上述列出來的都是如今互聯網企業經常使用的解決方案,暫時不考慮傳統的 SOAP,XML-RPC 等。這些是有網絡資料的,實際上不少公司內部都會針對本身的業務場景,以及和公司內的平臺相融合(好比監控平臺等),自研一套框架,可是異曲同工,都逃不掉剛剛上面所列舉的 RPC 的要考慮的各個部分。
http比如普通話,rpc比如團伙內部黑話。講普通話,好處就是誰都聽得懂,誰都會講。
講黑話,好處是能夠更精簡、更加保密、更加可定製,壞處就是要求「說」黑話的那一方(client端)也要懂,並且一旦你們都說一種黑話了,換黑話就困難了。
這個問題實際上是有理解誤區的,首先 http 和 rpc 並非一個並行概念。rpc是遠端過程調用,其調用協議一般包含傳輸協議和編碼協議。
傳輸協議包含: 如著名的 gRPC 使用的 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共存的一個示例