網易面經:深剖TCP協議的流量控制和擁塞控制,你懂了嗎?

1.自我介紹+項目

2.RPC框架和普通http有什麼區別和優勢? 基於Tcp封裝還是http封裝的

3.rpc是長連接嗎?如果要傳輸一個特別大的文件 底層還是基於流嗎? Nio是一個什麼IO模型?

4.github了的watch star fork

5.異常和error的區別,oom是error還是異常?什麼東西分配在堆上和棧上?

6.只對堆進行gc 這句話對不對 調用system.gc()馬上就執行gc嗎

7.缺頁中斷 分頁地址轉換 內存抖動

8.linux的fork指令對數據的拷貝是馬上就拷貝的嗎?

9.linux看網絡狀況用什麼 看日誌用什麼?

10.擁塞控制 以及裡面的算法?流量控制的協議

11.Ping命令做了什麼?基於那一個層?ping是哪一個層的?

12.Mysql和Redis最大的區別? MyISAM和InnoDB的區別?

13.Redis 的實現。為什麼這麼高性能 ?set kv鍵值對進去的時候,kv鍵值的長度是不一樣的 你覺得底層的數據結構是一樣的嗎? 持久化的策略 長久下來aof文件會很大 怎麼辦?InnoDB行鎖的分類 (其實就是排他鎖和共享鎖)

14.Select from update 是什麼效果?事務你平常是怎麼處理的?

15.兩個隊列實現一個棧、圓裡均勻地生成點(極座標系)

16.ps命令的底層實現?

17.類加載器


2. RPC框架和普通http有什麼區別和優勢? 基於Tcp封裝還是http封裝的

RPC是一種遠程過程調用的協議,使用這種協議向另一臺計算機上的程序請求服務,不需要了解底層網絡技術的協議。

在 RPC 中,發出請求的程序是客戶程序,而提供服務的程序是服務器。

HTTP是一種超文本傳輸協議。是WWW瀏覽器和WWW服務器之間的應用層通訊協議。

RPC協議與HTTP協議的區別

1、RPC是一種API,HTTP是一種無狀態的網絡協議。RPC可以基於HTTP協議實現,也可以直接在TCP協議上實現。

2、RPC主要是用在大型網站裡面,因為大型網站裡面系統繁多,業務線複雜,而且效率優勢非常重要的一塊,這個時候RPC的優勢就比較明顯了。

HTTP主要是用在中小型企業裡面,業務線沒那麼繁多的情況下。

3、HTTP開發方便簡單、直接。開發一個完善的RPC框架難度比較大。

4、HTTP發明的初衷是為了傳送超文本的資源,協議設計的比較複雜,參數傳遞的方式效率也不高。開源的RPC框架針對遠程調用協議上的效率會比HTTP快很多。

5、HTTP需要事先通知,修改Nginx/HAProxy配置。RPC能做到自動通知,不影響上游。

6、HTTP大部分是通過Json來實現的,字節大小和序列化耗時都比Thrift要更消耗性能。RPC,可以基於Thrift實現高效的二進制傳輸。

3. github了的watch star fork


Watch

Watch翻譯過來可以稱之為觀察。

默認每一個用戶都是處於Not watching的狀態,當你選擇Watching,表示你以後會關注這個項目的所有動態,以後只要這個項目發生變動,如被別人提交了pull request、被別人發起了issue等等情況,你都會在自己的個人通知中心,收到一條通知消息,如果你設置了個人郵箱,那麼你的郵箱也可能收到相應的郵件。

如果你不想接受這些通知,那麼點擊 Not Watching 即可。

Star

Star 翻譯過來應該是星星,這裡解釋為`關注`或者`點贊`更合適,當你點擊 Star,表示你喜歡這個項目或者通俗點,可以把他理解成朋友圈的點贊吧,表示對這個項目的支持。

不過相比朋友圈的點贊,github 裡面會有一個列表,專門收集了你所有 start 過的項目,點擊 github 個人頭像,可以看到 your star的條目,點擊就可以查看你 star 過的所有項目了。

