深度理解resp.body.close的奧祕

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端

http鏈接的疑問

http請求是咱們開發中最爲常見的一個東西了,特別微服務中,因爲服務的拆分,每一個人可能負責某一塊的業務,當A服務的某個業務依賴B服務的數據時,最多見的就是B服務提供一個接口了。golang提供的原生的httpclient仍是很是強大的,可是若是在某些場景中,用的不對,可能會形成意想不到的問題。
先來看個問題:golang

func main() {
   for i := 0; i < 100; i++ {
      resp, err := http.Get("http://www.baidu.com")
      if err != nil {
         fmt.Println(err)
         return
      }
      _, err = ioutil.ReadAll(resp.Body)
   }
   fmt.Println("goroutine num is", runtime.NumGoroutine())
}
複製代碼

第一次接觸go,我有如下幾個疑問:後端

  1. 100次循環,是否是就100個鏈接
  2. 若是是100個鏈接,那麼打印的gouroutine是否是就是101(100個子goroutine+1個主goroutine)
  3. 若是此處鏈接是複用的,那麼雖然是100個請求,是否是始終複用1個鏈接
  4. 若是是1個鏈接,那麼打印的gouroutine是否是就是2(1個子goroutine+1個主goroutine)

帶着以上的幾點疑問,我測試了幾個例子:markdown

http 不帶close

func main() {
   httpWithoutClose()
}

func httpWithoutClose() {
   for i := 0; i < 20; i++ {
      _, err := http.Get("http://www.baidu.com")
      if err != nil {
         fmt.Println(err)
         return
      }
   }
   fmt.Println("goroutine num is ", runtime.NumGoroutine())
}
複製代碼

直接發起20個請求,且不讀response的body,也不進行response的body的close。
結果: goroutine num is 41
居然有41個goroutine,21個我卻是能夠理解(起碼20個請求+1個主goroutine),41個說明每一個請求對應2個goroutine。經過閱讀源碼,發現大體請求的流程如圖:app

image.png 獲取鏈接以後,會新增兩個goroutine,readLoopwriteLoop,這樣就能夠理解通了,一個負責讀,一個負責寫。函數

http帶close

func httpWithClose() {
   for i := 0; i < 20; i++ {
      resp, err := http.Get("http://www.baidu.com")
      if err != nil {
         fmt.Println(err)
         return
      }
      resp.Body.Close()
   }
   fmt.Println("goroutine num is", runtime.NumGoroutine())
}
複製代碼

直接發起20個請求,且不讀response的body,可是進行response的body的close。
結果: goroutine num is 1
說明close以後,回收了readLoopwriteLoop
以readLoop中的一段代碼爲例:微服務

body := &bodyEOFSignal{
   body: resp.Body,
   earlyCloseFn: func() error {
      waitForBodyRead <- false
      <-eofc // will be closed by deferred call at the end of the function
      return nil

   },
   fn: func(err error) error {
      isEOF := err == io.EOF
      waitForBodyRead <- isEOF
      if isEOF {
         <-eofc // see comment above eofc declaration
      } else if err != nil {
         if cerr := pc.canceled(); cerr != nil {
            return cerr
         }
      }
      return err
   },
}
複製代碼

earlyCloseFn 未讀body就close的,會走此方法,能夠發現向waitForBodyRead推入一個false
Fn正常的讀body,當body讀完以後,會向waitForBodyRead推入一個true
waitForBodyRead這個chan對接下來的goroutine的生死起着關鍵做用。
readLoop 自己是個循環:oop

alive := true
	for alive {
		 ......

		// Before looping back to the top of this function and peeking on
		// the bufio.Reader, wait for the caller goroutine to finish
		// reading the response body. (or for cancellation or death)
		select {
		case bodyEOF := <-waitForBodyRead:
			pc.t.setReqCanceler(rc.cancelKey, nil) // before pc might return to idle pool
			alive = alive &&
				bodyEOF && // false的話就退出循環
				!pc.sawEOF &&
				pc.wroteRequest() &&
				tryPutIdleConn(trace)
			if bodyEOF {
				eofc <- struct{}{}
			}
		case <-rc.req.Cancel:
			alive = false
			pc.t.CancelRequest(rc.req)
		case <-rc.req.Context().Done():
			alive = false
			pc.t.cancelRequest(rc.cancelKey, rc.req.Context().Err())
		case <-pc.closech:
			alive = false
		}

		testHookReadLoopBeforeNextRead()
	}
