Requests: Kemungkinan Kebocoran Memori

Dibuat pada 17 Okt 2013  ·  53Komentar  ·  Sumber: psf/requests

Saya memiliki program yang sangat sederhana yang secara berkala mengambil gambar dari kamera IP. Saya perhatikan bahwa set kerja program ini tumbuh secara monoton. Saya telah menulis program kecil yang mereproduksi masalah tersebut.

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()

Penggunaan memori dicetak di akhir setiap iterasi. Ini adalah keluaran sampel.
* Iterasi 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..."

* Iterasi 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..."

Penggunaan memori tidak bertambah dengan setiap iterasi, tetapi terus meningkat dengan requests.get menjadi penyebab yang meningkatkan penggunaan memori.

Dengan ** Iterasi 99 ** seperti inilah tampilan profil memori.

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..."

Penggunaan memori tidak turun kecuali program dihentikan.

Apakah ada bug atau apakah itu kesalahan pengguna?

Bug

Komentar yang paling membantu

Tidak ada keluhan lebih lanjut tentang hal ini yang timbul dan saya pikir kami telah melakukan yang terbaik dalam hal ini. Saya akan dengan senang hati membukanya kembali dan menyelidikinya kembali jika perlu

Semua 53 komentar

Terima kasih telah mengungkapkan ini dan memberikan begitu banyak detail!

Katakan padaku, apakah Anda pernah melihat penggunaan memori turun pada suatu saat?

Saya belum melihat penggunaan memori turun. Saya bertanya-tanya apakah itu ada hubungannya dengan pengumpul sampah Python dan mungkin itu tidak memiliki kesempatan untuk masuk, jadi saya menambahkan panggilan ke gc.collect() setelah setiap unduhan. Tidak ada bedanya.

Bolehkah saya bertanya mengapa masalah ini telah ditutup?

Perusahaan saya telah mengalami masalah yang sama ini, yang menjadi semakin bertambah saat menggunakan pypy. Kami menghabiskan beberapa hari menelusuri asal mula masalah ini di basis kode kami ke permintaan-python.

Hanya untuk menyoroti keseriusan masalah ini, berikut tangkapan layar dari salah satu proses server kami saat menjalankan profiler memori:
http://cl.ly/image/3X3G2y3Y191h

Masalahnya masih ada dengan cpython biasa, tetapi kurang terlihat. Mungkin, inilah mengapa masalah ini tidak dilaporkan, terlepas dari konsekuensi serius yang dimilikinya bagi mereka yang menggunakan pustaka ini untuk proses yang berlangsung lama.

Pada titik ini, kami cukup putus asa untuk mempertimbangkan penggunaan curl dengan subproses.

Tolong beritahu saya apa yang Anda pikirkan dan jika ini akan diselidiki secara menyeluruh. Jika tidak, saya berpendapat bahwa permintaan-python terlalu berbahaya untuk digunakan untuk aplikasi misi kritis (misalnya: layanan terkait perawatan kesehatan).

Terima kasih,
-Matt

Itu ditutup karena tidak ada aktivitas. Jika Anda yakin dapat memberikan diagnosis yang berguna untuk mengarahkan kami ke arah yang benar, kami akan dengan senang hati membukanya kembali.

Baiklah, izinkan saya untuk membantu.

Saya membuat repo git kecil untuk membantu memfasilitasi pemeriksaan masalah ini.
https://github.com/mhjohnson/memory-profiling-requests

Berikut tangkapan layar dari grafik yang dihasilkannya:
http://cl.ly/image/453h1y3a2p1r

Semoga ini membantu! Beri tahu saya jika saya melakukan sesuatu yang salah.

-Matt

Terima kasih Matt! Saya akan mulai menyelidiki ini sekarang. Beberapa kali pertama saya menjalankan skrip, (dan variasi yang telah saya coba) menunjukkan bahwa ini mudah direproduksi. Saya harus mulai bermain-main dengan ini sekarang.

Jadi ini tumbuh sekitar 0,1MB / permintaan. Saya mencoba menempelkan dekorator profile pada metode tingkat yang lebih rendah tetapi semuanya terlalu lama untuk hasilnya berguna dari jarak jauh dan menggunakan interval yang lebih tinggi dari 0,1 tampaknya hanya berfungsi untuk melacak penggunaan keseluruhan, bukan per- penggunaan baris. Apakah ada alat yang lebih baik dari mprof?

