2017-02-02 4 views
1

대용량 쿼리를 일괄 처리하여 장고 앱의 메모리 사용을 줄이려고합니다. 비록 제가 영리하다고 생각한다고하더라도, 메모리 사용은 결국 프로세스가 죽을 때까지 계속 증가했습니다. 나는 현재 쿼리 세트가 가비지 수집되지 않는다는 이론을 가지고 있으며, 쿼리를 배치하는 방법과 관련이 있는지 궁금해하고 있습니다. itertools.chain()은 소비 된 아이템에서 메모리를 해제 할 수 있습니까?

def grouper(iterable, n, fillvalue=None): 
    "Collect data into fixed-length chunks or blocks" 
    args = [iter(iterable)] * n 
    return zip_longest(*args, fillvalue=fillvalue) 

def get_data(pks): 
    groups = list(grouper(pks, batch_size)) 
    querysets = [_queryset(pks) for pks in groups] 
    return itertools.chain(*querysets) 

for item in get_data([..big list of primary keys..]): 
    process(item) 

는의 3 개 검색어 세트가 get_data 기능에서 발생한다고 가정 해 봅시다. 첫 번째 쿼리 세트의 모든 항목을 소비하면 해당 쿼리 세트가 해제됩니까? 아니면 아직도 체인에서 기술적으로 참조를 얻었습니까?

메모리가 여기에 있습니다 (데이터베이스 드라이버 일 수도 있고 장고 자체 내부의 뭔가 일 수도 있음).하지만 괜찮은 후보로 보입니다. 객체 유형별로 메모리 사용을 측정하는 데 유용한 도구가 있습니까? 이 특정 코드는 Python 2에서 실행되고 있습니다 (현재).

필자는 ipython 셸에서이 프로그램을 실행하고 있음을 유의해야한다.

편집 :

그것은 chain처럼 보이는 여기에 책임을지지 않습니다. 클래스 당 객체 수를 출력하는 코드를 추가하고 Model 객체 수를 일정한 수로 유지합니다.

import gc 

def get_object_counts(): 
    from collections import Counter 
    classes = [] 
    for obj in gc.get_objects(): 
     if hasattr(obj, '__class__'): 
      classes.append(str(obj.__class__)) 
    return Counter(classes) 

그런 다음 특정 간격 (일괄 처리 크기)에서 : 완료 위해서

print(get_object_counts().most_common(30)) 

, 여기에 상단 9. 나는 주요 원인은 성장 여부를 수집하기 계속 django.db.models.base.ModelState,이라고 생각합니다.

첫째 :

("<type 'dict'>", 59184) 
("<type 'list'>", 48710) 
("<type 'function'>", 48300) 
("<type 'tuple'>", 38920) 
("<type 'cell'>", 10203) 
("<type 'weakref'>", 9957) 
("<type 'set'>", 7230) 
("<type 'type'>", 5947) 
("<class 'django.db.models.base.ModelState'>", 4682) 

둘째 :

("<type 'dict'>", 59238) 
("<type 'list'>", 48730) 
("<type 'function'>", 48315) 
("<type 'tuple'>", 38937) 
("<type 'cell'>", 10207) 
("<type 'weakref'>", 9959) 
("<type 'set'>", 7230) 
("<type 'type'>", 5950) 
("<class 'django.db.models.base.ModelState'>", 4696) 

답변

2

나는 비슷한 동작을 보았습니다. itertools.chain (및 itertools.chain.from_iterable)은 iterating이 중지 될 때까지 인수로 전달 된 모든 참조를 보유합니다. 이것이 캐시 된 결과와 함께 귀하의 장고 쿼리 세트가 가비지 수집되지 않는 이유입니다. 이 동작은 python2와 python3 모두에서 나타나는 것으로 사용자 정의 파이썬 함수 (see How to delete a function argument early?)에서 볼 수있는 동작이기도합니다. 아마도 itertools와 같은 C 언어 라이브러리의 함수는 종료하기 전에 인수에 대한 참조를 삭제할 자유를 더 많이 가질 수 있지만 분명히 그렇게하지 않기로 선택했을 것입니다.

해결 방법으로 자체를 사용하여 이터레이터의 itertools.chain에 개별 인수를 래핑 할 수 있습니다. iter 또는 itertools.chain 이러한 개별 반복기가 고갈되면 기본 반복기에 대한 참조를 삭제하고 가비지 수집되도록 할 수 있습니다.

