Requests: Possível Vazamento de Memória

Criado em 17 out. 2013  ·  53Comentários  ·  Fonte: psf/requests

Tenho um programa muito simples que recupera periodicamente uma imagem de uma câmera IP. Percebi que o conjunto de trabalho deste programa cresce monotonicamente. Escrevi um pequeno programa que reproduz o problema.

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

O uso da memória é impresso no final de cada iteração. Esta é a saída de amostra.
* Iteração 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..."

* Iteração 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..."

O uso de memória não aumenta a cada iteração, mas continua a aumentar com requests.get sendo o culpado que aumenta o uso de memória.

Por ** Iteração 99 ** é assim que o perfil de memória se parece.

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

O uso de memória não diminui a menos que o programa seja encerrado.

Existe um bug ou é um erro do usuário?

Bug

Comentários muito úteis

Não houve mais reclamações sobre o ocorrido e acho que fizemos o melhor possível. Estou feliz em reabri-lo e reinvestigar, se necessário

Todos 53 comentários

Obrigado por levantar isso e fornecer tantos detalhes!

Diga-me, você já viu o uso de memória diminuir em algum ponto?

Eu não vi o uso de memória diminuir. Eu queria saber se isso tinha a ver com o coletor de lixo do Python e talvez não tenha tido a oportunidade de entrar em ação, então adicionei uma chamada para gc.collect() após cada download. Isso não fez diferença.

Posso perguntar por que esse problema foi encerrado?

Minha empresa passou por esse mesmo problema, que ficou ainda mais agravado com o uso do pypy. Passamos vários dias rastreando a origem desse problema em nossa base de código para solicitações de python.

Apenas para destacar a gravidade desse problema, aqui está uma captura de tela da aparência de um de nossos processos de servidor ao executar um criador de perfil de memória:
http://cl.ly/image/3X3G2y3Y191h

O problema ainda está presente com cpython regular, mas é menos perceptível. Talvez seja por isso que este problema não foi relatado, apesar das graves consequências que tem para aqueles que utilizam esta biblioteca para processos de longa data.

Neste ponto, estamos desesperados o suficiente para considerar o uso de curl com um subprocesso.

Por favor, deixe-me saber o que você pensa e se isso será investigado a fundo. Caso contrário, considero que as solicitações de python são muito perigosas para serem usadas em aplicativos de missão crítica (por exemplo: serviços relacionados à saúde).

Obrigado,
-Matt

Foi encerrado por inatividade. Se você acredita que pode fornecer diagnósticos úteis para nos apontar a direção certa, ficaremos felizes em reabri-los.

Bem, deixe-me ajudar então.

Criei um pequeno repositório git para ajudar a facilitar o exame desse problema.
https://github.com/mhjohnson/memory-profiling-requests

Aqui está uma captura de tela do gráfico que ele gera:
http://cl.ly/image/453h1y3a2p1r

Espero que isto ajude! Deixe-me saber se eu fiz algo incorreto.

-Matt

Obrigado Matt! Vou começar a investigar isso agora. As primeiras vezes que executei o script (e as variações que tentei) demonstraram que isso é facilmente reproduzível. Vou ter que começar a brincar com isso agora.

Portanto, isso cresce em cerca de 0,1 MB / solicitação. Eu tentei colocar o decorador profile em métodos de nível inferior, mas eles são todos muito longos para que a saída seja remotamente útil e usar um intervalo superior a 0,1 parece servir apenas para rastrear o uso geral, não o per- uso de linha. Existem ferramentas melhores do que o mprof?

Portanto, decidi canalizar sua saída para | ag '.*0\.[1-9]+ MiB.*' para obter as linhas onde a memória é adicionada e movi o decorador profile para Session#send . Não é novidade que a maior parte vem da chamada para HTTPAdapter#send . Eu vou pela toca do coelho

E agora tudo está vindo da chamada para conn.urlopen em L355 e HTTPAdapter#get_connection . Se você decorar get_connection , há 7 vezes que ele aloca memória quando chama PoolManager#connection_from_url . Agora, considerando que a maioria está sendo acionada por HTTPResponse s retornados de urllib3, vou ver se há algo que _devemos_ fazer com eles que não devemos fazer para garantir que a memória seja liberada após o fato. Se eu não conseguir encontrar uma boa maneira de lidar com isso, vou começar a pesquisar o urllib3.