Jadi saya memutuskan untuk menyalurkan outputnya ke | ag '.*0\.[1-9]+ MiB.*' untuk mendapatkan baris di mana memori ditambahkan dan memindahkan profile dekorator ke Session#send . Tidak mengherankan, sebagian besar berasal dari panggilan ke HTTPAdapter#send . Aku pergi ke lubang kelinci

Dan sekarang semuanya berasal dari panggilan ke conn.urlopen pada L355 dan HTTPAdapter#get_connection . Jika Anda mendekorasi get_connection , ada 7 kali ia mengalokasikan memori ketika memanggil PoolManager#connection_from_url . Sekarang karena mayoritas dipicu oleh HTTPResponse s dikembalikan dari urllib3, saya akan melihat apakah ada sesuatu yang kita _harus_ lakukan dengan mereka sehingga kita tidak memastikan bahwa memori dilepaskan setelah kejadian. Jika saya tidak dapat menemukan cara yang baik untuk mengatasinya, saya akan mulai menggali urllib3.

@ sigmavirus Wow. Kerja bagus! Sepertinya Anda telah menunjukkan titik panas di kode.
Sedangkan untuk menelusuri objek mana yang bertanggung jawab atas kebocoran memori, Anda mungkin mendapatkan beberapa petunjuk tambahan dengan menggunakan objgraph seperti ini:

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

Beri tahu saya jika saya dapat membantu dengan cara apa pun.

-Matt

Tebakan pertama saya tentang pelakunya adalah objek soket. Itu akan menjelaskan mengapa ini lebih buruk di PyPy ...

