Очень забавная история. Был работающий и написанный сервер вещаний (a-la Red5) на Python. В связи с развитием основной класс - класс протокола - был разрезан на несколько более мелких классов, которые собирались вместе множественным наследованием в единый монолит, который должен был по функциональности повторять исходный класс (до рефакторинга). Как показало вскрытие, старая версия стабильно работает неделю за неделей, а новая версия через неопределенный, но достаточно короткий период времени (несколько часов) откушивает всю доступную память и погибает.
Типичная ситуация для memory leak, причём опытным путём стало понятно, что эта утечка как-то коррелирует с нагрузкой на сервер (что логично), но зависимость неоднозначная. Момент появления утечки был сужен до одного коммита - казалось бы дело за малым. Анализ исходников в поисках того момента, когда была внесена утечка ничего дает - исходники испаханы вдоль и поперек. Ничего.
Анализ во время выполнения с помощью с помощью довольно замысловатых утилит из этого поста
ни к чему не привёл - нет ничего. Я пробовал считать количество объектов, которые видит модуль gc
в питоне (то есть тех, кто поддерживает garbage collection), но количество не растет существенно и пропорционально нагрузке на сервер. Однако память процесс кушает и довольно быстро.
В проекте было расширение, написанное на Си, - его анализ, исправление мелких неточностей результата не дает.
Остается неутешительный вывод - в силу того, что число объектов в программе не растет, то значит либо растет размер каких-то объектов (например, строк путем конкатенации), либо это какая-то бага в Python, ОС или где-то еще.
Последнее выглядит романтично и привлекательно, но на самом деле ничего не дает - такую ошибку еще тяжелее найти и поправить, однако багтрекеры FreeBSD, Python и Twisted молчат по поводу таких ошибок.
Двигаемся дальше - создается тестовый клиент вещаний, который имитирует нагрузку на сервер: разное количество вещаний, авторы, подписчики вещания, различные ошибочные ситуации, чат. Тест, запущенный локально с локальным сервером (а нелокально тестовое окружение собрать тяжело - поток порядка 50-100 Мбит/с) не дает результатов - никаких следов утечки памяти ни за час, ни за два.
Отчаяние, практически неделя потерянного впустую времени, тестирование различных вариантов... Что еще? Есть еще Twisted-Conch+Twisted-Manhole - живая консоль с интерпретатором питона в работающем сервере. Но и она не дает никаких результатов - garbage collection работает, никаких разумных объектов, которые могли бы дать утечку памяти нет.
Озарение как всегда приходит в тот момент, когда его совсем не ждешь: ведь сервер вещает, то есть получает на вход поток в 0,5 Мбит/с, раздает его 200 клиентам и делает на выходе 100 Мбит/с, если у кого-то из этих 200 клиентов канал меньше по ширине 0,5 Мбит/с, то данные в буферах процессах начнут скапливаться. Для этого есть решение - дроп пакетов, он был реализован и работал...
Эврика! После разделения большого класса на кучу мелких фрагментов некоторые методы "разрезались" на части. И в них надо поставить вызов метода предка, чтобы все "куски" метода отработали, как и раньше. Но ведь в питоне есть еще MRO (Method Resolution Order), и надо отвыкнуть от того, что базовый класс в моём ощущении является последним в цепочке, то есть у него нет super(Klass, self)
- он есть! Просто метод, который включал анализ буфера записи, который в свою очередь вовремя включал механизм пропуска пакетов отключился. И хотя всё работало, но пропуск пакетов не включался и буферы записи росли неограниченно. Исправление в одной строке - вызова метода предка решило проблему. Обидно :)