You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从Google Cloud Storage Blob下载文件时的Python内存泄漏问题

问题描述

我开发了一个从GCS存储桶下载文件的函数,供线程用于下载小于150KB的压缩HTML文件并解析。问题在于,经过数千次下载后内存呈线性增长,这对于需要处理数百万文件的场景来说会导致内存不足。

函数代码

def download_html(self, md5_hash):
    filename = f'{md5_hash}.html.gz'
    gcs_object_key = f'{self.gcs_prefix}/pages/{filename}'

    try:
        blob = self.gcs_bucket.blob(gcs_object_key)

        # same result for gz_content = blob.download_as_bytes()
        with blob.open('rb') as f:
            gz_content = f.read()

        data = gzip.decompress(gz_content)
        del gz_content
        del blob
        return data
    except:
        return None

该函数被另一个函数调用,后者仅解码获取到的HTML字节数据并重复处理下一个文件。

内存分析结果

我使用tracemalloc对程序进行了性能分析,发现GCS的blob文件中存在持续增长的内存分配:

lib/python3.8/site-packages/google/cloud/storage/blob.py:830: size=398 B (+199 B), count=2 (+1), average=199 B
lib/python3.8/site-packages/google/cloud/storage/blob.py:830: size=597 B (+398 B), count=3 (+2), average=199 B
lib/python3.8/site-packages/google/cloud/storage/blob.py:830: size=796 B (+597 B), count=4 (+3), average=199 B
...

内存分配随每次迭代持续增长,且似乎从未释放。

我尝试在每次使用后删除blob并调用gc.collect(),但毫无效果。我还尝试始终下载同一个文件,此时内存会在某个点稳定下来不再增长,这让我怀疑存在文件缓存导致内存增加。仅使用本地文件时无此问题,显然问题来自文件下载过程。

基准测试更新

我运行了基准测试,对比下载5000个文件时有无线程的内存使用情况,结果如下:

  • 无线程:
    • 初始内存:112.0078125
    • 最终内存:116.48046875
    • 内存差值:4.47265625
  • 10个线程:
    • 初始内存:130.22265625
    • 最终内存:162.1328125
    • 内存差值:31.91015625

需要说明的是,我未使用线程池,而是创建了Thread的子类,实现了一个无限循环的run函数,从Redis数据库获取待下载的文件名。目前我并未对下载的文件进行任何处理,每次下载后都会手动调用gc.collect()。

求解决此内存问题的建议。


解决方案建议

1. 禁用GCS客户端缓存

Google Cloud Storage客户端默认会缓存请求相关数据,这是线程场景下内存增长的核心原因之一。可以通过两种方式禁用:

# 方式1:创建客户端时禁用HTTP缓存
from google.cloud import storage
client = storage.Client(_http=storage.requests.Session())

# 方式2:针对单个blob禁用属性缓存
blob = self.gcs_bucket.blob(gcs_object_key)
blob._properties_cache = {}

2. 改用线程池管理并发

自定义无限循环的Thread子类容易导致线程本地资源无法有效回收,改用concurrent.futures.ThreadPoolExecutor可以更好地管控线程生命周期:

from concurrent.futures import ThreadPoolExecutor

def process_single_file(md5_hash):
    data = download_html(md5_hash)
    # 后续解析逻辑...

# 初始化线程池,控制并发数
with ThreadPoolExecutor(max_workers=10) as executor:
    while True:
        md5_hash = redis_client.get_task()
        if not md5_hash:
            break
        executor.submit(process_single_file, md5_hash)

3. 流式处理压缩文件

避免一次性将整个gzip文件读入内存,改用流式解压减少内存占用:

import gzip

def download_html(self, md5_hash):
    filename = f'{md5_hash}.html.gz'
    gcs_object_key = f'{self.gcs_prefix}/pages/{filename}'

    try:
        blob = self.gcs_bucket.blob(gcs_object_key)
        with blob.open('rb') as f_in:
            with gzip.GzipFile(fileobj=f_in, mode='rb') as gz_in:
                data = gz_in.read()
        del blob
        return data
    except:
        return None

4. 升级GCS客户端库

旧版本的google-cloud-storage可能存在内存泄漏问题,升级到最新稳定版:

pip install --upgrade google-cloud-storage

5. 清理线程本地存储(若坚持自定义线程)

如果必须使用自定义Thread子类,在每次循环结束后手动清理线程本地资源:

import threading
import gc

class MyThread(threading.Thread):
    def run(self):
        while True:
            md5_hash = redis_client.get_task()
            if not md5_hash:
                break
            try:
                data = self.download_html(md5_hash)
                # 处理数据...
            finally:
                threading.local().__dict__.clear()
                gc.collect()

内容的提问来源于stack exchange,提问作者Franco Lopez Paviolo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 19:07:38