Проблемы с производительностью при использовании DialContext в Go - PullRequest
2 голосов
/ 28 мая 2020

Я провел быстрый тест, используя встроенные Go http.Client и net. Это привело к заметным проблемам с производительностью при использовании DialContext, а не при его неиспользовании.

Я в основном пытаюсь имитировать вариант использования, который есть в моей компании, где эта настройка http.Client намного менее эффективна, чем конфигурация по умолчанию, когда используется точно для тех же вещей. И я заметил, что комментирование части DialContext сделало его go быстрее.

Тест просто открывает пул потоков (8 в примере) для создания подключений к простому URL-адресу, используя буферизованный канал тот же размер, что и количество потоков (8).

Вот код с DialContext (2.266333805s):

func main() {
    var httpClient *http.Client

    httpClient = &http.Client{
        Transport: &http.Transport{
            DialContext: (&net.Dialer{
                Timeout:   3 * time.Second,
                KeepAlive: 30 * time.Second,
                DualStack: true,
            }).DialContext,
        },
    }

    url := "https://stackoverflow.com/"
    wg := sync.WaitGroup{}
    threads := 8
    wg.Add(threads)
    c := make(chan struct{}, threads)

    start := time.Now()
    for i := 0; i < threads; i++ {
        go func() {
            for range c {
                req, _ := http.NewRequest(http.MethodGet, url, nil)
                resp, err := httpClient.Do(req)
                if err == nil {
                    resp.Body.Close()
                }
            }
            wg.Done()
        }()
    }

    for i := 0; i < 200; i++ {
        c <- struct{}{}
    }
    close(c)
    wg.Wait()

    fmt.Println(time.Since(start))
}

Расчетное время было 2.266333805s

А вот код без DialContext (731,154103мс):

func main() {
    var httpClient *http.Client

    httpClient = &http.Client{
        Transport: &http.Transport{
        },
    }

    url := "https://stackoverflow.com/"
    wg := sync.WaitGroup{}
    threads := 8
    wg.Add(threads)
    c := make(chan struct{}, threads)

    start := time.Now()
    for i := 0; i < threads; i++ {
        go func() {
            for range c {
                req, _ := http.NewRequest(http.MethodGet, url, nil)
                resp, err := httpClient.Do(req)
                if err == nil {
                    resp.Body.Close()
                }
            }
            wg.Done()
        }()
    }

    for i := 0; i < 200; i++ {
        c <- struct{}{}
    }
    close(c)
    wg.Wait()

    fmt.Println(time.Since(start))
}

Выведенное время было 731,154103мс

Разница между результаты были одинаковыми для нескольких запусков программы.

Кто-нибудь знает, почему это происходит?

Спасибо!

EDIT: Итак Я попробовал net/http/httptrace и убедился, что основная часть ответа была полностью прочитана и закрыта:

go func() {
    for range c {
        req, _ := http.NewRequest(http.MethodGet, url, nil)
        req = req.WithContext(httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
            GotConn: t.gotConn,
        }))
        resp, _ := httpClient.Do(req)
        ioutil.ReadAll(resp.Body)
        resp.Body.Close()
    }
    wg.Done()
}()

Интересные выводы при использовании DialContext и неиспользовании

НЕ ИСПОЛЬЗУЕТ DialContext:

time taken to run 200 requests: 5.639808793s
new connections: 1
reused connections: 199

ИСПОЛЬЗУЕТ DialContext:

time taken to run 200 requests: 5.682882723s
new connections: 8
reused connections: 192

Это f астра ... Но почему один открывает 8 новых подключений, а другой всего 1?

1 Ответ

2 голосов
/ 28 мая 2020

Единственный способ получить такие большие различия - это если один транспорт повторно использует соединения, а другой - нет. Чтобы убедиться, что вы можете повторно использовать соединение, вы должны всегда читать тело ответа. Возможно, что в некоторых случаях соединение будет повторно использовано без явного чтения тела, но это никогда не гарантируется и зависит от многих вещей, таких как закрытие соединения удаленным сервером, был ли ответ полностью буферизован Transport, и если есть был контекст по запросу.

Пакет net/http/httptrace может дать вам представление о многих внутренних функциях запроса, в том числе о том, использовались ли соединения повторно или нет. См. Примеры https://blog.golang.org/http-tracing.

Настройка DisableKeepAlive всегда предотвращает повторное использование подключений, делая оба одинаково медленными. Постоянное чтение такого ответа сделает и то и другое одинаково быстрым:

req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := httpClient.Do(req)
if err != nil {
    // handle error
    continue
}
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()

Если вы хотите ограничить количество считываемых данных, прежде чем разорвать соединение, вы можете просто обернуть тело в io.LimitedReader

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...