Requests: احتمال تسرب الذاكرة

تم إنشاؤها على ١٧ أكتوبر ٢٠١٣  ·  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 باعتباره الجاني الذي يزيد من استخدام الذاكرة.

بواسطة ** Iteration 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..."

لا ينخفض ​​استخدام الذاكرة إلا إذا تم إنهاء البرنامج.

هل هناك خطأ أم أنه خطأ مستخدم؟

Bug

التعليق الأكثر فائدة

لم تكن هناك شكاوى أخرى من هذا الأمر وأعتقد أننا بذلنا قصارى جهدنا في هذا الشأن. يسعدني إعادة فتحه وإعادة التحقيق إذا لزم الأمر

ال 53 كومينتر

شكرا لرفع هذا وتقديم الكثير من التفاصيل!

أخبرني ، هل رأيت يومًا أن استخدام الذاكرة ينخفض ​​في أي وقت؟

لم أر استخدام الذاكرة ينخفض. كنت أتساءل عما إذا كان الأمر يتعلق بمجمع القمامة في Python وربما لم تتح له الفرصة للبدء ، لذلك أضفت مكالمة إلى gc.collect() بعد كل تنزيل. لم يحدث فرق.

هل لي أن أسأل لماذا تم إغلاق هذه القضية؟

واجهت شركتي هذه المشكلة نفسها ، والتي ازدادت حدتها عند استخدام pypy. لقد أمضينا عدة أيام في تتبع أصل هذه المشكلة في قاعدة الكود لدينا لطلبات بيثون.

فقط لتسليط الضوء على خطورة هذه المشكلة ، إليك لقطة شاشة لما بدت عليه إحدى عمليات الخادم لدينا عند تشغيل ملف تعريف الذاكرة:
http://cl.ly/image/3X3G2y3Y191h

لا تزال المشكلة موجودة مع cpython العادي ، ولكنها أقل وضوحًا. ربما لهذا السبب لم يتم الإبلاغ عن هذه المشكلة ، على الرغم من العواقب الوخيمة التي تترتب على استخدام هذه المكتبة لعمليات طويلة الأمد.

في هذه المرحلة ، نحن يائسون بما يكفي للنظر في استخدام curl مع عملية فرعية.

يرجى إعلامي برأيك وما إذا كان سيتم التحقيق في هذا الأمر بدقة. بخلاف ذلك ، أعتقد أن طلبات python خطيرة جدًا لاستخدامها في تطبيقات المهام الحرجة (على سبيل المثال: الخدمات المتعلقة بالرعاية الصحية).

شكر،
-غير لامع

تم إغلاقه بسبب عدم النشاط. إذا كنت تعتقد أنه يمكنك تقديم تشخيصات مفيدة لتوجيهنا في الاتجاه الصحيح ، فسيسعدنا إعادة فتحنا.

حسنًا ، اسمح لي بالمساعدة.

لقد أنشأت git repo صغيرًا للمساعدة في تسهيل فحص هذه المشكلة.
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()

Lemme أعرف ما إذا كان بإمكاني المساعدة بأي شكل من الأشكال.

-غير لامع

تخميني الأول للجاني سيكون كائنات مقبس. هذا من شأنه أن يفسر سبب كونه أسوأ على PyPy ...

أنا جالس في مطار الآن وسأكون في سهل لعدة ساعات قريبًا. ربما لن أتمكن من الوصول إلى هذه الليلة أو حتى وقت لاحق من هذا الأسبوع (إن لم يكن عطلة نهاية الأسبوع / الأسبوع القادمة). حتى الآن ، حاولت استخدام release_conn على HTTPResponse الذي نسترده. لقد راجعت مع gc.get_referents ما الذي يحتويه كائن الاستجابة والذي قد يفشل في الحصول على GC. يحتوي على HTplib HTTPResponse الأصلي (مخزن على هيئة _original_response وهذا (من ما أبلغ عنه get_referents ) يحتوي فقط على رسالة بريد إلكتروني (للرؤوس) وكل شيء آخر إما سلسلة أو قاموس (أو ربما القوائم) .إذا كانت مآخذ ، فأنا لا أرى أين لن يتم جمع القمامة.

أيضًا ، استخدام Session#close (لقد جعلت الكود يستخدم الجلسات بدلاً من واجهة برمجة التطبيقات الوظيفية أولاً) لا يساعد (وهذا يجب أن يمسح PoolManagers الذي يمسح تجمعات الاتصال). لذا فإن الشيء الآخر المثير للاهتمام هو أن 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 على اتصال HTTPC . لن يكون من السهل التنميط أكثر عن ذلك. إذا نظرنا إلى _validate_conn السطر المسبب لهذا هو conn.connect() وهو طريقة HTTPConnection أخرى. يكاد يكون من المؤكد أن connect هو المكان الذي يتم فيه إنشاء المقبس. إذا قمت بتعطيل تحديد سمات الذاكرة وألصقت print(old_pool) في HTTPConnectionPool#close فلن يطبع أي شيء أبدًا. يبدو أننا لا نغلق مجموعاتنا بالفعل حيث يتم تدمير الجلسة. أعتقد أن هذا هو سبب تسرب الذاكرة.

أرغب في المساعدة في تصحيح هذا الخطأ ، سأكون داخل / خارج IRC اليوم وغدًا.

لذلك ، إذا قمت بفتح python مع _make_request لا يزال مزينًا (بـ profile ) ، وأنشأت جلسة ، فقم بإجراء الطلبات كل 10 أو 20 ثانية (إلى نفس عنوان URL حتى) ، ستلاحظ أنه تم اعتبار conn قد تم إسقاطه ، لذلك تم إغلاق VerifiedHTTPSConnection ثم إعادة استخدامه. هذا يعني إعادة استخدام الفئة connection ، وليس المقبس الأساسي. الطريقة close هي الطريقة التي تعيش على httplib.HTTPConnection (L798). يؤدي هذا إلى إغلاق كائن مأخذ التوصيل ، ثم تعيينه إلى لا شيء. ثم يغلق (ويضبط على بلا) آخر 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 / HTplib مباشرة لأنني بدأت أعتقد أن هذه ليست مشكلتنا. بقدر ما أستطيع أن أقول ، Session#close يغلق بشكل صحيح جميع المقابس ويزيل الإشارات إليها للسماح لهم بأن يكونوا GC. علاوة على ذلك ، إذا كان هناك حاجة إلى استبدال المقبس بمجمع الاتصال ، يحدث نفس الشيء. حتى SSLSockets يبدو أنها تتعامل بشكل صحيح مع عملية جمع القمامة.

لذلك يبدو أن urllib2 ثابتًا حول 13.3 ميغا بايت. الاختلاف هو أنني اضطررت إلى لفه في محاولة / إلا لأنه سيتعطل باستمرار مع 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] لقد حددت بالفعل أن اتصال HTTPC به _HTTPConnection__response وهو مرجع إلى _original_response . لذلك كنت أتوقع أن يكون هذا الرقم 3. ما لا أستطيع معرفته هو ما يحمل الإشارة إلى النسخة الثالثة.

لمزيد من الترفيه ، أضف ما يلي

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

إلى بداية النص. هناك كائنان لا يمكن الوصول إليهما بعد إجراء استدعاء للطلبات وهو أمر مثير للاهتمام ، ولكن لا يوجد شيء غير قابل للتحصيل. إذا قمت بإضافة هذا إلى البرنامج النصي mhjohnson المقدم وقمت بتصفية الإخراج

mhjohnson لاختبار urllib3 ، ما عليك سوى استبدال مكالمتك بـ requests.get بـ urllib2.urlopen (كما كان ينبغي عليّ أن أعمل r.read() لكنني لم أكن كذلك).

لذلك أخذت اقتراحmhjohnson السابق واستخدمت objgraph لمعرفة مكان المرجع الآخر ، ولكن يبدو أن objgraph لا يمكنه العثور عليه. أضفت:

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

