性能是優化出來的,不管是在上生產前,還是在上生產后。大部分性能在性能測試階段就能發現問題,但也有一些性能問題,結合生產的環境,生產數據才能表現出來,成為一個顯著的瓶頸。
這次是生成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服務的負載,又能充分利用客戶端的資源,一舉兩得。