压力测试下的 golang http client 造成的连接数过多

发布于 2022-09-29 12:53:46 字数 6510 浏览 185 评论 0

看博客,自己运行验证

Tuning the Go HTTP Client Settings for Load Testing

loadtest.go

package main

import (
  "fmt"
  "io"
  "io/ioutil"
  "net/http"
)

func startWebserver() {
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q", r.URL.Path)
  })

  http.ListenAndServe(":8080", nil)
}

func startLoadTest() {
  for count := 0; ; count++ {
    resp, err := http.Get("http://localhost:8080/")
    if err != nil {
      panic(fmt.Sprintf("Got error: %v", err))
    }

    // io.Copy(ioutil.Discard, resp.Body) // <-- v2 add this line
    resp.Body.Close()
    fmt.Printf("Finished GET request #%v\n", count)
  }
}

func main() {
  // start a webserver in a goroutine
  go startWebserver()
  startLoadTest()
}
[2020-08-28 11:32:39.459] ❯ go run loadtest.go
2020/08/28 11:32:39 Finished GET request #96105
panic: Got error: Get "http://localhost:8080/": dial tcp [::1]:8080: connect: resource temporarily unavailable

goroutine 1 [running]:
main.startLoadTest()
  /Users/bingoobjca/github/weeklyreport/2020/loadtest.go:22 +0x168
main.main()
  /Users/bingoobjca/github/weeklyreport/2020/loadtest.go:32 +0x3a
exit status 2
[2020-08-28 11:30:18.925] ❯ while true; do date "+%Y-%m-%d %H:%M:%S"; netstat -n | grep -i 8080 | grep -i time_wait | wc -l; sleep 3; done
2020-08-28 11:30:27
     576
2020-08-28 11:30:30
    7229
2020-08-28 11:30:33
   13068
2020-08-28 11:30:36
   17130
2020-08-28 11:30:39
   19069
2020-08-28 11:30:42
   20892
2020-08-28 11:30:45
   22642
2020-08-28 11:30:48
   24283
2020-08-28 11:30:51
   25677
2020-08-28 11:30:55
   27111
2020-08-28 11:30:58
   28496
2020-08-28 11:31:01
   29842
2020-08-28 11:31:04
   31131
2020-08-28 11:31:07
    6979
2020-08-28 11:31:26
   11667
2020-08-28 11:31:29
   15000
2020-08-28 11:31:33
   18616
2020-08-28 11:31:36
   20287
2020-08-28 11:31:39
   20926
2020-08-28 11:31:42
   22444
2020-08-28 11:31:45
   24109
2020-08-28 11:31:48
   25698
2020-08-28 11:31:51
   27123
2020-08-28 11:31:54
   28364
2020-08-28 11:31:57
   29710
2020-08-28 11:32:01
   31009
2020-08-28 11:32:04
    6434
2020-08-28 11:32:23
    9741
2020-08-28 11:32:26
   13645
2020-08-28 11:32:29
   18158
2020-08-28 11:32:32
   20768
2020-08-28 11:32:35
   26138
2020-08-28 11:32:38
   32443
2020-08-28 11:32:41
   32689
2020-08-28 11:32:45
   32689
2020-08-28 11:32:48
   32689
2020-08-28 11:32:51
   32689
2020-08-28 11:32:54
       0

loadtest.go

add io.Copy(ioutil.Discard, resp.Body) // <-- v2 add this line before resp.Body.Close()

[2020-08-28 11:33:12.700] ❯ while true; do date "+%Y-%m-%d %H:%M:%S"; netstat -n | grep -i 8080 | grep -i time_wait | wc -l; sleep 3; done
2020-08-28 11:36:09
       0
2020-08-28 11:36:12
       0
2020-08-28 11:36:15
       0
2020-08-28 11:36:18
       0
2020-08-28 11:36:21
       0

好了,都是 TIME_WATI 都是 0 了,为啥呢,参考 Is it necessary to consume response body before closing it (net/http client code)? 里面的说法:

the io.Copy() drains the body meaning it can be reused via keepalive.
if there's still data pending, the Close() will actually close it and it can't be reused

并发 client

package main

import (
  "fmt"
  "io"
  "io/ioutil"
  "net/http"
  "time"
)

func startWebserver() {
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    time.Sleep(time.Millisecond * 50)
    fmt.Fprintf(w, "Hello, %q", r.URL.Path)
  })

  http.ListenAndServe(":8080", nil)
}

func startLoadTest() {
  for count := 0; ; count++ {
    resp, err := http.Get("http://localhost:8080/")
    if err != nil {
      panic(fmt.Sprintf("Got error: %v", err))
    }

    io.Copy(ioutil.Discard, resp.Body) // <-- v2 add this line
    resp.Body.Close()
    fmt.Printf("Finished GET request #%v\n", count)
  }
}

func resetClient() {
  // Customize the Transport to have larger connection pool
  defaultRoundTripper := http.DefaultTransport
  defaultTransportPointer, ok := defaultRoundTripper.(*http.Transport)
  if !ok {
    panic(fmt.Sprintf("defaultRoundTripper not an *http.Transport"))
  }
  defaultTransport := *defaultTransportPointer // dereference it to get a copy of the struct that the pointer points to
  defaultTransport.MaxIdleConns = 100
  defaultTransport.MaxIdleConnsPerHost = 100

  http.DefaultClient = &http.Client{Transport: &defaultTransport}
}

func main() {
  // start a webserver in a goroutine
  go startWebserver()
  // startLoadTest()

  // resetClient()
  for i := 0; i < 100; i++ {
    go startLoadTest()
  }

  time.Sleep(time.Second * 2400)
}

很快就 panic: Got error: Get "http://localhost:8080/": dial tcp [::1]:8080: socket: too many open files

放开 // startLoadTest() 注释,修复。

因为默认 const DefaultMaxIdleConnsPerHost = 2,造成每个主机最大闲置连接数为 2100 个并发,98 个需要等待 time_wait。

更多资源

  1. Drain Response.Body to enable TCP/TLS connection reuse (4x speedup) #317
  2. Do i need to read the body before close it?
$ lsof -p 23069

COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
go      23069 diego  cwd    DIR   8,18     4096 14686472 /home/diego/projects/go/1.8.1/src/sample
go      23069 diego  rtd    DIR   8,18     4096        2 /
go      23069 diego  txt    REG   8,18 10073055 13523309 /home/diego/programs/go/1.8.1/bin/go
go      23069 diego  mem    REG   8,18  1981712  8129743 /usr/lib/libc-2.25.so
go      23069 diego  mem    REG   8,18   146568  8129721 /usr/lib/libpthread-2.25.so
go      23069 diego  mem    REG   8,18   168656  8129742 /usr/lib/ld-2.25.so
go      23069 diego    0u   CHR  136,0      0t0        3 /dev/pts/0
go      23069 diego    1u   CHR  136,0      0t0        3 /dev/pts/0
go      23069 diego    2u   CHR  136,0      0t0        3 /dev/pts/0

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

苏佲洛

暂无简介

文章
评论
28 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文