Saya sedang duduk di bandara sekarang dan saya akan berada di dataran selama beberapa jam segera. Saya mungkin tidak akan bisa sampai malam ini atau sampai akhir pekan ini (jika tidak akhir pekan / minggu depan). Namun sejauh ini, saya mencoba menggunakan release_conn pada HTTPResponse kami terima kembali. Saya memeriksa dengan gc.get_referents apa objek Respon memiliki yang mungkin gagal untuk di-GC. Ini memiliki HTTPResponse httplib asli (disimpan sebagai _original_response dan itu (dari apa yang dilaporkan get_referents ) hanya memiliki pesan email (untuk tajuk) dan yang lainnya adalah string atau kamus (atau mungkin daftar). Jika soket, saya tidak melihat di mana mereka tidak akan sampah dikumpulkan.

Juga, menggunakan Session#close (saya membuat kode menggunakan sesi daripada API fungsional pertama) tidak membantu (dan itu harus menghapus PoolManagers yang menghapus kumpulan koneksi). Jadi hal lain yang menarik adalah bahwa PoolManager#connection_from_url akan menambah ~ 0.8 MB (memberi atau menerima 0.1) beberapa kali pertama dipanggil. Jadi itu menambahkan ~ 3MB tetapi sisanya berasal dari conn.urlopen dalam HTTPAdapter#send . Hal yang aneh adalah bahwa gc.garbage memiliki beberapa elemen aneh jika Anda menggunakan gc.set_debug(gc.DEBUG_LEAK) . Ini memiliki sesuatu seperti [[[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None]] dan seperti yang Anda harapkan gc.garbage[0] is gc.garbage[0][0] sehingga informasi itu sama sekali tidak berguna. Saya harus bereksperimen dengan objgraph saat mendapat kesempatan.

Jadi saya menggali urllib3 dan mengikuti lubang kelinci lebih jauh tadi pagi. Saya membuat profil ConnectionPool#urlopen yang membawa saya ke ConnectionPool#_make_request . Pada titik ini, ada banyak memori yang dialokasikan dari baris 306 dan 333 di urllib3/connectionpool.py . L306 adalah self._validate_conn(conn) dan L333 adalah conn.getresponse(buffering=True) . getresponse adalah metode httplib pada HTTPConnection . Membuat profil lebih jauh ke dalam hal itu tidak akan mudah. Jika kita melihat _validate_conn baris di sana yang menyebabkan ini adalah conn.connect() yang merupakan metode HTTPConnection lain. connect hampir pasti di mana soket dibuat. Jika saya menonaktifkan profil memori dan saya tetap menggunakan print(old_pool) di HTTPConnectionPool#close tidak akan pernah mencetak apa pun. Tampaknya kami tidak benar-benar menutup kumpulan saat sesi dihancurkan. Dugaan saya inilah penyebab kebocoran memori.

Akan sangat senang membantu debug ini, saya akan keluar / masuk IRC hari ini dan besok.

Jadi jika ditelusuri lebih lanjut, jika Anda membuka python dengan _make_request masih dihias (dengan profile ), dan Anda membuat sesi, lalu membuat permintaan setiap 10 atau 20 detik (untuk URL yang sama bahkan), Anda akan melihat koneksi telah dianggap terputus, jadi VerifiedHTTPSConnection ditutup dan kemudian digunakan kembali. Ini berarti kelas connection digunakan kembali, bukan soket yang mendasarinya. Metode close adalah yang hidup dengan httplib.HTTPConnection (L798). Ini menutup objek soket, lalu menyetelnya ke Tidak Ada. Kemudian menutup (dan menyetel ke Tidak Ada) httplib.HTTPResponse . Jika Anda juga membuat profil VerifiedHTTPSConnection#connect , semua memori yang dibuat / bocor terjadi di urllib3.util.ssl_.ssl_wrap_socket .

Jadi melihat ini, apa yang memory_profiler gunakan untuk melaporkan penggunaan memori adalah proses 'resident set size (rss). Ini adalah ukuran proses dalam RAM (vms, atau ukuran memori virtual, ada hubungannya dengan mallocs), jadi saya ingin melihat apakah kami membocorkan memori virtual, atau jika kami hanya mengalokasikan halaman untuk memori yang tidak akan hilang.

Jadi mengingat bahwa semua URL yang kami gunakan sejauh ini menggunakan HTTPS terverifikasi, saya beralih menggunakan http://google.com dan meskipun masih ada peningkatan memori yang konsisten, tampaknya itu menghabiskan ~ 11-14MiB lebih sedikit secara keseluruhan. Semuanya masih kembali ke baris conn.getresponse (dan pada tingkat yang lebih rendah sekarang, conn.request ).

Yang menarik adalah VMS sepertinya tidak banyak berkembang ketika saya memeriksanya di repl. Saya belum memodifikasi mprof untuk mengembalikan nilai tersebut, bukan nilai RSS. VMS yang terus meningkat pasti akan menunjukkan kebocoran memori sementara RSS bisa jadi hanya berupa mallocs dalam jumlah besar (yang mungkin saja). Sebagian besar sistem operasi (jika saya mengerti dengan benar) tidak mendapatkan kembali RSS dengan bersemangat, jadi sampai halaman aplikasi lain gagal dan tidak ada tempat lain untuk menetapkannya, RSS tidak akan pernah menyusut (bahkan jika bisa). Meskipun demikian, jika kami terus meningkat tanpa mencapai kondisi mapan, saya tidak dapat memastikan apakah itu permintaan / urllib3 atau hanya juru bahasa

Saya juga akan melihat apa yang terjadi jika kita menggunakan urllib2 / httplib secara langsung karena saya mulai berpikir bahwa ini bukan masalah kita. Sejauh yang saya tahu, Session#close dengan benar menutup semua soket dan menghapus referensi ke mereka untuk memungkinkan mereka untuk di-GC. Lebih lanjut, jika soket perlu diganti dengan Connection Pool, hal yang sama terjadi. Bahkan SSLSockets tampaknya menangani pengumpulan sampah dengan benar.

Jadi urllib2 secara konsisten terlihat rata di sekitar 13.3MiB. Perbedaannya adalah saya harus membungkusnya dalam percobaan / kecuali karena secara konsisten akan macet dengan URLError setelah beberapa saat. Jadi mungkin itu tidak benar-benar melakukan apa pun setelah beberapa saat.

@ sigmavirus24 Anda menghancurkannya! :)

Hmm ... Python hanya melepaskan memori untuk digunakan kembali dengan sendirinya, dan sistem tidak mendapatkan kembali memori sampai prosesnya selesai. Jadi, menurut saya garis datar yang Anda lihat di 13.3MiB mungkin merupakan indikasi tidak ada kebocoran memori dengan urllib2, tidak seperti urllib3.

Sebaiknya konfirmasikan bahwa masalah dapat diisolasi ke urllib3. Dapatkah Anda membagikan skrip yang Anda gunakan untuk menguji dengan urllib2?

Jadi saya mulai bertanya-tanya apakah ini tidak ada hubungannya dengan objek HTTPConnection . Jika kamu melakukan

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)

Tiga yang pertama harus mencetak 1, yang terakhir 3. [1] Saya sudah mengidentifikasi bahwa HTTPConnection memiliki _HTTPConnection__response yang merupakan referensi ke _original_response . Jadi saya mengharapkan nomor itu menjadi 3. Apa yang saya tidak tahu adalah apa yang memegang referensi ke salinan ke-3.

Untuk hiburan lebih lanjut, tambahkan yang berikut ini

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

ke awal skrip. Ada 2 objek yang tidak dapat dijangkau setelah melakukan panggilan ke permintaan yang menarik, tetapi tidak ada yang tidak dapat tertagih. Jika Anda menambahkan ini ke skrip @mhjohnson yang disediakan dan Anda memfilter keluaran untuk baris dengan unreachable di dalamnya, Anda akan melihat bahwa sering kali terdapat lebih dari 300 objek yang tidak dapat dijangkau. Saya belum tahu apa pentingnya benda yang tidak terjangkau itu. Seperti biasa, saya akan terus mengabari kalian.

@mhjohnson untuk menguji urllib3, cukup ganti panggilan Anda ke requests.get dengan urllib2.urlopen (juga saya seharusnya melakukan r.read() tetapi ternyata tidak).

Jadi saya mengambil saran sebelumnya dari @mhjohnson dan menggunakan objgraph untuk mencari tahu di mana referensi lainnya, tetapi objgraph sepertinya tidak dapat menemukannya. Saya tambahkan:

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

Dalam skrip 2 komentar di atas dan dapatkan sebagai berikut:
requests yang hanya menunjukkan bahwa akan ada 2 referensi untuk itu. Saya ingin tahu apakah ada sesuatu dengan cara kerja sys.getrefcount yang tidak dapat diandalkan.

Jadi itu ikan haring merah. urllib3.response.HTTPResponse memiliki _original_response dan _fp . Itu dikombinasikan dengan _HTTPConection__response memberi kita tiga referensi.

Jadi, urllib3.response.HTTPResponse memiliki atribut _pool yang juga direferensikan oleh PoolManager . Demikian juga, HTTPAdapter digunakan untuk membuat permintaan, memiliki referensi pada Response permintaan yang dikembalikan. Mungkin orang lain dapat mengidentifikasi sesuatu dari sini:

requests

Kode yang menghasilkan yaitu: https://gist.github.com/sigmavirus24/bc0e1fdc5f248ba1201d

@ sigmavirus
Ya, saya sedikit bingung dengan grafik terakhir itu. Mungkin karena saya tidak tahu basis kode dengan baik, saya juga tidak berpengalaman dalam men-debug kebocoran memori.

Apakah Anda tahu objek mana yang saya tunjuk dengan panah merah di tangkapan layar grafik Anda ini?
http://cl.ly/image/3l3g410p3r1C

Saya bisa mendapatkan kode untuk menunjukkan penggunaan memori yang meningkat secara perlahan
di python3 dengan mengganti urllib3 / request dengan urllib.request.urlopen.

Kode yang dimodifikasi di sini: https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin Burke
telepon: 925.271.7005 | twentymilliseconds.com

Pada hari Senin, 3 November 2014 pukul 21:28, Matthew Johnson [email protected]
menulis:

@ sigmavirus24 https://gub.com/igmavirus24
Ya, saya sedikit bingung dengan grafik terakhir itu. Mungkin karena saya tidak
mengetahui basis kode dengan sangat baik, dan saya juga tidak berpengalaman dalam debugging memori
kebocoran.

Tahukah Anda objek mana yang saya tunjuk dengan panah merah
di tangkapan layar grafik Anda ini?
http://cl.ly/image/3l3g410p3r1C

-
Balas email ini secara langsung atau lihat di GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