不過,在你的 star 列表很容易出現這樣的問題。就是你可能 star 成百上千個項目怎麼辦。這時,如果 github 可以提供一個分類功能該多好,就像微博網頁版的收藏,你在收藏的時候可以設置 tag這樣設置的好處是,以後再次查找項目時,可以根據歸類查找。

Fork

當選擇 fork,相當於你自己有了一份原項目的拷貝,當然這個拷貝只是針對當時的項目文件,如果後續原項目文件發生改變,你必須通過其他的方式去同步。

一般來說,我們不需要使用 fork 這個功能,至少我一般不會用,除非有一些項目,可能存在 bug 或者可以繼續優化的地方,你想幫助原項目作者去完善這個項目,那麼你可以 fork 一份項目下來,然後自己對這個項目進行修改完善,當你覺得項目沒問題了,你就可以嘗試發起 pull request給原項目作者了,然後就靜靜等待他的 merge。

當前很多人錯誤的在使用 fork。很多人把 fork 當成了收藏一樣的功能,包括一開始使用 github 的我,每次看到一個好的項目就先 fork,因為這樣,就可以我的 repository(倉庫)列表下查看 fork 的項目了。其實你完全可以使用 star 來達到這個目的。

7. 缺頁中斷 分頁地址轉換 內存抖動

一、什麼是缺頁中斷?

進程線性地址空間裡的頁面不必常駐內存,在執行一條指令時,如果發現他要訪問的頁沒有在內存中(即存在位為0),那麼停止該指令的執行,併產生一個頁不存在的異常,對應的故障處理程序可通過從外存加載該頁的方法來排除故障,之後,原先引起的異常的指令就可以繼續執行,而不再產生異常。

二、頁面調度算法

將新頁面調入內存時,如果內存中所有的物理頁都已經分配出去,就按照某種策略來廢棄整個頁面,將其所佔據的物理頁釋放出來;

三、查看進程發生缺頁中斷的次數

ps -o majflt,minflt -C program查看

majflt和minflt表示一個進程自啟動以來所發生的缺頁中斷的次數;

四、產生缺頁中斷的幾種情況

1、當內存管理單元(MMU)中確實沒有創建虛擬物理頁映射關係,並且在該虛擬地址之後再沒有當前進程的線性區(vma)的時候,可以肯定這是一個編碼錯誤,這將殺掉該進程;

2、當MMU中確實沒有創建虛擬頁物理頁映射關係,並且在該虛擬地址之後存在當前進程的線性區vma的時候,這很可能是缺頁中斷,並且可能是棧溢出導致的缺頁中斷;

3、當使用malloc/mmap等希望訪問物理空間的庫函數/系統調用後,由於linux並未真正給新創建的vma映射物理頁,此時若先進行寫操作,將和2產生缺頁中斷的情況一樣;若先進行讀操作雖然也會產生缺頁異常,將被映射給默認的零頁,等再進行寫操作時,仍會產生缺頁中斷,這次必須分配1物理頁了,進入寫時複製的流程;

4、當使用fork等系統調用創建子進程時,子進程不論有無自己的vma,它的vma都有對於物理頁的映射,但它們共同映射的這些物理頁屬性為只讀,即linux並未給子進程真正分配物理頁,當父子進程任何一方要寫相應物理頁時,導致缺頁中斷的寫時複製;

什麼是內存抖動(顛簸)

本質是頻繁的頁調度行為頻繁的頁調度,進程不斷產生缺頁中斷置換一個頁,又不斷再次需要這個頁

運行程序太多;頁面替換策略不好。通過殺掉一些無關的進程(終止進程)或者增加物理內存解決


10.擁塞控制 以及裡面的算法?流量控制的協議

TCP如何實現一個靠譜的協議

為了保證順序性,每個包都有一個ID。在建立連接的時候,商定起始的ID。然後按照ID一個一個發送。

收到包的一端需要對包做應答,一旦應答一個ID,就表明之前的ID都收到了,這個模式稱為累計確認或者累計應答。

為了記錄發送和接收的包,TCP在發送端和接收端都緩存記錄。

發送端緩存結構

發送端的緩存按照包的ID一個個排列,分成4個部分:

