@videxerion

Как устранить утечку памяти при множественных соединениях в net/http Golang?

У меня есть следующая функция:

var client = &http.Client{
    Timeout: time.Millisecond * 100,
}

func scan(ip string) (error, string, string, string) {
    resp, err := client.Get("http://" + ip)

    // Закрытие неиспользуемых соединений
    client.CloseIdleConnections()

    if err != nil {
        return err, "", "", ""
    }

    headers := buildHeaders(resp.Header)
    body, _ := io.ReadAll(resp.Body)

    // закрытие тела ответа
    resp.Body.Close()

    return err, headers, string(body), resp.Status
}


Данный код приводит к огромной утечке памяти. В процессе дебага выяснилось что в net/http в функции queueForIdleConn() в файле transport.go происходит следующее:

if t.idleConnWait == nil {
    t.idleConnWait = make(map[connectMethodKey]wantConnQueue)
}
q := t.idleConnWait[w.key]
q.cleanFront()
q.pushBack(w)
t.idleConnWait[w.key] = q
return false


Получается так что в idleConnWait = {map[http.connectMethodKey]http.wantConnQueue} постоянно записываются новые элементы, но судя по размеру никогда не освобождается. Как правильно сделать так что бы этот массив очищался?

Примечания:
  1. Программа кидает соединения множеству адресов 95% которых недействительны (не отвечают).
  2. Эта функция запускается сразу в большом кол-ве горутин (go scan())
  • Вопрос задан
  • 237 просмотров
Пригласить эксперта
Ответы на вопрос 2
Скорей всего, проблема в том, что вы возвращаете ошибку, не закрывая http соединение.
Для этого была специально введена одна из самых привлекательных конструкций языка - defer.

var client = &http.Client{
    Timeout: time.Millisecond * 100,
}

func scan(ip string) (error, string, string, string) {
    resp, err := client.Get("http://" + ip)
    // Здесь сразу желательно обработать ошибку ...
    
    // Закрываем соединение в конце выполнения
    // функции в любом случае,
    // даже если где-то возникнет ошибка
    defer resp.Body.Close()

    // Закрытие неиспользуемых соединений
    // Нужно ли это теперь?
    // client.CloseIdleConnections()

    // Проблема могла быть тут
    // if err != nil {
    //     return err, "", "", ""
    // }

    headers := buildHeaders(resp.Header)
    body, _ := io.ReadAll(resp.Body)

    // и это нам уже особо не нужно
    // resp.Body.Close()

    return err, headers, string(body), resp.Status
}
Ответ написан
@calculator212
Вообще есть сомнения что дело в этой мапе, первое что в глаза бросается, это то что вы неправильно обрабатываете ошибки пример из оф доки
resp, err := http.Get("http://example.com/")
if err != nil {
	// handle error
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)

Вы сначала проверяете на ошибку и если ошибки нет, через defer закрываете соединение.
Второе - вам стоит почитать про http.Transport и некоторые особенности клиентов, т.к. даже соединения которые должны были закрыться могут висеть очень долго
tr := &http.Transport{
	MaxIdleConns:       10,
	IdleConnTimeout:    30 * time.Second,
	DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")

Также желательно в самой системе посмотреть таймауты и настройки для tcp соединений (это не так часто нужно но если у вас много запросов клиентских, то может помочь).
Также можете поискать альтернативные клиенты и пулы клиентов, а также как правильно реюзать соединения

Получается так что в idleConnWait = {map[http.connectMethodKey]http.wantConnQueue} постоянно записываются новые элементы,
В целом есть вероятность, что "утечка" тут, но это очень легко проверить даже без профилировщика, т.к. эта мапа уникальна для каждого клиента, то можно напрямую проверять сколько клиент занимает памяти, но на самом деле есть большая вероятность, что проблема в том, что вы неправильно используете client, т.к. эта мапа периодически очищается и расти она должна только до определенного значения
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы