【问题标题】:Repeated insertions into sqlite database via sqlalchemy causing memory leak?通过sqlalchemy重复插入sqlite数据库导致内存泄漏?
【发布时间】:2019-10-18 12:06:55
【问题描述】:

当通过 sqlalchemy 和 pandas to_sql 和指定的卡盘大小将一个巨大的 pandas 数据框插入 sqlite 时,我会遇到内存错误。

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

在某些情况下,似乎存在内存泄漏,通过 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')

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

数据库本身不会导致内存泄漏,因为我可以重新启动我的环境,并且之前插入的数据仍然存在,并且连接到该数据库不会导致内存增加。问题似乎是在某些条件下通过循环 to_sql 或一个 to_sql 重复插入并指定了夹头大小。

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

编辑:

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

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

笔记本要求您将此文件夹导入 Google Drive 的主目录

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

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

【问题讨论】:

  • Alex Martelli says, "唯一真正可靠的方法是确保大量但临时的内存使用完成后确实将所有资源返回给系统,这是在子进程中进行该使用,这是否需要内存的工作然后终止。”
  • 是的,您可以尝试将占用大量内存的代码封装在一个函数中,然后使用multiprocessing 在单独的进程中调用该函数。有一个example here
  • @unutbu 不能解释这种情况,因为那里引用的现象不会导致内存错误。这可能是to_sql方法中的内存泄漏
  • @SantoshGupta7 好吧,多处理创建了独立的python 进程,因此如果不做一些工作就没有共享状态。不过,一般来说,如果使用多处理,您应该尽可能避免共享状态。

标签: python pandas sqlite memory-leaks sqlalchemy


【解决方案1】:

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

但是,如果我们使用您发布的代码,我们可以看到 RAM 使用量缓慢增加 因为theBigList的块被迭代地存储在数据库中。

theBigList DataFrame 将字符串存储在 NumPy 数组中。但在这个过程中 将字符串传输到 sqlite 数据库时,NumPy 字符串是 转换成 Python 字符串。这需要额外的内存。

根据讨论 Python 内部内存管理的 this Theano tutoral

为了加快内存分配(和重用)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')

即使许多死对象的位置被重新用于新字符串,它也是 对于像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)

打印出来的

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() 正在报告 进程的the size of the physical pages of the core image(包括文本、数据和堆栈空间)。


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

【讨论】:

  • 哇,这太棒了,并解释了为什么我无法创建一个最小的示例。 “对于本质上是随机字符串(例如 BigList 中的字符串)来说,偶尔会需要额外的空间,因此内存占用量不断增长,这并非令人难以置信。”因为我随机生成的文本大小都一样。当我改变字符串大小时,我在重现问题方面取得了一些成功,但它仍然没有我的数据那么多的变化,所以问题没有完全重现。
  • “对于本质上随机的字符串,例如 BigList 中的字符串,偶尔需要额外的空间,因此内存占用不断增长,这并非令人难以置信。”所以理论上,如果我插入数据帧字符串的第一个块(在我的示例代码中为 10000 行)比我的数据帧中的任何内容都大,那么错误不会发生?因为不需要创建新列表,所以之后的每个块都使用更少的空间?
  • 有不同大小的对象的免费列表。因此,即使您制作了 10K 行大字符串来“准备”空闲列表,如果稍后需要分配较小的字符串,它也无法满足对更多空间的需求。所以不幸的是,这不会阻止内存使用量的增长。
  • 是的,正确的。空闲列表的增长仍然存在,但主要不是问题,因为只要theBigList 不是一次全部实现,我们就远远低于 RAM 限制。
  • 如果您确实需要控制空闲列表的增长,那么将占用内存的部分推送到一个函数中,然后使用multiprocessing 在单独的进程中调用该函数就是答案。 (当单独的进程终止时,内存被释放给操作系统。)
猜你喜欢
  • 2021-02-06
  • 2011-07-30
  • 1970-01-01
  • 1970-01-01
  • 2014-06-19
  • 2011-09-27
  • 1970-01-01
  • 2017-12-30
  • 1970-01-01
相关资源
最近更新 更多