Ponowne wykorzystanie połączeń http w Golang

82

Obecnie staram się znaleźć sposób na ponowne wykorzystanie połączeń podczas tworzenia postów HTTP w Golang.

Stworzyłem transport i klienta takiego:

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

Następnie przekazuję ten wskaźnik klienta do goroutine, który tworzy wiele postów do tego samego punktu końcowego, tak jak poniżej:

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

Patrząc na netstat, wydaje się, że powoduje to nowe połączenie dla każdego posta, co powoduje otwarcie dużej liczby równoczesnych połączeń.

Jaki jest właściwy sposób ponownego wykorzystania połączeń w tym przypadku?

sicr
źródło
2
Prawidłowa odpowiedź na to pytanie jest zamieszczona w tym dwóch egzemplarzach: Program klienta Go generuje dużo do gniazd w stanie CZAS_OCZEKIWANIA
Brent Bradburn

Odpowiedzi:

96

Upewnij się, że czytasz, aż odpowiedź będzie kompletna ORAZ zadzwoń Close().

na przykład

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

Ponownie ... Aby zapewnić http.Clientponowne użycie połączenia, pamiętaj:

  • Czytaj do zakończenia odpowiedzi (tj. ioutil.ReadAll(resp.Body))
  • Połączenie Body.Close()
Matt Self
źródło
1
Piszę do tego samego hosta. Jednak rozumiem, że MaxIdleConnsPerHost spowoduje zamknięcie bezczynnych połączeń. Czy tak nie jest?
sicr
5
+1, ponieważ wywołałem defer res.Body.Close()podobny program, ale od czasu do czasu wracałem z funkcji, zanim ta część została wykonana ( resp.StatusCode != 200na przykład jeśli ), co pozostawiło wiele otwartych deskryptorów plików bezczynnych i ostatecznie zabiło mój program. Kliknięcie tego wątku sprawiło, że ponownie wróciłem do tej części kodu i sam facepalm. dzięki.
sa125
3
jedną interesującą uwagą jest to, że krok czytania wydaje się być konieczny i wystarczający. Sam krok odczytu przywróci połączenie do puli, ale samo zamknięcie nie zwróci; połączenie zakończy się w TCP_WAIT. Wpadłem też w kłopoty, ponieważ używałem json.NewDecoder () do odczytania odpowiedzi.Body, która nie w pełni ją odczytała. Jeśli nie masz pewności, pamiętaj o dołączeniu io.Copy (ioutil.Discard, res.Body).
Sam Russell
3
Czy istnieje sposób sprawdzenia, czy treść została w całości przeczytana? Czy ioutil.ReadAll()na pewno wystarczy, czy nadal muszę rozsiewać io.Copy()telefony w każdym miejscu, na wszelki wypadek?
Patrik Iselind
4
Spojrzałem na kod źródłowy i wygląda na to, że treść
dr.scre
45

Jeśli ktoś nadal znajduje odpowiedzi, jak to zrobić, to właśnie ja to robię.

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

Podsumowując, tworzę inną metodę tworzenia klienta HTTP i przypisywania go do zmiennej globalnej, a następnie używam jej do wykonywania żądań. Zanotuj

defer response.Body.Close() 

Spowoduje to zamknięcie połączenia i ponowne ustawienie go w stan gotowości do ponownego użycia.

Mam nadzieję, że to komuś pomoże.

bn00d
źródło
1
Czy używanie http.Client jako zmiennej globalnej jest bezpieczne przed warunkami wyścigu, jeśli istnieje wiele goroutines wywołujących funkcję używającą tej zmiennej?
Bart Silverstrim
3
@ bn00d jest defer response.Body.Close()poprawne? pytam, ponieważ odraczając zamknięcie, nie zamkniemy połączenia do ponownego użycia, dopóki główna funkcja nie zostanie zakończona, dlatego należy po prostu wywołać .Close()bezpośrednio po .ReadAll(). to może nie wydawać się problemem w twoim przykładzie b / c tak naprawdę nie demonstruje tworzenia wielu żądań, po prostu tworzy jedno żądanie, a następnie wychodzi, ale gdybyśmy mieli wykonać kilka żądań jeden po drugim, wydawałoby się, że skoro defered, .Close()nie będą nazywane do momentu wyjścia funkcji. czy ... czy coś mi brakuje? dzięki.
mad.meesh
1
@ mad.meesh jeśli wykonujesz wiele wywołań (np. wewnątrz pętli), po prostu zawiń wywołanie do Body.Close () wewnątrz zamknięcia, w ten sposób zostanie zamknięte, gdy tylko skończysz przetwarzanie danych.
Antoine Cotten
Jak mogę w ten sposób ustawić różne proxy dla każdego żądania? Czy to możliwe ?
Amir Khoshhal
@ bn00d Twój przykład nie działa. Po dodaniu pętli każde żądanie nadal skutkuje nowym połączeniem. play.golang.org/p/9Ah_lyfYxgV
Lewis Chan
37