@ sigmavirus24 Uau. Ótimo trabalho! Parece que você localizou o ponto de acesso no código.
Quanto a rastrear qual objeto é responsável pelo vazamento de memória, você pode obter algumas dicas extras usando objgraph assim:

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

Deixe-me saber se posso ajudar de alguma forma.

-Matt

Meu primeiro palpite sobre o culpado seriam os objetos de encaixe. Isso explicaria por que é pior no PyPy ...

Estou sentado em um aeroporto agora e estarei em uma planície por várias horas em breve. Provavelmente não vou conseguir fazer isso hoje à noite ou potencialmente até o final desta semana (se não no próximo fim de semana / semana). Até agora, porém, tentei usar release_conn no HTTPResponse que recebemos de volta. Eu verifiquei com gc.get_referents que o objeto Response tem que pode estar falhando ao ser GC. Ele tem o HTTPResponsehttplib original (armazenado como _original_response e que (pelo que get_referents relatou) tem apenas uma mensagem de e-mail (para os cabeçalhos) e todo o resto é uma string ou dicionário (ou talvez listas). Se forem soquetes, não vejo onde eles não seriam coletados.

Além disso, usar Session#close (eu fiz o código usar sessões em vez da API funcional primeiro) não ajuda (e isso deve limpar os PoolManagers que limpam os pools de conexão). A outra coisa interessante é que PoolManager#connection_from_url adicionaria ~ 0,8 MB (mais ou menos 0,1) nas primeiras vezes que fosse chamado. Isso adiciona ~ 3 MB, mas o resto vem de conn.urlopen em HTTPAdapter#send . O bizarro é que gc.garbage tem alguns elementos estranhos se você usar gc.set_debug(gc.DEBUG_LEAK) . Ele tem algo como [[[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None]] e como você esperaria gc.garbage[0] is gc.garbage[0][0] , essa informação é absolutamente inútil. Terei de experimentar o objgraph quando tiver oportunidade.

Então eu cavei em urllib3 e segui a toca do coelho ainda mais cedo esta manhã. Eu criei o perfil de ConnectionPool#urlopen que me levou a ConnectionPool#_make_request . Neste ponto, há muita memória alocada nas linhas 306 e 333 em urllib3/connectionpool.py . L306 é self._validate_conn(conn) e L333 é conn.getresponse(buffering=True) . getresponse é o método httplib em uma conexão HTTP . Criar um perfil mais profundo não será fácil. Se olharmos para _validate_conn a linha que causa isso é conn.connect() que é outro método HTTPConnection . connect é quase certo onde o soquete está sendo criado. Se eu desabilitar o perfil de memória e colocar print(old_pool) em HTTPConnectionPool#close ele nunca imprime nada. Parece que não estamos realmente fechando os pools, pois a sessão é destruída. Meu palpite é que essa é a causa do vazamento de memória.

Adoraria ajudar a depurar isso, estarei dentro / fora do IRC hoje e amanhã.

Seguindo adiante, se você abrir python com _make_request ainda decorado (com profile ) e criar uma sessão, faça solicitações a cada 10 ou 20 segundos (para mesmo URL), você verá que o conn foi considerado descartado, então VerifiedHTTPSConnection é fechado e reutilizado. Isso significa que a classe connection é reutilizada, não o soquete subjacente. O método close é aquele que vive em httplib.HTTPConnection (L798). Isso fecha o objeto de soquete e o define como Nenhum. Em seguida, ele fecha (e define como Nenhum) os httplib.HTTPResponse mais recentes. Se você também criar o perfil VerifiedHTTPSConnection#connect , toda a memória criada / vazada acontecerá em urllib3.util.ssl_.ssl_wrap_socket .

Portanto, olhando para isso, o que memory_profiler está usando para relatar o uso de memória é o tamanho do conjunto residente do processo (rss). Este é o tamanho do processo na RAM (o vms, ou tamanho da memória virtual, tem a ver com mallocs), então estou procurando ver se estamos perdendo memória virtual, ou se estamos apenas tendo páginas alocadas para memória que não estamos perdendo.

Assim, como todos os URLs que estávamos usando até agora estavam usando HTTPS verificado, mudei para http://google.com e, embora ainda haja um aumento consistente na memória, parece que consome cerca de 11-14 MiB menos no todo. Ainda assim, tudo volta para a linha conn.getresponse (e em menor grau agora, conn.request ).

O interessante é que o VMS não parece crescer muito quando estou examinando-o no repl. Ainda tenho que modificar o mprof para retornar esse valor em vez do valor RSS. Um VMS em constante aumento certamente apontará para um vazamento de memória, enquanto o RSS poderia ser simplesmente um grande número de mallocs (o que é possível). A maioria dos sistemas operacionais (se bem entendi) não recupera RSS avidamente, então, até que a página de outro aplicativo falhe e não haja outro lugar para atribuí-lo, o RSS nunca encolherá (mesmo que pudesse). Dito isso, se estivermos aumentando consistentemente sem atingir um estado estacionário, não posso ter certeza se é requisições / urllib3 ou apenas o intérprete

Também verei o que acontece quando usamos urllib2 / httplib diretamente porque estou começando a achar que esse não é o nosso problema. Pelo que eu posso dizer, Session#close fecha corretamente todos os sockets e remove referências a eles para permitir que sejam GC. Além disso, se um soquete precisar ser substituído pelo Conjunto de Conexões, o mesmo acontecerá. Mesmo SSLSockets parecem lidar adequadamente com a coleta de lixo.

Portanto, urllib2 parece consistentemente estabilizar em torno de 13,3 MiB. A diferença é que eu tive que envolvê-lo em uma tentativa / exceto porque ele travaria consistentemente com um URLError após um curto período. Então, talvez não esteja realmente fazendo nada depois de um tempo.

@ sigmavirus24 Você está

Hmm ... Python apenas libera memória para ser reutilizada por si mesmo novamente, e o sistema não recebe a memória de volta até que o processo termine. Portanto, eu acho que a linha plana que você está vendo em 13,3 MiB é provavelmente uma indicação de que não há um vazamento de memória presente com o urllib2, ao contrário do urllib3.

Seria bom confirmar que o problema pode ser isolado do urllib3. Você pode compartilhar os scripts que está usando para testar com urllib2?

Portanto, estou começando a me perguntar se isso não tem algo a ver com os objetos HTTPConnection . Se você fizer

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)

