http keep alive

写在开始:

在早期的HTTP/1.0中, 协议是一个建立连接, 交互数据, 断开连接的过程. 然后作为客户端, 我们并不是加载一个uri就完成任务, 客户端需要非常频繁的和同一个服务端交互数据, 这个时候就好比我们有很多很多的电话内容要沟通, 但是又需要一件事情一次电话. 这个能想的到的性能问题.
作为互联网如此重要的协议,几乎承担了互联网绝大多数的应用层流量,为了解决这个问题,HTTP/1.0后期使用请求头: Connection: keep-alive 来告诉对方我请求完毕请不要关闭连接,后面我还有继续复用这个连接, HTTP/1.1 版本则默认使用keep-alive特性(值得注意的是这里的keep-alive特性和tcp的keepalive是完全不同的概念, 解决的也不是同一类问题).

keep-alive特性如何开启

因为golang的client,server端实现均使用HTTP/1.1规范, 所以无论你是否传递Connection: keep-alive头, 均默认开启keep-alive特性.

服务端默认开启keep-alive的源码分析

这一点可以从golang源码中看到:

首先我们看作为服务端, 当启动一个server后, 首先listen, 然后发起server处理:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}
func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
        // 这里创建tcp链接
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
        // 这里开始接受tcp数据包, 进行处理
    return srv.Serve(ln)
}

之后进入for循环, 一个一个的请求都将启动新的协程去处理, 由此我们也可以发现, 每一个http的请求都是通过新的协程去处理

for {
        rw, err := l.Accept()
                // 此处省略N行代码
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(connCtx)
    }

再进入serve方法后, 可以发现整体就是for循环一直处理该请求链接上的数据, 同时我们可以发现中间有判断doKeepAlives而退出循环, 这也是作为服务端程序, 你如果不想启用http keepalive而可以设置的选项http.Server.SetKeepAlivesEnabled(false)

客户端默认开启keep-alive的源码分析

客户端的源码在net/http/client.go中, 追踪客户端的代码有个核心的思想就是拿req取resp, 在client每一次发起请求都是 client结构体的do方法

if resp, didTimeout, err = c.send(req, deadline); err != nil {

往下追踪后知道client的所有req得到resp都是从 resp, err = rt.RoundTrip(req)发起,这也是设计者设计RoundTripper接口的目的.

最后queueForIdleConn方法可以得知, 我们的客户端在当前client结构体实例上是预先申请好连接的, 所以也就得到了keep-alive的目的.

// queueForIdleConn queues w to receive the next idle connection for w.cm.
// As an optimization hint to the caller, queueForIdleConn reports whether
// it successfully delivered an already-idle connection.
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
    if t.DisableKeepAlives {
        return false
    }

    t.idleMu.Lock()
    defer t.idleMu.Unlock()

这是idleConn的直接体现, 同时, 方法一开始就判断了, 如果Transport结构体的实例如果设置了DisableKeepAlives的值为非0值, 则关闭连接池.

关于客户端连接池设计中的几个重要的参数

DisableKeepAlives

这个在上文中已经说过, 就是客户端Transport实例明确告诉服务端, 是否要开启长连接.

MaxConnsPerHost

代表每个客户端可以在一个host上创建最多几个连接

想了解 httpclient的连接池, 必须去阅读getConn方法, 如下有代码注释

// 这里获取连接池中的空闲链接
if delivered := t.queueForIdleConn(w); delivered {
    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 roundTrip
    t.setReqCanceler(treq.cancelKey, func(error) {})
    return pc, nil
}

cancelc := make(chan error, 1)
t.setReqCanceler(treq.cancelKey, func(err error) { cancelc <- err })

// 如果当前连接池中无空闲链接, 则开始异步创建连接
t.queueForDial(w)

进入 创建连接代码

// 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()
        // 如果MaxConnsPerHost不够, 创建
    if t.MaxConnsPerHost <= 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 + 1
        go 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 作为开发者是最最常用的类库, 其中有一些设计细节是有异于其他http类库的, 我们更需要keep-avlie的设计细节, 关注连接池的使用, 来达到更高的吞吐量, 这些地方优化好, 是可以整体提高我们程序的性能.

标签: none

评论已关闭