当前位置: 首页 > news >正文

golang长连接的误用

误用一:忘记读取响应的body

由于忘记读取响应的body导致创建大量处于TIME_WAIT状态的连接(同时产生大量处于transport.go的readLoop和writeLoop的协程)

在linux下运行下面的代码:

package mainimport ("fmt""html""log""net""net/http""time"
)func startWebserver() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))})go http.ListenAndServe(":8080", nil)}func startLoadTest() {count := 0for {resp, err := http.Get("http://localhost:8080/")if err != nil {panic(fmt.Sprintf("Got error: %v", err))}resp.Body.Close()log.Printf("Finished GET request #%v", count)count += 1}}func main() {startWebserver()startLoadTest()}

在程序运行时另外开一个终端运行下面的命令:

netstat -n | grep -i 8080 | grep -i time_wait | wc -l

你会看到TIME_WAIT数量在持续增长

root@myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
166
root@myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
231
root@myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
293
root@myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
349

解决办法: 读取响应的body
更改startLoadTest()函数,添加下面的代码:

func startLoadTest() {for {...if err != nil {panic(fmt.Sprintf("Got error: %v", err))}io.Copy(ioutil.Discard, resp.Body)  // <-- add this lineresp.Body.Close()...}}

现在再次运行netstat -n | grep -i 8080 | grep -i time_wait | wc -l,你会发现TIME_WAIT状态的连接数为0

误用二:空闲连接最大数量设置太小,实际连接数量超过连接池的限制

连接的数量超过连接池的限制导致出现大量TIME_WAIT状态的连接

这种情况时由于持续超过连接池导致许多短连接被打开。
请看下面的代码:

package mainimport ("fmt""html""io""io/ioutil""log""net/http""time"
)func startWebserver() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {time.Sleep(time.Millisecond * 50)fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))})go http.ListenAndServe(":8080", nil)}func startLoadTest() {count := 0for {resp, err := http.Get("http://localhost:8080/")if err != nil {panic(fmt.Sprintf("Got error: %v", err))}io.Copy(ioutil.Discard, resp.Body)resp.Body.Close()log.Printf("Finished GET request #%v", count)count += 1}}func main() {// start a webserver in a goroutinestartWebserver()for i := 0; i < 100; i++ {go startLoadTest()}time.Sleep(time.Second * 2400)}

在另外一个终端运行netstat,尽管响应已经被读取,TIME_WAIT的连接数还是持续增加

root@ myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
166
root@ myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
231
root@ myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
293
root@ myhost:/# netstat -n | grep -i 8080 | grep -i time_wait | wc -l
349

什么是TIME_WAIT状态呢?

就是当我们创建大量短连接时,linux内核的网络栈保持连接处于TIME_WAIT状态,以避免某些问题。
例如:避免来自一个关闭的连接延迟的包被后来的连接所接收。并发连接被用地址,端口,序列号等其他机制所隔离开。

为什么这么多的TIME_WAIT端口?

默认情况下,Golang的http client会做连接池。他会在完成一个连接请求后把连接加到一个空闲的连接池中。如果你想在这个连接空闲超时前发起另外一个http请求,它会复用现有的连接。
这会把总socket连接数保持的低一些,直到连接池满。如果连接池满了,它会创建一个新的连接来发起http请求。
那这个连接池有多大呢?看看transport.go:

var DefaultTransport RoundTripper = &Transport{... MaxIdleConns:          100,IdleConnTimeout:       90 * time.Second,... 
}// DefaultMaxIdleConnsPerHost is the default value of Transport's
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2
  • MaxIdleConns:100 设置连接池的大小为100个连接
  • IdleConnTimeOut被设置为90秒,意味着一个连接在连接池里最多保持90秒的空闲时间,超过这个时间将会被移除并关闭
  • DefaultMaxIdleConnsPerHost = 2 这个设置意思时尽管整个连接池是100个连接,但是每个host只有2个。

上面的例子中有100个gooutine尝试并发的对同一个主机发起http请求,但是连接池只能存放两个连接。所以,第一轮完成请求时,2个连接保持打开状态。但是剩下的98个连接将会被关闭并进入TIME_WAIT状态。

因为这在一个循环中出现,所以会很快就积累上成千上万的TIME_WAIT状态的连接。最终,会耗尽主机的所有可用端口,从而导致无法打开新的连接。

修复: 增加http client的连接池大小

import (.. 
)var myClient *http.Clientfunc startWebserver() {... same code as before}func startLoadTest() {... for {resp, err := myClient.Get("http://localhost:8080/")  // <-- use a custom client with custom *http.Transport... everything else is the same}}func main() {// Customize the Transport to have larger connection pooldefaultRoundTripper := http.DefaultTransportdefaultTransportPointer, 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 todefaultTransport.MaxIdleConns = 100defaultTransport.MaxIdleConnsPerHost = 100myClient = &http.Client{Transport: &defaultTransport}// start a webserver in a goroutinestartWebserver()for i := 0; i < 100; i++ {go startLoadTest()}time.Sleep(time.Second * 2400)}

当然,如果你的并发要求高,可以把连接池的数量改的更大些。
但是这样没有根本解决问题,因为go的http.Client在连接池被占满并且所有连接都在被使用的时候会创建一个新的连接。
具体可以看代码,http.Client处理请求的核心在用它的transport获取一个连接:

// roundTrip implements a RoundTripper over HTTP.
func (t *Transport) roundTrip(req *Request) (*Response, error) {//...省略部分代码// Get the cached or newly-created connection to either the// host (for http or https), the http proxy, or the http proxy// pre-CONNECTed to https server. In any case, we'll be ready// to send it requests.pconn, err := t.getConn(treq, cm)  //看这里if err != nil {t.setReqCanceler(req, nil)req.closeBody()return nil, err}var resp *Responseif pconn.alt != nil {// HTTP/2 path.t.setReqCanceler(req, nil) // not cancelable with CancelRequestresp, err = pconn.alt.RoundTrip(req)} else {resp, err = pconn.roundTrip(treq)}if err == nil {return resp, nil}//...省略部分代码}

getConn方法的实现核心如下:

// getConn dials and creates a new persistConn to the target as
// specified in the connectMethod. This includes doing a proxy CONNECT
// and/or setting up TLS.  If this doesn't return an error, the persistConn
// is ready to write requests to.
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {req := treq.Requesttrace := treq.tracectx := req.Context()if trace != nil && trace.GetConn != nil {trace.GetConn(cm.addr())}w := &wantConn{cm:         cm,key:        cm.key(),ctx:        ctx,ready:      make(chan struct{}, 1),beforeDial: testHookPrePendingDial,afterDial:  testHookPostPendingDial,}defer func() {if err != nil {w.cancel(t, err)}}()// Queue for idle connection.if delivered := t.queueForIdleConn(w); delivered { //注意这一行代码,看函数名意思是在Idle连接队列里等待,如果执行成功就拿到一个连接,如果拿不到连接就跳过下面这部分代码pc := w.pc// Trace only for HTTP/1.// HTTP/2 calls trace.GotConn itself.if pc.alt == nil && trace != nil && trace.GotConn != nil {trace.GotConn(pc.gotIdleConnTrace(pc.idleAt))}// set request canceler to some non-nil function so we// can detect whether it was cleared between now and when// we enter roundTript.setReqCanceler(req, func(error) {})return pc, nil}cancelc := make(chan error, 1)t.setReqCanceler(req, func(err error) { cancelc <- err })// Queue for permission to dial.t.queueForDial(w) /拿不到连接就放入等待拨号的队列//...省略部分代码
}

我们再看queueForDial方法的实现:

// queueForDial queues w to wait for permission to begin dialing.
// Once w receives permission to dial, it will do so in a separate goroutine.
func (t *Transport) queueForDial(w *wantConn) {w.beforeDial()if t.MaxConnsPerHost <= 0 { //看这里,如果这个值小于等于0,就直接创建连接了,我们之前没有设置这个选项导致的go t.dialConnFor(w)return}t.connsPerHostMu.Lock()defer t.connsPerHostMu.Unlock()if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {if t.connsPerHost == nil {t.connsPerHost = make(map[connectMethodKey]int)}t.connsPerHost[w.key] = n + 1go t.dialConnFor(w)return}if t.connsPerHostWait == nil {t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)}q := t.connsPerHostWait[w.key]q.cleanFront()q.pushBack(w)t.connsPerHostWait[w.key] = q
}

误用三:没有重用同一个长连接的http.Transport对象

我们的一个服务是用Go写的,在测试的时候发现几个小时之后它就会core掉,而且core的时候没有打出任何堆栈信息,简单分析后发现该服务中的几个HTTP服务的连接数不断增长,而我们的开发机的fd limit只有1024,当该服务所属进程的连接数增长到系统的fd limit的时候,它被操作系统杀掉了。。。

    HTTP Connection中连接未被释放的问题在https://groups.google.com/forum/#!topic/golang-nuts/wliZf2_LUag和https://groups.google.com/forum/#!topic/golang-nuts/tACF6RxZ4GQ都有提到。

    这个服务中,我们会定期向一个HTTP服务器发起POST请求,因为请求非常不频繁,所以想采用短连接的方式去做。请求代码大概长这样:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

func dialTimeout(network, addr string) (net.Conn, error) {

    return net.DialTimeout(network, addr, time.Second*POST_REMOTE_TIMEOUT)

}

 

func DoRequest(URL string) xx, error {

       transport := http.Transport{

                Dial:              dialTimeout,

        }

 

        client := http.Client{

                Transport: &transport,

        }

 

        content := RequestContent{}

        // fill content here

 

        postStr, err := json.Marshal(content)

        if err != nil {

                return nil, err

        }

 

        resp, err := client.Post(URL, "application/json", bytes.NewBuffer(postStr))

        if err != nil {

                return nil, err

        }

 

        defer resp.Body.Close()

        body, err := ioutil.ReadAll(resp.Body)

        if err != nil {

                return nil, err

        }

 

        // receive body, handle it

}

  运行这段代码一段时间后会发现,该进程下面有一堆ESTABLISHED状态的连接(用lsof -p pid查看某进程下的所有fd),因为每次DoRequest函数被调用后,都会新建一个TCP连接,如果对端不先关闭该连接(对端发FIN包)的话,我们这边即便是调用了resp.Body.Close()函数仍然不会改变这些处于ESTABLISHED状态的连接。为什么会这样呢?只有去源代码一探究竟了。

      Golang的net包中client.go, transport.go, response.go和request.go这几个文件中实现了HTTP Client。当应用层调用client.Do()函数后,transport层会首先找与该请求相关的已经缓存的连接(这个缓存是一个map,map的key是请求方法、请求地址和proxy地址,value是一个叫persistConn的连接描述结构),如果已经有可以复用的旧连接,就会在这个旧连接上发送和接受该HTTP请求,否则会新建一个TCP连接,然后在这个连接上读写数据。当client接受到整个响应后,如果应用层没有
调用response.Body.Close()函数,刚刚传输数据的persistConn就不会被加入到连接缓存中,这样如果您在下次发起HTTP请求的时候,就会重新建立TCP连接,重新分配persistConn结构,这是不调用response.Body.Close()的一个副作用。
      如果不调用response.Body.Close()还存在一个问题。如果请求完成后,对端关闭了连接(对端的HTTP服务器向我发送了FIN),如果这边不调用response.Body.Close(),那么可以看到与这个请求相关的TCP连接的状态一直处于CLOSE_WAIT状态(还记得么?CLOSE_WAIT是连接的半开半闭状态,它是收到对方的FIN并且我们也发送了ACK,但是本端还没有发送FIN到对端,如果本段不调用close关闭连接,那么连接将一直处于
CLOSE_WAIT状态,不会被系统回收)。

      调用了response.Body.Close()就万无一失了么?上面代码中也调用了body.Close()为什么还会有很多ESTABLISHED状态的连接呢?因为在函数DoRequest()的每次调用中,我们都会新创建transport和client结构,当HTTP请求完成并且接收到响应后,如果对端的HTTP服务器没有关闭连接,那么这个连接会一直处于ESTABLISHED状态。如何解呢?
有两个方法:
      第一个方法是用一个全局的client,函数DoRequest()中每次都只在这个全局client上发送数据。但是如果我就想用短连接呢?用方法二。
      第二个方法是在transport分配时将它的DisableKeepAlives参数置为true,此时发送的请求头里会包含Connection: close,像下面这样:

1

2

3

4

5

6

7

8

9

10

// ...

transport := http.Transport{

        Dial:              dialTimeout,

        DisableKeepAlives: true,

}

 

client := http.Client{

        Transport: &transport,

}

// ...

  从transport.go:L908可以看到,当应用层调用resp.Body.Close()时,如果DisableKeepAlives被开启,那么transport自动关闭本端连接。而不将它加入到连接缓存中。

    补充一下,在dialTimeout函数中disable tcp连接的keepalive选项是不可行的,它只是设置TCP连接的选项,不会影响到transport中对连接的控制。

1

2

3

4

5

6

7

8

9

10

11

func dialTimeout(network, addr string) (net.Conn, error) {

        conn, err := net.DialTimeout(network, addr, time.Second*POST_REMOTE_TIMEOUT)

    if err != nil {

        return conn, err

    }

 

    tcp_conn := conn.(*net.TCPConn)                                                                                                 

    tcp_conn.SetKeepAlive(false)                                           

    return tcp_conn, err

}

--end--

 

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 使用原生 HTML + JS 实现类似 ChatGPT 的文字逐字显示效果
  • 实现Nginx的反向代理和负载均衡
  • Vue中渲染函数
  • Elasticsearch:Java ECS 日志记录 - log4j2
  • GMSSL2.x编译鸿蒙静态库和动态库及使用
  • 【ELK】window下ELK的安装与部署
  • 【算法专题】双指针算法之LCR 179. 查找总价格为目标值的两个商品(力扣)
  • 【已解决】服务器无法联网与更换镜像源
  • JavaScript之WebAPIs-BOM
  • TCP重传机制详解
  • 【BUG】已解决:requests.exceptions.ProxyError: HTTPSConnectionPool
  • Python自动化DevOps任务入门
  • go语言Gin框架的学习路线(七)
  • python调用chrome浏览器自动化如何选择元素
  • 函数(递归)
  • [PHP内核探索]PHP中的哈希表
  • @angular/forms 源码解析之双向绑定
  • 《剑指offer》分解让复杂问题更简单
  • Django 博客开发教程 16 - 统计文章阅读量
  • Druid 在有赞的实践
  • javascript从右向左截取指定位数字符的3种方法
  • Java方法详解
  • node.js
  • overflow: hidden IE7无效
  • -- 数据结构 顺序表 --Java
  • 数组的操作
  • 微服务框架lagom
  • 关于Android全面屏虚拟导航栏的适配总结
  • ​如何在iOS手机上查看应用日志
  • #{}和${}的区别是什么 -- java面试
  • ${ }的特别功能
  • (007)XHTML文档之标题——h1~h6
  • (ctrl.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“
  • (Forward) Music Player: From UI Proposal to Code
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • (分类)KNN算法- 参数调优
  • (附源码)springboot宠物管理系统 毕业设计 121654
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (附源码)springboot助农电商系统 毕业设计 081919
  • (个人笔记质量不佳)SQL 左连接、右连接、内连接的区别
  • (黑客游戏)HackTheGame1.21 过关攻略
  • (十)Flink Table API 和 SQL 基本概念
  • (十二)Flink Table API
  • (十六)串口UART
  • (实战篇)如何缓存数据
  • (一)python发送HTTP 请求的两种方式(get和post )
  • (转)负载均衡,回话保持,cookie
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • .gitignore文件忽略的内容不生效问题解决
  • .net 4.0 A potentially dangerous Request.Form value was detected from the client 的解决方案
  • .NET 8 编写 LiteDB vs SQLite 数据库 CRUD 接口性能测试(准备篇)
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .Net Core/.Net6/.Net8 ,启动配置/Program.cs 配置
  • .NET Framework、.NET Core 、 .NET 5、.NET 6和.NET 7 和.NET8 简介及区别
  • .Net Remoting(分离服务程序实现) - Part.3