Os três primeiros devem imprimir 1, o último 3. [1] Já identifiquei que um HTTPConnection tem _HTTPConnection__response que é uma referência a _original_response . Portanto, esperava que esse número fosse 3. O que não consigo descobrir é o que contém a referência à 3ª cópia.

Para mais entretenimento, adicione o seguinte

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

para o início do script. Existem 2 objetos inacessíveis depois de fazer a chamada para as solicitações, o que é interessante, mas nada era incobrável. Se você adicionar isso ao script @mhjohnson fornecido e filtrar a saída para as linhas com inacessível nelas, verá que muitas vezes há bem mais de 300 objetos inacessíveis. Eu ainda não sei qual é o significado de objetos inalcançáveis. Como sempre, vou mantê-los informados.

@mhjohnson para testar urllib3, apenas substitua sua chamada para requests.get por urllib2.urlopen (também provavelmente deveria estar fazendo r.read() mas não estava).

Então eu peguei a sugestão anterior de @mhjohnson e usei objgraph para descobrir onde estava a outra referência, mas objgraph não consegue encontrá-la. Eu adicionei:

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

No roteiro 2 comentários acima e obtive o seguinte:
requests o que apenas mostra que haveria 2 referências a ele. Eu me pergunto se há algo em como sys.getrefcount funciona que não seja confiável.

Então isso é uma pista falsa. a urllib3.response.HTTPResponse tem _original_response e _fp . Isso combinado com _HTTPConection__response nos dá três referências.

Portanto, urllib3.response.HTTPResponse tem um atributo _pool que também é referenciado pelo PoolManager . Da mesma forma, o HTTPAdapter usado para fazer a solicitação, tem uma referência nos retornos das solicitações Response . Talvez outra pessoa possa identificar algo aqui:

requests

O código que o gera é: https://gist.github.com/sigmavirus24/bc0e1fdc5f248ba1201d

@ sigmavirus24
Sim, fiquei um pouco perdido com o último gráfico. Provavelmente porque não conheço a base de código muito bem, nem sou muito experiente em depurar vazamentos de memória.

Você sabe para qual objeto eu estou apontando com a seta vermelha nesta captura de tela do seu gráfico?
http://cl.ly/image/3l3g410p3r1C

Consegui fazer com que o código mostrasse o mesmo uso de memória, aumentando lentamente
em python3 substituindo urllib3 / requisições por urllib.request.urlopen.

Código modificado aqui: https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin burke
telefone: 925.271.7005 | twentymilliseconds.com

Na segunda-feira, 3 de novembro de 2014 às 21h28, Matthew Johnson [email protected]
escrevi:

@ sigmavirus24 https://github.com/sigmavirus24
Sim, fiquei um pouco perdido com o último gráfico. Provavelmente porque eu não
conheço a base de código muito bem, nem sou muito experiente em depuração de memória
vazamentos.

Você sabe para qual objeto eu estou apontando com a seta vermelha
nesta imagem do seu gráfico?
http://cl.ly/image/3l3g410p3r1C

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

Pelo que posso dizer, fazer solicitações a um site que retorna um
Conexão: fechar o cabeçalho (por exemplo https://api.twilio.com/2010-04-01.json)
não aumenta o uso de memória em uma quantidade significativa. A ressalva é
existem vários fatores diferentes e estou apenas assumindo que é um soquete
questão relacionada.

Kevin burke
telefone: 925.271.7005 | twentymilliseconds.com

Na segunda-feira, 3 de novembro de 2014 às 21h43 , Kevin Burke

Consegui fazer com que o código mostrasse o mesmo uso de memória, aumentando lentamente
em python3 substituindo urllib3 / requisições por urllib.request.urlopen.

Código modificado aqui:
https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin burke
telefone: 925.271.7005 | twentymilliseconds.com

Na segunda-feira, 3 de novembro de 2014 às 21h28, Matthew Johnson [email protected]
escrevi:

@ sigmavirus24 https://github.com/sigmavirus24
Sim, fiquei um pouco perdido com o último gráfico. Provavelmente porque eu
não conheço a base de código muito bem, nem sou muito experiente em depuração
perda de memória.

Você sabe para qual objeto eu estou apontando com a seta vermelha
nesta imagem do seu gráfico?
http://cl.ly/image/3l3g410p3r1C

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

@mhjohnson que parece ser o número de referências ao metatipo type por object que é do tipo type . Em outras palavras, acho que são todas as referências de object ou type , mas não tenho certeza. De qualquer forma, se eu tentar excluí-los, o gráfico se tornará algo como 2 nós.

Também estou muito preocupado com este problema de vazamento de memória porque usamos Requests em nosso sistema de rastreamento da web, no qual um processo geralmente é executado por vários dias. Existe algum progresso nesta questão?

Depois de passar algum tempo nisso junto com @mhjohnson , posso confirmar a teoria de @kevinburke relacionada à maneira como o GC trata os sockets no PyPy.

O commit 3c0b94047c1ccfca4ac4f2fe32afef0ae314094e é interessante. Especificamente a linha https://github.com/kennethreitz/requests/blob/master/requests/models.py#L736

Chamar self.raw.release_conn() antes de retornar o conteúdo reduziu significativamente a memória usada no PyPy, embora ainda haja espaço para melhorias.

Além disso, acho que seria melhor se documentássemos as chamadas .close() relacionadas às classes de sessão e resposta, como também mencionado por @ sigmavirus24. Os usuários de solicitações devem estar cientes desses métodos, porque na maioria dos casos os métodos não são chamados implicitamente.

Eu também tenho uma pergunta e uma sugestão relacionada ao controle de qualidade deste projeto. Posso perguntar aos mantenedores por que não usamos um CI para garantir a integridade de nossos testes? Ter um CI também nos permitiria escrever casos de teste de benchmark onde podemos traçar o perfil e acompanhar qualquer regressão de desempenho / memória.

Um bom exemplo de tal abordagem pode ser encontrado no projeto pq:
https://github.com/malthe/pq/blob/master/pq/tests.py#L287

Obrigado a todos que saltaram sobre isso e decidiram ajudar!
Continuaremos investigando outras teorias que causam isso.

@stas , quero abordar uma coisa:

Os usuários de solicitações devem estar cientes desses métodos, porque na maioria dos casos os métodos não são chamados implicitamente.

Deixando o PyPy de lado por um momento, esses métodos não devem _precisar_ ser chamados explicitamente. Se os objetos de soquete se tornarem inacessíveis em CPython, eles obterão o gc'd automático, o que inclui fechar os identificadores de arquivo. Este não é um argumento para não documentar esses métodos, mas é um aviso para não se concentrar muito neles.

Devemos usar um IC, mas parece que não está bem no momento e apenas @kennethreitz pode consertá-lo. Ele vai fazer isso quando tiver tempo. Observe, entretanto, que os testes de benchmark são extremamente difíceis de acertar de uma forma que não os torne extremamente ruidosos.

Deixando o PyPy de lado por um momento, esses métodos não precisam ser chamados explicitamente. Se os objetos de soquete se tornarem inacessíveis em CPython, eles obterão o gc'd automático, o que inclui fechar os identificadores de arquivo. Este não é um argumento para não documentar esses métodos, mas é um aviso para não se concentrar muito neles.

Eu meio que concordo com o que você diz, exceto pela parte que discutimos Python aqui. Não quero começar uma discussão, mas lendo _O Zen do Python_, a forma pítônica seria seguir a abordagem _Explícito é melhor do que implícito_. Também não estou familiarizado com esta filosofia de projeto, portanto, ignore meus pensamentos se isso não se aplicar a requests .

Eu ficaria feliz em ajudar com os testes de IC ou benchmark sempre que houver uma oportunidade! Obrigado por explicar a situação atual.

Então, acho que encontrei a causa do problema ao usar a API funcional. Se você fizer

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

O soquete parece ainda estar aberto. A razão pela qual digo _aparece_ é porque ainda tem um atributo _sock é porque se você fizer

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

Você verá None impresso. Então o que está acontecendo é que urllib3 inclui em cada HTTPResponse um atributo que aponta para o pool de conexão de onde veio. O pool de conexão tem a conexão na fila que tem o soquete não fechado. O problema, para a API funcional, seria corrigido se em requests/api.py fizéssemos:

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

Então r.raw._pool ainda será o pool de conexão, mas r.raw._pool.pool será None .

A parte complicada é o que acontece quando as pessoas estão usando sessões. Tê-los close a sessão após cada solicitação não faz sentido e vai contra o propósito da sessão. Na realidade, se você usar uma sessão (sem threads) e fizer 100 solicitações para o mesmo domínio (e o mesmo esquema, por exemplo, https ) usando uma sessão, o vazamento de memória é muito mais difícil de ver, a menos que você aguarde cerca de 30 segundos para que um novo soquete seja criado. O problema é que, como já vimos, r.raw._pool é um objeto muito mutável. É uma referência ao Connection Pool que é gerenciado pelo Pool Manager nas solicitações. Portanto, quando o soquete é substituído, ele é substituído por referências a ele em todas as respostas que ainda podem ser alcançadas (no escopo). O que preciso fazer mais é descobrir se algo ainda mantém as referências aos soquetes depois de fecharmos os pools de conexão. Se eu conseguir encontrar algo que esteja segurando referências, acho que encontraremos o vazamento de memória _real_.

Então, uma ideia que tive foi usar o objgraph para descobrir o que realmente faz referência a SSLSocket após uma chamada para requests.get e eu entendi:

socket

O interessante é que aparentemente existem 7 referências a SSLSocket mas apenas duas referências anteriores que o objgraph conseguiu encontrar. Eu acho que 1 das referências é aquela passada para objgraph e a outra é a ligação que eu faço no script que gera isso, mas que ainda deixa 3 ou 4 referências inexplicáveis ​​que eu não tenho certeza de onde elas vêm.

Este é meu script para gerar isso:

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)