في النص 2 تعليقات أعلاه وحصلت على ما يلي:
requests مما يدل فقط على أنه سيكون هناك إشارتان إليها. أتساءل عما إذا كان هناك شيء ما حول كيفية عمل 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 / طلبات بـ urllib.request.urlopen.

كود معدل هنا: https://gist.github.com/kevinburke/f99053641fab0e2259f0

كيفن بيرك
الهاتف: 925.271.7005 | twentymilliseconds.com

يوم الاثنين ، 3 نوفمبر 2014 الساعة 9: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 الساعة 9:43 مساءً ، كتب Kevin Burke [email protected] :

تمكنت من الحصول على الرمز لإظهار نفس زيادة استخدام الذاكرة ببطء
على python3 عن طريق استبدال urllib3 / طلبات بـ urllib.request.urlopen.

كود معدل هنا:
https://gist.github.com/kevinburke/f99053641fab0e2259f0

كيفن بيرك
الهاتف: 925.271.7005 | twentymilliseconds.com

يوم الاثنين ، 3 نوفمبر 2014 الساعة 9: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. يجب أن يكون المستخدمون على دراية بهذه الأساليب ، لأنه في معظم الحالات لا يتم استدعاء الطرق ضمنيًا.

لدي أيضًا سؤال واقتراح متعلق بضمان الجودة لهذا المشروع. هل لي أن أسأل المشرفين لماذا لا نستخدم 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 تلقائي ، والذي يتضمن إغلاق مقابض الملفات. هذه ليست حجة لعدم توثيق تلك الأساليب ، لكنها تحذير بعدم التركيز عليها كثيرًا.

أنا أتفق نوعًا ما مع ما تقوله ، باستثناء الجزء الذي نناقش فيه لغة بايثون هنا. لا أريد أن أبدأ مناقشة ، لكن قراءة _The Zen of Python_ ، فإن الطريقة البيثونية ستكون اتباع نهج _Explicit is better than implicit_view. أنا أيضًا لست على دراية بفلسفة هذا المشروع ، لذا يرجى تجاهل أفكاري إذا كان هذا لا ينطبق على requests .

سأكون سعيدًا للمساعدة في اختبارات CI أو الاختبارات المعيارية كلما سنحت لي الفرصة! شكرا لشرح الوضع الحالي.

لذلك ، أعتقد أنني وجدت سبب المشكلة عند استخدام واجهة برمجة التطبيقات الوظيفية. اذا فعلت

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

يبدو أن المقبس لا يزال مفتوحًا. السبب في أنني أقول _ يبدو_ هو أنه لا يزال يحتوي على سمة _sock لأنه إذا فعلت ذلك

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

سترى None مطبوعًا. إذن ما يحدث هو أن urllib3 يتضمن في كل HTTPResponse سمة تشير إلى مجموعة الاتصال التي جاءت منها. يحتوي تجمع الاتصال على الاتصال في قائمة الانتظار التي تحتوي على المقبس غير المغلق. سيتم إصلاح المشكلة ، بالنسبة لواجهة برمجة التطبيقات الوظيفية ، إذا قمنا بما يلي في 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 كائن قابل للتغيير للغاية. إنها إشارة إلى مجموعة الاتصال التي يديرها مدير التجمع في الطلبات. لذلك عند استبدال المقبس ، يتم استبداله بإشارات إليه من كل استجابة لا تزال قابلة للوصول (في النطاق). ما أحتاج إلى القيام به هو معرفة ما إذا كان هناك أي شيء لا يزال معلقًا على الإشارات إلى المقابس بعد إغلاق تجمعات الاتصال. إذا تمكنت من العثور على شيء ما يحتفظ بالمراجع ، أعتقد أننا سنجد تسرب الذاكرة _real_.

لذا كانت إحدى الأفكار التي خطرت لي هي استخدام الرسم البياني لمعرفة ما يشير في الواقع إلى 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 من هذه المناقشة لأنها موضوع كيف يقوم الجميع بتعديل VM وتحسين رمز JIT.

