Requests: Возможная утечка памяти

Созданный на 17 окт. 2013  ·  53Комментарии  ·  Источник: psf/requests

У меня очень простая программа, которая периодически получает изображение с IP-камеры. Заметил, что рабочий набор этой программы монотонно растет. Я написал небольшую программу, которая воспроизводит проблему.

import requests
from memory_profiler import profile


<strong i="6">@profile</strong>
def lol():
    print "sending request"
    r = requests.get('http://cachefly.cachefly.net/10mb.test')
    print "reading.."
    with open("test.dat", "wb") as f:
        f.write(r.content)
    print "Finished..."

if __name__=="__main__":
    for i in xrange(100):
        print "Iteration", i
        lol()

Использование памяти печатается в конце каждой итерации. Это пример вывода.
* Итерация 0 *

Iteration 0
sending request
reading..
Finished...
Filename: test.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     12.5 MiB      0.0 MiB   <strong i="12">@profile</strong>
     6                             def lol():
     7     12.5 MiB      0.0 MiB       print "sending request"
     8     35.6 MiB     23.1 MiB       r = requests.get('http://cachefly.cachefly.net/10mb.test')
     9     35.6 MiB      0.0 MiB       print "reading.."
    10     35.6 MiB      0.0 MiB       with open("test.dat", "wb") as f:
    11     35.6 MiB      0.0 MiB        f.write(r.content)
    12     35.6 MiB      0.0 MiB       print "Finished..."

* Итерация 1 *

Iteration 1
sending request
reading..
Finished...
Filename: test.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     35.6 MiB      0.0 MiB   <strong i="17">@profile</strong>
     6                             def lol():
     7     35.6 MiB      0.0 MiB       print "sending request"
     8     36.3 MiB      0.7 MiB       r = requests.get('http://cachefly.cachefly.net/10mb.test')
     9     36.3 MiB      0.0 MiB       print "reading.."
    10     36.3 MiB      0.0 MiB       with open("test.dat", "wb") as f:
    11     36.3 MiB      0.0 MiB        f.write(r.content)
    12     36.3 MiB      0.0 MiB       print "Finished..."

Использование памяти не растет с каждой итерацией, но оно продолжает расти, поскольку requests.get является виновником увеличения использования памяти.

По ** Итерации 99 ** так выглядит профиль памяти.

Iteration 99
sending request
reading..
Finished...
Filename: test.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     40.7 MiB      0.0 MiB   <strong i="23">@profile</strong>
     6                             def lol():
     7     40.7 MiB      0.0 MiB       print "sending request"
     8     40.7 MiB      0.0 MiB       r = requests.get('http://cachefly.cachefly.net/10mb.test')
     9     40.7 MiB      0.0 MiB       print "reading.."
    10     40.7 MiB      0.0 MiB       with open("test.dat", "wb") as f:
    11     40.7 MiB      0.0 MiB        f.write(r.content)
    12     40.7 MiB      0.0 MiB       print "Finished..."

Использование памяти не снижается, если программа не завершена.

Это ошибка или ошибка пользователя?

Самый полезный комментарий

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

Все 53 Комментарий

Спасибо, что подняли этот вопрос и предоставили столько подробностей!

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

Я не видел, чтобы использование памяти уменьшалось. Мне было интересно, связано ли это со сборщиком мусора Python, и, возможно, у него просто не было возможности сработать, поэтому я добавил вызов gc.collect() после каждой загрузки. Это не имело значения.

Могу я спросить, почему закрыли этот вопрос?

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

Чтобы подчеркнуть серьезность этой проблемы, вот скриншот того, как выглядел один из наших серверных процессов при запуске профилировщика памяти:
http://cl.ly/image/3X3G2y3Y191h

Проблема по-прежнему присутствует с обычным cpython, но менее заметна. Возможно, поэтому об этой проблеме не сообщалось, несмотря на серьезные последствия, которые она имеет для тех, кто использует эту библиотеку для длительных процессов.

На данный момент мы достаточно отчаялись, чтобы рассмотреть возможность использования curl с подпроцессом.

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

Благодаря,
-Матт

Он был закрыт из-за бездействия. Если вы считаете, что можете предоставить полезную диагностику, которая укажет нам правильное направление, мы будем рады возобновить работу.

Что ж, тогда позволь мне помочь.

Я создал небольшое репозиторий git, чтобы облегчить рассмотрение этой проблемы.
https://github.com/mhjohnson/memory-profiling-requests

Вот скриншот созданного им графика:
http://cl.ly/image/453h1y3a2p1r

Надеюсь это поможет! Сообщите мне, если я что-то сделал неправильно.

-Матт

