事情的原由是個人一個同窗讓我幫他看一個問題,當時的描述是: 服務器在請求較多時,出現不響應的狀況。
現場環境是html
網絡拓撲大概是 client->內網穿透frp->httpServer 的樣子
前置條件就是這樣。java
首先 netstat na|grep port 看了下網絡狀態,並用awk統計了下鏈接的狀態:web
CLOSE_WAIT 613 ESTABLISHED 53 TIME_WAIT 17
netstat result like :spring
[root@localhost backend]# netstat -nalt |grep 8077 tcp6 0 0 :::8077 :::* LISTEN tcp6 1 174696 192.168.2.195:8077 172.16.1.10:49588 CLOSE_WAIT tcp6 1 188280 192.168.2.195:8077 172.16.1.10:49576 CLOSE_WAIT ...
underTow 的http處理線程總共32個 且都處於下面的狀態 :編程
"XNIO-1 task-32" #69 prio=5 os_prio=0 tid=0x0000000001c8e800 nid=0x1e10 runnable [0x00007f0a20dc1000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.PollArrayWrapper.poll0(Native Method) at sun.nio.ch.PollArrayWrapper.poll(PollArrayWrapper.java:115) at sun.nio.ch.PollSelectorImpl.doSelect(PollSelectorImpl.java:87) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86) - locked <0x00000006cf91e858> (a sun.nio.ch.Util$3) - locked <0x00000006cf91e848> (a java.util.Collections$UnmodifiableSet) - locked <0x00000006cf91e5f0> (a sun.nio.ch.PollSelectorImpl) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:101) at org.xnio.nio.SelectorUtils.await(SelectorUtils.java:46) at org.xnio.nio.NioSocketConduit.awaitWritable(NioSocketConduit.java:263) at org.xnio.conduits.AbstractSinkConduit.awaitWritable(AbstractSinkConduit.java:66) at io.undertow.conduits.ChunkedStreamSinkConduit.awaitWritable(ChunkedStreamSinkConduit.java:379) at org.xnio.conduits.ConduitStreamSinkChannel.awaitWritable(ConduitStreamSinkChannel.java:134) at io.undertow.channels.DetachableStreamSinkChannel.awaitWritable(DetachableStreamSinkChannel.java:87) at io.undertow.server.HttpServerExchange$WriteDispatchChannel.awaitWritable(HttpServerExchange.java:2039) at io.undertow.servlet.spec.ServletOutputStreamImpl.writeBufferBlocking(ServletOutputStreamImpl.java:577) at io.undertow.servlet.spec.ServletOutputStreamImpl.write(ServletOutputStreamImpl.java:150) at org.springframework.security.web.util.OnCommittedResponseWrapper$SaveContextServletOutputStream.write(OnCommittedResponseWrapper.java:639) at org.springframework.util.StreamUtils.copy(StreamUtils.java:143) at com.berry.oss.service.impl.ObjectServiceImpl.handlerResponse(ObjectServiceImpl.java:732) ...
至此,咱們看到了兩個疑點,瀏覽器
之前在cjh的產線問題中見過TIME_WAIT 太多的狀況,CLOSE_WAIT 太多的狀況我沒見過,因此又回顧了一遍TCP的握手揮手,如圖: 服務器
因此 CLOSE_WAIT 狀態其實是客戶端發送了揮手的FIN以後的服務器的狀態,此時客戶端已經完成本身的 write操做,只會read服務端發來的數據,而等服務端發送完了,就會發本身的FIN包。
查了些資料以後,看到不少人CLOSE_WAIT的緣由是由於 服務端有很是耗時的操做,致使會話超時,客戶端發FIN,而服務端線程已經阻塞在操做上,致使服務端無法發FIN包。
因此我沿着jstack的線程棧看了下相關的方法,大體以下:網絡
@GetMapping("/hello") public void hello(HttpServletResponse response) throws IOException { String path="C:\\Users\\Administrator\\Desktop\\over.mp4"; FileInputStream fileInputStream=new FileInputStream(path); OutputStream outputStream=response.getOutputStream(); response.setContentType("video/mp4"); int byteCount = 0; byte[] buffer = new byte[4096]; int bytesRead = -1; while ((bytesRead = fileInputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); byteCount += bytesRead; } outputStream.flush(); }
只是一個http發送大文件視頻的服務,沒有什麼耗時操做。因此到這我已經機關用盡了。app
由於判斷問題出在tcp上,因此咱們簡單看了看tcpdump的抓包資料,而後決定抓包看下網絡上到底發生了什麼。抓到的大概信息以下:
socket
能夠看到在客戶端發了 FIN以後,疑點:
因此win 表明什麼意思,
經過查看tcp的格式:
咱們發現: 窗口大小 字段佔了16位,指明TCP接收方緩衝區的長度,以字節爲單位,最大長度是65535字節,0指明發送方應中止發送,由於接收方的TCP的緩衝區已滿,
因此主要的問題是這個網絡代理frp 有bug, 或者是部署這個服務的機器太垃圾,致使服務器一直阻塞在寫方法上沒法繼續。
這部分我以爲是一個很重要的,且之前都沒有被我注意過的點,固然我也沒什麼機會作socket編程,查看了以後總結起來以下:
參考:
https://blog.csdn.net/zlfing/...
至此已經定位了這個CLOSE_WAIT的問題所在,但我手賤又測了幾回,發現了一個很奇怪的現象:我在服務端tcpdump,本身瀏覽器作client,視頻點開以後直接關閉頁面,一直抓不到FIN包,且鏈接也一直ESTABLISHED,服務器也已經報錯java.nio.channels.ClosedChannelException且中止數據傳輸, 這讓我很疑惑,若是鏈接一直存在,那是什麼中止了服務端的傳輸。
在嘗試了無數次以後,終於發現了一個疑點:
00:52:57.745946 IP 116.237.229.239.64575 > izuf6buyhgwtrvp2bv981yz.8077: Flags [P.], seq 1400:1442, ack 2399686, win 513, length 42
每次在我關閉頁面以後,抓包總會抓到客戶端發的一個42長度的數據報。P表明馬上上送到上層應用,無需等待。
因此看起來服務端中止傳輸數據是因爲應用的行爲致使,而並不禁TCP控制,漫無目的的測了好久後終於 在瀏覽器的控制檯,看到了協議是h2,表明了使用的是http2協議。
關於http2的詳解:
https://blog.wangriyu.wang/20...
關於http2 網上已經不少資料,我只簡單記錄下關鍵點:
http1.1 已經出現了tcp鏈接的複用(keep-alived) ,可是一個http的狀態總歸仍是由tcp的打開和關閉來掌管,且同一時刻tcp上只能存在一個http鏈接,即便使用了管道化技術,同時能夠發不少個http請求,但服務器依舊是FIFO的策略進行處理並按順序返回。
而在http2中客戶端和服務器只須要一個tcp鏈接,每個http被稱做一個流,幀被看成一個http報文的單位,每個幀會標明本身屬於哪一個流,且幀會分類型,來控制流的狀態:
這是我比較好奇的地方,服務端客戶端究竟在哪裏協商升級到http2。
參考:
https://imququ.com/post/proto...
簡單來講就是 Google 在 SPDY 協議中開發了一個名爲 NPN(Next Protocol Negotiation,下一代協議協商)的 TLS 擴展,在這個擴展中會進行協議選擇,很幸運的是我在本地wireshark報文中找到了他:
Extension: application_layer_protocol_negotiation (len=14) Type: application_layer_protocol_negotiation (16) Length: 14 ALPN Extension Length: 12 ALPN Protocol ALPN string length: 2 ALPN Next Protocol: h2 ALPN string length: 8 ALPN Next Protocol: http/1.1
能夠看到 客戶端傳了 http1.1 h2給服務端 ,而服務端的握手返回:
Extension: application_layer_protocol_negotiation (len=5) Type: application_layer_protocol_negotiation (16) Length: 5 ALPN Extension Length: 3 ALPN Protocol ALPN string length: 2 ALPN Next Protocol: h2
這就是http2的協商握手過程。
我還須要確認一件事,就是 以前說的42長度的數據包究竟是什麼樣子,但由於http2必須基於https,致使wireshark沒法看到這個包內容,搜了下資料操做以下:
https://zhuanlan.zhihu.com/p/...
最終看到這個42長度的包 是:
HyperText Transfer Protocol 2 Stream: RST_STREAM, Stream ID: 3, Length 4 Length: 4 Type: RST_STREAM (3) Flags: 0x00 0... .... .... .... .... .... .... .... = Reserved: 0x0 .000 0000 0000 0000 0000 0000 0000 0011 = Stream Identifier: 3 Error: CANCEL (8)
這個幀的狀態是 RST_STREAM ,他會通知應用層中止這個流。
至此全部的疑惑都解除了。
當我在解決問題的時候,纔會明白本身到底有多菜。