Usando

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)

O socket-after.png mostra isso:

socket-after

Portanto, eliminamos uma referência ao soquete SSL. Dito isso, quando eu olho para s._sock o socket.socket subjacente está fechado.

Depois de executar vários benchmarks de longa duração, aqui está o que descobrimos:

  • chamar close() ajuda explicitamente!
  • os usuários que executam várias solicitações devem usar Session e fechá-lo adequadamente após terminar. Por favor, mescle # 2326
  • Os usuários do PyPy ficam melhores sem o JIT! Ou eles deveriam chamar gc.collect() explicitamente!

TL; DR; requests parece bom, abaixo você encontrará alguns gráficos executando este snippet:

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 sem 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 com 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.'

Acredito que um dos motivos pelos quais todos ficamos confusos inicialmente é porque a execução dos benchmarks requer um conjunto maior para excluir a maneira como o GC se comporta de uma implementação para outra.

Além disso, a execução de solicitações em um ambiente encadeado requer um conjunto maior de chamadas devido à maneira como os encadeamentos funcionam (não vimos nenhuma variação importante no uso de memória após a execução de vários conjuntos de encadeamentos).

Em relação ao PyPy com JIT, chamando gc.collect() pelo mesmo número de chamadas, economizou cerca de 30% da memória. É por isso que acredito que os resultados do JIT devem ser excluídos desta discussão, uma vez que é um assunto de como todos ajustam a VM e otimizam o código para o JIT.

