Устранение утечек памяти

В Scrapy такие объекты, как запросы, ответы и элементы, имеют ограниченное время жизни: они создаются, используются в течение некоторого времени и, наконец, уничтожаются.

Из всех данных объектов запрос, вероятно, имеет самое длинное время жизни, поскольку он остаётся в очереди планировщика, пока не придет время его обработать. Для получения дополнительной информации см. Обзор архитектуры.

Поскольку данные объекты Scrapy имеют (довольно долгое) время жизни, всегда существует риск накопления их в памяти без их правильного освобождения, что вызывает так называемую «утечку памяти».

Чтобы помочь отладить утечки памяти, Scrapy предоставляет встроенный механизм для отслеживания ссылок на объекты под названием trackref, и вы также можете использовать стороннюю библиотеку под названием muppy для более продвинутой отладки памяти (дополнительную информацию см. ниже). Оба механизма должны использоваться в Telnet консоли.

Распространенные причины утечек памяти

Довольно часто (иногда случайно, иногда специально) разработчик Scrapy передаёт объекты, указанные в Requests (например, используя атрибуты cb_kwargs или meta или функцию обратного вызова запроса), и это фактически ограничивает время жизни данных объектов, на которые имеются ссылки, для время жизни Request. Это, безусловно, самая частая причина утечек памяти в проектах Scrapy, и новичкам её довольно сложно отлаживать.

В больших проектах пауки обычно пишутся разными людьми, и некоторые из данных пауков могут «протекать» и, таким образом, влиять на остальных (хорошо написанных) пауков, когда они запускаются одновременно, что, в свою очередь, влияет на весь процесс сканирования.

Утечка также может происходить из написанного вами специального промежуточного программного обеспечения, конвейера или расширения, если вы не освобождаете (ранее выделенные) ресурсы должным образом. Например, выделение ресурсов на spider_opened, но не высвобождение их на spider_closed может вызвать проблемы, если вы используете несколько пауков на процесс.

Слишком много запросов?

По умолчанию Scrapy хранит очередь запросов в памяти; он включает объекты Request и все объекты, указанные в атрибутах запроса (например, в cb_kwargs и meta). Хотя это не обязательно утечка, это может занять много памяти. Включение постоянной очереди заданий может помочь контролировать использование памяти.

Отладка утечек памяти с помощью trackref

trackref — это модуль, предоставляемый Scrapy для отладки наиболее распространенных случаев утечек памяти. Он в основном отслеживает ссылки на все живые объекты Request, Response, Item, Spider и Selector.

Вы можете войти в консоль telnet и проверить, сколько объектов (из упомянутых выше классов) в настоящее время живы, используя функцию prefs(), которая является псевдонимом функции print_live_refs():

telnet localhost 6023

>>> prefs()
Live References

ExampleSpider                       1   oldest: 15s ago
HtmlResponse                       10   oldest: 1s ago
Selector                            2   oldest: 0s ago
FormRequest                       878   oldest: 7s ago

Как видите, данный отчёт также показывает «возраст» самого старого объекта в каждом классе. Если вы запускаете несколько пауков для каждого процесса, скорее всего, вы сможете выяснить, какой из них даёт утечку, посмотрев на самый старый запрос или ответ. Вы можете получить самый старый объект каждого класса с помощью функции get_oldest() (из консоли telnet).

Какие объекты отслеживаются?

Все объекты, отслеживаемые trackrefs, принадлежат этим классам (и всем его подклассам):

  • scrapy.Request

  • scrapy.http.Response

  • scrapy.Item

  • scrapy.Selector

  • scrapy.Spider

Реальный пример

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

return Request(f"http://www.somenastyspider.com/product.php?pid={product_id}",
               callback=self.parse, cb_kwargs={'referer': response})

Эта строка передаёт ссылку на ответ внутри запроса, которая эффективно связывает время жизни ответа с запросом, что определенно вызовет утечку памяти.

Давайте посмотрим, как мы можем обнаружить причину (конечно, не зная её априори) с помощью инструмента trackref.

После того, как краулер проработает несколько минут и мы заметим, что использование его памяти сильно выросло, мы можем войти в его консоль telnet и проверить действующие ссылки:

>>> prefs()
Live References

SomenastySpider                     1   oldest: 15s ago
HtmlResponse                     3890   oldest: 265s ago
Selector                            2   oldest: 0s ago
Request                          3878   oldest: 250s ago

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

Иногда может быть полезна дополнительная информация о живых объектах. Проверим самый старый ответ:

>>> from scrapy.utils.trackref import get_oldest
>>> r = get_oldest('HtmlResponse')
>>> r.url
'http://www.somenastyspider.com/product.php?pid=123'

Если вы хотите перебрать все объекты, вместо получения самого старого, вы можете использовать функцию scrapy.utils.trackref.iter_all():

