Это был вторник, 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, поскольку их неправильное использование может ухудшить ситуацию.
- Нужно быть готовым, что поиск утечек памяти потребует терпения, настойчивости и небольшой детективной работы.
-
Почему Люди Любят 3D-Печать
19 Oct, 24 -
Ремонт Компьютера: Замена Жесткого Диска
19 Oct, 24 -
Компьютерные Вирусы, Черви И Мистификации
19 Oct, 24 -
Диссоциативные Расстройства
19 Oct, 24 -
Управление Структурой Бэм С Помощью Bemy
19 Oct, 24 -
Youtube Разоблачает Нерадивых Полицейских
19 Oct, 24