Повторные вставки в базу данных sqlite через sqlalchemy вызывает утечку памяти? - PullRequest
3 голосов
/ 02 июня 2019

При вставке огромного кадра данных панд в sqlite с помощью sqlalchemy и pandas to_sql и заданного размера chucksize я получаю ошибки памяти.

Сначала я подумал, что это проблема с to_sql, но я попробовал обходной путь, где вместо размера куска я использовал for i in range(100): df.iloc[i * 100000:(i+1):100000].to_sql(...), и это все равно приводило к ошибке.

Кажется, что при определенных условиях происходит утечка памяти с повторными вставками в sqlite через sqlalchemy.

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

import string
import numpy as np
import pandas as pd
from random import randint
import random

def make_random_str_array(size=10, num_rows=100, chars=string.ascii_uppercase + string.digits):
    return (np.random.choice(list(chars), num_rows*size)
            .view('|U{}'.format(size)))

def alt(size, num_rows):
    data = make_random_str_array(size, num_rows=2*num_rows).reshape(-1, 2)
    dfAll = pd.DataFrame(data)
    return dfAll

dfAll = alt(randint(1000, 2000), 10000)

for i in range(330):
    print('step ', i)
    data = alt(randint(1000, 2000), 10000)
    df = pd.DataFrame(data)
    dfAll = pd.concat([ df,  dfAll ])

import sqlalchemy

from sqlalchemy import create_engine
engine = sqlalchemy.create_engine('sqlite:///testtt.db')

for i in range(500):
    print('step', i)
    dfAll.iloc[(i%330)*10000:((i%330)+1)*10000].to_sql('test_table22', engine, index = False, if_exists= 'append')

Это было выполнено в среде ЦП Google Colab.

Сама база данных не вызывает утечку памяти, потому что я могу перезапустить свою среду, и ранее вставленные данные все еще там, и подключение к этой базе данных не вызывает увеличение памяти.Кажется, проблема заключается в повторных вставках при определенных условиях с помощью цикла to_sql или одного to_sql с указанным размером chucksize.

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

Редактировать:

Чтобы полностью воспроизвести ошибку, запустите этот блокнот

https://drive.google.com/open?id=1ZijvI1jU66xOHkcmERO4wMwe-9HpT5OS

Блокнот требует, чтобы вы импортировали эту папку в основной каталогвашего Google Диска

https://drive.google.com/open?id=1m6JfoIEIcX74CFSIQArZmSd0A8d0IRG8

Ноутбук также будет монтировать ваш диск Google, вам нужно дать ему разрешение на доступ к вашему диску Google.Поскольку данные хранятся на моем диске Google, импорт данных не должен занимать какие-либо из ваших выделенных данных.

1 Ответ

5 голосов
/ 09 июня 2019

Экземпляр Google Colab начинается с 12,72 ГБ оперативной памяти. После создания DataFrame theBigList было использовано около 9,99 ГБ оперативной памяти. Это уже довольно неудобная ситуация, поскольку для Операции Pandas требуют столько же дополнительного пространства, сколько и DataFrame, на котором они работают. Поэтому мы должны стараться избегать использования даже такого большого объема ОЗУ, если это возможно, и, к счастью, есть простой способ сделать это: просто загрузить каждый файл .npy и сохранить его данные в базе данных sqlite по одному , даже не создавая theBigList (см. Ниже).

Однако, если мы используем код, который вы разместили, мы увидим, что использование ОЗУ медленно увеличивается так как куски theBigList хранятся в базе данных итеративно.

theBigList DataFrame хранит строки в массиве NumPy. Но в процессе передачи строк в базу данных sqlite, строки NumPy преобразован в строки Python. Это требует дополнительной памяти.

За это Theano tutoral , в котором обсуждается управление внутренней памятью Python,

Для ускорения выделения памяти (и повторного использования) Python использует несколько списков для маленькие предметы. Каждый список будет содержать объекты аналогичного размера: будет список для объектов размером от 1 до 8 байт, один для 9 до 16 и т. д. При небольшом объекте необходимо создать, либо мы повторно используем свободный блок в списке, либо мы выделяем новый.

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

Действительно: если предмет (размером х) освобожден (освобожден из-за отсутствия ссылки), его расположение не возвращается в глобальный пул памяти Python (и тем более в система), но просто помечается как свободный и добавляется в свободный список предметов размером Икс. Местоположение мертвого объекта будет использовано повторно, если другой объект совместим размер нужен. Если доступных мертвых объектов нет, создаются новые.

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

Я полагаю, что это точно описывает поведение, которое вы видите при выполнении этого цикла:

for i in range(0, 588):
    theBigList.iloc[i*10000:(i+1)*10000].to_sql(
        'CS_table', engine, index=False, if_exists='append')

Несмотря на то, что многие мертвые объекты используются повторно для новых строк, это Невероятно, если использовать по существу случайные строки, такие как в theBigList, что дополнительное пространство иногда будет необходимо, и поэтому объем памяти продолжает расти.

Процесс в конечном итоге достигает предела оперативной памяти Google Colab в 12,72 ГБ, и ядро ​​завершается с ошибкой памяти.


В этом случае самый простой способ избежать использования большого объема памяти - никогда не создавать экземпляр всего DataFrame - вместо этого просто загружайте и обрабатывайте небольшие порции DataFrame по одному:

import numpy as np
import pandas as pd
import matplotlib.cbook as mc
import sqlalchemy as SA

def load_and_store(dbpath):
    engine = SA.create_engine("sqlite:///{}".format(dbpath))    
    for i in range(0, 47):
        print('step {}: {}'.format(i, mc.report_memory()))                
        for letter in list('ABCDEF'):
            path = '/content/gdrive/My Drive/SummarizationTempData/CS2Part{}{:02}.npy'.format(letter, i)
            comb = np.load(path, allow_pickle=True)
            toPD = pd.DataFrame(comb).drop([0, 2, 3], 1).astype(str)
            toPD.columns = ['title', 'abstract']
            toPD = toPD.loc[toPD['abstract'] != '']
            toPD.to_sql('CS_table', engine, index=False, if_exists='append')

dbpath = '/content/gdrive/My Drive/dbfile/CSSummaries.db'
load_and_store(dbpath)

который печатает

step 0: 132545
step 1: 176983
step 2: 178967
step 3: 181527
...         
step 43: 190551
step 44: 190423
step 45: 190103
step 46: 190551

Последнее число в каждой строке - это объем памяти, потребляемый процессом, как сообщается matplotlib.cbook.report_memory . Существует ряд различных мер использования памяти. В Linux mc.report_memory() сообщает размер физических страниц образа ядра процесса (включая текст, данные и пространство стека).


Кстати, еще один простой прием управления памятью - использование функций. Локальные переменные внутри функции освобождаются, когда функция завершается. Это избавляет вас от необходимости вручную набирать del и gc.collect().

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...