一次服務器中止響應引發的網絡知識的補習記錄

事情的原由是個人一個同窗讓我幫他看一個問題,當時的描述是: 服務器在請求較多時,出現不響應的狀況。
現場環境是html

  • os: CentOS Linux release 7.6.1810 (Core)
  • web server: undertow v2.0.20.Final
  • springBootVersion: 2.1.5.RELEASE
  • 一個內網穿透工具 frp

網絡拓撲大概是 client->內網穿透frp->httpServer 的樣子
前置條件就是這樣。java

嘗試看網絡狀態和線程狀態

netstat

首先 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 
...

jstatck

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)
...

至此,咱們看到了兩個疑點,瀏覽器

  1. 爲何有這麼多CLOSE_WAIT狀態的鏈接
  2. 爲何全部的處理線程都在等待寫的方法上

CLOSE_WAIT

之前在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

tcpdump 抓包

由於判斷問題出在tcp上,因此咱們簡單看了看tcpdump的抓包資料,而後決定抓包看下網絡上到底發生了什麼。抓到的大概信息以下:
socket

能夠看到在客戶端發了 FIN以後,疑點:

  1. 很明顯的服務端和客戶端一直在互相發ACK包,
  2. 客戶端的ack包裏 win一直是0

因此win 表明什麼意思,

tcp窗口

經過查看tcp的格式:


咱們發現: 窗口大小 字段佔了16位,指明TCP接收方緩衝區的長度,以字節爲單位,最大長度是65535字節,0指明發送方應中止發送,由於接收方的TCP的緩衝區已滿,

問題總結

因此主要的問題是這個網絡代理frp 有bug, 或者是部署這個服務的機器太垃圾,致使服務器一直阻塞在寫方法上沒法繼續。

java socket的關閉操做對應的tcp行爲

這部分我以爲是一個很重要的,且之前都沒有被我注意過的點,固然我也沒什麼機會作socket編程,查看了以後總結起來以下:

  1. close() 方法會關閉讀寫操做,已經在內核send-q的數據會嘗試發送完,以後再發FIN包。對端接收FIN以後,read會讀到-1 。 若是設置了setSoLinger()配置,那麼在嘗試發送send-q數據時若是超時,則會發RST包直接關閉鏈接。若是對端write,則已經close()的端會發RST包,在對端第二次write時會報錯SocketException。
  2. shutdownOutput() 會發FIN包,且中止寫操做 ,但還能夠讀
  3. shutdownInput() 中止讀操做,對端的全部數據包都會ACK。

參考:

https://blog.csdn.net/zlfing/...

http2

至此已經定位了這個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 網上已經不少資料,我只簡單記錄下關鍵點:

http2 多路複用

http1.1 已經出現了tcp鏈接的複用(keep-alived) ,可是一個http的狀態總歸仍是由tcp的打開和關閉來掌管,且同一時刻tcp上只能存在一個http鏈接,即便使用了管道化技術,同時能夠發不少個http請求,但服務器依舊是FIFO的策略進行處理並按順序返回。
而在http2中客戶端和服務器只須要一個tcp鏈接,每個http被稱做一個流,幀被看成一個http報文的單位,每個幀會標明本身屬於哪一個流,且幀會分類型,來控制流的狀態:

  • HEADERS: 報頭幀 (type=0x1),用來打開一個流或者攜帶一個首部塊片斷
  • DATA: 數據幀 (type=0x0),裝填主體信息,能夠用一個或多個 DATA 幀來返回一個請求的響應主體
  • PRIORITY: 優先級幀 (type=0x2),指定發送者建議的流優先級,能夠在任何流狀態下發送 PRIORITY 幀,包括空閒 (idle) 和關閉 (closed) 的流
  • RST_STREAM: 流終止幀 (type=0x3),用來請求取消一個流,或者表示發生了一個錯誤,payload 帶有一個 32 位無符號整數的錯誤碼 (Error Codes),不能在處於空閒 (idle) 狀態的流上發送 RST_STREAM 幀
  • SETTINGS: 設置幀 (type=0x4),設置此 鏈接 的參數,做用於整個鏈接
  • PUSH_PROMISE: 推送幀 (type=0x5),服務端推送,客戶端能夠返回一個 RST_STREAM 幀來選擇拒絕推送的流
  • PING: PING 幀 (type=0x6),判斷一個空閒的鏈接是否仍然可用,也能夠測量最小往返時間 (RTT)
  • GOAWAY: GOWAY 幀 (type=0x7),用於發起關閉鏈接的請求,或者警示嚴重錯誤。GOAWAY 會中止接收新流,而且關閉鏈接前會處理完先前創建的流
  • WINDOW_UPDATE: 窗口更新幀 (type=0x8),用於執行流量控制功能,能夠做用在單獨某個流上 (指定具體 Stream Identifier) 也能夠做用整個鏈接 (Stream Identifier 爲 0x0),只有 DATA 幀受流量控制影響。初始化流量窗口後,發送多少負載,流量窗口就減小多少,若是流量窗口不足就沒法發送,WINDOW_UPDATE 幀能夠增長流量窗口大小
  • CONTINUATION: 延續幀 (type=0x9),用於繼續傳送首部塊片斷序列,見 首部的壓縮與解壓縮

如何從http升級到http2

這是我比較好奇的地方,服務端客戶端究竟在哪裏協商升級到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的協商握手過程。

wireshark抓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 ,他會通知應用層中止這個流。
至此全部的疑惑都解除了。

最後總結

當我在解決問題的時候,纔會明白本身到底有多菜。

相關文章
相關標籤/搜索