Wiederverwendung von http-Verbindungen in Golang

81

Ich habe derzeit Probleme, einen Weg zu finden, um Verbindungen beim Erstellen von HTTP-Posts in Golang wiederzuverwenden.

Ich habe einen Transport und einen Client wie folgt erstellt:

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

Ich übergebe diesen Client-Zeiger dann an eine Goroutine, die mehrere Posts an denselben Endpunkt wie folgt erstellt:

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

Wenn man sich netstat ansieht, scheint dies zu einer neuen Verbindung für jeden Beitrag zu führen, was dazu führt, dass eine große Anzahl gleichzeitiger Verbindungen offen ist.

Was ist in diesem Fall der richtige Weg, um Verbindungen wiederzuverwenden?

sicr
quelle
2
Die richtige Antwort auf diese Frage finden Sie in diesem Duplikat: Go-Client-Programm generiert viele Sockets im Status TIME_WAIT
Brent Bradburn

Antworten:

94

Stellen Sie sicher, dass Sie lesen, bis die Antwort vollständig ist UND anrufen Close().

z.B

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

Nochmals ... Um die http.ClientWiederverwendung der Verbindung sicherzustellen , gehen Sie wie folgt vor:

  • Lesen, bis die Antwort abgeschlossen ist (dh ioutil.ReadAll(resp.Body))
  • Anruf Body.Close()
Matt Self
quelle
1
Ich poste auf demselben Host. Nach meinem Verständnis würde MaxIdleConnsPerHost jedoch dazu führen, dass inaktive Verbindungen geschlossen werden. Ist das nicht der Fall?
Sicr
5
+1, weil ich defer res.Body.Close()ein ähnliches Programm aufgerufen habe , aber gelegentlich von der Funktion zurückgekehrt bin , bevor dieser Teil ausgeführt wurde (wenn resp.StatusCode != 200zum Beispiel), wodurch viele offene Dateideskriptoren im Leerlauf blieben und schließlich mein Programm beendet haben. Wenn ich diesen Thread getroffen habe, habe ich diesen Teil des Codes und des Facepalms selbst noch einmal besucht. Vielen Dank.
Sa125
3
Ein interessanter Hinweis ist, dass der Leseschritt notwendig und ausreichend erscheint. Der Leseschritt allein gibt die Verbindung zum Pool zurück, der Abschluss allein jedoch nicht. Die Verbindung würde in TCP_WAIT enden. Es gab auch Probleme, weil ich einen json.NewDecoder () zum Lesen der Antwort verwendet habe. Body, der sie nicht vollständig gelesen hat. Stellen Sie sicher, dass Sie die io.Copy (ioutil.Discard, res.Body) einschließen, wenn Sie sich nicht sicher sind.
Sam Russell
2
Gibt es eine Möglichkeit zu überprüfen, ob der Körper vollständig gelesen wurde? Ist eine ioutil.ReadAll()garantiert genug oder muss ich immer noch io.Copy()Anrufe überall verteilen, nur für den Fall?
Patrik Iselind
4
Ich habe mir den Quellcode angesehen und es scheint, dass der Antwortkörper Close () sich bereits um die Entleerung des Körpers kümmert: github.com/golang/go/blob/…
dr.scre
44

Wenn noch jemand Antworten dazu findet, mache ich das so.

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))    
}

Go Playground: http://play.golang.org/p/oliqHLmzSX

Zusammenfassend erstelle ich eine andere Methode, um einen HTTP-Client zu erstellen, ihn einer globalen Variablen zuzuweisen und ihn dann zum Erstellen von Anforderungen zu verwenden. Beachten Sie das

defer response.Body.Close() 

Dadurch wird die Verbindung geschlossen und wieder für die Wiederverwendung vorbereitet.

Hoffe das wird jemandem helfen.

bn00d
quelle
1
Ist die Verwendung des http.Client als globale Variable vor Rennbedingungen sicher, wenn mehrere Goroutinen eine Funktion mit dieser Variablen aufrufen?
Bart Silverstrim
3
@ bn00d ist defer response.Body.Close()richtig? Ich frage, weil wir durch Verschieben des Schließens den Conn erst wieder schließen, wenn die Hauptfunktion beendet ist. Daher sollte man einfach .Close()direkt danach aufrufen .ReadAll(). Dies scheint in Ihrem Beispiel kein Problem zu sein. B / C zeigt nicht, dass mehrere Anforderungen gestellt werden. Es wird lediglich eine Anforderung erstellt und dann beendet. Wenn wir jedoch mehrere Anforderungen hintereinander stellen, scheint es, dass seit defered. .Close()wird nicht genannt, bis func beendet wird. oder ... vermisse ich etwas? Vielen Dank.
mad.meesh
1
@ mad.meesh Wenn Sie mehrere Anrufe tätigen (z. B. innerhalb einer Schleife), schließen Sie den Aufruf von Body.Close () einfach in einen Abschluss ein. Auf diese Weise wird er geschlossen, sobald Sie mit der Verarbeitung der Daten fertig sind.
Antoine Cotten
Wie kann ich auf diese Weise für jede Anfrage unterschiedliche Proxys festlegen? Ist es möglich ?
Amir Khoshhal
@ bn00d Dein Beispiel scheint nicht zu funktionieren. Nach dem Hinzufügen einer Schleife führt jede Anforderung weiterhin zu einer neuen Verbindung. play.golang.org/p/9Ah_lyfYxgV
Lewis Chan
37