Sejauh yang saya tahu membuat permintaan ke situs web yang mengembalikan file
Koneksi: tutup header (misalnya https://api.twilio.com/2010-04-01.json)
tidak meningkatkan penggunaan memori secara signifikan. Peringatannya adalah
ada beberapa faktor yang berbeda dan saya hanya menganggap itu soket
masalah terkait.

Kevin Burke
telepon: 925.271.7005 | twentymilliseconds.com

Pada hari Senin, 3 November 2014 pukul 21:43, Kevin Burke [email protected] menulis:

Saya bisa mendapatkan kode untuk menunjukkan penggunaan memori yang meningkat secara perlahan
di python3 dengan mengganti urllib3 / request dengan urllib.request.urlopen.

Kode diubah di sini:
https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin Burke
telepon: 925.271.7005 | twentymilliseconds.com

Pada hari Senin, 3 November 2014 pukul 21:28, Matthew Johnson [email protected]
menulis:

@ sigmavirus24 https://gub.com/igmavirus24
Ya, saya sedikit bingung dengan grafik terakhir itu. Mungkin karena saya
tidak tahu basis kode dengan baik, saya juga tidak berpengalaman dalam debugging
kebocoran memori.

Tahukah Anda objek mana yang saya tunjuk dengan panah merah
di tangkapan layar grafik Anda ini?
http://cl.ly/image/3l3g410p3r1C

-
Balas email ini secara langsung atau lihat di GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

@mhjohnson yang tampaknya merupakan jumlah referensi ke metatype type oleh object yang merupakan tipe type . Dengan kata lain, saya pikir itu semua adalah referensi dari object atau type , tapi saya tidak begitu yakin. Bagaimanapun, jika saya mencoba untuk mengecualikannya, grafik menjadi seperti 2 node.

Saya juga sangat prihatin tentang masalah kebocoran memori ini karena kami menggunakan Permintaan dalam sistem perayapan web kami yang biasanya menjalankan proses selama beberapa hari. Apakah ada kemajuan dalam masalah ini?

Setelah menghabiskan beberapa waktu bersama dengan @mhjohnson , saya dapat mengonfirmasi teori @kevinburke terkait dengan cara GC memperlakukan soket di PyPy.

Komit 3c0b94047c1ccfca4ac4f2fe32afef0ae314094e adalah salah satu yang menarik. Secara khusus baris https://github.com/kennethreitz/requests/blob/master/requests/models.py#L736

Memanggil self.raw.release_conn() sebelum mengembalikan konten mengurangi secara signifikan memori yang digunakan di PyPy, meskipun masih ada ruang untuk perbaikan.

Juga, saya pikir akan lebih baik jika kita mendokumentasikan panggilan .close() yang berhubungan dengan sesi dan kelas respon, seperti yang juga disebutkan oleh @ sigmavirus24. Pengguna permintaan harus mengetahui metode tersebut, karena dalam kebanyakan kasus metode tidak dipanggil secara implisit.

Saya juga punya pertanyaan dan saran terkait QA proyek ini. Bolehkah saya bertanya kepada pengelola mengapa kami tidak menggunakan CI untuk memastikan integritas pengujian kami? Memiliki CI juga akan memungkinkan kami menulis kasus uji benchmark di mana kami dapat membuat profil dan melacak setiap regresi kinerja / memori.

Contoh yang baik dari pendekatan semacam itu dapat ditemukan di proyek pq:
https://github.com/malthe/pq/blob/master/pq/tests.py#L287

Terima kasih kepada semua orang yang melompat dan memutuskan untuk membantu!
Kami akan terus menyelidiki teori lain yang menyebabkan ini.

@stas saya ingin membahas satu hal:

Pengguna permintaan harus mengetahui metode tersebut, karena dalam kebanyakan kasus metode tidak dipanggil secara implisit.

Mengesampingkan PyPy sejenak, metode tersebut tidak perlu _need_ dipanggil secara eksplisit. Jika objek socket menjadi tidak terjangkau di CPython mereka akan mendapatkan auto gc'd, termasuk menutup file handle. Ini bukan argumen untuk tidak mendokumentasikan metode tersebut, tetapi ini adalah peringatan untuk tidak terlalu fokus pada metode tersebut.

Kami dimaksudkan untuk menggunakan CI, tetapi tampaknya saat ini sedang tidak sehat, dan hanya @kennethreitz yang berada dalam posisi untuk memperbaikinya. Dia akan melakukannya ketika dia punya waktu. Perhatikan, bagaimanapun, bahwa tes benchmark sangat sulit dilakukan dengan cara yang tidak membuatnya sangat berisik.

Mengesampingkan PyPy sejenak, metode tersebut tidak perlu dipanggil secara eksplisit. Jika objek socket menjadi tidak terjangkau di CPython mereka akan mendapatkan auto gc'd, termasuk menutup file handle. Ini bukan argumen untuk tidak mendokumentasikan metode tersebut, tetapi ini adalah peringatan untuk tidak terlalu fokus pada metode tersebut.

Saya agak setuju dengan apa yang Anda katakan, kecuali bagian yang kita bahas tentang Python di sini. Saya tidak ingin memulai argumen, tetapi membaca _The Zen of Python_, cara pythonic akan mengikuti _Explicit lebih baik daripada pendekatan implisit_. Saya juga tidak terbiasa dengan filosofi proyek ini, jadi abaikan pemikiran saya jika ini tidak berlaku untuk requests .

Saya akan dengan senang hati membantu dengan CI atau tes benchmark setiap kali ada kesempatan! Terima kasih telah menjelaskan situasi saat ini.

Jadi, saya rasa saya telah menemukan penyebab masalah saat menggunakan API fungsional. Jika kamu melakukan

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

Soket tampaknya masih terbuka. Alasan saya mengatakan _appears_ adalah karena masih memiliki atribut _sock adalah karena jika Anda melakukannya

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

Anda akan melihat None dicetak. Jadi apa yang terjadi adalah bahwa urllib3 disertakan pada setiap HTTPResponse atribut yang menunjuk ke Connection Pool asalnya. Pool koneksi memiliki Koneksi dalam antrian yang memiliki soket tidak tertutup. Masalahnya, untuk API fungsional, akan diperbaiki jika di requests/api.py kita melakukan:

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

Kemudian r.raw._pool akan tetap menjadi kumpulan koneksi tetapi r.raw._pool.pool akan menjadi None .

Bagian yang rumit menjadi apa yang terjadi ketika orang menggunakan sesi. Memberi mereka close sesi setelah setiap permintaan tidak masuk akal dan mengalahkan tujuan sesi. Pada kenyataannya, jika Anda menggunakan sesi (tanpa utas) dan membuat 100 permintaan ke domain yang sama (dan skema yang sama, misalnya, https ) menggunakan Sesi kebocoran memori jauh lebih sulit untuk dilihat, kecuali jika Anda tunggu sekitar 30 detik agar soket baru dibuat. Masalahnya adalah seperti yang telah kita lihat, r.raw._pool adalah objek yang sangat bisa berubah. Ini adalah referensi ke Connection Pool yang dikelola oleh Pool Manager dalam permintaan. Jadi ketika soket diganti diganti dengan referensi ke sana dari setiap respon yang masih bisa dijangkau (dalam cakupan). Yang perlu saya lakukan lebih banyak adalah mencari tahu apakah masih ada referensi ke soket setelah kami menutup kumpulan koneksi. Jika saya dapat menemukan sesuatu yang menyimpan referensi, saya pikir kita akan menemukan kebocoran memori _real_.

Jadi satu ide yang saya miliki adalah menggunakan objgraph untuk mencari tahu apa yang sebenarnya mereferensikan SSLSocket setelah panggilan ke requests.get dan saya mendapatkan ini:

socket

Hal yang menarik adalah ternyata ada 7 referensi ke SSLSocket tetapi hanya dua referensi belakang yang bisa ditemukan oleh objgraph. Saya pikir 1 dari referensi adalah yang diteruskan ke objgraph dan yang lainnya adalah pengikatan yang saya buat dalam skrip yang menghasilkan ini tetapi masih menyisakan 3 atau 4 referensi yang belum ditemukan yang saya tidak yakin dari mana asalnya.

Inilah skrip saya untuk menghasilkan ini:

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)

Menggunakan

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 menunjukkan ini:

socket-after

Jadi kami menghilangkan satu referensi ke soket ssl. Yang mengatakan, ketika saya melihat s._sock yang mendasari socket.socket ditutup.

Setelah menjalankan banyak tolok ukur yang berjalan lama, inilah yang kami temukan:

  • memanggil close() secara eksplisit membantu!
  • pengguna yang menjalankan banyak permintaan, harus menggunakan Session dan menutupnya dengan benar setelah selesai. Harap gabungkan # 2326
  • Pengguna PyPy lebih baik tanpa JIT! Atau mereka harus menyebut gc.collect() secara eksplisit!

TL; DR; requests terlihat bagus, di bawah ini Anda akan menemukan beberapa grafik yang menjalankan cuplikan ini:

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 tanpa 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 dengan 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.'

Saya percaya salah satu alasan mengapa kita semua pada awalnya bingung adalah karena menjalankan tolok ukur membutuhkan set yang lebih besar untuk mengecualikan cara GC berperilaku dari satu implementasi ke implementasi lainnya.

Juga menjalankan permintaan di lingkungan berulir membutuhkan kumpulan panggilan yang lebih besar karena cara kerja utas (kami tidak melihat variasi utama dalam penggunaan memori setelah menjalankan beberapa kumpulan utas).

