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를 살펴보면 모든 게시물에 대해 새로운 연결이 발생하여 많은 수의 동시 연결이 열려있는 것으로 보입니다.

이 경우 연결을 재사용하는 올바른 방법은 무엇입니까?


답변:


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

5
+1, defer res.Body.Close()유사한 프로그램을 호출 했지만 해당 부분이 실행되기 전에 가끔 함수에서 반환되어 ( resp.StatusCode != 200예를 들어) 많은 열린 파일 설명자가 유휴 상태로 남아 결국 내 프로그램이 종료되었습니다. 이 스레드를 치면 코드의 해당 부분을 다시 방문하고 직접 얼굴을 펴게되었습니다. 감사.
sa125 2014

3
한 가지 흥미로운 점은 읽기 단계가 필요하고 충분하다는 것입니다. 읽기 단계만으로는 풀에 대한 연결을 반환하지만 닫기만으로는 그렇지 않습니다. 연결은 TCP_WAIT에서 끝납니다. 또한 json.NewDecoder ()를 사용하여 response.Body를 읽고 완전히 읽지 않았기 때문에 문제가 발생했습니다. 확실하지 않은 경우 io.Copy (ioutil.Discard, res.Body)를 포함해야합니다.
Sam Russell

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

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

요약하면 HTTP 클라이언트를 만들고이를 전역 변수에 할당 한 다음이를 사용하여 요청하는 다른 방법을 만들고 있습니다. 참고

defer response.Body.Close() 

이렇게하면 연결이 닫히고 다시 사용할 준비가됩니다.

이것이 누군가를 도울 수 있기를 바랍니다.


1
해당 변수를 사용하여 함수를 호출하는 여러 고 루틴이있는 경우 경합 조건으로부터 안전한 전역 변수로 http.Client를 사용하고 있습니까?
Bart Silverstrim

3
@ bn00d가 defer response.Body.Close()맞습니까? 나는 때문에 우리가 실제로 가까운 주요 함수가 종료 될 때까지 재사용에 대한 CONN가, 따라서 하나는 단순히 호출해야합니다 가까운 defering로 문의 .Close()후 직접 .ReadAll(). 이것은 귀하의 예제 b / c에서 문제처럼 보이지 않을 수 있습니다. 실제로 여러 요청을 만드는 것을 보여주지 않고 단순히 하나의 요청을 만들고 종료하지만 여러 요청을 연속적으로 만들면 defered 이후로 보일 것입니다 . .Close()func가 종료 될 때까지 호출되지 않습니다. 아니면 ... 내가 뭔가 빠졌나요? 감사.
mad.meesh

1
@ mad.meesh 여러 호출을 수행하는 경우 (예 : 루프 내부), Body.Close () 호출을 클로저 내부에 래핑하면 데이터 처리가 완료되는 즉시 닫힙니다.
Antoine Cotten

이런 식으로 모든 요청에 ​​대해 다른 프록시를 어떻게 설정할 수 있습니까? 가능합니까?
Amir Khoshhal

@ bn00d 귀하의 예제가 작동하지 않는 것 같습니다. 루프를 추가 한 후에도 각 요청은 여전히 ​​새로운 연결로 이어집니다. play.golang.org/p/9Ah_lyfYxgV
Lewis Chan

37

편집 : 이것은 모든 요청에 ​​대해 전송 및 클라이언트를 구성하는 사람들을위한 메모입니다.

Edit2 : 링크를 godoc로 변경했습니다.

Transport재사용을 위해 연결을 보유하는 구조체입니다. https://godoc.org/net/http#Transport를 참조 하십시오 ( "기본적으로 Transport는 향후 재사용을 위해 연결을 캐시합니다.").

따라서 각 요청에 대해 새 전송을 생성하면 매번 새로운 연결이 생성됩니다. 이 경우 해결 방법은 클라이언트간에 하나의 전송 인스턴스를 공유하는 것입니다.


특정 커밋을 사용하여 링크를 제공하십시오. 링크가 더 이상 올바르지 않습니다.
Inanc Gumus

play.golang.org/p/9Ah_lyfYxgV이 예제는 하나의 전송 만 표시하지만 요청 당 하나의 연결을 생성합니다. 왜 그런 겁니까 ?
Lewis Chan

12

IIRC, 기본 클라이언트 연결을 재사용합니다. 응답 을 닫으 십니까?

발신자는 읽기가 끝나면 resp.Body를 닫아야합니다. resp.Body가 닫히지 않으면 클라이언트의 기본 RoundTripper (일반적으로 전송)가 후속 "연결 유지"요청을 위해 서버에 대한 영구 TCP 연결을 재사용하지 못할 수 있습니다.


안녕하세요, 응답 해 주셔서 감사합니다. 예, 그것도 포함시켜야 했어요. r.Body.Close ()와의 연결을 닫습니다.
sicr

@sicr, 서버가 실제로 연결 자체를 닫지 않는다고 확신합니까? 내 말은,이 뛰어난 연결 중 하나에있을 수 *_WAIT주 또는이 같은
kostix

1
@kostix netstat를 볼 때 ESTABLISHED 상태와 많은 연결이 있습니다. 동일한 연결이 재사용되는 것과 달리 모든 POST 요청에서 새 연결이 생성되는 것으로 보입니다.
sicr

@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를 사용하면 모든 요청에 ​​하나의 인스턴스 만 사용되도록 할 수 있습니다.

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


이것은 100 개의 HTTP 요청을 처리하는 데 완벽하게 작동했습니다.
philip mudenyo

0

여기서 누락 된 부분은 "고 루틴"입니다. 전송에는 자체 연결 풀이 있으며 기본적으로 해당 풀의 각 연결은 재사용되지만 (본문이 완전히 읽고 닫힌 경우) 여러 고 루틴이 요청을 보내는 경우 새 연결이 생성됩니다 (풀에는 모든 연결이 사용 중이며 새 연결이 생성됩니다. ). 이를 해결하려면 호스트 당 최대 연결 수를 제한해야합니다 Transport.MaxConnsPerHost( https://golang.org/src/net/http/transport.go#L205 ).

아마도 당신은 설정 IdleConnTimeout및 / 또는 ResponseHeaderTimeout.


0

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

당신은 설정해야합니다 MaxConnsPerHost귀하의 명시 적 http.Client. TransportTCP 연결을 재사용하지만 제한해야합니다 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

가능한 두 가지 방법이 있습니다.

  1. 각 요청과 관련된 파일 설명자를 내부적으로 재사용하고 관리하는 라이브러리를 사용합니다. Http Client는 내부적으로 동일한 작업을 수행하지만 동시에 열 수있는 연결 수와 리소스 관리 방법을 제어 할 수 있습니다. 관심이 있다면 내부적으로 epoll / kqueue를 사용하여 관리하는 netpoll 구현을 살펴보십시오.

  2. 쉬운 방법은 네트워크 연결을 풀링하는 대신 고 루틴에 대한 작업자 풀을 만드는 것입니다. 이것은 현재 코드베이스를 방해하지 않고 약간의 변경이 필요한 쉽고 더 나은 솔루션입니다.

요청을받은 후 n 개의 POST 요청을해야한다고 가정 해 보겠습니다.

여기에 이미지 설명 입력

여기에 이미지 설명 입력

이를 구현하기 위해 채널을 사용할 수 있습니다.

또는 단순히 타사 라이브러리를 사용할 수 있습니다.
좋아요 : https://github.com/ivpusic/grpool

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.