>>> from scrapy.utils.trackref import iter_all
>>> [r.url for r in iter_all('HtmlResponse')]
['http://www.somenastyspider.com/product.php?pid=123',
 'http://www.somenastyspider.com/product.php?pid=584',
...]

Слишком много пауков?

Если в вашем проекте слишком много пауков, выполняемых параллельно, вывод prefs() может быть трудночитаемым. По этой причине данная функция имеет аргумент ignore, который можно использовать для игнорирования определенного класса (и всех его подклассов). Например, здесь не будут отображаться живые ссылки на пауков:

>>> from scrapy.spiders import Spider
>>> prefs(ignore=Spider)

Модуль scrapy.utils.trackref

Вот функции, доступные в модуле trackref.

class scrapy.utils.trackref.object_ref

Наследуйте данный класс, если вы хотите отслеживать живые экземпляры с помощью модуля trackref.

scrapy.utils.trackref.print_live_refs(class_name, ignore=NoneType)

Распечатать отчёт живых ссылок, сгруппированных по имени класса.

Параметры

ignore (type or tuple) – если указано, все объекты из указанного класса (или кортежа классов) будут проигнорированы.

scrapy.utils.trackref.get_oldest(class_name)

Возвращает самый старый живой объект с заданным именем класса или None, если ничего не найдено. Сначала используйте print_live_refs(), чтобы получить список всех отслеживаемых живых объектов по имени класса.

scrapy.utils.trackref.iter_all(class_name)

Возвращает итератор по всем живым объектам с заданным именем класса или None, если ничего не найдено. Сначала используйте print_live_refs(), чтобы получить список всех отслеживаемых живых объектов по имени класса.

Отладка утечек памяти с помощью muppy

trackref предоставляет очень удобный механизм для отслеживания утечек памяти, но отслеживает только те объекты, которые с большей вероятностью могут вызвать утечки памяти. Однако есть и другие случаи, когда утечки памяти могут исходить от других (более или менее непонятных) объектов. Если это ваш случай, и вы не можете найти свои утечки с помощью trackref, у вас все ещё есть другой ресурс: библиотека muppy.

Можно использовать muppy от Pympler.

Если вы используете pip, вы можете установить muppy с помощью следующей команды:

pip install Pympler

Вот пример просмотра всех объектов Python, доступных в куче, с помощью muppy:

>>> from pympler import muppy
>>> all_objects = muppy.get_objects()
>>> len(all_objects)
28667
>>> from pympler import summary
>>> suml = summary.summarize(all_objects)
>>> summary.print_(suml)
                               types |   # objects |   total size
==================================== | =========== | ============
                         <class 'str |        9822 |      1.10 MB
                        <class 'dict |        1658 |    856.62 KB
                        <class 'type |         436 |    443.60 KB
                        <class 'code |        2974 |    419.56 KB
          <class '_io.BufferedWriter |           2 |    256.34 KB
                         <class 'set |         420 |    159.88 KB
          <class '_io.BufferedReader |           1 |    128.17 KB
          <class 'wrapper_descriptor |        1130 |     88.28 KB
                       <class 'tuple |        1304 |     86.57 KB
                     <class 'weakref |        1013 |     79.14 KB
  <class 'builtin_function_or_method |         958 |     67.36 KB
           <class 'method_descriptor |         865 |     60.82 KB
                 <class 'abc.ABCMeta |          62 |     59.96 KB
                        <class 'list |         446 |     58.52 KB
                         <class 'int |        1425 |     43.20 KB

Для получения дополнительной информации о muppy обратитесь к документации muppy.

Утечки без утечек

Иногда вы можете заметить, что использование памяти вашим процессом Scrapy будет только увеличиваться, но никогда не уменьшаться. К сожалению, это может произойти, даже если ни у Scrapy, ни в вашем проекте нет утечки памяти. Это связано с (не так хорошо) известной проблемой Python, которая в некоторых случаях может не возвращать освобожденную память операционной системе. Для получения дополнительной информации по этому вопросу см:

  • Управление памятью Python

  • Управление памятью Python часть 2

  • Управление памятью Python часть 3

Усовершенствования, предложенные Эваном Джонсом и подробно описанные на этой странице, были объединены в Python 2.5, но это только уменьшает проблему, но не решает её полностью. Процитирую газету:

К сожалению, этот патч может освободить арену, только если у неё больше нетвыделенных объектов. Это означает, что фрагментация большаяпроблема. У приложения может быть много мегабайт свободной памяти, разбросанной на всех аренах, но оно не сможет освободить ни одну из них. Это проблема, с которой сталкиваются все распределители памяти. Единственный способ решить её перейти к уплотняющему сборщику мусора, который умеет перемещать объекты в памяти. Это потребует значительных изменений в интерпретаторе Python.

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