前些天的時候,一位同僚問了一些REDIS的網絡協定相關的問題,然後交流中談了一些我的想法,又參考了一些資料,記錄下來。
我們在設計一個應用層網絡協定是,我們需要關注哪些方面? 或者說一個好的應用層協定應該有哪些屬性?
好的應用層協定是可伸縮的。一些應用層協定比如HTTP,會建立幾條并行的連結的到服務端,這樣做可以減少延遲,增加吞吐量,但是在傳輸層和服務端看來,這些連結互相之間是沒有關聯關系的。而且可能會造成額外的一些問題。
在傳輸層,比如TCP協定采用自适應算法根據網絡的條件進行高效的資料傳輸。這個自适應的過程是在每個連結上進行的。如果同時有多個連結到服務端,那也就是需要針對每個連結進行自适應的調節過程。這就在網絡中引入了額外的不必要的壓力,而且TCP如果用了慢啟動擁塞算法,那麼,應用層序還是可以感受到這種額外的延遲。如果用戶端的帶寬不足,這種問題反而會有可能變的更明顯。
在服務端,每個從用戶端建立的連結有可能都要經過認證,是以,服務端的負載會随着遠大于實際使用者數的連結的增加而增大。更嚴重的情況是,如果有一個使用者發起了非常多的連結,服務端的性能可能會嚴重下降,甚至會導緻無法為其他使用者提供服務。
HTTP協定1.0版本,提供了持久連結機制(Keep-alive),允許一個連結在處理完事務後可以不用關閉連結,以便為将來的HTTP請求複用現存的連結。這種機制可以避開緩慢的連結建立階段,而且還可以避免慢啟動的擁塞适應階段,進行更快速的資料傳輸。但是管理持久連結時要特别小心,防止積累出大量的空閑連結,耗費本地以及遠端的資源。并行連結和持久連結配合使用可能是最高效的方式。
類似HTTP的協定一般都是隻允許用戶端發送請求到服務端,而不允許服務端直接發送資料到用戶端。當想要完成服務端推送一些消息到用戶端之類的業務需求時,隻能讓用戶端不斷的向服務端發送輪詢請求。這樣會導緻浪費網絡資源,也會對服務端的資源造成浪費。是以後來引入了WeSocket,可以讓HTTP連結轉換為一般的連結,就可以避開HTTP協定的要求了。
另外就是類似HTTP的協定,一般都是一個請求發送過去,然後等待一個回應完成,然後再發下一個請求,這種串行化處理會因為網絡往返時延導緻延遲較高。是以一般會用到Pipeline技術。也就是一次發送多個請求,要求服務端按照相應的順序發送回來。 這裡面的重中之重就是服務端必須要確定發送的順序是請求的順序。還有就是如果用戶端發送了10個請求,但是服務端在處理完5個請求後發生了錯誤就關閉了連結,那麼用戶端就要確定能夠重新發送剩餘的5個請求,這帶來了一定的實作複雜度。
但是類似HTTP的協定,在我看來還有另外一個好處。因為隻允許用戶端到服務端發送請求,那麼會讓C/S雙方的業務變得調理清晰。比如說,業務服務由用戶端請求觸發,專門有另外一個連結提供服務端推送資料到用戶端。這種職責單一性會帶來更好的可維護性。而且這種職責單一性,可以很容易的用其他語言實作,也可以采用同步阻塞IO或者非阻塞IO甚至異步IO機制實作。假設如果一個連結即處理業務請求,又能夠讓服務端推送資料,那麼更好的實作模型是event-driven,如果用同步阻塞IO實作,就會變得異常複雜。在我最近的接觸中,發現REDIS的協定就是此種設計。REDIS的協定裡大部分都是用戶端發起請求,服務端處理後傳回結果; 但是有一個特殊的請求,就是SUBSCRIBE,用戶端發起這個指令後,如果再通過這個連結發送資料到服務端,服務端會直接傳回錯誤。處于SUBSCRIBE狀态的連結隻能等着服務端主動推送來的資料。
可伸縮性的另外一個方面就是服務端和用戶端的部署關系。功能實作在服務端總是比實作在用戶端能夠帶來更多的可伸縮性,畢竟大多數情況下,服務端更新總是要比用戶端容易一些。
好的應用層協定是高效的。我們大多數都會非常關心傳輸的效率,總是認為傳輸速度越快越好。一般的做法是批量請求,一個請求中包含了很多要一起處理的事情;或者重疊沖球,比如HTTP PIPELINE機制和IMAP中針對請求打标機的方式,讓用戶端無需等待回應就可以繼續向服務端發送請求,然後在接收時可以通過用戶端的狀态機處理回應資料,這樣就能夠減少等待時間。有些時候,性能可能會和擴充性産生沖突,這個時候就需要自己根據實際場景和需求做權衡。
好的應用層協定是簡單的。經驗法則是如果一個協定設計的很差,那麼在做一些簡單的事情時反而變成了挑戰。正确的方式是讓簡單的事情簡單的完成,複雜的事情通過複雜的方式完成。另外一條經驗法則是,如果應用層協定有兩種方式完成同樣的事情,那麼可能在基礎協定設計架構上存在問題。簡單并不意味着想法簡單,而是說,簡單是一種精心設計的,可以解決包括了問題域中的任何事情,甚至是邊界的更複雜的問題。一緻性是會讓設計變得優雅和簡單。
好的應用層協定是可擴充的。我們程式員經常會遇到需求的變更,或者一些政策變動較多的問題。這也就意味着,協定本身是逐漸演進的。是以一個可擴充的協定對我們來說非常的重要。可擴充的協定可以友善的向前或者向後相容,比如protobuff,json,http等協定。
好的應用層協定是人類可讀的。可讀性要根據自己實際場景和需要進行權衡。但是在我個人看來,可讀的協定容易幫助排查問題。想象一下,遇到了一個網絡問題,需要抓包分析,滿眼的二進制流是什麼感受?再想象下,如果是HTTP協定,你還有這種困難麼?
好的應用層協定是健壯的。Postel的健壯性法則“be conservative in what you send, be liberal in what you accept.(發送時保守,接手時開放)”講究的更多的是吞掉對方的錯誤,但是如果對接收到的資料進行檢查并傳回了錯誤,那麼對對方可能會有更多的好處。假設跨語言實作的時候或者多版本用戶端的時候,如果沒有一個一緻的錯誤檢查,會讓人很蛋疼的吧。
參考文獻:
- 《On the Design of Application Protocols》
- 《UNIX程式設計藝術》