再记一次Memory Leak分析

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了再记一次Memory Leak分析相关的知识,希望对你有一定的参考价值。

性能是优化出来的,不管是在上生产前,还是在上生产后。大部分性能在性能测试阶段就能发现问题,但也有一些性能问题,结合生产的环境,生产数据才能表现出来,成为一个显著的瓶颈。

这次是生成pdf造成的内存泄露,大体代码如下,具体表现是内存缓慢增长,在docker中比windows增长速度要快,但都有只增不回收的特点。

using System.Data;
using System.Reflection.Metadata;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Fluent;
using QRCoder;
using Microsoft.AspNetCore.Mvc;
using QuestPDF.Drawing;
using QuestPDF;


var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();


app.MapGet("/getpdf", () =>

    var table = new DataTable();
    for (var i = 0; i < 10; i++)
    
        table.Columns.Add(i.ToString());
    
    for (var row = 0; row < 1000; row++)
    
        table.Rows.Add(row.ToString(), row.ToString(), row.ToString(), row.ToString(), row.ToString(), row.ToString(), row.ToString(), DateTime.Now + "wwewrwerewfdsfdswefwefewfwefwefew" + row, row.ToString(), row.ToString());
     
    return TypedResults.File(GetPDF(table), contentType: "application/pdf", fileDownloadName: "a.pdf");
);


app.Run();
static IContainer CellStyle(IContainer container)

    return container.DefaultTextStyle(x => x.SemiBold().FontSize(11)).PaddingVertical(5).BorderBottom(1).BorderColor(Colors.Black);

static byte[] GetPDF(DataTable dt)

    var doc = QuestPDF.Fluent.Document.Create(container =>
    
        using var stream = File.OpenRead(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "fonts", "MEIRYO.TTC"));
        FontManager.RegisterFont(stream);
        Settings.EnableCaching = true;
        Settings.EnableDebugging = false;
        container.Page(page =>
        
            page.Size(PageSizes.A4);
            page.Margin(2, Unit.Centimetre);
            page.PageColor(Colors.White);
            page.DefaultTextStyle(x => x.FontSize(14).FontFamily("Meiryo"));
            page.Content()
                .PaddingVertical(1, Unit.Centimetre)
                .Column(x =>
                
                    x.Item().Table(table =>
                    
                        table.ColumnsDefinition(columns =>
                        
                            for (var i = 0; i < dt.Columns.Count; i++)
                            
                                columns.RelativeColumn();
                            
                        );
                        table.Header(header =>
                        
                            foreach (DataColumn col in dt.Columns)
                            
                                header.Cell().Element(CellStyle).Text(col.ColumnName);
                            


                        );
                        foreach (DataRow row in dt.Rows)
                        
                            for (var i = 0; i < dt.Columns.Count; i++)
                            
                                if (i == 7)
                                
                                    byte[] qrCodeAsBitmapByteArr = PngByteQRCodeHelper.GetQRCode(row[i].ToString(), QRCodeGenerator.ECCLevel.Q, 20, false);
                                    using var ms = new MemoryStream(qrCodeAsBitmapByteArr);
                                    table.Cell().Image(qrCodeAsBitmapByteArr);
                                
                                else
                                
                                    table.Cell().Element(CellStyle).Text(row[i].ToString());
                                
                            
                        
                    );
                );
            page.Footer()
                .AlignCenter()
                .Text(x =>
                
                    x.Span("Page ");
                    x.CurrentPageNumber();
                    x.Span("/ ");
                    x.TotalPages();
                );
        );
    );
    return doc.GeneratePdf();

各种.net的调试工具用上,都只能证明内存只增不减,连pmap都用上了,发现下面的一个大文件的内存占用,并且很多,大体都是在65M左右。

Address           Kbytes     RSS   Dirty Mode  Mapping
00007f4d54000000   65536   65536   65536 rw---   [ anon ]

经过一天的测试,找不到具体的问,最后推测是生成Pdf后,生成的内存是非托管内存,GC回收不掉,每当有用户下载PDF时,就会积累内存,直到Pod崩掉。

干不掉bug,就干掉需求吧,因为经过统计这个下载pdf的使用量很低,可以下掉功能。同时把这个问题给QuestPDF提了个issues。就准备躺平。QuestPDF回应很快,说注册字体只用注册一次就行,多次注册会重复累加字体,37到40行代码,只在服务启动时加载一次即可,经过测试果然有效果。

在复盘时,总结,常规的思维在win下注册字体,多次注册不会让相同的字体保存多份,从而认知QuestPDF注册也是同理,造成下载一次就注册一次,造成内存积加,这也是为什么65M左右的内存一起在累加的原因。

后来,我又思考了一下,如果做一个支持高并发,高性能的web服务,像下载Excel,PDF这些操作,是不能在后台生成的,因为用户的一个文件的体积和用户的数据是有关系的,大小很难控件,如果耦合在web服务端来做这件事,肯定是个“雷”。也有的建议是用其他服务或cli异步生成,然后放在云存储上让用户下载,这也是一种解决方案。

我觉还是让web服务做自己擅长的事,提供数据,把文件生成组装操作转嫁给客户的浏览器来完成,这样既能减轻web服务的负载,又能充分利用客户端的资源,一举两得。

以上是关于再记一次Memory Leak分析的主要内容,如果未能解决你的问题,请参考以下文章

unity再记一次shader渲染性能工具

记一次通过Memory Analyzer分析内存泄漏的解决过程

记一次使用Memory Analyzer工具分析堆内存溢出问题

b.Jre Memory Leak Prevention Listener

JavaScript :memory leak [转]

For Memory Leak Debug