Tudo bem, então o problema parece estar explicitamente na maneira como lidamos com a memória interagindo com o PyPy JIT. Pode ser uma boa ideia convocar um especialista em PyPy: @alex?

Realmente não consigo imaginar quais solicitações (e empresas) estão possivelmente fazendo para causar algo assim. Você pode executar seu teste com PYPYLOG=jit-summary:- no env e colar os resultados (isso irá imprimir algumas coisas quando o processo terminar)

Espero que isto ajude:

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}

Estou em um servidor confiável de 32 bits usando o PyPy mais recente de https://launchpad.net/~pypy/+archive/ubuntu/ppa

31 caminhos compilados não explicam mais de 200 MB de RAM em uso.

Você pode colocar algo em seu programa para ser executado
gc.dump_rpy_heap('filename.txt') enquanto está em uma memória muito alta
uso? (Só precisa executá-lo uma vez, isso irá gerar um despejo de todos os
memória que o CG conhece).

Então, com uma verificação da árvore de origem do PyPy, execute ./pypy/tool/gcdump.py filename.txt e nos mostre os resultados.

Obrigado!

No sábado, 8 de novembro de 2014 às 15:20:52 Stas Sușcov [email protected]
escrevi:

Espero que isto ajude:

Uso da linha # Mem Incremento do conteúdo da linha

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] {resumo-jit
Rastreamento: 41 0,290082
Back-end: 30 0,029096
TOTAL: 1612.933400
ops: 79116
operações gravadas: 23091
ligações: 2567
guardas: 7081
ops opt: 5530
guardas opt: 1400
forçantes: 198
abortar: traço muito longo: 2
abortar: compilar: 0
abortar: escape vable: 9
abortar: loop ruim: 0
abortar: forçar quase-imut: 0
nvirtuals: 9318
nvholes: 1113
nvreused: 6666
Nº total de loops: 23
Nº total de pontes: 8
Liberado # de loops: 0
Liberado # de pontes: 0
[2cbb7c242e8b] resumo-jit}

Estou em 32 bits de confiança usando o mais recente PyPy da
https://launchpad.net/~pypy/+archive/ubuntu/ppa
https://launchpad.net/%7Epypy/+archive/ubuntu/ppa

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62269627
.

Registro:

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}

O despejo aqui: https://gist.github.com/stas/ad597c87ccc4b563211a

Obrigado por dedicar seu tempo para ajudar com isso!

