go net-http 5 timeout

日志报错:

 can't connect to remote host (123.125.14.188): Address not avaibal

根据日志查看对应代码

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    var netClient = &http.Client{Transport: tr, Timeout: time.Second * 120}

抓包显示tcp层一直在keepalive,

根据之前的文章:https://www.cnblogs.com/codestack/p/18218899  https://www.cnblogs.com/codestack/p/17980027

没有设置tcp层的idle timeout,代码修改这样即可

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        IdleConnTimeout: 60 * time.Second,
    }
    var netClient = &http.Client{Transport: tr, Timeout: time.Second * 120}

 

  • http.Client.Timeout:http.Client.Timeout涵盖了HTTP请求从连接建立,请求发送,接收回应,重定向,以及读取http.Response.Body的整个生命周期的超时时间
  • http.Client.IdleConnTimeout:HTTP keep-alives超时时间(http.persistConn在使用完毕放入空闲连接池时,会重置http.Client.IdleConnTimeout的HTTP keep-alives超时时间,如果在这段时间内还没有被复用,则会触发超时,执行关闭逻辑;http.persistConn在从空闲连接池中被捞出来复用时,会设置为非空闲状态,不会触发实际的超时操作)
  • net.Dialer.Timeout:底层连接建立的超时时间
  • net.Dialer.KeepAlive:设置TCP的keepalive参数(syscall.TCP_KEEPINTVL以及syscall.TCP_KEEPIDLE)

  

// tryPutIdleConn adds pconn to the list of idle persistent connections awaiting
// a new request.
// If pconn is no longer needed or not in a good state, tryPutIdleConn returns
// an error explaining why it wasn't registered.
// tryPutIdleConn does not close pconn. Use putOrCloseIdleConn instead for that.
func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
    if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 {
        return errKeepAlivesDisabled
    }
    if pconn.isBroken() {
        return errConnBroken
    }
    pconn.markReused()

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

    // HTTP/2 (pconn.alt != nil) connections do not come out of the idle list,
    // because multiple goroutines can use them simultaneously.
    // If this is an HTTP/2 connection being “returned,” we're done.
    if pconn.alt != nil && t.idleLRU.m[pconn] != nil {
        return nil
    }

    // Deliver pconn to goroutine waiting for idle connection, if any.
    // (They may be actively dialing, but this conn is ready first.
    // Chrome calls this socket late binding.
    // See https://insouciant.org/tech/connection-management-in-chromium/.)
    key := pconn.cacheKey
    if q, ok := t.idleConnWait[key]; ok {
        done := false
        if pconn.alt == nil {
            // HTTP/1.
            // Loop over the waiting list until we find a w that isn't done already, and hand it pconn.
            for q.len() > 0 {
                w := q.popFront()
                if w.tryDeliver(pconn, nil) {
                    done = true
                    break
                }
            }
        } else {
            // HTTP/2.
            // Can hand the same pconn to everyone in the waiting list,
            // and we still won't be done: we want to put it in the idle
            // list unconditionally, for any future clients too.
            for q.len() > 0 {
                w := q.popFront()
                w.tryDeliver(pconn, nil)
            }
        }
        if q.len() == 0 {
            delete(t.idleConnWait, key)
        } else {
            t.idleConnWait[key] = q
        }
        if done {
            return nil
        }
    }

    if t.closeIdle {
        return errCloseIdle
    }
    if t.idleConn == nil {
        t.idleConn = make(map[connectMethodKey][]*persistConn)
    }
    idles := t.idleConn[key]
    if len(idles) >= t.maxIdleConnsPerHost() {
        return errTooManyIdleHost
    }
    for _, exist := range idles {
        if exist == pconn {
            log.Fatalf("dup idle pconn %p in freelist", pconn)
        }
    }
    t.idleConn[key] = append(idles, pconn)
    t.idleLRU.add(pconn)
    if t.MaxIdleConns != 0 && t.idleLRU.len() > t.MaxIdleConns {
        oldest := t.idleLRU.removeOldest()
        oldest.close(errTooManyIdle)
        t.removeIdleConnLocked(oldest)
    }

    // Set idle timer, but only for HTTP/1 (pconn.alt == nil).
    // The HTTP/2 implementation manages the idle timer itself
    // (see idleConnTimeout in h2_bundle.go).
    if t.IdleConnTimeout > 0 && pconn.alt == nil {
        if pconn.idleTimer != nil {
            pconn.idleTimer.Reset(t.IdleConnTimeout)
        } else {
            pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
        }
    }
    pconn.idleAt = time.Now()
    return nil
}
View Code

 

  在一次请求(send http.Request以及read http.Response.Body)之后,http.persistConn.readLoop会执行tryPutIdleConn操作 tryPutIdleConn首先查看idleConnWait(map[connectMethodKey]wantConnQueue)中是否存在需要http.persistConn对应host的连接。

如果存在,则将该连接http.persistConn赋予http.wantConn,同时将http.wantConn从Transport.idleConnWait中移除,并返回

如果不存在,则将http.persistConn添加到http.Transport.idleConn(map[connectMethodKey][]*persistConn)空闲连接池中(不超过MaxIdleConns以及MaxIdleConnsPerHost的情况下)

最后设置http.persistConn空闲超时时间(HTTP keep-alives时间):

 

// Set idle timer, but only for HTTP/1 (pconn.alt == nil).
// The HTTP/2 implementation manages the idle timer itself
// (see idleConnTimeout in h2_bundle.go).
if t.IdleConnTimeout > 0 && pconn.alt == nil {
    if pconn.idleTimer != nil {
        pconn.idleTimer.Reset(t.IdleConnTimeout)
    } else {
        pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
    }
}
pconn.idleAt = time.Now()

...
// closeConnIfStillIdle closes the connection if it's still sitting idle.
// This is what's called by the persistConn's idleTimer, and is run in its
// own goroutine.
func (pc *persistConn) closeConnIfStillIdle() {
    t := pc.t
    t.idleMu.Lock()
    defer t.idleMu.Unlock()
    if _, ok := t.idleLRU.m[pc]; !ok {
        // Not idle.
        return
    }
    t.removeIdleConnLocked(pc)
    pc.close(errIdleConnTimeout)
}

// t.idleMu must be held.
func (t *Transport) removeIdleConnLocked(pconn *persistConn) {
    if pconn.idleTimer != nil {
        pconn.idleTimer.Stop()
    }
    t.idleLRU.remove(pconn)
    key := pconn.cacheKey
    pconns := t.idleConn[key]
    switch len(pconns) {
    case 0:
        // Nothing
    case 1:
        if pconns[0] == pconn {
            delete(t.idleConn, key)
        }
    default:
        for i, v := range pconns {
            if v != pconn {
                continue
            }
            // Slide down, keeping most recently-used
            // conns at the end.
            copy(pconns[i:], pconns[i+1:])
            t.idleConn[key] = pconns[:len(pconns)-1]
            break
        }
    }
}

在http.Transport.IdleConnTimeout时间内,如果http.persistConn一直没有使用,则会触发超时逻辑,将连接从http.Transport.idleConn连接池中剔除;同时删除http.persistConn对应的底层TCP连接

  这里如果说http.persistConn从连接池中被再次使用了,则不会触发超时逻辑,如下:

// 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.Request
    trace := treq.trace
    ctx := 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 {
        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(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)

    // Wait for completion or cancellation.
    select {
    case <-w.ready:
        // Trace success but only for HTTP/1.
        // HTTP/2 calls trace.GotConn itself.
        if w.pc != nil && w.pc.alt == nil && trace != nil && trace.GotConn != nil {
            trace.GotConn(httptrace.GotConnInfo{Conn: w.pc.conn, Reused: w.pc.isReused()})
        }
        if w.err != nil {
            // If the request has been cancelled, that's probably
            // what caused w.err; if so, prefer to return the
            // cancellation error (see golang.org/issue/16049).
            select {
            case <-req.Cancel:
                return nil, errRequestCanceledConn
            case <-req.Context().Done():
                return nil, req.Context().Err()
            case err := <-cancelc:
                if err == errRequestCanceled {
                    err = errRequestCanceledConn
                }
                return nil, err
            default:
                // return below
            }
        }
        return w.pc, w.err
    case <-req.Cancel:
        return nil, errRequestCanceledConn
    case <-req.Context().Done():
        return nil, req.Context().Err()
    case err := <-cancelc:
        if err == errRequestCanceled {
            err = errRequestCanceledConn
        }
        return nil, err
    }
}

// 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()

    // Stop closing connections that become idle - we might want one.
    // (That is, undo the effect of t.CloseIdleConnections.)
    t.closeIdle = false

    if w == nil {
        // Happens in test hook.
        return false
    }

    // Look for most recently-used idle connection.
    if list, ok := t.idleConn[w.key]; ok {
        stop := false
        delivered := false
        for len(list) > 0 && !stop {
            pconn := list[len(list)-1]
            if pconn.isBroken() {
                // persistConn.readLoop has marked the connection broken,
                // but Transport.removeIdleConn has not yet removed it from the idle list.
                // Drop on floor on behalf of Transport.removeIdleConn.
                list = list[:len(list)-1]
                continue
            }
            delivered = w.tryDeliver(pconn, nil)
            if delivered {
                if pconn.alt != nil {
                    // HTTP/2: multiple clients can share pconn.
                    // Leave it in the list.
                } else {
                    // HTTP/1: only one client can use pconn.
                    // Remove it from the list.  如果http.persistConn从空闲连接池中被捞了出来复用,则会执行:t.idleLRU.remove(pconn),将persistConn从http.Transport.idleLRU中剔除(表示不空闲了)
          //这样当timer时间触发(HTTP keep-alives超时)调用closeConnIfStillIdle时就会直接返回,不会进行实际性的操作(空操作)
t.idleLRU.remove(pconn) list = list[:len(list)-1] } } stop = true } if len(list) > 0 { t.idleConn[w.key] = list } else { delete(t.idleConn, w.key) } if stop { return delivered } } // Register to receive next connection that becomes idle. if t.idleConnWait == nil { t.idleConnWait = make(map[connectMethodKey]wantConnQueue) } q := t.idleConnWait[w.key] q.cleanFront() q.pushBack(w) t.idleConnWait[w.key] = q return false }

  如上所示,如果http.persistConn从空闲连接池中被捞了出来复用,则会执行:t.idleLRU.remove(pconn),将persistConn从http.Transport.idleLRU中剔除(表示不空闲了)

 http.persistConn在使用完毕放入空闲连接池时,会重置http.Client.IdleConnTimeout的HTTP keep-alives超时时间,如果在这段时间内还没有被复用,则会触发超时,执行关闭逻辑

 

posted @ 2024-07-22 17:41  codestacklinuxer  阅读(6)  评论(0编辑  收藏  举报