Спасибо, Мэтт! Я собираюсь заняться этим сейчас. Первые несколько раз, когда я запускал сценарий (и варианты, которые я пробовал), показали, что его легко воспроизвести. Я собираюсь начать играть с этим сейчас.

Таким образом, он увеличивается примерно до 0,1 МБ / запрос. Я попытался применить декоратор profile к методам более низкого уровня, но все они слишком длинные для удаленного использования вывода, и использование более высокого интервала, чем 0,1, похоже, служит только для отслеживания общего использования, а не для каждого. использование линии. Есть ли инструменты лучше, чем mprof?

Поэтому я решил вместо этого передать его вывод в | ag '.*0\.[1-9]+ MiB.*' чтобы получить строки, в которых добавляется память, и переместил декоратор profile в Session#send . Неудивительно, что большая часть его поступает от звонка на HTTPAdapter#send . Я иду по кроличьей норе

А теперь все идет от звонка на conn.urlopen на L355 и HTTPAdapter#get_connection . Если вы украсите get_connection , он будет выделять память 7 раз при вызове PoolManager#connection_from_url . Теперь, учитывая, что большинство из них запускается HTTPResponse s, возвращаемым из urllib3, я собираюсь посмотреть, есть ли что-то, что мы _ должны_ делать с ними, что мы не можем гарантировать, что память будет освобождена постфактум. Если я не могу найти способ справиться с этим, я начну копаться в urllib3.

@ sigmavirus24 Вау. Отличная работа! Похоже, вы определили горячую точку в коде.
Что касается отслеживания того, какой объект ответственен за утечку памяти, вы можете получить дополнительные подсказки, используя objgraph следующим образом:

import gc
import objgraph
# garbage collect first
gc.collect()  
# print most common python types
objgraph.show_most_common_types()

Дай мне знать, могу ли я чем-нибудь помочь.

-Матт

Мое первое предположение относительно виновника - объекты сокета. Это объясняет, почему на PyPy хуже ...

Я сейчас сижу в аэропорту и скоро буду на равнине несколько часов. Я, вероятно, не смогу добраться до этого сегодня вечером или, возможно, позже на этой неделе (если не в следующие выходные / неделю). Однако до сих пор я пробовал использовать release_conn в HTTPResponse мы получаем обратно. Я проверил с помощью gc.get_referents что есть у объекта Response, который не может быть GC'd. Он имеет исходный HTTPResponse httplib (хранится как _original_response и (из того, что сообщил get_referents ) имеет только сообщение электронной почты (для заголовков), а все остальное - либо строка, либо словарь (или, может быть, списки) .Если это сокеты, я не вижу, где бы они не собирались сборщиком мусора.

Кроме того, использование Session#close (я сначала заставил код использовать сеансы вместо функционального API) не помогает (и это должно очистить PoolManager, который очищает пулы соединений). Другим интересным моментом было то, что PoolManager#connection_from_url добавляло ~ 0,8 МБ (плюс-минус 0,1) при первых нескольких вызовах. Таким образом, добавляется ~ 3 МБ, но остальная часть поступает из conn.urlopen в HTTPAdapter#send . Странно то, что gc.garbage содержит некоторые странные элементы, если вы используете gc.set_debug(gc.DEBUG_LEAK) . В нем есть что-то вроде [[[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None]] и, как и следовало ожидать, gc.garbage[0] is gc.garbage[0][0] так что эта информация абсолютно бесполезна. Придется поэкспериментировать с objgraph, когда у меня будет возможность.

Итак, я закопался в urllib3 и проследил за кроличьей норой сегодня утром. Я профилировал ConnectionPool#urlopen что привело меня к ConnectionPool#_make_request . На данный момент в строках 306 и 333 в urllib3/connectionpool.py выделено много памяти. L306 - это self._validate_conn(conn) а L333 - conn.getresponse(buffering=True) . getresponse - это метод httplib для HTTPConnection . Дальнейшее профилирование будет непростым. Если мы посмотрим на _validate_conn строка, вызывающая это, будет conn.connect() которая является другим методом HTTPConnection . connect почти наверняка там, где создается сокет. Если я отключу профилирование памяти и вставлю print(old_pool) в HTTPConnectionPool#close он никогда ничего не напечатает. Казалось бы, мы на самом деле не закрываем пулы, поскольку сеанс разрушается. Я предполагаю, что это причина утечки памяти.

С удовольствием помогу отладить это, я буду в / из IRC сегодня и завтра.