Edycja: jest to bardziej uwaga dla osób, które konstruują transport i klienta dla każdego żądania.

Edit2: Zmieniono link do godoc.

Transportjest strukturą przechowującą połączenia do ponownego wykorzystania; zobacz https://godoc.org/net/http#Transport („Domyślnie Transport buforuje połączenia do ponownego wykorzystania w przyszłości”).

Jeśli więc utworzysz nowy transport dla każdego żądania, za każdym razem utworzy on nowe połączenia. W tym przypadku rozwiązaniem jest udostępnienie jednej instancji Transport między klientami.

DrJosh9000
źródło
Podaj linki, używając konkretnego zatwierdzenia. Twój link nie jest już poprawny.
Inanc Gumus
play.golang.org/p/9Ah_lyfYxgV ten przykład pokazuje tylko jeden transport, ale nadal tworzy jedno połączenie na żądanie. Dlaczego ?
Lewis Chan
12

IIRC, domyślny klient robi ponownego wykorzystania połączeń. Zamykasz odpowiedź ?

Dzwoniący powinni zamknąć lub zamknąć ciało po zakończeniu czytania. Jeśli odpowiednie ciało nie zostanie zamknięte, bazowy RoundTripper klienta (zazwyczaj Transport) może nie być w stanie ponownie użyć trwałego połączenia TCP z serwerem dla kolejnego żądania „utrzymywania aktywności”.

zzzz
źródło
Cześć, dzięki za odpowiedź. Tak, przepraszam, że powinienem to również uwzględnić. Zamykam połączenie z r.Body.Close ().
sicr
@sicr, czy jesteś pewien, że serwer w rzeczywistości sam nie zamyka połączeń? Chodzi mi o to, że te wybitne połączenia mogą być w jednym ze *_WAITstanów lub coś w tym rodzaju
kostix
1
@kostix Patrząc na netstat, widzę dużą liczbę połączeń ze stanem ESTABLISHED. Wygląda na to, że nowe połączenie jest tworzone przy każdym żądaniu POST, w przeciwieństwie do tego samego połączenia, które jest ponownie używane.
sicr
@sicr, czy znalazłeś rozwiązanie dotyczące ponownego wykorzystywania połączenia? wielkie dzięki, Daniele
Daniele B
3

o ciele

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

Więc jeśli chcesz ponownie używać połączeń TCP, musisz za każdym razem zamykać Body po zakończeniu odczytu. W ten sposób sugeruje się funkcję 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
}
Billy Yuan
źródło
2

Innym podejściem init()jest użycie metody singleton w celu pobrania klienta http. Używając sync.Once możesz mieć pewność, że tylko jedno wystąpienie będzie używane we wszystkich Twoich żądaniach.

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
źródło
To zadziałało idealnie dla mnie, obsługując 100 żądań HTTP per seond
philip mudenyo
0

Brakującym punktem jest tutaj kwestia „gorutyny”. Transport ma własną pulę połączeń, domyślnie każde połączenie w tej puli jest ponownie wykorzystywane (jeśli treść jest w pełni odczytana i zamknięta), ale jeśli kilka goroutin wysyła żądania, zostaną utworzone nowe połączenia (pula ma wszystkie połączenia zajęte i utworzy nowe ). Aby rozwiązać ten problem, musisz ograniczyć maksymalną liczbę połączeń na hosta: Transport.MaxConnsPerHost( https://golang.org/src/net/http/transport.go#L205 ).

Prawdopodobnie chcesz również skonfigurować IdleConnTimeouti / lub ResponseHeaderTimeout.

Fulldump
źródło
0

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

powinieneś ustawić MaxConnsPerHostjawnie na swoje http.Client. Transportponownie wykorzystuje połączenie TCP, ale należy ograniczyć MaxConnsPerHost(domyślnie 0 oznacza brak ograniczeń).

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
źródło
-3

Istnieją dwa możliwe sposoby:

  1. Użyj biblioteki, która wewnętrznie ponownie wykorzystuje i zarządza deskryptorami plików, powiązanymi z każdym żądaniem. Klient HTTP robi to samo wewnętrznie, ale wtedy masz kontrolę nad tym, ile równoczesnych połączeń chcesz otworzyć i jak zarządzać swoimi zasobami. Jeśli jesteś zainteresowany, spójrz na implementację netpoll, która wewnętrznie używa epoll / kqueue do zarządzania nimi.

  2. Najprościej byłoby, zamiast gromadzić połączenia sieciowe, utworzyć pulę pracowników dla swoich gorutyn. Byłoby to łatwe i lepsze rozwiązanie, które nie utrudniałoby pracy z obecną bazą kodu i wymagałoby niewielkich zmian.

Załóżmy, że po otrzymaniu żądania musisz wysłać n żądania POST.

wprowadź opis obrazu tutaj

wprowadź opis obrazu tutaj

Możesz użyć kanałów, aby to zaimplementować.

Lub po prostu możesz skorzystać z bibliotek innych firm.
Na przykład: https://github.com/ivpusic/grpool

Prakhar Agnihotri
źródło