問題のHTTPタイムアウトの調査を行きます

背景

最新同僚フィードバックは、サービス間のコールのタイムアウト、確率及び発生頻度の現象がピーク期間中比較的高いがあります。ビューのログ関係の観点から呼び出して、2タイムアウトの問題が頻繁にチェーンを呼び出す発生があります。

問題1:HTTP1.1サービスのタイムアウトを使用してBに送信されたサービス要求。

問題2:軽量のhttp-SDKを使用してサービス(内部http2.0)Cは、サービス時間にリクエストを送信します。

エラーメッセージを与えGolang:

Post http://host/v1/xxxx: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

調査にIDを追跡する通知ログではなく、サービス側にいくつかの要求がタイムアウトしたことがわかりました。

いくつかはサービス側にあったが、また、残業しています。

ここで質問2の最初の調査では、以下のプロセスです。

調査

推測

HTTP呼び出し側は、要求タイムアウトが1秒で設定します。

サーバーへの要求もタイムアウトになった理由は次のようになります。

  1. サービス応答が遅い側。いくつかのログによって本当に存在します。

  2. クライアントの呼び出しが990msを取った、唯一の10msのサーバに、これは確かにアウトになります。

サーバのタイムアウトへの要求のための理由は、しないかもしれません。

  1. しかし、CPUのスケジューリングをgolang。CPUモニタすることによって、この可能性を除外

  2. golangネットワークライブラリ原因。捜査の焦点

トラブルシューティングのヒント:

ローカルテストプログラムを書き、C 1000同時通話サービスのテスト環境:

n := 1000
var waitGroutp = sync.WaitGroup{}
waitGroutp.Add(n)
for i := 0; i < n; i++ {
       go func(x int) {
         httpSDK.Request()
     }
}
waitGroutp.Wait()

エラー:

too many open files    // 这个错误是笔者本机ulimit太小的原因,可忽略
net/http: request canceled (Client.Timeout exceeded while awaiting headers)

同時の数は、テストを続行する500に調整し、又は同じミスを報告しました。

接続がタイムアウトしました

あなたが問題を再現できる場合はローカル、一般的に、より良いものを確認してください。

次は、HttpClientをコードに作成され、ソースコードで始まるGolang、これはグローバルのHttpClientの再利用です。

func createHttpClient(host string, tlsArg *TLSConfig) (*http.Client, error) {
    httpClient := &http.Client{
        Timeout: time.Second,
    }
    tlsConfig := &tls.Config{InsecureSkipVerify: true}
    transport := &http.Transport{
        TLSClientConfig:     tlsConfig,
        MaxIdleConnsPerHost: 20,
    }
    http2.ConfigureTransport(transport)
    return httpClient, nil
}
// 使用httpClient
httpClient.Do(req)

ネット/ HTTP / client.go DO方式にスキップ

func (c *Client) do(req *Request) (retres *Response, reterr error) {
    if resp, didTimeout, err = c.send(req, deadline); err != nil {
    }
}

この方法は継続送る、実際の要求は、往復の機能を送信することにより行われます。

func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
     rt.RoundTrip(req) 
}

RT送信関数のパラメータがintefaceを受けている、それはhttp.Transport関数からの往復に移行します。

どちらlog.Println("getConn time", time.Now().Sub(start), x)を作成するには、接続時間のかかるを確認するために、私は追加のログです。

var n int
// roundTrip implements a RoundTripper over HTTP.
func (t *Transport) roundTrip(req *Request) (*Response, error) {
    // 检查是否有注册http2,有的话直接使用http2的RoundTrip
    if t.useRegisteredProtocol(req) {
        altProto, _ := t.altProto.Load().(map[string]RoundTripper)
        if altRT := altProto[scheme]; altRT != nil {
            resp, err := altRT.RoundTrip(req)
            if err != ErrSkipAltProtocol {
                return resp, err
            }
        }
    }
    for {
        //n++
        // start := time.Now()
        pconn, err := t.getConn(treq, cm)
         // log.Println("getConn time", time.Now().Sub(start), x)
        if err != nil {
            t.setReqCanceler(req, nil)
            req.closeBody()
            return nil, err
        }
    }
}

結論:ログの添加が、それは多くの持っている、ダウンを実行getConn time残業を。

疑い

二つの質問があります。

  1. HTTP2接続を再利用しない理由、それは多数の接続を作成しますか?

  2. なぜ接続が遅くなり、作成?

getConnソースに進み、getConn最初のステップは、ここにHTTP2があるので、あなたはそれを無視することはできません、自由な接続を取得することになります。

追加の時間がかかりログを、確認がdialConn時間がかかります。

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) {
   if pc, idleSince := t.getIdleConn(cm); pc != nil {
   }
    //n++
    go func(x int) {
        // start := time.Now()
        // defer func(x int) {
        //  log.Println("getConn dialConn time", time.Now().Sub(start), x)
        // }(n)
        pc, err := t.dialConn(ctx, cm)
        dialc <- dialRes{pc, err}
    }(n)
}

dialConn機能を続行し、2時間のかかる場所があります。

  1. 接続は、3ウェイハンドシェイクを確立しています。

  2. TLSは、時間のかかるハンドシェイク、HTTP2 dialConn元以下のセクションを参照してください。

そしてdialConn機能の各サプリメンタル・ロギングのt.dialのaddTLSの位置。

あなたは時間のかかる後ろの接続TLSで上記のハンドシェイクを、ほぼ1秒を消費し、3ウェイハンドシェイク接続がまだ比較的安定している、見ることができます。

2019/10/23 14:51:41 DialTime 39.511194ms https.Handshake 1.059698795s
2019/10/23 14:51:41 DialTime 23.270069ms https.Handshake 1.064738698s
2019/10/23 14:51:41 DialTime 24.854861ms https.Handshake 1.0405369s
2019/10/23 14:51:41 DialTime 31.345886ms https.Handshake 1.076014428s
2019/10/23 14:51:41 DialTime 26.767644ms https.Handshake 1.084155891s
2019/10/23 14:51:41 DialTime 22.176858ms https.Handshake 1.064704515s
2019/10/23 14:51:41 DialTime 26.871087ms https.Handshake 1.084666172s
2019/10/23 14:51:41 DialTime 33.718771ms https.Handshake 1.084348815s
2019/10/23 14:51:41 DialTime 20.648895ms https.Handshake 1.094335678s
2019/10/23 14:51:41 DialTime 24.388066ms https.Handshake 1.084797011s
2019/10/23 14:51:41 DialTime 34.142535ms https.Handshake 1.092597021s
2019/10/23 14:51:41 DialTime 24.737611ms https.Handshake 1.187676462s
2019/10/23 14:51:41 DialTime 24.753335ms https.Handshake 1.161623397s
2019/10/23 14:51:41 DialTime 26.290747ms https.Handshake 1.173780655s
2019/10/23 14:51:41 DialTime 28.865961ms https.Handshake 1.178235202s

結論:2つ目の質問の答えは、時間のかかるTLSハンドシェイクであります

HTTP2

HTTP2接続を再利用しない理由、それは多数の接続を作成しますか?

以前http.Clientを作成したとき、http2.ConfigureTransportによって(輸送)メソッドは、内部configureTransportを呼び出します。

func configureTransport(t1 *http.Transport) (*Transport, error) {
    // 声明一个连接池
   // noDialClientConnPool 这里很关键,指明连接不需要dial出来的,而是由http1连接升级而来的 
    connPool := new(clientConnPool)
    t2 := &Transport{
        ConnPool: noDialClientConnPool{connPool},
        t1:       t1,
    }
    connPool.t = t2
// 把http2的RoundTripp的方法注册到,http1上transport的altProto变量上。
// 当请求使用http1的roundTrip方法时,检查altProto是否有注册的http2,有的话,则使用
// 前面代码的useRegisteredProtocol就是检测方法
    if err := registerHTTPSProtocol(t1, noDialH2RoundTripper{t2}); err != nil           {
        return nil, err
    }
   // http1.1 升级到http2的后的回调函数,会把连接通过 addConnIfNeeded 函数把连接添加到http2的连接池中
    upgradeFn := func(authority string, c *tls.Conn) http.RoundTripper {
        addr := authorityAddr("https", authority)
        if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil {
            go c.Close()
            return erringRoundTripper{err}
        } else if !used {
            go c.Close()
        }
        return t2
    }
    if m := t1.TLSNextProto; len(m) == 0 {
        t1.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{
            "h2": upgradeFn,
        }
    } else {
        m["h2"] = upgradeFn
    }
    return t2, nil
}

http.Transport-> dialConn中でTLSNextProto。ALTに割り当てられたRoundTripperのHTTP2を返すupgradeFn関数を呼び出します。

RoundTripper内部チェックでhttp.Transportで代替のコール。

