Golang 优化之路——HTTP长连接

写在前面

压测的是否发现服务端TIME_WAIT状态的连接很多。

netstat -nat | grep :8080 | grep TIME_WAIT | wc -l   
17731

TIME_WAIT状态多,简单的说就是服务端主动关闭了TCP连接。

IMG-THUMBNAIL

TCP频繁的建立连接,会有一些问题:

  1. 三次握手建立连接、四次握手断开连接都会对性能有损耗;
  2. 断开的连接断开不会立刻释放,会等待2MSL的时间,据我观察是1分钟;
  3. 大量TIME_WAIT会占用内存,一个连接实测是3.155KB。而且占用太多,有可能会占满端口,一台服务器最多只能有6万多个端口;

TCP 相关

长连接的概念包括TCP长连接和HTTP长连接。首先得保证TCP是长连接。我们就从它说起。

func (c *TCPConn) SetKeepAlive(keepalive bool) error

SetKeepAlive sets whether the operating system should send keepalive messages on the connection. 这个方法比较简单,设置是否开启长连接。

func (c *TCPConn) SetReadDeadline(t time.Time) error

SetReadDeadline sets the deadline for future Read calls and any currently-blocked Read call. A zero value for t means Read will not time out.这个函数就很讲究了。我之前的理解是设置读取超时时间,这个方法也有这个意思,但是还有别的内容。它设置的是读取超时的绝对时间。

func (c *TCPConn) SetWriteDeadline(t time.Time) error

SetWriteDeadline sets the deadline for future Write calls and any currently-blocked Write call. Even if write times out, it may return n > 0, indicating that some of the data was successfully written. A zero value for t means Write will not time out. 这个方法是设置写超时,同样是绝对时间。

HTTP 包如何使用 TCP 长连接?

http 服务器启动之后,会循环接受新请求,为每一个请求(连接)创建一个协程。

// net/http/server.go L1892
for {
    rw, e := l.Accept()
    go c.serve()
}

下面是每个协程的执行的代码,我只摘录了一部分关键的逻辑。可以发现,serve方法里面还有一个for循环。

// net/http/server.go L1320
func (c *conn) serve() {
    defer func() {
        if !c.hijacked() {
            c.close()
        }
    }()

    for {
        w, err := c.readRequest()
        
        if err != nil {
        }
        
        serverHandler{c.server}.ServeHTTP(w, w.req)
    }
}

这个循环是用来做什么的?其实也容易理解,如果是长连接,一个协程可以执行多次响应。如果只执行了一次,那就是短连接。长连接会在超时或者出错后退出循环,也就是关闭长连接。defer函数可以让协程结束之后关闭 TCP 连接。

readRequest函数用来解析 HTTP 协议。

// net/http/server.go
func (c *conn) readRequest() (w *response, err error) {
    if d := c.server.ReadTimeout; d != 0 {
        c.rwc.SetReadDeadline(time.Now().Add(d))
    }
    if d := c.server.WriteTimeout; d != 0 {
        defer func() {
            c.rwc.SetWriteDeadline(time.Now().Add(d))
        }()
    }
    
    if req, err = ReadRequest(c.buf.Reader); err != nil {
        if c.lr.N == 0 {
            return nil, errTooLarge
        }
        return nil, err
    }
}

func ReadRequest(b *bufio.Reader) (req *Request, err error) {
    // First line: GET /index.html HTTP/1.0
    var s string
    if s, err = tp.ReadLine(); err != nil {
        return nil, err
    }
    
    req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
    
    mimeHeader, err := tp.ReadMIMEHeader()
}

具体参与解析 HTTP 协议的部分是ReadRequest方法,而调用它之前,设置了读写超时时间。根据前面的描述,超时时间设置的是绝对时间。所以这里都是通过time.Now().Add(d)来设置的。不同的是写超时是defer执行,也就是函数返回后才执行。

我们的程序为啥长连接失效?

通过源码我们能大概知道程序流程了,按道理是支持长连接的。为啥我们的程序不行呢?

我们的程序使用的是 beego 框架,它支持的超时是同时设置读写超时。而我们的设置是1秒。

beego.HttpServerTimeOut = 1

我对读写超时的理解,读超时是收到数据到读取完毕的时间;写超时是从一开始写到写完的时间。我对这两个超时的理解都不对。

实际上,从上面的源码可以发现,写超时是读取完毕之后设置的超时时间。也就是读取完毕之后的时间,加上逻辑执行时间,加上内容返回时间的总和。按照我们的设置,超过1秒就算超时。

下面详细说说读超时。ReadRequest是堵塞执行的,如果没有用户请求,它会一直等待着。而读超时是ReadRequest之前设置的,它除了读取数据之外,还有一部分耗时,那就是等待时间。假如一直没有用户请求,此时读超时已经被设置成1秒后了,超过1秒之后,这个连接还是会被断开。

如何解决问题?

原因已经说明白了。大量TIME_WAIT是超时引起的,有可能是等待时间过长引起的读超时;也有可能是程序在压测情况下出现一部分执行超时,这样会导致写超时。

我们目前使用的是 beego 框架,它并不支持单独设置读写超时,所以我目前的解决方式是将读写超时调整得大一些。

从1.6版本开始,Golang 能够支持空闲超时IdleTimeout,可以认为读超时就是读取数据的时间,空闲超时来控制等待时间。但是它有一个问题,如果空闲超时没有设置,而读超时设置了,那么读超时还是会作为空闲超时时间来使用。我估计这么做的原因是为了向前兼容。再一个问题就是 beego 并不支持这个时间的设置,所以我目前也没有别的太好的方法来控制超时时间。

后续

其实服务端最合理的超时控制需要这几个方面:

  1. 读超时。就是单纯的读超时,不要包括等待时间,否则无法区分超时是读数据引起的还是等待引起的。

  2. 写超时。最好也是单纯的写数据超时。如果网络良好,因为逻辑执行慢就把连接断开,这样也不是很合适。读写超时都应该和目前逻辑设置的一样,设置得短一些。

  3. 空闲超时。这个可以根据实际情况配置,可以适当大一些。

  4. 逻辑超时。一般情况下是不会发生网络层面的读写超时的,压测情况下超时大部分都是由于逻辑超时引起的。Golang 原生包支持了TimeoutHandler。它可以控制逻辑的超时。可惜 beego 目前不支持设置逻辑超时。而我也没有想到太好的方法把 beego 中接入它。

     func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
    

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容