Итак, отслеживая это дальше, если вы откроете python с _make_request все еще украшенным (с profile ), и вы создадите сеанс, а затем будете делать запросы каждые 10 или 20 секунд (чтобы тот же URL-адрес даже), вы увидите, что соединение считается отключенным, поэтому VerifiedHTTPSConnection закрывается, а затем используется повторно. Это означает, что повторно используется класс connection , а не базовый сокет. Метод close - это тот, который живет на httplib.HTTPConnection (L798). Это закрывает объект сокета, а затем устанавливает для него значение None. Затем он закрывает (и устанавливает значение None) самый последний httplib.HTTPResponse . Если вы также профилируете VerifiedHTTPSConnection#connect , вся созданная / утечка памяти происходит в urllib3.util.ssl_.ssl_wrap_socket .

Итак, глядя на это, memory_profiler использует для отчета об использовании памяти размер резидентного набора процесса (rss). Это размер процесса в ОЗУ (vms, или размер виртуальной памяти, имеет отношение к mallocs), поэтому я хочу посмотреть, не происходит ли утечка виртуальной памяти или у нас просто есть страницы, выделенные для память, которую мы не теряем.

Итак, учитывая, что все URL-адреса, которые мы использовали до сих пор, использовали проверенный HTTPS, я переключился на использование http://google.com и, хотя объем памяти все еще увеличивается, кажется, что он потребляет на ~ 11-14 МБ меньше в целом. По-прежнему все возвращается к строке conn.getresponse (и, в меньшей степени, сейчас, conn.request ).

Интересно то, что VMS, похоже, не сильно разрастается, когда я рассматриваю это в ответе. Мне еще предстоит изменить mprof, чтобы вернуть это значение вместо значения RSS. Постоянно увеличивающаяся VMS обязательно укажет на утечку памяти, в то время как RSS может быть просто большим количеством mallocs (что возможно). Большинство операционных систем (если я правильно понимаю) не восстанавливают RSS с энтузиазмом, поэтому до тех пор, пока другая страница приложения не выйдет из строя и не будет никуда его назначить, RSS никогда не сжимается (даже если бы мог). Тем не менее, если мы постоянно увеличиваем, не достигая устойчивого состояния, я не могу быть уверен, что это запросы / urllib3 или просто интерпретатор

Я также собираюсь посмотреть, что происходит, когда мы используем urllib2 / httplib напрямую, потому что я начинаю думать, что это не наша проблема. Насколько я могу судить, Session#close правильно закрывает все сокеты и удаляет ссылки на них, чтобы их можно было использовать для сборки мусора. Далее, если сокет необходимо заменить пулом подключений, произойдет то же самое. Даже SSLSockets, похоже, правильно справляется со сборкой мусора.

Таким образом, urllib2 постоянно составляет около 13,3 МБ. Разница в том, что мне пришлось обернуть его в try / за исключением того, что через короткое время он постоянно вылетал с ошибкой URLError. Так что, возможно, через некоторое время он на самом деле ничего не делает.

@ sigmavirus24 Вы

Хм ... Python только освобождает память для повторного использования, а система не получает память обратно, пока процесс не завершится. Итак, я бы подумал, что плоская линия, которую вы видите на уровне 13,3 МБ, вероятно, указывает на отсутствие утечки памяти с urllib2, в отличие от urllib3.

Было бы неплохо подтвердить, что проблема может быть связана с urllib3. Можете ли вы поделиться скриптами, которые вы используете для тестирования с urllib2?

Итак, я начинаю задаваться вопросом, не имеет ли это какое-то отношение к объектам HTTPConnection . Если вы сделаете

import sys
import requests

s = requests.Session()
r = s.get('https://httpbin.org/get')
print('Number of response refs: ', sys.getrefcount(r) - 1)
print('Number of session refs: ', sys.getrefcount(s) - 1)
print('Number of raw refs: ', sys.getrefcount(r.raw) - 1)
print('Number of original rsponse refs: ', sys.getrefcount(r.raw._original_response) - 1)

Первые три должны вывести 1, последние 3. [1] Я уже определил, что HTTPConnection имеет _HTTPConnection__response который является ссылкой на _original_response . Так что я ожидал, что это число будет 3. Что я не могу понять, так это то, что содержит ссылку на третью копию.

Для дальнейшего развлечения добавьте следующие

import gc
gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_UNCOLLECTABLE)

к началу сценария. После вызова запросов есть 2 недостижимых объекта, что интересно, но ничего не удалось собрать. Если вы добавите это в предоставленный сценарий @mhjohnson и отфильтруете вывод для строк с недоступными в них, вы увидите, что во многих случаях имеется более 300 недостижимых объектов. Я пока не знаю, какое значение имеют недостижимые объекты. Как всегда, буду держать вас в курсе.

