http的keepalive在go中的设计与实现
写在开始:
在早期的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的设计细节, 关注连接池的使用, 来达到更高的吞吐量, 这些地方优化好, 是可以整体提高我们程序的性能.
评论已关闭