通过sqlalchemy重复插入sqlite数据库会导致内存泄漏?

San*_*ta7 4 python sqlite memory-leaks sqlalchemy pandas

当通过to_sqlsqlalchemy 和pandas 以及指定的chucksize 将巨大的pandas数据帧插入sqlite时,会出现内存错误。

起初我以为这是一个问题,to_sql但是我尝试了一种解决方法,而不是使用我使用的块大小for i in range(100): df.iloc[i * 100000:(i+1):100000].to_sql(...),这仍然导致错误。

似乎在某些情况下,通过sqlalchemy重复插入sqlite会导致内存泄漏。

通过一个最小的示例,我很难尝试复制在转换数据时发生的内存泄漏。但这非常接近。

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')
Run Code Online (Sandbox Code Playgroud)

这是在Google Colab CPU环境上运行的。

数据库本身不会引起内存泄漏,因为我可以重新启动环境,并且先前插入的数据仍然存在,并且连接到该数据库不会导致内存增加。问题似乎是在某些情况下通过循环重复插入to_sqlto_sql指定了chucksize的情况。

有没有一种方法可以运行此代码而不会导致内存使用量的最终增加?

编辑:

要完全重现该错误,请运行此笔记本

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

笔记本需要您将此文件夹导入到Google云端硬盘的主目录中

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

笔记本电脑还将安装您的Google驱动器,您需要授予它访问Google驱动器的权限。由于数据托管在我的Google驱动器上,因此导入数据不会占用您分配的任何数据。

unu*_*tbu 5

Google Colab实例从大约12.72GB的可用RAM开始。创建DataFrame之后,theBigList已使用了大约9.99GB的RAM。这已经是一种相当不舒服的情况,因为Pandas操作需要与其正在操作的DataFrame一样多的额外空间已经很普遍了。因此,我们应该努力避免使用尽可能多的RAM,幸运的是,有一种简单的方法可以做到这一点:只需加载每个.npy文件,一次将其数据存储在sqlite数据库中,而无需创建theBigList(见下文)。

但是,如果使用您发布的代码,则可以看到RAM的使用随着块的theBigList迭代存储在数据库中而缓慢增加。

theBigListDataFrame将字符串存储在NumPy数组中。但是在将字符串传输到sqlite数据库的过程中,NumPy字符串被转换为Python字符串。这需要更多的内存。

根据Theano教程,它讨论了Python内部内存管理,

为了加快内存分配(和重用)的速度,Python使用了许多小对象列表。每个列表将包含大小相似的对象:将有一个列表,其中包含1到8个字节大小的对象,一个9到16个字节,等等。当需要创建一个小对象时,我们可以在列表中重用一个空闲块,或者我们分配一个新的。

……重要的是,这些清单永远不会减少。

的确:如果一个项目(大小为x)被释放(由于缺少引用而被释放),则它的位置不会返回到Python的全局内存池中(甚至更少返回到系统中),而只是标记为空闲并添加到的空闲列表中尺寸为x的物品。如果需要另一个兼容大小的对象,则将重用死对象的位置。如果没有可用的失效对象,则会创建新的失效对象。

如果永远不会释放小对象内存,那么不可避免的结论是,就像金鱼一样,这些小对象列表只会不断增长,而不会缩小,并且应用程序的内存占用量由在给定给定条件下分配的最大数量的小对象所控制。点。

我相信这可以准确地描述您在执行此循环时看到的行为:

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

即使许多死对象的位置都已被重新用于新的字符串,但对于本质上随机的字符串(例如那些theBigList偶尔需要额外空间的字符串)来说,这也不是不可能的,因此内存占用量会不断增长。

该过程最终达到Google Colab的12.72GB RAM限制,内核因内存错误而被杀死。


在这种情况下,避免使用大量内存的最简单方法是永远不要实例化整个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)
Run Code Online (Sandbox Code Playgroud)

哪个打印

step 0: 132545
step 1: 176983
step 2: 178967
step 3: 181527
...         
step 43: 190551
step 44: 190423
step 45: 190103
step 46: 190551
Run Code Online (Sandbox Code Playgroud)

每行的最后一个数字是该进程消耗的内存量,如matplotlib.cbook.report_memory所报告 。有许多不同的内存使用量度。在Linux上,mc.report_memory()报告 是进程核心映像的物理页面大小(包括文本,数据和堆栈空间)。


顺便说一句,您可以使用管理内存的另一个基本技巧是使用函数。函数终止时,将释放函数内部的局部变量。这样可以减轻您手动调用del和的负担gc.collect()