KAFKA 遇到 TCP 之 認識 TCP
零、背景
最近在做一個kafka相關專案時,遇到一個悖論問題:業務即想要資料有序,又想要高效能,還想要高可靠。
這個初步看顯然不可能。
有序就要一個一個發,而且只能上個包正常返回時才能發下一個。
這樣算下來,假設資料一次來回是 20ms ,那每秒處理了就是 50個。
在這樣的背景下,我初步實現了相關專案程式碼。
實現程式碼與測試程式碼的過程中,我對 kafka 理解越來越深了,對這個問題理解也越來越深了。
靜下來想了想,大部分情況下,想要同時滿足資料有序、高可靠、高效能也是有可能的,只是實現複雜了點。
在和 vic 討論相關實現的時候,發現 我的專案的實現其實就是 TCP 傳送資料的過程,而且而這個還可能比 TCP 的某些情況複雜一些。
有序的 kafka 訊息其實就是 有序的 TCP 資料流,最終目的都是要客戶端可靠的快速的收到有序的資料。
在傳遞過程中,傳送的某個資料包都可能失敗,此時需要進行重傳。
由於網路或者客戶端可能處理不過來,傳送端還要考慮流量控制。
恩,現在看看,是要在應用層實現一個比 TCP 還複雜的資料傳遞應用了。
假設我直接說我的專案,由於大家沒有真實面臨這個問題,既有可能感受不到問題的複雜性。
因為我之前分享過《 ofollow,noindex" target="_blank">靜態庫遇到靜態庫 》系列,結果即使現在 99% 的人也不知道我說的問題是什麼。
所以這次先來看看 TCP,看看 TCP 在傳遞資料的時候面臨了什麼問題,最終又是怎麼解決的。
TCP的問題雖然大家還是沒有真正遇到過(TCP底層已經幾乎都解決了),但是畢竟大家都聽過。所以說TCP的時候大家更有感覺,也知道我在說什麼。
一、TCP 概覽
TCP 是一個複雜的協議,因為它要解決很多問題,然而引入一個方案來解決這個問題時,又會帶來很多子問題。
所以 TCP 發展這麼多年後,變得非常龐大、複雜、冗餘。
如果沉下心來完整的學習一下 TCP 協議,我們也可以收穫很多東西。
在大學的時候(2012年或2013年),我們的課程教材是《 TCP/IP 詳解 卷1:協議 》,我當時感覺這本書很有意思,把 TCP 相關的內容翻了很多遍。
畢業的時候,一個同學說自己沒教材,以後做網路相關的,可能會用到這本書的知識,問我能不能送給他。我就隨手送給他了。
現在想看的時候才記起這個事情,剛才去京東上看了下,這麼多年了,還是沒降價,快一百大洋一本。
這個文章主要來表達資料流在傳輸面臨的問題,我就不去講解 TCP 的狀態了。
三、TCP 亂序問題
我們知道服務端給客戶端傳資料的時候,客戶端需要給服務端傳送 ACK 確認包。
例如
服務端:給第一個資料包(DATA 1)。 客戶端:收到第一個資料包了(ACK 1)。 服務端:給第二個資料包(DATA 2)。 ... 服務端:這麼久了,還沒回ACK呢,再發一次,給第二個資料包(DATA 2) 客戶端:給第二個資料包(ACK 2) 客戶端:給第二個資料包(ACK2)
可以看到,資料包有可能丟失,此時服務端需要進行重傳。
另外由於資料有可能會被拆分成很小的片段,這個 ACK 就變得複雜了。
不能簡單的使用第幾個資料包了,只能使用第幾個位元組的資料了。
例如
服務端:給以seq 101開始長度為 10的資料包 客戶端:收到seq 110之前的包了(包括110)。 服務端:給以seq 111 開始長度為30的資料包。 ... 客戶端:奇怪,怎麼來了一個 seq 121開始長度為10的資料包? 客戶端:奇怪,怎麼來了一個 seq 131開始長度為10的資料包? 客戶端:終於收到 seq 111開始的資料包了,長度為10,需要回ACK 120。 客戶端:不對。seq 121開始的資料包我好像收到過,需要回ACK 130才對。 客戶端:還不對。seq 131開始的資料包我也收到過,需要回ACK 140才對。
這裡可以發現,每次會的ACK回的是最大連續資料的偏移量。
由於各種原因,TCP的實際情況回的ACK是最大連續資料偏移量加一。
從上面的例子上還可以看出來,TCP的客戶端需要維護哪些資料包已經收到了。
這個維護就需要成本了,比如需要使用連結串列維護有序資料。
連結串列的複雜度是 O(n)
的,不可接收,所以 TCP 內部使用紅黑樹來維護這個有序資料。
上面是站在客戶端接收資料的角度來看亂序問題的。
站在服務端傳送資料的角度,會發現問題很簡單:只需要從最小的ACK 開始傳送資料即可。
但是,從最小的ACK發資料很容易發現,之後可能某個包丟了,之後的包客戶端即使收到了,也會被重傳。
這就造成了網路上傳了大量的無效包,浪費流量頻寬。
於是 TCP 增加了 SACK的功能,即回覆一個ACK區間,代表這個區間的資料收到了,不需要再發送了。
這樣,服務端也需要維護一個連結串列,來標示哪些資料已經發送了,哪些還沒有傳送。
是的,這些也是使用紅黑樹來維護的,直接使用連結串列複雜度是不能接受的。
四、TCP 丟包問題
前面討論的是亂序問題,接下來就是傳送端的速度問題了。
傳送端該以怎麼樣的速度來發送資料呢?
瞭解 TCP 的人會馬上說,慢啟動、擁塞避免、快速恢復什麼的。
是的,這些都是TCP根據自己已有的資訊,來實現的演算法。
先來看看服務端能收集到哪些資訊吧。
假設正常通行的話,服務端可以知道通訊的來回延時,稱為RTT。
是的,服務端只能收集這樣一個資訊。
異常通訊時,服務端可以知道客戶端遲遲沒有會ACK,此時根據正常情況的RTT是不好判斷異常情況該如何傳送資料的。
所以,這個只能依靠演算法來探測。
經典的探測策略是慢啟動演算法。
剛開始傳的資料小點,為1(還記得上面舉例時有個資料長度嗎),正常收到資料了,資料長度就加1。
由於資料的長度是從1開始的,這樣就長的太慢了,於是就加了第二個策略:過一個RTT,資料長度就翻倍。
翻倍是一個很恐怖的策略,所以這裡還設定了一個上線,到達上限是就不再翻倍了。
翻倍的上限一般是 65536,當到達這個值後,就使用一些較慢的策略來增。
當發生超時未收到ACK時,我們就認為丟包了。
此時就需要緊急控制,也就是長度馬上變為1,然後進入慢啟動演算法。
當然,現在的演算法優化的更高階了。
丟包時不會馬上把長度置為1,而是長度減半的,然後進入快速恢復演算法。
為什麼要進入快速恢復演算法呢?
因為之前已經傳了那麼多包了,已經收集了很多資訊了,比如正常傳輸資料的長度上限。
通過之前收集的資訊,TCP也就恢復的更快了。
五、最後
TCP 的亂序與丟包問題已經看完了,算是複習了一下大學的知識。 下篇文章就好理解我描述的問題了。
本文首發於公眾號:天空的程式碼世界,微信號:tiankonguse-code。