Portanto, isso representa talvez 100 MB de uso. Existem dois lugares para descansar
disso pode ser, na "memória sobressalente", o GC mantém ao redor para várias coisas, e
em alocações não-GC - isso significa coisas como o OpenSSL interno
alocações. Eu me pergunto se há uma boa maneira de ver se as estruturas OpenSSL
estão vazando, é o que está sendo testado aqui com TLS, se sim, você pode
tente com um site não TLS e veja se reproduz?

No sábado, 8 de novembro de 2014 às 17:38:04 Stas Sușcov [email protected]
escrevi:

Registro:

Uso da linha # Mem Incremento do conteúdo da linha

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] {resumo-jit
Rastreamento: 41 0,293192
Back-end: 30 0,026873
TOTAL: 1615.665337
ops: 79116
operações gravadas: 23091
ligações: 2567
guardas: 7081
ops opt: 5530
guardas opt: 1400
forçantes: 198
abortar: traço muito longo: 2
abortar: compilar: 0
abortar: escape vable: 9
abortar: loop ruim: 0
abortar: forçar quase-imut: 0
nvirtuals: 9318
nvholes: 1113
nvreused: 6637
Nº total de loops: 23
Nº total de pontes: 8
Liberado # de loops: 0
Liberado # de pontes: 0
[3fd756c29302] resumo do jit}

O despejo aqui: https://gist.github.com/stas/ad597c87ccc4b563211a

Obrigado por dedicar seu tempo para ajudar com isso!

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62277822
.

@alex ,

Eu acredito que @stas usou uma conexão http (não SSL / TLS) regular para este benchmark. Por precaução, também usei o script de benchmark de @stas e o pré-

Se ajudar, aqui estão meus resultados para comparar (usando suas instruções):
https://gist.github.com/mhjohnson/a13f6403c8c3a3d49b8d

Diz-me o que pensas.

Obrigado,

-Matt

A expressão regular do GitHub é muito solta. Estou reabrindo porque não acho que esteja totalmente consertado.

Olá, talvez eu possa ajudar apontando o problema existe. Eu tenho um rastreador que usa solicitações e processo usando multiprocessamento. Está acontecendo que mais de uma instância está recebendo o mesmo resultado. Talvez haja algum vazamento no buffer de resultado ou no próprio soquete.

Deixe-me saber se posso enviar alguma amostra do código ou como gerar a árvore de referência para identificar qual parte da informação está sendo "compartilhada" (vazada)

obrigado

@barroca esse é um problema diferente. Você provavelmente está usando uma sessão entre threads e stream=True . Se você estiver fechando uma resposta antes de terminar de lê-la, o soquete é colocado de volta no pool de conexão com os dados ainda nele (se bem me lembro). Se isso não estiver acontecendo, também é plausível que você esteja captando a conexão mais recente e recebendo uma resposta em cache do servidor. De qualquer forma, isso não é uma indicação de vazamento de memória.

@ sigmavirus24 Obrigado Ian, Foi uma falta de uso da Sessão entre threads, como você mencionou. Obrigado pela explicação e desculpe por atualizar o problema errado.

Não se preocupe @barroca :)

Não houve mais reclamações sobre o ocorrido e acho que fizemos o melhor possível. Estou feliz em reabri-lo e reinvestigar, se necessário

então, qual é a solução desse problema?

@Makecodeeasy eu também quero saber isso

até agora, meu problema em torno de requests não é thread-safe,
melhor usar sessões separadas para diferentes tópicos,

meu trabalho contínuo para percorrer milhões de url para validar a resposta do cache me leva até aqui

conforme eu descubro que o uso de memória cresce além do razoável quando requests interage com ThreadPoolExecutor ou threading ,
no final, eu apenas uso multiprocessing.Process para isolar o trabalhador e ter uma sessão independente para cada trabalhador

@AndCycle, então seu problema não está aqui. Houve um PR mesclado para corrigir esse caso específico de vazamento de memória. Não regrediu porque existem testes em torno dele. E seu problema parece ser completamente diferente.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

eromoe picture eromoe  ·  3Comentários

remram44 picture remram44  ·  4Comentários

ghtyrant picture ghtyrant  ·  3Comentários

justlurking picture justlurking  ·  3Comentários

brainwane picture brainwane  ·  3Comentários