(一)發送並且已經確認的

(二)發送了並且尚未確認的

(三)沒有發送,但是已經確認要發送在等待的

(四)沒有發送,並且暫時不會發送的

第三和第四部分區分開是為了流量控制,流量控制的依據是什麼?TCP裡接收端會給發送端報告一個窗口大小,叫Advertised Window。發送端需要保證上面第二和第三部分的長度加起來等於Advertised Window。


網易面經:深剖TCP協議的流量控制和擁塞控制,你懂了嗎?


tcp發送端緩存結構

接收端緩存結構

接收端的緩存分成三個部分:

(一)接受並且確認過的

(二)還沒接收,但是馬上就能接收的,要等空格填滿

(三)還沒接收,也沒法接收的,也就是超過工作量(max buffer)的部分

網易面經:深剖TCP協議的流量控制和擁塞控制,你懂了嗎?


tcp接收端緩存結構

MaxRcvBuffer:最大緩存的量

LastByteRead之後是已經接收了,但是還沒被應用層消耗

NextByteExpected之後是等待接收的

Advertised Window其實就是等待接收未確認部分的大小。其中這部分中有可能是有空擋的,比如7到14有,但6是空的。那NextByteExpected就只能待在這個位置了。

TCP的確認和重發機制

發送端在發出一個包後,會設置一個定時器,超過一定時間沒收到ACK,就會把這個包重發。應該如何評估這個時間的大小呢?這個時間不能太短,必須大於正常的一次往返的時間(RTT)。也不能太長,太長了發送的速度就太慢了。

所以最關鍵的是估算一個平均RTT,所以TCP需要不斷地採樣RTT,還要根據RTT的波動範圍,做加權平均算出一個值來。由於重傳時間是不斷變化的,稱為自適應重傳算法。

快速重傳算法

以上算法的問題是超時時間會越加越長,所以有一個快速重傳的機制。當接收方收到一個序號大於下一個期望的報文段時,就是說接收緩存有了空格,那還是發送原來的ACK,比如我在等6,這時候收到7,我ACK 5。然後又收到8和9,我還都是ack 5。這樣發送端接連收到3個ACK,發送端收到後,會馬上重發6,不再等超時。這裡還是有一個問題,發送端這時候可能已經發到20了,收到的3個ACK只能知道6沒收到,7,8,9有沒有收到不知道,只能把6之後的全部重發一遍。

Selctive Acknowledgment(SACK)

還有一種重傳的方式稱為SACK,這種方式是在TCP頭裡加一個SACK的東西,將緩存地圖放進去,比如還是7丟了,可以地圖是ACK6,SACK8,SACK9。發送方收到後,立馬能看出是7丟了。Linux下面可以通過tcp_ack參數打開這個功能。

網易面經:深剖TCP協議的流量控制和擁塞控制,你懂了嗎?


sack示意圖

這裡還是有一個問題,就是SACK不是最終保證,就是說接收端在發送SACK後是可以把數據再丟了的。雖然這麼做不鼓勵,但是不排除極端情況,比如內存不夠了。所以發送端不能想當然的再也不發SACK的那些包了,還是要看這些包有沒有正式的ACK才能最終確認。

Duplicate SACK(D-SACK)

D-SACK的作用是接收端告訴發送端包發重了。比如ACK 6丟了,那麼發送端會重發,接收端發現收到兩個6,現在我都要ACK 8了,則回覆ACK 8,SACK 6。

引入D-SACK,有這麼幾個好處:

1.讓發送方知道,是發的包丟了還是對端回的ACK丟了

2.是不是自己的timeout設置小了,導致重傳了

3.網絡上出現先發後到的情況

4.網絡上有可能把我的包複製了

5.基於以上的認知,可以更好的做流控。Linux下使用參數tcp_dsack開啟這個功能。

RTT算法策略

以上提到重傳的Timeout很重要,這個超時時間在不同的網絡的情況下,根本沒有辦法設置一個死的值。只能動態地設置。 為了動態地設置,TCP引入了RTT——Round Trip Time,也就是一個數據包從發出去到回來的時間。這樣發送端就大約知道需要多少的時間,從而可以方便地設置Timeout——RTO(Retransmission TimeOut),以讓我們的重傳機制更高效。

