Программисты-Сантехники, Или История Одной Протечки И Трудности Борьбы С Ней

Это был вторник, 25 февраля.

Сложный выпуск версии в субботу, 22 февраля, уже остался в прошлом.

Казалось, худшее уже позади, и ничто не предвещало беды.

Но все изменилось в один момент, когда мониторинг получил ошибку об утечке памяти на согласовывающем процессе службы контроля доступа.

Откуда? Последние серьезные изменения в кодовой базе координатора были в предыдущей версии более двух месяцев назад, и с тех пор с памятью не произошло ничего примечательного.

Но, к сожалению, графики мониторинга были непреклонны — у координатора явно где-то начала утекать память, на служебном этаже образовалась большая лужа, а это означало, что сантехникам предстоит серьезная работа.



Программисты-сантехники, или История одной протечки и трудности борьбы с ней

Для начала сделаем небольшое отступление.

Помимо прочего, СБИС позволяет вести учет рабочего времени и выполнять контроль доступа по модели лица, отпечатку пальца или карте доступа.

При этом СБИС обменивается данными с контроллерами конечных устройств (замков, турникетов, терминалов доступа и т. д.).

С устройствами общается отдельный сервис.

Он пассивен и взаимодействует с устройствами контроля доступа на основе собственных протоколов, реализованных поверх HTTP(S).

Написан на основе стандартного для сервисов нашей компании стека: базы данных PostgreSQL, для бизнес-логики используется Python 3, расширенный методами C/C++ с нашей платформы.

Типичный узел веб-сервиса состоит из следующих процессов:

  • Монитор — это корневой процесс.

  • Координатор — это дочерний процесс монитора.

  • Рабочие процессы.

Монитор — это первый процесс веб-сервиса.

Его задача — вызвать fork(), запустить дочерний процесс-координатор и контролировать его работу.

Координатор — это основной процесс веб-сервиса; именно он принимает запросы от внешних устройств, отправляет ответы и балансирует нагрузку.

Координатор отправляет запрос на выполнение рабочим процессам, они его выполняют, передают ответ в разделяемую память и сообщают координатору, что задача выполнена и результат можно собирать.



Кто виноват и что делать?

Итак, координатор службы контроля доступа отличается от координаторов других служб нашей компании наличием веб-сервера.

Координаторы других сервисов работали без утечек, поэтому проблему пришлось искать в нашей конфигурации.

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

Стали искать внимательнее и обнаружили, что память текла только на одном из стендов, да и то с переменным успехом — а значит, проблема не так-то легко воспроизводится.



Программисты-сантехники, или История одной протечки и трудности борьбы с ней

Что делать? Как найти причину? Первым делом мы сделали дамп памяти и отправили его на анализ специалистам платформы.

В результате в свалке нет ничего: ни причины, ни подсказки, в каком направлении искать дальше.

Мы проверили изменения в коде координатора от предыдущей версии — а вдруг мы внесли какие-то ужасные изменения, но просто не сразу это поняли? Ан нет — в код координатора добавили лишь несколько комментариев и пару методов перенесли в новые файлы — в общем, ничего криминального.

Мы стали смотреть в сторону наших коллег — разработчиков ядра нашего сервиса.

Они уверенно отрицали саму возможность причастности к нашей беде, но предложили внедрить в сервис мониторинг трассировки.

Сказано-сделано, в следующем хотфиксе мы доработаем сервис, быстро протестируем его и выпустим в бой.

И что мы видим? Теперь наша память утекает не просто быстро, а очень быстро — рост стал экспоненциальным.

Первый пик мы приписываем злым силам и сопутствующим факторам, но второй пик, спустя несколько часов после первого, дает понять, что с таким аварийным поведением сервиса ждать выхода очередного хотфикса слишком долго.

Поэтому берем результаты трассировки и патчим сервис, откатывая изменения с мониторингом, чтобы вернуться хотя бы к линейному росту.



Программисты-сантехники, или История одной протечки и трудности борьбы с ней

