Reutilizar conexiones http en Golang

81

Actualmente estoy luchando por encontrar una manera de reutilizar las conexiones cuando hago publicaciones HTTP en Golang.

He creado un transporte y un cliente así:

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

Luego paso este puntero de cliente a una goroutine que está haciendo múltiples publicaciones en el mismo punto final así:

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

Al observar netstat, esto parece estar dando como resultado una nueva conexión para cada publicación, lo que resulta en una gran cantidad de conexiones simultáneas abiertas.

¿Cuál es la forma correcta de reutilizar las conexiones en este caso?

sicr
fuente
2
La respuesta correcta a esta pregunta se publica en este duplicado: El programa cliente Go genera muchos sockets en el estado TIME_WAIT
Brent Bradburn

Respuestas:

94

Asegúrese de leer hasta que la respuesta esté completa Y llame Close().

p.ej

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

Nuevamente ... Para garantizar la http.Clientreutilización de la conexión, asegúrese de:

  • Leer hasta que la respuesta esté completa (es decir ioutil.ReadAll(resp.Body))
  • Llamada Body.Close()
Matt Self
fuente
1
Estoy publicando en el mismo anfitrión. Sin embargo, tengo entendido que MaxIdleConnsPerHost daría como resultado el cierre de conexiones inactivas. No es ese el caso?
sicr
5
+1, porque llamé defer res.Body.Close()a un programa similar, pero terminé volviendo de la función ocasionalmente antes de que se ejecutara esa parte (si resp.StatusCode != 200, por ejemplo), lo que dejó muchos descriptores de archivos abiertos inactivos y finalmente mató mi programa. Golpear este hilo me hizo volver a visitar esa parte del código y hacerme facepalm. Gracias.
sa125
3
una nota interesante es que el paso de lectura parece ser necesario y suficiente. El paso de lectura solo devolverá la conexión al grupo, pero el cierre solo no lo hará; la conexión terminaría en TCP_WAIT. También tuve problemas porque estaba usando un json.NewDecoder () para leer el response.Body, que no lo leyó completamente. Asegúrese de incluir io.Copy (ioutil.Discard, res.Body) si no está seguro.
Sam Russell
2
¿Existe alguna forma de comprobar si el cuerpo se ha leído por completo? ¿Se ioutil.ReadAll()garantiza que será suficiente o todavía necesito esparcir io.Copy()llamadas por todos lados, por si acaso?
Patrik Iselind
4
Miré el código fuente y parece que el cuerpo de respuesta Close () ya se encarga de drenar el cuerpo: github.com/golang/go/blob/…
dr.scre
44

Si alguien todavía encuentra respuestas sobre cómo hacerlo, así es como lo estoy haciendo.

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

Ir al patio de juegos: http://play.golang.org/p/oliqHLmzSX

En resumen, estoy creando un método diferente para crear un cliente HTTP y asignarlo a una variable global y luego usarlo para realizar solicitudes. Nota la

defer response.Body.Close() 

Esto cerrará la conexión y la preparará para su reutilización nuevamente.

Espero que esto ayude a alguien.

bn00d
fuente
1
¿El uso de http.Client como variable global está a salvo de las condiciones de carrera si hay múltiples goroutines llamando a una función usando esa variable?
Bart Silverstrim
3
@ bn00d es defer response.Body.Close()correcto? pregunto porque al aplazar el cierre no cerraremos la conexión para su reutilización hasta que la función principal salga, por lo que simplemente se debe llamar .Close()directamente después .ReadAll(). esto puede no parecer un problema en su ejemplo porque en realidad no demuestra hacer múltiples req, simplemente hace un req y luego sale, pero si tuviéramos que hacer varios req seguidos, parecería que desde defered, .Close()no se llamará hasta que la función salga. ¿O me estoy perdiendo algo? Gracias.
mad.meesh
1
@ mad.meesh si realiza varias llamadas (por ejemplo, dentro de un bucle), simplemente envuelva la llamada a Body.Close () dentro de un cierre, de esta manera se cerrará tan pronto como haya terminado de procesar los datos.
Antoine Cotten
¿Cómo puedo configurar diferentes proxies para cada solicitud de esta manera? Es posible ?
Amir Khoshhal
@ bn00d Parece que tu ejemplo no funciona. Después de agregar un bucle, cada solicitud da como resultado una nueva conexión. play.golang.org/p/9Ah_lyfYxgV
Lewis Chan
37