@mhjohnson, чтобы проверить urllib3, просто замените вызов requests.get на urllib2.urlopen (также я, вероятно, должен был сделать r.read() но я этого не сделал).

Итак, я взял предыдущее предложение @mhjohnson и использовал objgraph чтобы выяснить, где находится другая ссылка, но objgraph не может ее найти. Я добавил:

objgraph.show_backrefs([r.raw._original_response], filename='requests.png')

В скрипте 2 комментария выше и получилось следующее:
requests что только показывает, что на него было бы 2 ссылки. Интересно, есть ли что-то ненадежное в том, как работает sys.getrefcount .

Так что это отвлекающий маневр. urllib3.response.HTTPResponse содержит как _original_response и _fp . Это в сочетании с _HTTPConection__response дает нам три ссылки.

Итак, urllib3.response.HTTPResponse имеет атрибут _pool который также ссылается PoolManager . Точно так же HTTPAdapter использованное для выполнения запроса, имеет ссылку на возвращаемые Response запросы. Может, отсюда кто-то еще что-нибудь узнает

requests

Код, который генерирует это: https://gist.github.com/sigmavirus24/bc0e1fdc5f248ba1201d

@ sigmavirus24
Да, я немного заблудился с этим последним изображением. Вероятно, потому что я не очень хорошо знаю кодовую базу и не очень опытен в отладке утечек памяти.

Вы знаете, на какой объект я указываю красной стрелкой на этом снимке экрана вашего рисунка?
http://cl.ly/image/3l3g410p3r1C

Мне удалось получить код, показывающий то же медленно увеличивающееся использование памяти.
на python3, заменив urllib3 / requests на urllib.request.urlopen.

Модифицированный код здесь: https://gist.github.com/kevinburke/f99053641fab0e2259f0

Кевин Берк
телефон: 925.271.7005 | twentymilliseconds.com

В пн, 3 ноября 2014 г., в 21:28, Мэтью Джонсон [email protected]
написал:

@ sigmavirus24 https://github.com/sigmavirus24
Да, я немного заблудился с этим последним изображением. Наверное, потому что я не
очень хорошо знаю кодовую базу, и я не очень опытен в отладке памяти
утечки.

Вы знаете, на какой объект я указываю красной стрелкой
на этом скриншоте вашего рисунка?
http://cl.ly/image/3l3g410p3r1C

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

Насколько я могу судить, отправляя запросы на веб-сайт, который возвращает
Подключение: закрыть заголовок (например https://api.twilio.com/2010-04-01.json)
не увеличивает использование памяти значительно. Предупреждение
есть несколько разных факторов, и я просто предполагаю, что это сокет
связанный вопрос.

Кевин Берк
телефон: 925.271.7005 | twentymilliseconds.com

В понедельник, 3 ноября 2014 г., в 21:43 Кевин Берк [email protected] написал:

Мне удалось получить код, показывающий то же медленно увеличивающееся использование памяти.
на python3, заменив urllib3 / requests на urllib.request.urlopen.

Модифицированный код здесь:
https://gist.github.com/kevinburke/f99053641fab0e2259f0

Кевин Берк
телефон: 925.271.7005 | twentymilliseconds.com

В пн, 3 ноября 2014 г., в 21:28, Мэтью Джонсон [email protected]
написал:

@ sigmavirus24 https://github.com/sigmavirus24
Да, я немного заблудился с этим последним изображением. Наверное, потому что я
не очень хорошо знаю кодовую базу, и я не очень опытен в отладке
утечки памяти.

Вы знаете, на какой объект я указываю красной стрелкой
на этом скриншоте вашего рисунка?
http://cl.ly/image/3l3g410p3r1C

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

@mhjohnson, который, кажется, является количеством ссылок на метатип type от object который имеет тип type . Другими словами, я думаю, что это все ссылки либо на object либо на type , но я не совсем уверен. В любом случае, если я попытаюсь их исключить, граф станет чем-то вроде двух узлов.

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

Потратив некоторое время на это вместе с @mhjohnson , я могу подтвердить теорию @kevinburke, связанную с тем, как GC обрабатывает сокеты на PyPy.

Коммит 3c0b94047c1ccfca4ac4f2fe32afef0ae314094e является интересным. В частности, строка https://github.com/kennethreitz/requests/blob/master/requests/models.py#L736

Вызов self.raw.release_conn() перед возвратом контента значительно уменьшил используемую память в PyPy, хотя все еще есть возможности для улучшений.

Кроме того, я думаю, что было бы лучше, если бы мы задокументировали вызовы .close() которые относятся к классам сеанса и ответа, как также упоминается @ sigmavirus24. Пользователи запросов должны знать об этих методах, потому что в большинстве случаев методы не вызываются неявно.

У меня также есть вопрос и предложение, связанные с QA этого проекта. Могу я спросить разработчиков, почему мы не используем CI для обеспечения целостности наших тестов? Наличие CI также позволит нам писать тестовые сценарии, в которых мы можем профилировать и отслеживать любые падения производительности / памяти.

Хороший пример такого подхода можно найти в проекте pq:
https://github.com/malthe/pq/blob/master/pq/tests.py#L287

Спасибо всем, кто ухватился за это и решил помочь!
Мы продолжим исследовать другие теории, вызывающие это.

@stas Я хочу сказать об одном:

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

Оставив на время PyPy в стороне, эти методы не нужно_ вызывать явно. Если объекты сокета станут недоступными в CPython, они получат автоматический gc'd, который включает закрытие дескрипторов файлов. Это не аргумент, чтобы не документировать эти методы, но это предупреждение, чтобы не заострять на них внимание.

Мы должны использовать CI, но, похоже, в данный момент он нездоров, и только @kennethreitz может это исправить. Он доберется до этого, когда у него будет время. Обратите внимание, однако, что тесты производительности чрезвычайно сложно провести правильно, чтобы они не были слишком шумными.

Оставив на время PyPy в стороне, эти методы не нужно вызывать явно. Если объекты сокета станут недоступными в CPython, они получат автоматический gc'd, который включает закрытие дескрипторов файлов. Это не аргумент, чтобы не документировать эти методы, но это предупреждение, чтобы не заострять на них внимание.

Я вроде как согласен с тем, что вы говорите, за исключением той части, которую мы обсуждаем здесь Python. Я не хочу начинать споры, но, читая _The Zen of Python_, питонический путь будет заключаться в том, чтобы следовать _Explicit лучше, чем implicit_. Я также не знаком с философией этого проекта, поэтому проигнорируйте мои мысли, если это не относится к requests .

Я был бы рад помочь с CI или тестами производительности, когда есть возможность! Спасибо за объяснение текущей ситуации.

Итак, я думаю, что нашел причину проблемы при использовании функционального API. Если вы сделаете

import requests
r = requests.get('https://httpbin.org/get')
print(r.raw._pool.pool.queue[-1].sock)

Сокет все еще открыт. Причина, по которой я говорю _appears_, заключается в том, что он все еще имеет атрибут _sock потому что если вы это сделаете

r.raw._pool.queue[-1].close()
print(repr(r.raw._pool.queue[-1].sock))

Вы увидите напечатанные None . Итак, что происходит, так это то, что urllib3 включает в каждый HTTPResponse атрибут, указывающий на пул соединений, из которого он был получен. Пул соединений имеет в очереди соединение с незакрытым сокетом. Проблема для функционального API будет устранена, если в requests/api.py мы сделаем:

def request(...):
    """..."""
    s = Session()
    response = s.request(...)
    s.close()
    return s

Тогда r.raw._pool прежнему будет пулом соединений, но r.raw._pool.pool будет None .

Сложнее всего становится то, что происходит, когда люди используют сеансы. Наличие у них close сеанса после каждого запроса бессмысленно и сводит на нет цель сеанса. На самом деле, если вы используете сеанс (без потоков) и делаете 100 запросов к тому же домену (и по той же схеме, например, https ), используя сеанс, утечку памяти гораздо труднее увидеть, если вы не подождите около 30 секунд, пока не будет создан новый сокет. Проблема в том, что, как мы уже видели, r.raw._pool - очень изменчивый объект. Это ссылка на пул соединений, который управляется диспетчером пула в запросах. Поэтому, когда сокет заменяется, он заменяется ссылками на него из каждого ответа, который все еще доступен (в области видимости). Что мне нужно сделать больше, так это выяснить, сохраняется ли что-нибудь на ссылках на сокеты после закрытия пулов соединений. Если я смогу найти что-то, что хранит ссылки, я думаю, мы найдем реальную утечку памяти.

Итак, у меня была одна идея - использовать objgraph, чтобы выяснить, что на самом деле ссылается на SSLSocket после вызова requests.get и я получил следующее:

socket

Интересно то, что, по-видимому, существует 7 ссылок на SSLSocket но только две обратные ссылки, которые мог найти objgraph. Я думаю, что одна из ссылок - это одна, переданная в objgraph, а другая - это привязка, которую я делаю в сценарии, который генерирует это, но по-прежнему оставляет 3 или 4 неучтенных ссылки, и я не уверен, откуда они.

Вот мой сценарий для его создания:

import objgraph
import requests

r = requests.get('https://httpbin.org/get')
s = r.raw._pool.pool.queue[-1].sock
objgraph.show_backrefs(s, filename='socket.png', max_depth=15, refcounts=True)

С помощью

import objgraph
import requests

r = requests.get('https://httpbin.org/get')
s = r.raw._pool.pool.queue[-1].sock
objgraph.show_backrefs(s, filename='socket-before.png', max_depth=15,
                       refcounts=True)
r.raw._pool.close()
objgraph.show_backrefs(s, filename='socket-after.png', max_depth=15,
                       refcounts=True)

socket-after.png показывает это:

socket-after

Итак, мы исключаем одну ссылку на ssl-сокет. Тем не менее, когда я смотрю на s._sock базовый socket.socket закрыт.

После нескольких длительных тестов мы обнаружили следующее:

  • вызов close() явно помогает!
  • пользователи, выполняющие несколько запросов, должны использовать Session и должным образом закрыть его после завершения. Пожалуйста, объедините # 2326
  • Пользователям PyPy лучше без JIT! Или они должны явно вызвать gc.collect() !

TL; DR; requests выглядит неплохо, ниже вы найдете пару графиков с этим фрагментом:

import requests
from memory_profiler import profile

<strong i="15">@profile</strong>
def get(session, i):
    return session.get('http://stas.nerd.ro/?{0}'.format(i))

<strong i="16">@profile</strong>
def multi_get(session, count):
    for x in xrange(count):
        resp = get(session, count+1)
        print resp, len(resp.content), x
        resp.close()

<strong i="17">@profile</strong>
def run():
    session = requests.Session()
    print 'Starting...'
    multi_get(session, 3000)
    print("Finished first round...")
    session.close()
    print 'Done.'

if __name__ == '__main__':
    run()

CPython:

Line #    Mem usage    Increment   Line Contents
================================================
    15      9.1 MiB      0.0 MiB   <strong i="23">@profile</strong>
    16                             def run():
    17      9.1 MiB      0.0 MiB       session = requests.Session()
    18      9.1 MiB      0.0 MiB       print 'Starting...'
    19      9.7 MiB      0.6 MiB       multi_get(session, 3000)
    20      9.7 MiB      0.0 MiB       print("Finished first round...")
    21      9.7 MiB      0.0 MiB       session.close()
    22      9.7 MiB      0.0 MiB       print 'Done.'

PyPy без JIT:

Line #    Mem usage    Increment   Line Contents
================================================
    15     15.0 MiB      0.0 MiB   <strong i="29">@profile</strong>
    16                             def run():
    17     15.4 MiB      0.5 MiB       session = requests.Session()
    18     15.5 MiB      0.0 MiB       print 'Starting...'
    19     31.0 MiB     15.5 MiB       multi_get(session, 3000)
    20     31.0 MiB      0.0 MiB       print("Finished first round...")
    21     31.0 MiB      0.0 MiB       session.close()
    22     31.0 MiB      0.0 MiB       print 'Done.'

PyPy с JIT:

Line #    Mem usage    Increment   Line Contents
================================================
    15     22.0 MiB      0.0 MiB   <strong i="35">@profile</strong>
    16                             def run():
    17     22.5 MiB      0.5 MiB       session = requests.Session()
    18     22.5 MiB      0.0 MiB       print 'Starting...'
    19    219.0 MiB    196.5 MiB       multi_get(session, 3000)
    20    219.0 MiB      0.0 MiB       print("Finished first round...")
    21    219.0 MiB      0.0 MiB       session.close()
    22    219.0 MiB      0.0 MiB       print 'Done.'

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

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

Что касается PyPy с JIT, вызов gc.collect() для того же количества вызовов сэкономил ~ 30% памяти. Вот почему я считаю, что результаты JIT следует исключить из этого обсуждения, поскольку это предмет того, как каждый настраивает виртуальную машину и оптимизирует код для JIT.

Итак, проблема явно связана с тем, как мы обрабатываем память, взаимодействуя с PyPy JIT. Было бы неплохо вызвать эксперта PyPy: @alex?

Я действительно не могу представить, какие запросы (и компания), возможно, делают, что может вызвать что-то подобное. Можете ли вы запустить свой тест с PYPYLOG=jit-summary:- в env и вставить результаты (которые распечатают некоторые данные, когда процесс завершится)

Надеюсь это поможет:

Line #    Mem usage    Increment   Line Contents
================================================
    15     23.7 MiB      0.0 MiB   <strong i="6">@profile</strong>
    16                             def run():
    17     24.1 MiB      0.4 MiB       session = requests.Session()
    18     24.1 MiB      0.0 MiB       print 'Starting...'
    19    215.1 MiB    191.0 MiB       multi_get(session, 3000)
    20    215.1 MiB      0.0 MiB       print("Finished first round...")
    21    215.1 MiB      0.0 MiB       session.close()
    22    215.1 MiB      0.0 MiB       print 'Done.'


[2cbb7c1bbbb8] {jit-summary
Tracing:        41  0.290082
Backend:        30  0.029096
TOTAL:              1612.933400
ops:                79116
recorded ops:       23091
  calls:            2567
guards:             7081
opt ops:            5530
opt guards:         1400
forcings:           198
abort: trace too long:  2
abort: compiling:   0
abort: vable escape:    9
abort: bad loop:    0
abort: force quasi-immut:   0
nvirtuals:          9318
nvholes:            1113
nvreused:           6666
Total # of loops:   23
Total # of bridges: 8
Freed # of loops:   0
Freed # of bridges: 0
[2cbb7c242e8b] jit-summary}

Я нахожусь на надежной 32-битной версии, используя последнюю версию PyPy с https://launchpad.net/~pypy/+archive/ubuntu/ppa

31 скомпилированный путь не объясняет использование 200+ МБ ОЗУ.

Можете ли вы поместить что-нибудь в свою программу для запуска
gc.dump_rpy_heap('filename.txt') при очень высоком уровне памяти
Применение? (Просто нужно запустить его один раз, это создаст дамп всех
память, о которой знает GC).

Затем, проверив дерево исходного кода PyPy, запустите ./pypy/tool/gcdump.py filename.txt и покажите нам результаты.

Благодаря!

В субботу, 8 ноября 2014 г., в 15:20:52 Стас Сухцов [email protected]
написал:

Надеюсь это поможет:

Строка # Использование памяти Приращение Содержание строки

15     23.7 MiB      0.0 MiB   <strong i="20">@profile</strong>
16                             def run():
17     24.1 MiB      0.4 MiB       session = requests.Session()
18     24.1 MiB      0.0 MiB       print 'Starting...'
19    215.1 MiB    191.0 MiB       multi_get(session, 3000)
20    215.1 MiB      0.0 MiB       print("Finished first round...")
21    215.1 MiB      0.0 MiB       session.close()
22    215.1 MiB      0.0 MiB       print 'Done.'

[2cbb7c1bbbb8] {jit-summary
Трассировка: 41 0,290082
Бэкэнд: 30 0,029096
ИТОГО: 1612.933400
ops: 79116
записано операций: 23091
звонки: 2567
охранников: 7081
opt ops: 5530
выбор охранников: 1400
форсингов: 198
прерывание: слишком длинная трассировка: 2
прерывание: компиляция: 0
abort: vable escape: 9
прерывание: плохой цикл: 0
abort: force quasi-immut: 0
nвиртуалов: 9318
nvholes: 1113
nvreused: 6666
Общее количество петель: 23
Общее количество мостов: 8
Освобождено Кол-во петель: 0
Освобождено Кол-во мостов: 0
[2cbb7c242e8b] jit-summary}

Я нахожусь на надежной 32-битной версии, использую последнюю версию PyPy от
https://launchpad.net/~pypy/+archive/ubuntu/ppa
https://launchpad.net/%7Epypy/+archive/ubuntu/ppa

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62269627
.

Журнал:

Line #    Mem usage    Increment   Line Contents
================================================
    16     22.0 MiB      0.0 MiB   <strong i="6">@profile</strong>
    17                             def run():
    18     22.5 MiB      0.5 MiB       session = requests.Session()
    19     22.5 MiB      0.0 MiB       print 'Starting...'
    20    217.2 MiB    194.7 MiB       multi_get(session, 3000)
    21    217.2 MiB      0.0 MiB       print("Finished first round...")
    22    217.2 MiB      0.0 MiB       session.close()
    23    217.2 MiB      0.0 MiB       print 'Done.'
    24    221.0 MiB      3.8 MiB       gc.dump_rpy_heap('bench.txt')


[3fd7569b13c5] {jit-summary
Tracing:        41  0.293192
Backend:        30  0.026873
TOTAL:              1615.665337
ops:                79116
recorded ops:       23091
  calls:            2567
guards:             7081
opt ops:            5530
opt guards:         1400
forcings:           198
abort: trace too long:  2
abort: compiling:   0
abort: vable escape:    9
abort: bad loop:    0
abort: force quasi-immut:   0
nvirtuals:          9318
nvholes:            1113
nvreused:           6637
Total # of loops:   23
Total # of bridges: 8
Freed # of loops:   0
Freed # of bridges: 0
[3fd756c29302] jit-summary}

Дамп здесь: https://gist.github.com/stas/ad597c87ccc4b563211a

Спасибо, что нашли время помочь с этим!

Таким образом, это составляет около 100 МБ использования. Есть два места для отдыха
из этого может быть, в "свободной памяти" GC хранит для различных вещей, и
в распределениях, отличных от GC - это означает такие вещи, как внутренний OpenSSL
распределения. Интересно, есть ли хороший способ узнать, есть ли структуры OpenSSL
утекают, это то, что здесь тестируется с помощью TLS, если да, можете ли вы
попробуйте с сайтом без TLS и посмотрите, воспроизводится ли он?

В субботу, 8 ноября 2014 г., в 17:38:04 Стас Суков [email protected]
написал:

Журнал:

Строка # Использование памяти Приращение Содержание строки

16     22.0 MiB      0.0 MiB   <strong i="18">@profile</strong>
17                             def run():
18     22.5 MiB      0.5 MiB       session = requests.Session()
19     22.5 MiB      0.0 MiB       print 'Starting...'
20    217.2 MiB    194.7 MiB       multi_get(session, 3000)
21    217.2 MiB      0.0 MiB       print("Finished first round...")
22    217.2 MiB      0.0 MiB       session.close()
23    217.2 MiB      0.0 MiB       print 'Done.'
24    221.0 MiB      3.8 MiB       gc.dump_rpy_heap('bench.txt')

[3fd7569b13c5] {jit-summary
Трассировка: 41 0,293192
Бэкэнд: 30 0,026873
ИТОГО: 1615.665337
ops: 79116
записано операций: 23091
звонки: 2567
охранников: 7081
opt ops: 5530
выбор охранников: 1400
форсингов: 198
прерывание: слишком длинная трассировка: 2
прерывание: компиляция: 0
abort: vable escape: 9
прерывание: плохой цикл: 0
abort: force quasi-immut: 0
nвиртуалов: 9318
nvholes: 1113
nvreused: 6637
Общее количество петель: 23
Общее количество мостов: 8
Освобождено Кол-во петель: 0
Освобождено Кол-во мостов: 0
[3fd756c29302] jit-summary}

Дамп здесь: https://gist.github.com/stas/ad597c87ccc4b563211a

Спасибо, что нашли время помочь с этим!

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62277822
.

@alex ,

Я считаю, что @stas использовал обычное соединение http (не SSL / TLS) для этого теста. На всякий случай я также использовал тестовый сценарий сгенерировал его на своем Mac (OSX 10.9.5 2,5 ГГц, i5 8 ГБ 1600 МГц DDR3) с обычным http-соединением.

Если это поможет, вот мои результаты для сравнения (используя ваши инструкции):
https://gist.github.com/mhjohnson/a13f6403c8c3a3d49b8d

Дайте мне знать, что вы думаете.

Благодаря,

-Матт

Регулярное выражение GitHub слишком расплывчатое. Я открываю это снова, потому что не думаю, что это полностью решено.

Здравствуйте, может быть, я смогу указать, что проблема существует. У меня есть сканер, который использует запросы, а процесс использует многопроцессорность. Бывает, что один и тот же результат получают несколько экземпляров. Возможно, есть утечка в буфере результата или в самом сокете.

Сообщите мне, могу ли я отправить образец кода или как сгенерировать справочное дерево, чтобы определить, какая часть информации «совместно используется» (утечка)

благодаря

@barroca , это другая проблема. Скорее всего, вы используете сеанс между потоками и используете stream=True . Если вы закрываете ответ до того, как закончили его читать, сокет снова помещается в пул соединений с этими данными (если я правильно помню). Если этого не происходит, вероятно, вы выбираете самое последнее соединение и получаете кешированный ответ от сервера. В любом случае это не показатель утечки памяти.

@ sigmavirus24 Спасибо, Ян, Вы упомянули, что сеанс не использовался по потокам. Спасибо за объяснение и извините за обновление не той проблемы.

Не беспокойся @barroca :)

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

Итак, каково решение этого вопроса?

@Makecodeeasy, я тоже хочу это знать

пока моя проблема с requests является поточно-ориентированной,
лучше всего использовать отдельный сеанс для другого потока,

Моя постоянная работа по прохождению миллионов URL-адресов для проверки ответа кеша привела меня сюда

когда я обнаружил, что использование памяти превышает разумное, когда requests взаимодействуют с ThreadPoolExecutor или threading ,
в конце я просто использую multiprocessing.Process для изоляции рабочего и независимый сеанс для каждого рабочего

@AndCycle, значит, вашей проблемы здесь нет. Был объединен PR, чтобы исправить этот конкретный случай утечки памяти. Он не регрессировал, так как вокруг него есть испытания. И ваша проблема звучит совсем иначе.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги