Golangでのhttp接続の再利用


82

私は現在、GolangでHTTP投稿を行うときに接続を再利用する方法を見つけるのに苦労しています。

私は次のようなトランスポートとクライアントを作成しました:

// Create a new transport and HTTP client
tr := &http.Transport{}
client := &http.Client{Transport: tr}

次に、このクライアントポインターを、次のように同じエンドポイントに複数の投稿を行うゴルーチンに渡します。

r, err := client.Post(url, "application/json", post)

netstatを見ると、これによりすべての投稿に新しい接続が発生し、多数の同時接続が開かれているように見えます。

この場合、接続を再利用する正しい方法は何ですか?


2
この質問に対する正解は、この複製に投稿されています。Goクライアントプログラムは、TIME_WAIT状態で多くのソケットを生成します
BrentBradburn19年

回答:


96

あなたがいることを確認し、応答が完了するまで読んで、コールClose()

例えば

res, _ := client.Do(req)
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()

繰り返しますが...http.Client接続を確実に再利用するには、次のことを確認してください。

  • 応答が完了するまで読み取ります(つまりioutil.ReadAll(resp.Body)
  • コール Body.Close()

1
同じホストに投稿しています。ただし、私の理解では、MaxIdleConnsPerHostによってアイドル接続が閉じられます。そうではありませんか?
sicr 2013

5
+1、defer res.Body.Close()同様のプログラムを呼び出したが、その部分が実行される前に関数から戻ることがあり(resp.StatusCode != 200たとえば)、多くの開いているファイル記述子がアイドル状態のままになり、最終的にプログラムが強制終了されたため。このスレッドを押すと、コードのその部分に再度アクセスして、自分自身を悩ませました。ありがとう。
sa125 2014

3
興味深い注意点の1つは、読み取りステップが必要かつ十分であるように見えることです。読み取りステップだけでは接続がプールに戻りますが、閉じるだけでは戻りません。接続はTCP_WAITになります。また、json.NewDecoder()を使用してresponse.Bodyを読み取っていたため、問題が発生しましたが、完全には読み取られませんでした。よくわからない場合は、必ずio.Copy(ioutil.Discard、res.Body)を含めてください。
サムラッセル

3
本文が完全に読み取られたかどうかを確認する方法はありますか?ioutil.ReadAll()十分であることが保証されていますio.Copy()か、それとも万が一の場合に備えて、あちこちに電話をかける必要がありますか?
Patrik Iselind 2018

4
私は、ソースコードを見て、そのレスポンスボディ閉じる()すでに身体排水の面倒を見るようだ:github.com/golang/go/blob/...
dr.scre

45

誰かがまだそれを行う方法についての答えを見つけているなら、これは私がそれをしている方法です。

package main

import (
    "bytes"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

var httpClient *http.Client

const (
    MaxIdleConnections int = 20
    RequestTimeout     int = 5
)

func init() {
    httpClient = createHTTPClient()
}

// createHTTPClient for connection re-use
func createHTTPClient() *http.Client {
    client := &http.Client{
        Transport: &http.Transport{
            MaxIdleConnsPerHost: MaxIdleConnections,
        },
        Timeout: time.Duration(RequestTimeout) * time.Second,
    }

    return client
}

func main() {
    endPoint := "https://localhost:8080/doSomething"

    req, err := http.NewRequest("POST", endPoint, bytes.NewBuffer([]byte("Post this data")))
    if err != nil {
        log.Fatalf("Error Occured. %+v", err)
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    response, err := httpClient.Do(req)
    if err != nil && response == nil {
        log.Fatalf("Error sending request to API endpoint. %+v", err)
    }

    // Close the connection to reuse it
    defer response.Body.Close()

    // Let's check if the work actually is done
    // We have seen inconsistencies even when we get 200 OK response
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatalf("Couldn't parse response body. %+v", err)
    }

    log.Println("Response Body:", string(body))    
}

遊び場に行く:http//play.golang.org/p/oliqHLmzSX

要約すると、HTTPクライアントを作成し、それをグローバル変数に割り当ててから、それを使用してリクエストを行うための別のメソッドを作成しています。注意してください

defer response.Body.Close() 

これにより、接続が閉じられ、再利用できるようになります。

これが誰かを助けることを願っています。


1
http.Clientをグローバル変数として使用することは、その変数を使用して関数を呼び出す複数のゴルーチンがある場合、競合状態から安全ですか?
Bart Silverstrim

3
@ bn00dはdefer response.Body.Close()正しいですか?クローズを延期することにより、メイン関数が終了するまで実際に再利用のためにconnをクローズしないため.Close().ReadAll()。の直後に呼び出す必要があります。これはあなたの例では問題のようには見えないかもしれませんが、実際には複数のreqを作成することを示していません。単に、1つのreqを作成してから終了しますが、複数のreqを連続して作成すると、defered以降、.Close()funcが終了するまでは呼び出されません。または...何かが足りないのですか?ありがとう。
mad.meesh 2017

1
@ mad.meesh複数の呼び出しを行う場合(ループ内など)、Body.Close()の呼び出しをクロージャ内にラップするだけです。これにより、データの処理が完了するとすぐに閉じられます。
アントワーヌコッテン2018年

この方法で、リクエストごとに異なるプロキシを設定するにはどうすればよいですか?出来ますか ?
AmirKhoshhal20年

@ bn00dあなたの例は機能していないようです。ループを追加した後でも、各リクエストは新しい接続になります。play.golang.org/p/9Ah_lyfYxgV
ルイスチャン

37

編集:これは、すべてのリクエストに対してトランスポートとクライアントを構築する人々のためのメモです。

Edit2:リンクをgodocに変更しました。

Transport再利用のための接続を保持する構造体です。https://godoc.org/net/http#Transportを参照してください(「デフォルトでは、トランスポートは将来の再利用のために接続をキャッシュします。」)

したがって、リクエストごとに新しいトランスポートを作成すると、毎回新しい接続が作成されます。この場合の解決策は、クライアント間で1つのトランスポートインスタンスを共有することです。


特定のコミットを使用してリンクを提供してください。あなたのリンクはもう正しくありません。
Inanc Gumus 2018

play.golang.org/p/9Ah_lyfYxgVこの例は、トランスポートを1つだけ示していますが、リクエストごとに1つの接続を生成します。何故ですか ?
ルイスチャン

12

IIRC、デフォルトのクライアント接続を再利用します。応答を閉じていますか?

呼び出し元は、resp.Bodyからの読み取りが完了したら、resp.Bodyを閉じる必要があります。resp.Bodyが閉じられていない場合、クライアントの基盤となるRoundTripper(通常はTransport)は、後続の「キープアライブ」要求のためにサーバーへの永続的なTCP接続を再利用できない可能性があります。


こんにちは、返信ありがとうございます。はい、申し訳ありませんが、私もそれを含めるべきでした。r.Body.Close()で接続を閉じています。
sicr 2013

@sicr、サーバーが実際に接続自体を閉じないことを確信していますか?*_WAITつまり、これらの優れた接続は、いずれかの州または次のようなものである可能性があります
kostix 2013

1
@kostix netstatを見ると、ESTABLISHED状態との接続が多数あります。同じ接続が再利用されるのではなく、すべてのPOSTリクエストで新しい接続が生成されているようです。
sicr 2013

@sicr、接続の再利用に関する解決策を見つけましたか?どうもありがとう、ダニエレ
ダニエレB

3

ボディについて

// It is the caller's responsibility to
// close Body. The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.

したがって、TCP接続を再利用する場合は、読み取りが完了するたびにBodyを閉じる必要があります。関数ReadBody(io.ReadCloser)はこのように提案されています。

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "time"
)

func main() {
    req, err := http.NewRequest(http.MethodGet, "https://github.com", nil)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    client := &http.Client{}
    i := 0
    for {
        resp, err := client.Do(req)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        _, _ = readBody(resp.Body)
        fmt.Println("done ", i)
        time.Sleep(5 * time.Second)
    }
}

func readBody(readCloser io.ReadCloser) ([]byte, error) {
    defer readCloser.Close()
    body, err := ioutil.ReadAll(readCloser)
    if err != nil {
        return nil, err
    }
    return body, nil
}

2

別のアプローチinit()は、シングルトンメソッドを使用してhttpクライアントを取得することです。sync.Onceを使用すると、すべてのリクエストで1つのインスタンスのみが使用されるようになります。

var (
    once              sync.Once
    netClient         *http.Client
)

func newNetClient() *http.Client {
    once.Do(func() {
        var netTransport = &http.Transport{
            Dial: (&net.Dialer{
                Timeout: 2 * time.Second,
            }).Dial,
            TLSHandshakeTimeout: 2 * time.Second,
        }
        netClient = &http.Client{
            Timeout:   time.Second * 2,
            Transport: netTransport,
        }
    })

    return netClient
}

func yourFunc(){
    URL := "local.dev"
    req, err := http.NewRequest("POST", URL, nil)
    response, err := newNetClient().Do(req)
    // ...
}


これは、1秒あたり100件のHTTPリクエストを処理するのに最適でした
philipmudenyo20年

0

ここで欠けているのは「ゴルティン」なことです。トランスポートには独自の接続プールがあり、デフォルトではそのプール内の各接続が再利用されます(本体が完全に読み取られて閉じられている場合)が、複数のゴルーチンがリクエストを送信している場合、新しい接続が作成されます(プールにはすべての接続がビジーであり、新しい接続が作成されます) )。これを解決するには、ホストごとの接続の最大数を制限する必要があります:Transport.MaxConnsPerHosthttps://golang.org/src/net/http/transport.go#L205)。