經典算法

首先採樣最近幾次的RTT,然後做平滑計算,算法叫加權移動平均。公式如下:(α取值在0.8到0.9之間)

SRTT = ( α * SRTT ) + ((1- α) * RTT)

然後計算RTO,公式如下

RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ]

其中:

UBOUND是最大的timeout時間,上限值

LBOUND是最小的timeout時間,下限值

β 值一般在1.3到2.0之間。

Karn / Partridge 算法

經典算法的問題就是RTT在有重傳的時候怎麼算?是用第一次發的時間算做開始時間還是重傳的時間作為開始時間。比如下面這樣:

網易面經:深剖TCP協議的流量控制和擁塞控制,你懂了嗎?


RTT計算

所以為了避免這個坑,搞了一個Karn / Partridge 算法,這個算法的最大特點是——忽略重傳,不把重傳的RTT做採樣。這樣有一個問題就是突然有一段時間重傳很多,如果都不算的話RTT就一直是原來的值,顯然這個值已經不合適了。所以,算法想了一個辦法,只要一發生重傳,RTO翻倍。

Jacobson / Karels 算法

以上兩種算法的問題就是RTT容易被平均掉,不能很好的應對突發情況。新的算法的公式如下:

SRTT = SRTT + α (RTT – SRTT) —— 計算平滑RTT

DevRTT = (1-β)DevRTT + β(|RTT-SRTT|) ——計算平滑RTT和真實的差距(加權移動平均)

RTO= µ * SRTT + ∂ *DevRTT —— 神一樣的公式

(其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——這就是算法中的“調得一手好參數”,nobody knows why, it just works…) 這個算法就是今天的TCP協議中用的算法。

RTO終於被算出來了,接下來就是兩個最重要的窗口了。

流量控制

上面講到TCP在包的ACK中會攜帶一個窗口大小,發送方就可以做一個滑動窗口了(簡稱rwnd)。每收到一個ACK就把窗口往右移動一個,也就是從未發送待發送中取一個發出去,然後從不可發送中取一個出來放到待發送裡面。

如果接收方下次來的ack中帶的窗口大小變小,則說明接收方處理不過來了,那發送方就不能將窗口右移了,而是要將窗口變小。最後如果窗口變成0,發送端窗口就變成0,不會再發了。這個時候發送方會一直髮送窗口探測數據包ZWP(Zero Window Probe),看是否有機會調整窗口的大小。一般會探測3次,每次間隔30-60s,當然不同的實現可能配置不一樣。

SockStress攻擊

有等待就有攻擊,利用ZWP的攻擊方式就是,客戶端連上服務端後就把窗口設置成0,然後服務端就等,然後ZWP。等的多了自然資源就耗盡了,這種攻擊叫做SockStress。

Silly Window Syndrome (糊塗窗口綜合徵)

在接收端將窗口調整成0後,如果這個時候應用消耗了一個包,那窗口會變成1,如果這時候發送端立馬發送一個包會發生什麼?TCP+IP的頭加起來有40字節,如果為了幾個字節直接發送的話主要的工作都消耗在發頭上了,這是一個問題。

還有就是網絡裡有個MTU的概念,就是一次發送的包大小,以太網是1500字節,出去TCP+IP的40字節,本來可以用1460字節而只發幾個字節,那就是浪費帶寬。這個1460就是俗稱的MSS(Max Segment Size)。以上的表現就被稱為糊塗窗口綜合徵。

發送端和接收端是怎麼來處理這種病的呢?

如果是接收端造成窗口是0,一旦緩衝區開始有地方了,接收端不會立馬發送一個窗口大小1給發送端,而是會等窗口達到一定大小,比如緩衝區一半為空或者空間大於MSS了,才會更新窗口大小,在此之前還是一直回答0。

如果是發送端造成包比較小,那就是發送端負責攢數據,他有兩個主要的條件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前發送數據的ack回包,他才會發數據。這個就是著名的 Nagle’s algorithm 。