func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) {
    pconn := &persistConn{
        t:             t,
    }
    if cm.scheme() == "https" && t.DialTLS != nil {
     // 没有自定义DialTLS方法,不会走到这一步
    } else {
        conn, err := t.dial(ctx, "tcp", cm.addr())
        if err != nil {
            return nil, wrapErr(err)
        }
        pconn.conn = conn
        if cm.scheme() == "https" {
         // addTLS 里进行 tls 握手,也是建立新连接最耗时的地方。
            if err = pconn.addTLS(firstTLSHost, trace); err != nil {
                return nil, wrapErr(err)
            }
        }
    }
    if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" {
        if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok {
            // next 调用注册的升级函数
            return &persistConn{t: t, cacheKey: pconn.cacheKey, alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil
        }
    }
    return pconn, nil
}

結論:

接続されていない場合、この時点で大きな波を要求する場合、複数の接続を作成し、N HTTP1.1、アップグレードと握手、およびTLSハンドシェイクが非常に遅くなる増加に接続されています。

タイムアウトを解決

上記の結論は完全に、接続の再利用の問題を説明していません。加えて、第一のネットワークの理由を接続または切断するように、サービスの正常な動作は、接続が切断されていない、要求された場合、通常の状況下でHTTP2で再接続しなければならないからです。

次のテストを通じ、既存のHTTP2接続を再利用、またはNよりも多くの新しい接続を作成することができます。

sdk.Request()  // 先请求一次,建立好连接,测试是否一直复用连接。
time.Sleep(time.Second)
n := 1000
var waitGroutp = sync.WaitGroup{}
waitGroutp.Add(n)
for i := 0; i < n; i++ {
       go func(x int) {
         sdk.Request()
     }
}
waitGroutp.Wait()

彼らはまだ直接利用されるように、アップグレード疑いにこの時間をHTTP1.1をリードhttp2.Transport

httpClient.Transport = &http2.Transport{
            TLSClientConfig: tlsConfig,
}

変更後、試験には、誤った報告は認められませんでした。

アップグレードモードとダイレクトHTTP2モードとの違いを確認するために。ここに戻ってaddConnCall run関数を呼び出すモードaddConnIfNeeded機能を、アップグレードするには:

func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) {
    cc, err := t.NewClientConn(tc)
}

実行パラメータは、輸送のHTTP2を渡されます。

HTTP1.1全体の説明が作成された後、接続は、接続を構成addConnIfNeeded->ラン> Transport.NewClientConn HTTP2によって、層接続を輸送します。HTTP2 HTTP1.1と本質的にアプリケーション層プロトコル、トランスポート層接続が同じであるからです。

そして、newClientConnをログ追加連結しました。

func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, error) {
    //  log.Println("http2.newClientConn")
}

結論:

アップグレードモードは、経由で話されている前回の調査によると、http2.newClientConnの多くを印刷します。次の簡単なHTTP2モードでは、まれにかかわらず、新しい接続を作成します。

同時接続数

それはHTTP2モードどのような状況下で、新しい接続を作成しますか?

ここでnewClientConn呼び出すHTTP2どのような状況の下で確認してください。バックClientConnPool、HTTP2モードでdialOnMissは、ダイヤルイン> dialClientConn-> newClientConnの呼び出しで、getStartDialLocked真です。

func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) {
    p.mu.Lock()
    for _, cc := range p.conns[addr] {
        if st := cc.idleState(); st.canTakeNewRequest {
            if p.shouldTraceGetConn(st) {
                traceGetConn(req, addr)
            }
            p.mu.Unlock()
            return cc, nil
        }
    }
    if !dialOnMiss {
        p.mu.Unlock()
        return nil, ErrNoCachedConn
    }
    traceGetConn(req, addr)
    call := p.getStartDialLocked(addr)
    p.mu.Unlock()
  }

例接続は、canTakeNewRequestがfalseの場合、それは新しい接続を作成します。だから、この変数を見て来ています:

func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) {
    if cc.singleUse && cc.nextStreamID > 1 {
        return
    }
    var maxConcurrentOkay bool
    if cc.t.StrictMaxConcurrentStreams {
        maxConcurrentOkay = true
    } else {
        maxConcurrentOkay = int64(len(cc.streams)+1) < int64(cc.maxConcurrentStreams)
    }
    st.canTakeNewRequest = cc.goAway == nil && !cc.closed && !cc.closing && maxConcurrentOkay &&
        int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32
    // if st.canTakeNewRequest == false {
    //  log.Println("clientConnPool", cc.maxConcurrentStreams, cc.goAway == nil, !cc.closed, !cc.closing, maxConcurrentOkay, int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32)
    // }
    st.freshConn = cc.nextStreamID == 1 && st.canTakeNewRequest
    return
}

問題に加え、ここで詳細なログを調べるために。ダウンテストし、それが偽であるcanTakeNewRequest、maxConcurrentStreamsスーパーを発見しました。

1000 maxConcurrentStreamsデフォルトでnewClientConnで初期設定HTTP2で:

   maxConcurrentStreams:  1000,     // "infinite", per spec. 1000 seems good enough.

しかし、本当のテストでは、500の同時の新しい接続を作成することを発見しました。場所を設定して、この変数を追求するために存在し続けます。

func (rl *clientConnReadLoop) processSettings(f *SettingsFrame) error {
    case SettingMaxConcurrentStreams:
            cc.maxConcurrentStreams = s.Val
           //log.Println("maxConcurrentStreams", s.Val)
}

テストを実行し、構成上のサービスパスは、値が250であることを見出しました。

結論:サーバーは、同時接続数の単一の接続を制限し、それがスーパーの後に新しい接続を作成します。

サーバーの制限

> ServeTLS-> Serve-> setupHTTP2_Serve-> onceSetNextProtoDefaults_Serve-> onceSetNextProtoDefaults-> http2ConfigureServer - サーバーのフレームワークでは、ダウンとListenAndServeTLS機能を、見つけます。

また、HTTP1.1をサポートすることですWebフレームワークとして、新しい(http2Server)ステートメントをHTTP2をサポートしていますが見つかり、それがどのHTTP2の設定、デフォルトの使用を指定しません。

// Server is an HTTP/2 server.
type http2Server struct {
    // MaxConcurrentStreams optionally specifies the number of
    // concurrent streams that each client may have open at a
    // time. This is unrelated to the number of http.Handler goroutines
    // which may be active globally, which is MaxHandlers.
    // If zero, MaxConcurrentStreams defaults to at least 100, per
    // the HTTP/2 spec's recommendations.
    MaxConcurrentStreams uint32
}   

デフォルトの変数http2defaultMaxStreamsを使用して、このフィールド、HTTP2少なくとも100の推奨標準、golangのコメントから見ると、その値は250です。

真実

上記の手順は、障害の発生源と原因で多くの重要なポイントを記録するために、将来の参照を容易にするために、同様の問題があります。

それを簡素化:

  1. サービスと呼び出し側の使用はHTTP2モード通信にアップグレードHTTP1.1
  2. シングルサービス側http2Serverは、同時接続250の数を制限します
  3. ときに250以上の同時、など1000と、呼び出し側は、750の同時接続を作成します。これらの接続のTLSは時間が長くなりますハンドシェイク。唯一の残業の多くで、その結果、タイムアウト1Sを呼び出します。
  4. これらの接続のいくつかは、サービス残業に当事者ではない、いくつかのサービスに対処するのに十分な時間をパーティーではなくするために、呼び出し側はまた、残業接続を解除し、します。

高並行性の場合、ネットワークから切断すると、このような状況は、伝送につながります。

リトライ

軽量サービスをHTTP-SDK再試行メカニズムを使用して、一時的なエラーが検出されたときに、それが二回再試行します。

Temporary() bool // Is the error temporary?

そして、このタイムアウトエラーは、このようにこの問題が発生した増幅、一時的なエラーに属します。

ソリューション

HTTP2は、モードをアップグレードすることはできません。

httpClient.Transport = &http2.Transport{
            TLSClientConfig: tlsConfig,
}

なぜHTTP2接続、それを大量に作成していないのだろうか?

HTTP2のロックが解除、新しい接続がロックされます作成後に要求した後に、より多くの同時接続、直接多重接続の数よりも、存在していないことがわかったからです。だから、clientConnPool.getStartDialLockedのソースコードでロックケースではありません。

質問1

問題1:HTTP1.1サービスのタイムアウトを使用してBに送信されたサービス要求。

質問1と2の問題の原因の、高い同時実行の場合は来ているように、多数の接続を作成し、接続を作成するには、より遅いので、タイムアウトになります。

この状況は良い解決策ではありません、HTTP2をお勧めします。

あなたは、HTTP2を使用する大規模なMaxIdleConnsPerHostパラメータを転送することができない場合は、この状況を緩和することができます。同時1000年に2つしかない接続を残して、各ホストへのデフォルトHTTP1.1は、998の新しい接続を作成します。

調整の数は、50,100などのシステム条件に応じて調整することができます。

おすすめ

転載: www.cnblogs.com/mushroom/p/11756631.html
おすすめ