おそらく、セットアップIdleConnTimeoutおよび/またはResponseHeaderTimeout


0

https://golang.org/src/net/http/transport.go#L196

MaxConnsPerHost明示的に設定する必要がありますhttp.ClientTransportTCP接続を再利用しますが、制限する必要がありますMaxConnsPerHost(デフォルトの0は制限がないことを意味します)。

func init() {
    // singleton http.Client
    httpClient = createHTTPClient()
}

// createHTTPClient for connection re-use
func createHTTPClient() *http.Client {
    client := &http.Client{
        Transport: &http.Transport{
            MaxConnsPerHost:     1,
            // other option field
        },
        Timeout: time.Duration(RequestTimeout) * time.Second,
    }

    return client
}

-3

2つの可能な方法があります:

  1. 各リクエストに関連付けられたファイル記述子を内部で再利用および管理するライブラリを使用します。Httpクライアントは内部で同じことを行いますが、開く同時接続の数とリソースの管理方法を制御できます。興味がある場合は、内部でepoll / kqueueを使用して管理するnetpollの実装をご覧ください。

  2. 簡単な方法は、ネットワーク接続をプールする代わりに、ゴルーチン用のワーカープールを作成することです。これは簡単でより良い解決策であり、現在のコードベースを妨げることはなく、小さな変更が必要になります。

リクエストを受け取った後、nPOSTリクエストを行う必要があると仮定しましょう。

ここに画像の説明を入力してください

ここに画像の説明を入力してください

これを実装するには、チャネルを使用できます。

または、単にサードパーティのライブラリを使用することもできます。
いいね:https//github.com/ivpusic/grpool

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.