Berkenaan dengan PyPy dengan JIT, memanggil gc.collect() untuk jumlah panggilan yang sama, menghemat ~ 30% memori. Itulah mengapa saya percaya hasil JIT harus dikecualikan dari diskusi ini karena ini adalah subjek tentang bagaimana semua orang mengubah VM dan mengoptimalkan kode untuk JIT.

Baiklah, jadi masalahnya secara eksplisit muncul pada cara kita menangani memori yang berinteraksi dengan PyPy JIT. Mungkin ide yang bagus untuk memanggil pakar PyPy: @alex?

Saya benar-benar tidak dapat membayangkan permintaan apa (dan perusahaan) yang mungkin dilakukan yang akan menyebabkan hal seperti ini. Dapatkah Anda menjalankan pengujian dengan PYPYLOG=jit-summary:- di env dan menempelkan hasilnya (yang akan mencetak beberapa hal saat proses berakhir)

Semoga ini membantu:

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}

Saya menggunakan 32bit terpercaya menggunakan PyPy terbaru dari https://launchpad.net/~pypy/+archive/ubuntu/ppa

31 jalur yang dikompilasi tidak menjelaskan 200+ MB RAM yang digunakan.

Dapatkah Anda memasukkan sesuatu ke dalam program Anda untuk dijalankan
gc.dump_rpy_heap('filename.txt') saat memori sangat tinggi
pemakaian? (Hanya perlu menjalankannya sekali, ini akan menghasilkan dump dari semua file
memori yang diketahui GC).

Kemudian dengan checkout dari pohon sumber PyPy, jalankan ./pypy/tool/gcdump.py filename.txt dan tunjukkan hasilnya.

Terima kasih!

Pada Sabtu, 08 November 2014 pukul 15:20:52. Stas Sușcov [email protected]
menulis:

Semoga ini membantu:

Baris # Memakai Isi Baris Kenaikan

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
Menelusuri: 41 0,290082
Backend: 30 0,029096
JUMLAH: 1612.933400
ops: 79116
operasi yang direkam: 23091
panggilan: 2567
penjaga: 7081
opt ops: 5530
memilih penjaga: 1400
pukulan: 198
batalkan: jejak terlalu panjang: 2
abort: compiling: 0
batalkan: vable escape: 9
abort: loop buruk: 0
batalkan: paksa quasi-immut: 0
nvirtuals: 9318
nvholes: 1113
nvreused: 6666
Total # loop: 23
Total # jembatan: 8
Membebaskan # loop: 0
Bebaskan # jembatan: 0
[2cbb7c242e8b] jit-summary}

Saya menggunakan 32bit terpercaya menggunakan PyPy terbaru dari
https://launchpad.net/~pypy/+archive/ubuntu/ppa
https://launchpad.net/%7Epypy/+archive/ubuntu/ppa

-
Balas email ini secara langsung atau lihat di GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62269627
.

Catatan:

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}

Buangnya di sini: https://gist.github.com/stas/ad597c87ccc4b563211a

Terima kasih telah meluangkan waktu Anda untuk membantu!

Jadi ini mungkin menyumbang 100MB penggunaan. Ada dua tempat istirahat
Bisa jadi, dalam "memori cadangan", GC menyimpan untuk berbagai hal, dan
dalam alokasi non-GC - ini berarti hal-hal seperti internal OpenSSL
alokasi. Saya ingin tahu apakah ada cara yang baik untuk melihat apakah struktur OpenSSL
sedang bocor, apakah hal yang diuji di sini dengan TLS, jika ya, dapatkah Anda
coba dengan situs non-TLS dan lihat apakah itu mereproduksi?

Pada Sabtu, 08 November 2014 pukul 17:38:04 Stas Sușcov [email protected]
menulis:

Catatan:

