Повторне використання http-з’єднань у Golang


82

Зараз я намагаюся знайти спосіб повторного використання з’єднань під час створення повідомлень HTTP у Golang.

Я створив транспорт і клієнта так:

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

Відповіді:


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, наприклад), через що багато відкритих дескрипторів файлів простоювали і врешті-решт вбили мою програму. Потрапивши в цю тему, я сам переглянув цю частину коду та facepalm. Дякую.
sa125

3
одна цікава примітка полягає в тому, що етап прочитання видається необхідним і достатнім. Крок читання сам по собі поверне підключення до пулу, але лише закриття - ні; з'єднання опиниться в TCP_WAIT. Також зіткнувся з проблемами, оскільки я використовував json.NewDecoder () для читання response.Body, який прочитав її не повністю. Обов’язково додайте io.Copy (ioutil.Discard, res.Body), якщо не впевнені.
Сем Рассел

3
Чи є спосіб перевірити, чи повністю прочитано тіло? Чи ioutil.ReadAll()гарантовано цього буде достатньо, чи все-таки мені потрібно обсипати io.Copy()дзвінки всюди, про всяк випадок?
Патрік Іселінд

4
Я подивився вихідний код і, схоже, тіло відповіді Close () вже піклується про злив тіла: 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 як глобальної змінної безпечним від расових умов, якщо існує кілька програм, що викликають функцію, що використовує цю змінну?
Барт Сільверстрім

3
@ bn00d defer response.Body.Close()правильно? Я запитую, тому що, відкладаючи закриття, ми фактично не закриємо conn для повторного використання, поки не вийде основна функція, тому слід просто зателефонувати .Close()безпосередньо після .ReadAll(). це може не здаватися проблемою у вашому прикладі b / c, насправді це не демонструє створення декількох req, він просто робить один req, а потім виходить, але якби ми робили кілька req назад до спини, здавалося б, що з defered, .Close()не буде називатися до виходу функції. або ... мені чогось не вистачає? Дякую.
mad.meesh

1
@ mad.meesh, якщо ви робите кілька викликів (наприклад, всередині циклу), просто оберніть виклик Body.Close () всередині закриття, таким чином він закриється, як тільки ви закінчите обробку даних.
Антуан Коттен,

Як я можу встановити різні проксі для кожного запиту таким чином? Це можливо ?
Амір Хошаль

@ bn00d Ваш приклад, здається, не працює. Після додавання циклу кожен запит все одно призводить до нового з'єднання. play.golang.org/p/9Ah_lyfYxgV
Льюїс Чан,

37

Редагувати: Це більше примітка для людей, які створюють транспорт і клієнта для кожного запиту.

Edit2: Змінено посилання на godoc.

Transportє структурою, що містить з'єднання для повторного використання; подивитися https://godoc.org/net/http#Transport ("За замовчуванням Транспорт кешує з'єднання для подальшого повторного використання.")

Отже, якщо ви створюєте новий транспорт для кожного запиту, він створюватиме нові зв’язки кожного разу. У цьому випадку рішення полягає у спільному використанні одного транспортного екземпляра між клієнтами.


Будь ласка, дайте посилання, використовуючи конкретний коміт. Ваше посилання більше не правильне.
Inanc Gumus,

play.golang.org/p/9Ah_lyfYxgV Цей приклад показує лише один транспорт, але він все одно породжує одне підключення на запит. Чому так ?
Lewis Chan

12

IIRC, клієнт за замовчуванням виконує повторне використання з'єднань. Ви закриваєте відповідь ?

Користувачі, які телефонують, повинні закрити відповідно тіло, коли читають з нього. Якщо resp.Body не закрито, основний RoundTripper Клієнта (як правило, Transport) може не мати змоги повторно використовувати постійне TCP-з'єднання з сервером для подальшого запиту "підтримувати живий".


Привіт, дякую за відповідь. Так, вибачте, я також повинен був це включити. Я закриваю зв’язок з r.Body.Close ().
sicr

@sicr, ти впевнений, що сервер насправді сам не закриває з'єднання? Я маю на увазі, що ці видатні зв’язки можуть бути в одному із *_WAITштатів або щось подібне
kostix

1
@kostix Я бачу велику кількість зв'язків із державою ВСТАНОВЛЕНО, дивлячись на netstat. Здається, що під час кожного запиту POST створюється нове підключення, на відміну від того ж підключення, яке використовується повторно.
sicr

@sicr, ти знайшов рішення щодо повторного використання з'єднання? велике спасибі, Даніеле
Даніеле Б

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.Oncely ви можете бути впевнені, що тільки один екземпляр буде використаний для всіх ваших запитів.

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 за секунду
Філіп Муденйо

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. Transportне використовує TCP-з'єднання повторно, але слід обмежити 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 робить те саме внутрішньо, але тоді ви будете мати контроль над тим, скільки одночасних з'єднань відкрити, і як керувати своїми ресурсами. Якщо вам цікаво, подивіться на реалізацію netpoll, яка внутрішньо використовує epoll / kqueue для управління ними.

  2. Найпростішим було б замість об’єднання мережевих з'єднань створити робочий пул для своїх програм. Це було б легким та кращим рішенням, яке не заважало б вашій поточній кодовій базі і вимагало б незначних змін.

Припустимо, вам потрібно зробити n запит POST після отримання запиту.

введіть тут опис зображення

введіть тут опис зображення

Ви можете використовувати канали для реалізації цього.

Або просто ви могли б використовувати сторонні бібліотеки.
Подобається: https://github.com/ivpusic/grpool

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.