Nagle算法默認是打開的,所以,對於一些需要小包場景的程序——比如像telnet或ssh這樣的交互性比較強的程序,你需要關閉這個算法。你可以在Socket設置TCP_NODELAY選項來關閉這個算法。還有一個類似的參數TCP_CORK,其實是更激進的Nagle算法,完全禁止小包發送,而Nagle算法沒有禁止小包發送,只是禁止了大量的小包發送。所以要分清楚這兩個參數。

以上就是TCP的流量控制策略。

擁塞控制

流量控制通過滑動窗口來實現,但是rwnd窗口只考慮了發送端和接收端的問題,沒考慮網絡的問題。有可能接收端很快,但是網絡擁塞了,所以加了一個擁塞窗口(cwnd)。擁塞窗口的意思就是一次性可以連續提交多少個包到網絡中。最終的形態是LastByteSent-LastByteAcked<=min(cwnd,rwnd),由兩個窗口共同控制發送速度。

TCP的擁塞控制主要避免兩種現象,包丟失和包重傳。網絡的帶寬是固定的,當發送端發送速度超過帶寬後,中間設備處理不完多出來的包就會被丟棄,這就是包丟失。如果我們在中間設備上加上緩存,處理不過來的包就會被加到緩存隊列中,不會丟失,但是會增加時延。如果時延到達一定的程度,就會超時重傳,這就是包重傳。

擁塞控制主要是四個算法:1)慢啟動,2)擁塞避免,3)擁塞發生,4)快速恢復

慢啟動

擁塞窗口(cwnd)的大小應該怎麼設置呢?一個TCP連接,開始的時候cwnd設置成一個報文段(MSS),一次只能發送一個;當收到ACK後則cwnd++;如果ACK正常收到,每當過了一個RTT則翻倍,以指數增長。如果網速很快的話增長速度還是很可觀的。

網易面經:深剖TCP協議的流量控制和擁塞控制,你懂了嗎?


慢啟動

[注] TCP的實現中cwnd並不都是從1個MSS開始的,Linux 3.0後依據google的論文《An Argument for Increasing TCP’s Initial Congestion Window》,初始化cwnd從10個MSS開始。Linux 3.0之前,cwnd是跟MSS的值來變的,如果MSS< 1095,則cwnd = 4;如果MSS>2190,則cwnd=2;其它情況下,則是3。

擁塞避免

cwnd一直漲下去不是辦法,要設置一個限制。當漲到一次發送超過ssthresh(65535個字節),就會減速,改成每次加1/ cwnd,比如之前一次發送cwnd是10個MSS,現在每次收到一個確認cwnd加1/10個MSS,每過一個RTT加1個。這樣一直加下去知道擁塞出現,比如包丟失。

擁塞發生時

前面雖然超過ssthresh時會減速,但是還是在漲,早晚會產生擁塞的。這時候有兩種處理方式,1)一旦超時重傳出現,則把ssthresh改成cwnd/2,cwnd窗口改成1,重新從頭開始慢啟動。2)還有一種情況就是收到接收端SACK要求重傳,這種TCP認為不嚴重,ssthresh改成cwnd/2,cwnd降為cwnd/2+3進入快速恢復算法。

快速恢復

接著上一段擁塞發生的第二種情況,快速恢復算法的邏輯如下:

cwnd = sshthresh + 3 * MSS (3的意思是確認有3個數據包被收到了)

重傳Duplicated ACKs指定的數據包

如果再收到 duplicated Acks,那麼cwnd = cwnd +1

如果收到了新的Ack,那麼,cwnd = sshthresh ,然後就進入了擁塞避免的算法了。

其他的恢復算法有FACK,TCP Vegas等。

存在問題

擁塞控制用以上的方法控制窗口的大小有兩個問題:

1)丟包不代表通道滿了,也有可能是網絡本來就有問題,所以這個時候收縮時不對的

2)等到發生丟包再收縮,其實已經晚了,應該在剛好用滿時就不再加了

基於以上兩個問題,又出現了TCP BBR擁塞算法。

最近有點懶,今天就不手撕代碼。希望往期文檔的猿猿們,私信給我。


分享到:


相關文章: