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