결과를 완전히 소비한다고해도 메모리를 해제하기에는 충분하지 않습니다. 컨트롤은 해당 반복기에서 사용하는 메모리보다 먼저 반복기 (또는 체인의 하위 반복자)에게 반환되어야합니다. 하위 반복자)가 해제됩니다. 다시 말하지만, 이것은 일반적인 파이썬 함수에서 기대할 수있는 것입니다. 이 도움이

from __future__ import print_function 
import itertools 
import gc 

def print_whats_left_after(num, numbers_iter): 
    """ Read three numbers and print what pairs haven't been gc'd """ 
    for _ in range(num): 
     next(numbers_iter, None) 
    gc.collect() 
    # Print integer pairs that were not garbage collected 
    print(sorted([o for o in gc.get_objects() 
        if isinstance(o, list) and len(o) == 2 and 
        all(isinstance(i, int) for i in o)])) 

print_whats_left_after(2, itertools.chain([1, 2], [3, 4])) 
# -> [[1, 2], [3, 4]] 
print_whats_left_after(3, itertools.chain([1, 2], [3, 4])) 
# -> [[1, 2], [3, 4]] 
print_whats_left_after(4, itertools.chain([1, 2], [3, 4])) 
# -> [[1, 2], [3, 4]] 
print_whats_left_after(5, itertools.chain([1, 2], [3, 4])) 
# -> [] 

print_whats_left_after(2, itertools.chain.from_iterable([[1, 2], [3, 4]])) 
# -> [[1, 2], [3, 4]] 
print_whats_left_after(3, itertools.chain.from_iterable([[1, 2], [3, 4]])) 
# -> [[1, 2], [3, 4]] 

print_whats_left_after(2, itertools.chain(itertools.chain([1, 2]), [3, 4])) 
# -> [[1, 2], [3, 4]] 
print_whats_left_after(3, itertools.chain(itertools.chain([1, 2]), [3, 4])) 
# -> [[3, 4]] # [1, 2] was gc'd!!! 

print_whats_left_after(2, itertools.chain(iter([1, 2]), [3, 4])) 
# -> [[1, 2], [3, 4]] 
print_whats_left_after(3, itertools.chain(iter([1, 2]), [3, 4])) 
# -> [[3, 4]] # [1, 2] was gc'd!!! 
print_whats_left_after(4, itertools.chain(iter([1, 2]), [3, 4])) 
# -> [[3, 4]] 
print_whats_left_after(5, itertools.chain(iter([1, 2]), [3, 4])) 
# -> [] 

def arg_clobberer(arg): 
    arg = None 
    yield 
print_whats_left_after(0, arg_clobberer([1, 2])) 
# -> [[1, 2]] 
print_whats_left_after(1, arg_clobberer([1, 2])) 
# -> [] 

def arg_deleter(arg): 
    del arg 
    yield 
print_whats_left_after(0, arg_deleter([1, 2])) 
# -> [[1, 2]] 
print_whats_left_after(1, arg_deleter([1, 2])) 
# -> [] 

희망 :

은 아래 코드는이 모든 일이 보여!

+0

고마워, 그게 정말 도움이되는 정보 야. 다른 발전기의 체인에 인수를 래핑 멋진 트릭. 원래 코드를 리팩토링하여 실제로 테스트 할 수 없게되었지만 충분히 질문에 답한 것 같습니다. –

0

확실하지만 당신은 임시 목록을 만들지 않도록 (소스의 할당 문제를 해결) 반복 가능 객체/발전기로 변환 할 수 없음 :

def get_data(pks): 
    groups = grouper(pks, batch_size) # turn off explicit list conversion 
    querysets = (_queryset(pks) for pks in groups) # gencomp not listcomp 
    return itertools.chain.from_iterable(querysets) # nicer with "from_iterable" 
+0

'grouper' 함수는 itertools recipes doc에서 가져온 것입니다 : https://docs.python.org/2/library/itertools.html#recipes 범위가 [] * n과 같은 listcomp가 아닌가요? 불변의 객체에 대해서는 –

+0

예,하지만 my construct는 요소마다 하나의 다른 iterable을 생성합니다. 어쩌면 내가 틀렸어, 특히 조리법의 코드일지도 모르겠다. 그 순간 바. –