حسنًا ، يبدو أن المشكلة تكمن في الطريقة التي نتعامل بها مع الذاكرة التي تتفاعل مع PyPy JIT. قد يكون من الجيد استدعاء خبير PyPy:alex؟

لا أستطيع حقًا أن أتخيل الطلبات (والشركة) التي من المحتمل أن تسبب أي شيء من هذا القبيل. هل يمكنك إجراء الاختبار باستخدام PYPYLOG=jit-summary:- في بيئة العمل ولصق النتائج (التي ستطبع بعض الأشياء عند انتهاء العملية)

أتمنى أن يساعدك هذا:

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 وأظهر لنا النتائج.

شكر!

يوم السبت 08 نوفمبر 2014 الساعة 3:20:52 مساءً Stas Sușcov [email protected]
كتب:

أتمنى أن يساعدك هذا:

السطر # Mem استخدام زيادة محتويات السطر

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-abstract
التتبع: 41 0.290082
الخلفية: 30 0.029096
المجموع: 1612.933400.000
العمليات: 79116
العمليات المسجلة: 23091
المكالمات: 2567
الحراس: 7081
عمليات الاشتراك: 5530
حراس الاختيار: 1400
التأثيرات: 198
إحباط: تتبع طويل جدًا: 2
إجهاض: تجميع: 0
إجهاض: هروب vable: 9
إحباط: حلقة سيئة: 0
إجهاض: قوة شبه ثابتة: 0
nvirtuals: 9318
nvholes: 1113
nvreused: 6666
إجمالي عدد الحلقات: 23
إجمالي عدد الجسور: 8
تم تحرير عدد الحلقات: 0
عدد الجسور المحررة: 0
[2cbb7c242e8b] ملخص جيت}

أنا على ثقة 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 ومعرفة ما إذا كان يتكاثر؟

يوم السبت 08 نوفمبر 2014 الساعة 5:38:04 مساءً Stas Sușcov [email protected]
كتب:

سجل:

السطر # Mem استخدام زيادة محتويات السطر

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-abstract
التتبع: 41 0.293192
الخلفية: 30 0.026873
المجموع: 1615.665337.37
العمليات: 79116
العمليات المسجلة: 23091
المكالمات: 2567
الحراس: 7081
عمليات الاشتراك: 5530
حراس الاختيار: 1400
التأثيرات: 198
إحباط: تتبع طويل جدًا: 2
إجهاض: تجميع: 0
إجهاض: هروب vable: 9
إحباط: حلقة سيئة: 0
إجهاض: قوة شبه ثابتة: 0
nvirtuals: 9318
nvholes: 1113
nvreused: 6637
إجمالي عدد الحلقات: 23
إجمالي عدد الجسور: 8
تم تحرير عدد الحلقات: 0
عدد الجسور المحررة: 0
[3fd756c29302] ملخص جيت}

التفريغ هنا: https://gist.github.com/stas/ad597c87ccc4b563211a

شكرا لأخذ وقتك للمساعدة في ذلك!

-
قم بالرد على هذا البريد الإلكتروني مباشرةً أو قم بعرضه على GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62277822
.

alex ،

أعتقد أن stas قد استخدم اتصال http منتظمًا (بخلاف SSL / TLS) لهذا المعيار. فقط في هذه الحالة ، استخدمت أيضًا البرنامج النصي المعياري الخاص بـ stas وقمت بتشكيله على جهاز 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 ثم مشكلتك ليست هنا. تم دمج العلاقات العامة لإصلاح حالة تسرب الذاكرة هذه. لم يتراجع لأن هناك اختبارات حوله. ويبدو أن مشكلتك مختلفة تمامًا.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات

القضايا ذات الصلة

cnicodeme picture cnicodeme  ·  3تعليقات

ghtyrant picture ghtyrant  ·  3تعليقات

brainwane picture brainwane  ·  3تعليقات

jakul picture jakul  ·  3تعليقات

remram44 picture remram44  ·  4تعليقات