掘金 后端 ( ) • 2024-04-27 09:48

缘起

今天公司的顾问写了一段Python用于大量作图并保存在本地,但是运行了一段时间后出现快速的内存泄露,本文还原内存泄露发现的过程。

建议

如果不想看过程,使用这些建议可能会解决90%的Matplotlib后台绘制导致的内存泄漏问题

  1. 使用matplotlib.figure.Figure代替matplotlib.pyplot.figure

  2. 保存图片用figure.savefig代替plt.savefig

  3. 使用非互动式后端 matplotlib.use('agg')

  4. 及时关闭绘制资源figure.clf() plt.close(figure)

过程

发生的环境是:

python==3.7.7
matplotlib==3.5.3
numpy==1.21.1
pandas==1.3.5
seaborn==0.12.2

代码涉及商业代码我就用一段测试代码代替了:

import pandas as pd  
import numpy as np  
import seaborn as sns  
import matplotlib.pyplot as plt
import os
import gc
import uuid
import time
import timeit

class ChartHelper:
    def createImg(self,chart_key, data, folderPath_Scatter):
        df = pd.DataFrame(data)
        self.createScatterImg(chart_key,  df,folderPath_Scatter)
        plt.close('all')
        df=None
        data=None

    def createScatterImg(self,chart_key,df, folderPath):
        num_series = df.shape[1]  
        print("scatter")
        figure = plt.figure(figsize=(12, 8))  # 你可以根据需要调整图形大小  
        grid = plt.GridSpec(num_series, num_series, figure=figure, hspace=0.1, wspace=0.1)  # 调整间距  
        # 绘制散点图  
        for i in range(num_series):  
            for j in range(num_series):  
                ax = figure.add_subplot(grid[i, j])  
                ax.scatter(df.iloc[:, j], df.iloc[:, i],s=3,color='black', alpha=0.5)  
                ax.set_xticks([])  # 隐藏x轴刻度  
                ax.set_yticks([])  # 隐藏y轴刻度  
                if i == num_series - 1:  # 最下方的图显示x轴标签  
                    ax.set_xlabel(df.columns[j])  
                if j == 0:  # 最左侧的图显示y轴标签  
                    ax.set_ylabel(df.columns[i], rotation=0, labelpad=15)  

        # 保存散点矩阵图为图片
        t_start = timeit.default_timer()
        plt.savefig(os.path.join(folderPath, chart_key+'_scatter_matrix.png'),dpi=10)
        figure.clear()
        figure.clf()
        figure=None
        plt.clf()
        plt.close('all')
        gc.collect()
        t = timeit.default_timer() - t_start
        print(t)

        print("heatmap")
        correlation_matrix = df.corr()  
        figure=plt.figure(figsize=(8, 6))  
        # 绘制相关性热图
        sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')  
        # 保存相关性热图为图片
        t_start = timeit.default_timer()
        plt.savefig(os.path.join(folderPath, chart_key+'_correlation_heatmap.png'),dpi=10)
        figure.clear()
        figure.clf()
        figure=None
        plt.clf()
        plt.close('all')
        gc.collect()
        t = timeit.default_timer() - t_start
        print(t)

if __name__ == "__main__":
    np.random.seed(0)  
    data = np.random.rand(4, 8)  
    df = pd.DataFrame(data)
    chart = ChartHelper()
    for i in range(5):
        for j in range(50):
            print(i,j)
    chart.createImg(data=df,folderPath_Scatter="download",chart_key=str(uuid.uuid4()))
        time.sleep(20)

顾问原本是写C#的,不知道聪明的你有没有看出问题在哪里。其实罪魁祸首都藏在createScatterImg方法中。

我呢,直接上三板斧

第一步 确定是否存在泄露

安装工具

pip install memory_profiler

在犯罪嫌疑人createScatterImg加上装饰器@profile

运行测试mprof run plot查看结果mprof plot

68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032342f706e672f313238393638382f313731343035393038373435322d30383238613934322d386337302d343862362d623739612d6336396232663866623533352e706e67236176657.png 可以看到每一轮运行内存都稳定增长,肯定是有部分资源一直没有得到释放

第二步找到泄漏点

上 fil-profilepip install filprofiler

开始分析fil-profile run plot.py

目录下多了一个文件夹,打开html文件查看结果

image.png简单看是使用tk产生了一个窗口估计是进程未结束系统未能回收资源,由于是后台服务,不需要展示,所以想办法抑制窗口的创建

将所有创建figure的地方添加frameon参数figure=plt.figure(figsize=(8, 6),frameon=False)  

不行,这参数名不副实啊

发现官方更加推荐使用from matplotlib.figure import Figure来代替plt.figure,再次测试一下

因为plt.figure造成的问题没有了,但是plt.savefig保存图片的这一句代码依然存在问题

image.png 罪魁祸首依然是由tk创建的窗口,但是从文档中没有找到能抑制窗口创建的参数或者替代方法

看一下Matplotlab的源代码,gcf获取一个figure,如果没有的话使用plt.figure创建,淦,又是这个东西

matplotlib/pyplot.py:852

def gcf():
    """
    Get the current figure.
    If there is currently no figure on the pyplot figure stack, a new one is
    created using `~.pyplot.figure()`.  (To test whether there is currently a
    figure on the pyplot figure stack, check whether `~.pyplot.get_fignums()`
    is empty.)
    """
    manager = _pylab_helpers.Gcf.get_active()
    if manager is not None:
        return manager.canvas.figure
    else:
        return figure()

我们将plt.savefig修改成figure.savefig官方推荐用Figure来代替plt.figure,我听劝

现在plt.savefig的问题解决了,但是有有新的了

image.png 我都无语死了,删除所有的plt.clf()虽然可以解决这个问题,但是所有的问题都指向一个,那就是figure中创建的tk窗口,肯定有方法可以全局抑制窗口创建的办法。

根据网上的资料发现Matplotlib的后端分成了互动式和非互动式

  • 互动式: GTK3Agg, GTK3Cairo, GTK4Agg, GTK4Cairo, MacOSX, nbAgg, QtAgg, QtCairo, TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo, Qt5Agg, Qt5Cairo

  • 非互动式: agg, cairo, pdf, pgf, ps, svg, template

matplotlib.get_backend()获取默认的后端:TkAgg!!

设置成agg再试试

68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032342f706e672f313238393638382f313731343130393632313531302d39636131336331642d636530612d343632642d616231332d3266643839326133313066392e706e67236176657.png 稳得很