В этой статье я хочу рассказать вам о том, как мы обнаружили проблему, которая была вызвана неправильной конфигурацией пула соединений и как настраивать пулы HTTP-соединений в микросервисах GO. Проблема была скрыта в нашем API Gateway. Это сервис, который реализует модель фасада и предоставляет микросервисам единое окошко, обращенное наружу. Данная статья будет полезна тем кто занимается бэкенд-разработкой в Ozon — пишет микросервисы для личного кабинета продавца.
В упрощенном виде его работу API Gateway можно представить так:
Проверить аутентификацию и авторизацию с помощью HTTP-запроса в сервис аутентификации
Спроксировать запрос в нужный сервис, который определяем по пути и методу запроса пользователя
Иллюстрация работы API Gateway
На стороне API Gateway были обнаружены ошибки. Мы заглянули в логи за подробной информацией и обнаружили, что ошибки были похожи на таймауты обращения к сервизу аутентификацию:
{err_type: context.deadlineExceededError, err: context deadline exceeded} {err_type: *errors.errorString, err: context canceled}
Трейсы в Jaeger показали такую же ситуацию— мы не дожидались ответа от сервиса аутентификации за 2 секунды.
Для подтверждения своих слов, разработчики сервиса аутентификации скинул нам скриншот логов с подтверждением. В нем много ошибок об отмене запроса со стороны того, кто его инициировал.
Скриншот с множеством ошибок Cancelled by client
Итого:
Используемый нами сервис аутентификации стабильно отрабатывает за 200 миллисекунд.
Многие наши обращения к этому сервису таймаутят за 2 секунды.
Один из авторов нашего API Gatcheway отметил, что давно заметил странную особенность сервиса – он внезапно начинает открывать множество подключений к удаленным портам. После запуска команды из-под контейнера, вы увидите:
$ ss -natp state time-wait | awk '{print $4}' | sort -nr | uniq -c | sort -nr | head
1053 10.20.49.117:80
1030 10.20.49.92:80
1016 10.20.49.91:80
1014 10.20.54.129:80
1013 10.20.53.213:80
1008 10.20.53.173:80
969 10.20.53.172:80
Эта команда показывает количество сокетов TCP в состоянии TIME_WAIT до различных удаленных портов. Короче говоря, состояние TIME_WAIT — это де-факто соединение, закрытое клиентом. Linux предотвращает повторное использование этих пар, когда это возможно, в течение 60 секунд для защиты от старых пакетов, мешающих вновь установленному TCP-соединению.
Нам важна другая сторона вопроса. Само существование TCP-соединения означает, что соединение установлено и закрыто. Если такая ситуация возникнет массово, мы будем иметь дело с накладными расходами на разрешение DNS и настройку подключения. В результате время HTTP-запроса может увеличиться. Пулы подключений помогают избежать этой проблемы. Go использует для этой цели абстракцию http.Transport.
А именно, в этом месте мы подходим к истокам проблемы. Все запросы, которые нам поступали, мы обрабатывали с помощью http-DefaultTransport.
Его параметры:
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
Среди перечисленных выше параметров к настройке пула соединений имеют отношения два:
MaxIdleConns
— число соединений, которое разрешается иметь в состоянии Idle (т.е. открытых TCP-соединений, которые в данный момент не используются);
IdleConnTimeout
— время, через которое закрываются такие неактивные соединения.
Но в DefaultTransport
совсем не упомянут другой ключевой параметр — MaxIdleConnsPerHost
. В его обязанности входит контроль за количеством активных TCP-соединений, которые можно установить на один хост.
При этом если MaxIdleConnsPerHost
не указан, тогда используется значение по умолчанию:
const DefaultMaxIdleConnsPerHost = 2
В результате мы столкнулись с проблемой, которая была связана с использованием http.DefaultTransport
для всех запросов.
А теперь представьте себе, что нам нужно установить 10 подключаний одновременно перед службой регистрации. В дальнейшем для 8 из них TCP/IP-соединения будут открыты и сразу же закрыты из-за ограничения MaxIdleConnsPerHost
. А если это будет повторяться постоянно и в больших объемах, то мы будем вынуждены увеличить свои накладные расходы на один HTTP-запрос, поскольку для этого потребуется новое соединение. Поэтому возрастает риск возникновения тайм-аутов.
Мы решили эту проблему следующим образом:
MaxIdleConnsPerHost
соответствующим значению MaxIdleConns
:func createOneHostTransport() *http.Transport {
result := http.DefaultTransport.(*http.Transport).Clone()
result.MaxIdleConnsPerHost = result.MaxIdleConns
return result
}
График response time обращения к сервису аутентификации
Здесь вы можете увидеть значительное уменьшение квантиля 0,99 по времени обращения графика (синий цвет) — с 2-3 секунд до менее 300 миллисекунд. Я должен признать, что даже после этого мы иногда наблюдали таймауты при доступе к сервису аутентификации. Но теперь, по крайней мере, мы видели такие же таймауты на графиках от другого сервиса.
Теперь вы можете спросить: зачем делать такие настройки, которые нужно изменить позже? Задумывались ли создатели языка Go и библиотек о том, как его будут использовать на практике?
Думаю, да: настройки по умолчанию имеют смысл при работе с большим количеством хостов и не слишком большим количеством запросов. В этом случае значение MaxIdleConnsPerHost
защищает нас от того, что в случае большого количества запросов один из хостов исчерпывает оставшийся ресурс свободных подключений и не позволяет другой службе создать хотя бы одно долгосрочное подключение.
Если вдруг вы сталкиваетесь с непонятными таймаутами, попробуйте следующее:
Все чаще компании, занимающиеся разработкой распределенных систем, используют Go. При создании облачной распределенной системы вам может понадобиться поддержка различных конкретных функций в ваших сервизах – например, такие как транспортный протокол (например, HTTP, GRPC и т.д.), а также форматы кодирования сообщений для них, надежность RPC, ведение журнала,трассировка, метрики и профилизации, запросы прерывания, ограничение количества запросов, интеграция их в инфраструктуру и даже описания архитектуры. Благодаря своей простоте и отсутствию магии пакеты Go, такие как Стандартная библиотечная система, уже лучше подходят для разработки распределенной системы, чем использование полноценного фреймворк с большим количеством магии под капотом.
Чтобы взломать сеть Wi-Fi с помощью Kali Linux, вам нужна беспроводная карта, поддерживающая режим мониторинга…
Работа с консолью считается более эффективной, чем работа с графическим интерфейсом по нескольким причинам.Во-первых, ввод…
Конечно, вы также можете приобрести подписку на соответствующую услугу, но наличие SSH-доступа к компьютеру с…
С тех пор как ChatGPT вышел на арену, возросла потребность в поддержке чата на базе…
Если вы когда-нибудь окажетесь в ситуации, когда вам нужно взглянуть на спектр беспроводной связи, будь…
Elastic Security стремится превзойти противников в инновациях и обеспечить защиту от новейших технологий злоумышленников. В…