複製代碼

只要alive=true,循環就會一直進行下去,當從bodyEOF := <-waitForBodyRead讀出的是false,循環退出。而後 readLoop 執行defer函數:post

defer func() {
   pc.close(closeErr) // 關閉自身的通道 closech
   pc.t.removeIdleConn(pc) // 回收鏈接
}()
複製代碼

其中pc.close(closeErr),會關閉pc自己的通道closech,而後不是還有個writeLoop嗎,writeLoop自己也是個循環,主要負責寫的。測試

func (pc *persistConn) writeLoop() {
   defer close(pc.writeLoopDone)
   for {
      select {
      case wr := <-pc.writech:
         startBytesWritten := pc.nwrite
         err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
         if bre, ok := err.(requestBodyReadError); ok {
            err = bre.error
            // Errors reading from the user's
            // Request.Body are high priority.
            // Set it here before sending on the
            // channels below or calling
            // pc.close() which tears town
            // connections and causes other
            // errors.
            wr.req.setError(err)
         }
         if err == nil {
            err = pc.bw.Flush()
         }
         if err != nil {
            wr.req.Request.closeBody()
            if pc.nwrite == startBytesWritten {
               err = nothingWrittenError{err}
            }
         }
         pc.writeErrCh <- err // to the body reader, which might recycle us
         wr.ch <- err         // to the roundTrip function
         if err != nil {
            pc.close(err)
            return
         }
      case <-pc.closech: //收到消息後,退出
         return
      }
   }
}
複製代碼

當收到pc.closech信號的時候,writeLoop也就退出了。

image.png 因此只剩一個主goroutine了。

http 不帶close,可是有read

func httpWithoutCloseButRead() {
   for i := 0; i < 20; i++ {
      resp, err := http.Get("http://www.baidu.com")
      if err != nil {
         fmt.Println(err)
         return
      }
      _, err = ioutil.ReadAll(resp.Body)
   }
   fmt.Println("goroutine num is ", runtime.NumGoroutine())
}
複製代碼

直接發起20個請求,讀取body,但不close。
結果: goroutine num is 3
由上個例子咱們知道,當body讀取完以後,會向waitForBodyRead推入個true,在true的狀況下,readLoop會一直循環,且會把當前的鏈接放入空閒列表中,供下次使用:

select {
case bodyEOF := <-waitForBodyRead:
	pc.t.setReqCanceler(rc.cancelKey, nil) // before pc might return to idle pool
	alive = alive &&
		bodyEOF &&
		!pc.sawEOF &&
		pc.wroteRequest() &&
		tryPutIdleConn(trace) //放入idle list,供下次使用
	if bodyEOF {
		eofc <- struct{}{}
	}
	....
複製代碼

由於沒有回收readLoopwirteLoop兩個goroutine,且下一個請求能夠複用鏈接,因此就是3個

http 帶close 且 read

func main() {
   httpCloseAndRead()
}

func httpCloseAndRead() {
   for i := 0; i < 20; i++ {
      resp, err := http.Get("http://www.baidu.com")
      if err != nil {
         fmt.Println(err)
         return
      }
      _, err = ioutil.ReadAll(resp.Body)
      resp.Body.Close()
   }
   fmt.Println("goroutine num is ", runtime.NumGoroutine())
}
複製代碼

http 不帶close,可是有read的同樣。

建議

任何http request的時候,必定要加close

func doRequest() {
   resp, err := http.Get("http://www.baidu.com")
   if err != nil {
      fmt.Println(err)
      return
   }
   defer resp.Body.Close() // 很重要
   _, err = ioutil.ReadAll(resp.Body)
}
複製代碼

不加close,可能形成goroutine泄漏,加了必定不會。

相關文章
相關標籤/搜索