Baris # Memakai Isi Baris Kenaikan

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
Menelusuri: 41 0,293192
Backend: 30 0,026873
JUMLAH: 1615.665337
ops: 79116
operasi yang direkam: 23091
panggilan: 2567
penjaga: 7081
opt ops: 5530
memilih penjaga: 1400
pukulan: 198
batalkan: jejak terlalu panjang: 2
abort: compiling: 0
batalkan: vable escape: 9
abort: loop buruk: 0
batalkan: paksa quasi-immut: 0
nvirtuals: 9318
nvholes: 1113
nvreused: 6637
Total # loop: 23
Total # jembatan: 8
Membebaskan # loop: 0
Bebaskan # jembatan: 0
[3fd756c29302] ringkasan-jit}

Buangnya di sini: https://gist.github.com/stas/ad597c87ccc4b563211a

Terima kasih telah meluangkan waktu Anda untuk membantu!

-
Balas email ini secara langsung atau lihat di GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62277822
.

@ex ,

Saya yakin @stas telah menggunakan koneksi http (non-SSL / TLS) biasa untuk tolok ukur ini. Untuk berjaga-jaga, saya juga menggunakan skrip benchmark @stas dan membuatnya di Mac (OSX 10.9.5 2.5 GHz i5 8 GB 1600 MHz DDR3) dengan koneksi http biasa.

Jika membantu, berikut adalah hasil saya untuk membandingkan (menggunakan instruksi Anda):
https://gist.github.com/mhjohnson/a13f6403c8c3a3d49b8d

Biarkan aku tahu apa yang Anda pikirkan.

Terima kasih,

-Matt

Ekspresi reguler GitHub terlalu longgar. Saya membuka kembali ini karena menurut saya itu belum sepenuhnya diperbaiki.

Halo, mungkin saya bisa membantu menunjukkan bahwa masalahnya ada. Saya memiliki crawler yang menggunakan permintaan dan Proses menggunakan multiprocessing. Itu terjadi bahwa lebih dari satu contoh menerima hasil yang sama. Mungkin ada kebocoran pada buffer hasil atau soket itu sendiri.

Beri tahu saya jika saya dapat mengirimkan beberapa contoh kode atau bagaimana cara membuat pohon referensi untuk mengidentifikasi bagian mana dari informasi yang sedang "dibagikan" (bocor)

Terima kasih

@barroca itu masalah yang berbeda. Anda mungkin menggunakan Sesi di utas dan menggunakan stream=True . Jika Anda menutup respons sebelum selesai membacanya, soket ditempatkan kembali ke kumpulan koneksi dengan data yang masih ada di dalamnya (jika saya ingat dengan benar). Jika itu tidak terjadi, masuk akal juga bahwa Anda mengambil koneksi terbaru dan menerima respons yang di-cache dari server. Bagaimanapun, ini bukan merupakan indikasi kebocoran memori.

@ sigmavirus24 Terima kasih Ian, Ada beberapa

Jangan khawatir @barroca :)

Tidak ada keluhan lebih lanjut tentang hal ini yang timbul dan saya pikir kami telah melakukan yang terbaik dalam hal ini. Saya akan dengan senang hati membukanya kembali dan menyelidikinya kembali jika perlu

Jadi, apa solusi dari masalah ini?

@Makecodeeasy Saya ingin tahu itu juga

sejauh ini masalah saya sekitar requests adalah tidak aman untuk thread,
sebaiknya gunakan sesi terpisah untuk utas berbeda,

pekerjaan saya yang sedang berjalan untuk berjalan melalui jutaan url untuk memvalidasi respons cache membawa saya ke sini

karena saya menemukan penggunaan memori tumbuh menjadi tidak masuk akal ketika requests berinteraksi dengan ThreadPoolExecutor atau threading ,
pada akhirnya saya hanya menggunakan multiprocessing.Process untuk mengisolasi pekerja dan memiliki sesi independen untuk setiap pekerja

@AndCycle maka masalah Anda tidak ada di sini. Ada PR yang digabungkan untuk memperbaiki kasus kebocoran memori khusus ini. Itu belum mundur karena ada tes di sekitarnya. Dan masalah Anda terdengar sangat berbeda.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

NoahCardoza picture NoahCardoza  ·  4Komentar

ReimarBauer picture ReimarBauer  ·  4Komentar

tiran picture tiran  ·  3Komentar

JimHokanson picture JimHokanson  ·  3Komentar

thadeusb picture thadeusb  ·  3Komentar