Editar: Esto es más una nota para las personas que construyen un Transporte y un Cliente para cada solicitud.

Edit2: enlace cambiado a godoc.

Transportes la estructura que contiene las conexiones para su reutilización; consulte https://godoc.org/net/http#Transport ("De forma predeterminada, el transporte almacena en caché las conexiones para reutilizarlas en el futuro").

Entonces, si crea un nuevo transporte para cada solicitud, creará nuevas conexiones cada vez. En este caso, la solución es compartir la única instancia de transporte entre clientes.

DrJosh9000
fuente
Proporcione enlaces utilizando el compromiso específico. Tu enlace ya no es correcto.
Inanc Gumus
play.golang.org/p/9Ah_lyfYxgV este ejemplo muestra solo un transporte, pero aún genera una conexión por solicitud. Porqué es eso ?
Lewis Chan
12

IIRC, el cliente predeterminado hace conexiones de reutilización. ¿Estás cerrando la respuesta ?

Las personas que llaman deben cerrar el cuerpo cuando terminen de leerlo. Si resp. Body no está cerrado, es posible que el RoundTripper subyacente del Cliente (normalmente Transporte) no pueda reutilizar una conexión TCP persistente al servidor para una solicitud subsiguiente de "mantener vivo".

zzzz
fuente
Hola, gracias por la respuesta. Sí, lo siento, debería haber incluido eso también. Estoy cerrando la conexión con r.Body.Close ().
sicr
@sicr, ¿está seguro de que el servidor en realidad no cierra las conexiones? Quiero decir, estas conexiones sobresalientes podrían estar en uno de los *_WAITestados o algo así
kostix
1
@kostix Veo una gran cantidad de conexiones con el estado ESTABLECIDO al mirar netstat. Parece que se genera una nueva conexión en cada solicitud POST en lugar de reutilizar la misma conexión.
sicr
@sicr, ¿encontró una solución sobre la reutilización de la conexión? muchas gracias, Daniele
Daniele B
3

sobre el cuerpo

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

Entonces, si desea reutilizar las conexiones TCP, debe cerrar Body cada vez que se completa la lectura. Se sugiere una función ReadBody (io.ReadCloser) así.

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
fuente
2

Otro enfoque init()es utilizar un método singleton para obtener el cliente http. Al usar sync.Once, puede estar seguro de que solo se usará una instancia en todas sus solicitudes.

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
fuente
Esto funcionó perfectamente para mí, manejando 100 solicitudes HTTP por segundo
philip mudenyo
0

El punto que falta aquí es la cosa de "goroutine". El transporte tiene su propio grupo de conexiones, de forma predeterminada, cada conexión en ese grupo se reutiliza (si el cuerpo se lee y cierra por completo), pero si varias rutinas están enviando solicitudes, se crearán nuevas conexiones (el grupo tiene todas las conexiones ocupadas y creará otras nuevas ). Para resolver eso, deberá limitar el número máximo de conexiones por host: Transport.MaxConnsPerHost( https://golang.org/src/net/http/transport.go#L205 ).

Probablemente también desee configurar IdleConnTimeouty / o ResponseHeaderTimeout.

Fulldump
fuente
0

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

debe establecer MaxConnsPerHostexplícitamente su http.Client. Transportreutiliza la conexión TCP, pero debe limitar la MaxConnsPerHost(el valor predeterminado 0 significa que no hay límite).

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
fuente
-3

Hay dos formas posibles:

  1. Utilice una biblioteca que reutilice y administre internamente los descriptores de archivos asociados con cada solicitud. Http Client hace lo mismo internamente, pero entonces usted tendría el control sobre cuántas conexiones simultáneas abrir y cómo administrar sus recursos. Si está interesado, mire la implementación de netpoll, que internamente usa epoll / kqueue para administrarlos.

  2. La más fácil sería, en lugar de agrupar las conexiones de red, crear un grupo de trabajadores para sus rutinas gordas. Esta sería una solución fácil y mejor, que no obstaculizaría su base de código actual y requeriría cambios menores.

Supongamos que necesita realizar una solicitud POST, después de recibir una solicitud.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

Podrías usar canales para implementar esto.

O simplemente puede usar bibliotecas de terceros.
Me gusta: https://github.com/ivpusic/grpool

Prakhar Agnihotri
fuente