Bearbeiten: Dies ist eher ein Hinweis für Personen, die für jede Anforderung einen Transport und einen Client erstellen.

Edit2: Link zu Godoc geändert.

Transportist die Struktur, die Verbindungen zur Wiederverwendung enthält; Siehe https://godoc.org/net/http#Transport ("Standardmäßig transportiert Transport Verbindungen für die zukünftige Wiederverwendung zwischen.")

Wenn Sie also für jede Anforderung einen neuen Transport erstellen, werden jedes Mal neue Verbindungen erstellt. In diesem Fall besteht die Lösung darin, die eine Transportinstanz zwischen Clients zu teilen.

DrJosh9000
quelle
Bitte geben Sie Links an, indem Sie das spezifische Commit verwenden. Ihr Link ist nicht mehr korrekt.
Inanc Gumus
play.golang.org/p/9Ah_lyfYxgV Dieses Beispiel zeigt nur einen Transport, aber es entsteht immer noch eine Verbindung pro Anforderung. Warum das ?
Lewis Chan
12

IIRC, der Standardclient, verwendet Verbindungen wieder. Schließen Sie die Antwort ?

Anrufer sollten resp.Body schließen, wenn sie mit dem Lesen fertig sind. Wenn resp.Body nicht geschlossen ist, kann der zugrunde liegende RoundTripper des Clients (normalerweise Transport) eine dauerhafte TCP-Verbindung zum Server möglicherweise nicht für eine nachfolgende "Keep-Alive" -Anforderung wiederverwenden.

zzzz
quelle
Hallo, danke für die Antwort. Ja, tut mir leid, ich hätte das auch einschließen sollen. Ich schließe die Verbindung mit r.Body.Close ().
Sicr
@sicr, sind Sie sicher, dass der Server die Verbindungen selbst nicht schließt? Ich meine, diese hervorragenden Verbindungen könnten in einem der *_WAITStaaten oder so ähnlich sein
kostix
1
@kostix Ich sehe eine große Anzahl von Verbindungen mit dem Status ESTABLISHED, wenn ich mir netstat anschaue. Es scheint, dass bei jeder POST-Anforderung eine neue Verbindung erzeugt wird, anstatt dass dieselbe Verbindung wiederverwendet wird.
Sicr
@sicr, haben Sie eine Lösung für die Wiederverwendung von Verbindungen gefunden? Vielen Dank, Daniele
Daniele B
3

über Körper

// 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.

Wenn Sie also TCP-Verbindungen wiederverwenden möchten, müssen Sie Body jedes Mal nach dem vollständigen Lesen schließen. Eine Funktion ReadBody (io.ReadCloser) wird wie folgt vorgeschlagen.

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
}
Billy Yuan
quelle
2

Ein anderer Ansatz init()besteht darin, eine Singleton-Methode zu verwenden, um den http-Client abzurufen. Mit sync.Once können Sie sicher sein, dass für alle Ihre Anforderungen nur eine Instanz verwendet wird.

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)
    // ...
}

Cyaconi
quelle
Dies funktionierte perfekt für mich bei der Bearbeitung von 100 HTTP-Anfragen pro Sekunde
philip mudenyo
0

Der fehlende Punkt hier ist die "Goroutine" Sache. Transport hat einen eigenen Verbindungspool. Standardmäßig wird jede Verbindung in diesem Pool wiederverwendet (wenn der Text vollständig gelesen und geschlossen ist). Wenn jedoch mehrere Goroutinen Anforderungen senden, werden neue Verbindungen erstellt (der Pool hat alle Verbindungen besetzt und erstellt neue ). Um dies zu lösen, müssen Sie die maximale Anzahl von Verbindungen pro Host begrenzen: Transport.MaxConnsPerHost( https://golang.org/src/net/http/transport.go#L205 ).

Wahrscheinlich möchten Sie auch einrichten IdleConnTimeoutund / oder ResponseHeaderTimeout.

Fulldump
quelle
0

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

Sie sollten MaxConnsPerHostexplizit auf Ihre setzen http.Client. Transportverwendet die TCP-Verbindung wieder, aber Sie sollten die einschränken MaxConnsPerHost(Standard 0 bedeutet keine Begrenzung).

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
}
yeqown
quelle
-3

Es gibt zwei Möglichkeiten:

  1. Verwenden Sie eine Bibliothek, die die mit den einzelnen Anforderungen verknüpften Dateideskriptoren intern wiederverwendet und verwaltet. Der HTTP-Client macht intern dasselbe, aber dann haben Sie die Kontrolle darüber, wie viele gleichzeitige Verbindungen geöffnet werden sollen und wie Sie Ihre Ressourcen verwalten. Wenn Sie interessiert sind, schauen Sie sich die Netpoll-Implementierung an, die intern epoll / kqueue verwendet, um sie zu verwalten.

  2. Am einfachsten wäre es, anstatt Netzwerkverbindungen zu bündeln, einen Arbeiterpool für Ihre Goroutinen zu erstellen. Dies wäre eine einfache und bessere Lösung, die Ihre aktuelle Codebasis nicht behindert und geringfügige Änderungen erfordert.

Nehmen wir an, Sie müssen n POST-Anfragen stellen, nachdem Sie eine Anfrage erhalten haben.

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

Sie könnten Kanäle verwenden, um dies zu implementieren.

Oder Sie können einfach Bibliotheken von Drittanbietern verwenden.
Gefällt mir : https://github.com/ivpusic/grpool

Prakhar Agnihotri
quelle