Казалось бы, у нас есть результаты трассировки по выделенной в Python памяти, сейчас мы разберемся в них и найдем виновника утечки, но это не так — в собранных данных нет пиков в 5,5 ГБ, которые мы видели на графиках мониторинга.

Максимально используемый объем памяти составляет всего 250 МБ, и даже тогда 130 МБ из них потребляет сам Tracemalloc. Отчасти это понятно — трассировка позволяет видеть динамику памяти в Python, но не знает о распределении памяти в пакетах C и C++, которые реализованы нашей платформой.

В полученных данных найти ничего интересного не удалось; память выделяется в приемлемых объемах обычным объектам типа потоков, строк и словарей — в общем, ничего подозрительного.

Тогда они решили удалить из данных все лишнее, оставив только общее потребление памяти и время, и визуализировать это.

Хоть визуализация и не помогла ответить на вопросы «что происходит» и «почему», с ее помощью мы увидели корреляцию с данными мониторинга — а это значит, что у нас где-то определенно есть проблема, и ее нужно искать.



Программисты-сантехники, или История одной протечки и трудности борьбы с ней

На тот момент у нашей бригады сантехников уже не было идей, где искать утечку.

К счастью, птица пропела нам, что одно серьезное изменение платформы все же произошло — версия Python изменилась с 3.4 на 3.7, а это огромное поле для поиска.

Мы решили поискать проблемы, связанные с утечками памяти в Python 3.7, в Интернете, поскольку наверняка кто-то уже сталкивался с подобным поведением.

В конце концов, Python 3.7 был выпущен довольно давно; мы перешли на него только с текущим обновлением.

К счастью, отвечать на наш вопрос ответили быстро, а также нашли проблема И запрос на включение чтобы исправить проблему, но само по себе это оказались изменения, внесенные разработчиками Python. Что случилось? Начиная с версии 3.7 изменилось поведение класса ThreadingMixIn, от которого мы наследуем на нашем веб-сервере обработку каждого запроса в отдельном потоке.

В классе ThreadingMixIn мы добавили добавление всех созданных потоков в массив.

Из-за этих изменений экземпляры класса, обрабатывающие подключения устройств, не освобождаются после завершения, а сборщик мусора Python не может очищать память в израсходованных потоках.

Именно это привело к линейному увеличению выделяемой памяти прямо пропорционально количеству запросов к нашему серверу.

Вот он, коварный код Python-модуля с большой дырой (слева код на Python 3.5 до изменений, справа — на 3.7, после):

Программисты-сантехники, или История одной протечки и трудности борьбы с ней

Выяснив причину, мы легко устранили утечку: в нашем классе-наследнике мы изменили значение флага, возвращающего старое поведение, и всё — победа! Потоки создаются как и раньше, без записи в переменную класса, и на графиках мониторинга мы видим приятную картину — утечка устранена!

Программисты-сантехники, или История одной протечки и трудности борьбы с ней

Приятно об этом писать после победы.

Вероятно, мы не первые, кто столкнулся с этой проблемой после перехода на Python 3.7, но, скорее всего, не будем последними.

Для себя мы пришли к выводам, которые нам нужны:

  • Серьезнее оценить возможные последствия при внесении серьезных изменений, особенно если от нас зависят другие решения приложения.

  • В случае глобальных изменений в платформе, например, смены версии Python, проверьте свой код на наличие возможных проблем.

  • Реагируйте на любое подозрительное изменение графиков мониторинга не только боевых служб, но и тестовых.

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

  • Вам следует быть осторожными с инструментами анализа памяти, такими как Tracemalloc, поскольку их неправильное использование может ухудшить ситуацию.

  • Нужно быть готовым, что поиск утечек памяти потребует терпения, настойчивости и небольшой детективной работы.

Ну и хотелось бы выразить огромную благодарность всем, кто помог справиться с аварийными сантехническими работами и вернуть нашему сервису былую функциональность! Теги: #python #облачные сервисы #утечки памяти
Вместе с данным постом часто просматривают: