網絡編程怎么做才算是優雅?Xjjdog 來波總結
據說,web2.0的魅力在于由靜態資源變成交互性資源,web3.0的魅力在于其去中心化的資源,大家都可以參與其中得享時代的福利。但是,無論上層概念玩的再花哨,最下層的通信還是基于web1.0所形成的技術。
我們的終極目標,其實就是打著去中心化的名義,做實際上的中心化。
當流量增加到一定程度,網絡編程會發生各種怪異的場景。下面將以十幾個實際的案例,來說明xjjdog平常在工作中遇到的與網絡相關的高頻問題,希望能夠助你一臂之力。
1. 大量客戶端上線注意躲避
無論你的服務器能力多強,在大批量連接到來,進行業務服務的時候,都會產生瞬時的問題。
舉個例子,如果你的MQTT服務器連接了幾十萬臺設備。當你的MQTT服務器宕機重啟的時候,就要接受幾十萬的并發,這幾乎沒有任何服務能夠受得了。
在xjjdog以往的經驗中,因為服務端重啟問題而造成的阻塞事故,數不勝數。
這個場景,其實和緩存的擊穿概念非常的相似。當緩存中的熱點數據集中失效的時候,請求就會全部擊穿到數據庫層面,造成問題。
如上圖,解決緩存擊穿問題就是給每個key加個失效時間的隨機值,讓它們不要在同一時間失效。類似的,我們可以在客戶端重連服務端的時候,加上一個隨機的時間。隨機數是個好東西,它能讓我們的海量連接在隨機時間窗口內保持類線性的增長。
2. 多網卡隊列
在類似openstack等虛擬平臺上假設的虛擬機,往往因為網卡能力不強而造成流量在達到一定程度之后,服務發生卡頓。這是因為單個cpu在處理中斷時,產生了瓶頸。通過dstat或者iftop命令,可以看到當前的網絡流量。
比如,Kafka新機器上線之后,會進行大規模的數據拷貝,這個時候如果你去ping相關的機器,會發現ping值變的非常大。同時,Recv-Q和Send-Q的值也會增大。
這個時候,就需要開啟網卡多隊列模式。
使用ethtool可以看到網卡的隊列信息。
- ethtool -l eth0 | grep 'Combined'
- Combined: 1
當然,通過下面的命令,可以增加網卡的隊列。
- ethtool -L eth0 combined 2
建議同時開啟中斷平衡服務。
- systemctl start irqbalance
3. 不定時的切斷一下長連接
如果客戶端和服務端連接上了,并一直保持連接不關閉對方,那么它就是一條長連接。長連接可以避免頻繁的連接創建所產生的開銷。從HTTP1到HTTP2再到HTTP3,一直在向減少連接,復用連接方面去努力。通常情況下,長連接是第一選擇。
但有一些特殊情況,我們希望長連接并不要一直在那里保持著,需要給它增加TTL。這種情況通常發生在負載均衡場景里。
比如LVS、HAProxy等。
如果后端有A、B、C三臺機器,經過LVS負載之后,90條連接被分散到三臺機器。但某個時刻,A宕機了,它所持有的30個連接就會被重新負載到B、C上,這時候它們都持有45條連接。
當A重啟之后,它卻再也拿不到新的連接。如果LVS運算一次再平衡的話,產生的影響也比較大。所以我們希望創建的長連接能夠有一個生存時長的屬性,在某個時間間隔內達到漸進式的再平衡。
4. k8s端口范圍
為了k8s和別的程序不起沖突,默認端口的范圍是 30000-32767。如果你在使用k8s平臺,配置了nodeport但是無法訪問到,要注意是不是設置的端口號太小了。
5. TIME_WAIT
TIME_WAIT是主動關閉連接的一方保持的狀態,像nginx、爬蟲服務器,經常發生大量處于time_wait狀態的連接。TCP一般在主動關閉連接后,會等待2MS,然后徹底關閉連接。由于HTTP使用了TCP協議,所以在這些頻繁開關連接的服務器上,就積壓了非常多的TIME_WAIT狀態連接。
某些系統通過dmesg可以看到以下信息。
- __ratelimit: 2170 callbacks suppressed
- TCP: time wait bucket table overflow
- TCP: time wait bucket table overflow
- TCP: time wait bucket table overflow
- TCP: time wait bucket table overflow
sysctl命令可以設置這些參數,如果想要重啟生效的話,加入/etc/sysctl.conf文件中。
- # 修改閾值
- net.ipv4.tcp_max_tw_buckets = 50000
- # 表示開啟TCP連接中TIME-WAIT sockets的快速回收
- net.ipv4.tcp_tw_reuse = 1
- #啟用timewait 快速回收。這個一定要開啟,默認是關閉的。
- net.ipv4.tcp_tw_recycle= 1
- # 修改系統默認的TIMEOUT時間,默認是60s
- net.ipv4.tcp_fin_timeout = 10
測試參數的話,可以使用 sysctl -w net.ipv4.tcp_tw_reuse = 1 這樣的命令。如果是寫入進文件的,則使用sysctl -p生效。
6. CLOSE_WAIT
CLOSE_WAIT一般是由于對端主動關閉,而我方沒有正確處理的原因引起的。說白了,就是程序寫的有問題,屬于危害比較大的一種。
大家都知道TCP的連接是三次握手四次揮手,這是由于TCP連接允許單向關閉。
如圖,當一個連接發起主動關閉之后,它將進入fin_wait_1狀態。同時,收到fin報文的被動關閉方,進入close_wait狀態,然后回復ack后,主動關閉方進入fin_wait_2狀態。這就是單向的關閉。
此時,如果被動關閉方因為某些原因,沒有發送fin報文給主動關閉方,那么它就會一直處于close_wait狀態。比如,收到了EOF但沒有發起close操作。
顯然,這多數是一種編程bug,只能通過代碼review來解決。
7. 一個進程能夠打開的網絡連接
Linux即使放開一個端口,能夠接受的連接也是海量的。這些連接的上限,受到單進程文件句柄數量和操作系統文件句柄數量的限制,也就是ulimit和file-max。
為了能夠將參數修改持久化,我們傾向于將改動寫入到文件里。進程的文件句柄限制,可以放在/etc/security/limits.conf中,它的上限受到fs.nr_open的制約;操作系統的文件句柄限制,可以放到/etc/sysctl.conf文件中。最后,別忘了在/proc/$id/limits文件中,確認修改是否對進程生效了。
/etc/security/limits.conf配置案例:
- root soft nofile 1000000
- root hard nofile 1000000
- * soft nofile 1000000
- * hard nofile 1000000
- es - nofile 65535
8. SO_KEEPALIVE
如果將這個Socket選項打開,客戶端Socket每隔段的時間(大約兩個小時)就會利用空閑的連接向服務器發送一個數據包。
這個數據包并沒有其它的作用,只是為了檢測一下服務器是否仍處于活動狀態。
如果服務器未響應這個數據包,在大約11分鐘后,客戶端Socket再發送一個數據包,如果在12分鐘內,服務器還沒響應,那么客戶端Socket將關閉。如果將Socket選項關閉,客戶端Socket在服務器無效的情況下可能會長時間不會關閉。
9. SO_REUSEADDR是為了解決什么問題
當我們在網絡開發時,時常會碰到address already in use的異常,這是由于關閉應用程序時,還有對應端口的網絡連接處于TIME_WAIT狀態而造成的。
TIME_WAIT狀態通常會持續一段時間(2ML),設置SO_REUSEADDR可以支持快速端口復用,支持應用的快速重啟。
10. 健康檢查采用應用心跳
tcp自身的keepalived機制非常的雞肋,它靜悄悄的在底層運行,無法產生應用層的語義。
在我們的想象里,連接就應該是一條線。但其實,它只是2個點,而且每次走的路徑都可能不一樣。一個點,需要在發出心跳包然后收到回復之后,才能知道對方是否存活。
tcp自帶的心跳機制,僅僅能知道對方是否存活,對于服務是否可用,健康狀況這些東西一概不知,而且超時配置常常與超時重傳機制相沖突。
所以,有確切含義的應用層心跳是必要的。
11. SO_LINGER
這個Socket選項可以影響close方法的行為。
在默認情況下,當調用close方法后,將立即返回;如果這時仍然有未被送出的數據包,那么這些數據包將被丟棄。
如果將linger參數設為一個正整數n時(n的值最大是65,535),在調用close方法后,將最多被阻塞n秒。
在這n秒內,系統將盡量將未送出的數據包發送出去;如果超過了n秒,如果還有未發送的數據包,這些數據包將全部被丟棄;而close方法會立即返回。
如果將linger設為0,和關閉SO_LINGER選項的作用是一樣的。
12. SO_TIMEOUT
可以通過這個選項來設置讀取數據超時。
當輸入流的read方法被阻塞時,如果設置timeout(timeout的單位是毫秒),那么系統在等待了timeout毫秒后會拋出一個InterruptedIOException例外。
在拋出例外后,輸入流并未關閉,你可以繼續通過read方法讀取數據。
13. SO_SNDBUF,SO_RCVBUF
在默認情況下,輸出流的發送緩沖區是8096個字節(8K)。這個值是Java所建議的輸出緩沖區的大小。
如果這個默認值不能滿足要求,可以用setSendBufferSize方法來重新設置緩沖區的大小。但最好不要將輸出緩沖區設得太小,否則會導致傳輸數據過于頻繁,從而降低網絡傳輸的效率。
14. SO_OOBINLINE
如果這個Socket選項打開,可以通過Socket類的sendUrgentData方法向服務器發送一個單字節的數據。
這個單字節數據并不經過輸出緩沖區,而是立即發出。
雖然在客戶端并不是使用OutputStream向服務器發送數據,但在服務端程序中這個單字節的數據是和其它的普通數據混在一起的。因此,在服務端程序中并不知道由客戶端發過來的數據是由OutputStream還是由sendUrgentData發過來的。
End
我非常驚訝的發現,現在有些網絡環境,依然還是千兆網卡,包括一些比較專業的測試環境。當在這些環境上進行實際的壓測時,當流量突破了網卡的限制,應用響應將會變的異常緩慢。計算機系統是一個整體,CPU、內存、網絡、IO,任何一環出現瓶頸,都會造成問題。
在分布式系統中,網絡是一個非常重要的因素。但由于它相對來說比較底層,所以大多數開發對其了解較少。加上現在各種云原生組件的流行,接觸這些底層設施的機會就越來越少。但如果系統真的發生了問題,在排除掉其他最可能出問題的組件后,千萬別忘了--
還有網絡這一攤子等著你。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高并發世界,給你不一樣的味道。