AWS S3 .NET Client高内存使用率

将AWS S3 .NET客户端LOH分配减少98%

内容

问题发现

我们在Codeweavers所做的一件事就是帮助人们找到他们的下一辆车。 通常,这涉及到客户看到他们要购买的汽车-我的意思是,您会在不看外观的情况下购买汽车吗? 持有这个责任该应用程序是为淫秽金额分配,时间GC上花费的,而像饼干怪兽吃得好...饼干一般吃RAM 最坏的罪犯。

我们不时地希望从生产环境中获取此应用程序的内存转储。 我们已经完成了足够的次数,以使我们执行的最常见诊断步骤自动化,并将其捆绑到一个称为ADA(自动转储分析)的小工具中。 如果你有兴趣,你可以找到的工具, 在这里 ,所有谈到本文中的代码在这里

我们运行的分析器之一是转储在大对象堆(LOH)上找到的所有byte []数组。 针对我们的8 GB内存转储运行该分析器之后,我们发现了数百个byte []数组,长度为131,096或131,186。 好吧,这很奇怪。 Notepad ++中打开某些文件只会给我们带来很多随机字符。

我把科学方法丢到窗外一秒钟,我决定将所有转储的byte []数组重命名为* .jpg-嘿,现在有些文件现在显示缩略图! 经过仔细检查,大约50%的文件是图像。 其余50%都无法完全打开为图像。 在Notepad ++中打开少​​数非图像文件显示,它们在文件开头都有一行类似于此的行:-

0;chunk-signature=48ebf1394fcc452801d4ccebf0598177c7b31876e3fbcb7f6156213f931b261d

好的,这开始变得更有意义了。 长度为131,096的byte []数组是纯图像。 不是图像的byte []数组的长度为131,186,并在其余内容之前具有块签名行。 猜想签名是内容的SHA256哈希。

在继续进行之前,值得确定该应用程序在图像处理方面的繁忙程度。 我们所有的图像处理都使用AWS SNSSQS在整个服务器场中分布。 使用CloudWatch指标,我们可以轻松看到:

AWS S3 .NET Client高内存使用率

好了,所以相当繁忙。 值得注意的是,在进行任何以性能为中心的工作之前,请务必确定执行代码的频率和当前成本。 如果代码路径的成本很高(例如,花费二十秒钟),但是每天只命中一次,则不值得研究。 但是,如果相同的代码路径被击中很多(例如一天一百万次),那么绝对值得研究。

此时,我想到了两个罪魁祸首。 我们已经建立了有问题的应用程序来做很多图像处理。 但是有一些运动的部分和两种启动图像处理的方式:

  1. 图片被推送给我们
  2. 我们从SFTP提取图像

之后,我们转换图像,然后将其上传到AWS S3 在此阶段,我倾向于使用SFTP,因为它可能需要验证从服务器接收到的每个块。 但是,随着我的预感,我开始追逐野鹅,于是我无视我的直觉,便将大块签名插入了Google并粉碎了输入。 谷歌指出AWS S3是罪魁祸首 但这仅仅是理论,我们需要证明这一点。

如果我们上传相同的图像十次并使用dotTrace来查看LOH,则会看到一个有趣的模式:

AWS S3 .NET Client高内存使用率

每次我们在AWS S3 .NET客户端上调用PutObject时,看起来固定成本为0.3 MB。 这是一个问题,因为这意味着每次使用PutObject时,每次上载都要付出0.3 MB的高昂费用。 只想确认一下; 如果将上传次数从十次增加到一百次会发生什么?

AWS S3 .NET Client高内存使用率

是的,我们可以肯定地说,每次调用PutObject都会产生0.3 MB的昂贵分配。 更进一步,并使用ProcDump转储该过程:-

procdump64.exe -ma -64 AWS-S3.exe

通过ADA运行转储文件,我们看到有两组byte []数组具有完全相同的特性; 50%的长度为131,096,其他50%的长度为131,186。 重命名时,文件的一半是图像,文件的一半具有块签名起始行。 至此,我们可以确定,AWS S3 .NET客户端正在将byte []数组直接分配到LOH上。 个问题。

为什么会出问题呢?

LOH是一个已收集但从未压缩的内存区域,尽管从现在开始可以进行.NET v4.5.1 压缩 ,但警告词对LOH的压缩非常昂贵。 每兆字节大约2.3毫秒 一条很好的经验法则是,短暂存在的物体不能进入LOH。

等于或大于85,000字节的对象直接进入LOH。 LOH的操作与其他内存区域大不相同。 其他内存区域会定期收集和压缩,这意味着您可以在垃圾收集器运行后将新对象添加到末尾。 而LOH会尝试将新分配的对象放入废弃对象遗留的剩余空间中。 如果新分配的对象的大小与可用空间完全相同或较小,则此方法很好用。 如果找不到空间,则LOH必须增长以容纳该对象。

它有助于像书架一样思考它; 在内存的其他区域中,不再使用的书籍被简单地扔掉,剩余的书籍被推到一起,所有新书都放在书架的尽头。

在不可能的LOH内,将丢弃书籍(对象),并记录该空间中曾经使用的页数(字节),并在下次将书分配到该书架时(LOH)它试图找到一个可以容纳那么多页面(字节)的空白空间。 如果书架无法容纳新分配的书(对象),则必须扩展书架以容纳新书(对象)。

垃圾收集器将从LOH收集死对象,与此同时,将新对象分配给LOH。 这可能导致长时间运行的应用程序出现生命周期的情况,其中LOH大小已增加到几GB(因为新对象无法容纳到现有的空白空间中),但实际上仅包含几个活动对象。 这称为LOH碎片。 在这种情况下,我们感到非常幸运,因为将其放入LOH的byte []数组有两种大小。 131,186和131,096。 这意味着,无论哪种大小的旧对象都死掉并被收集起来,新分配的对象恰好适合将其放入空白空间的大小。

好吧,回到有趣的东西。

推出最佳魔幻数字— 81,920

多亏了dotTrace,我们才能够准确地确定导致LOH碎片的原因。 它还向我们显示,每次调用PutObject的固定成本0.3 MB发生在ChunkedUploadWrapperStream的构造函数内部:

AWS S3 .NET Client高内存使用率

快速访问该文件在AWS-SDK-网库。 显示创建的两个byte []数组的长度至少为131,072:-

这就是为什么将这些byte []数组直接分配给LOH的原因,它们高于LOH阈值(85,000字节)。 此时,有几种可能的解决方案:

  1. 使用System.Buffers从byte []数组池中租用两个byte []数组
  2. 使用Microsoft.IO.RecycableMemoryStream并使用Stream的池直接对传入流进行操作
  3. 公开DefaultChunkSize,以便API的使用者可以自行设置
  4. 将DefaultChunkSize降低到低于LOH阈值的数字(85,000字节)

第一个和第二个解决方案可能是获得最大胜利的解决方案,但这将需要大量的请求,并引入了库维护者可能不希望的依赖项²。 第三种解决方案意味着图书馆的使用者必须了解该问题并将其设置为合理的数目,以避免分配LOH。 不,似乎第四种解决方案最有可能被接受,破坏现有功能的可能性也最小。

我们所需要的是一个低于85,000的数字,通常84,000之类的数字非常合适。 但是,在发现此问题的几周前,当我偶然发现这个宝石时,我正在参考参考源 (正在研究另一个问题):

Windows内存页的大小4,096字节 ,因此选择低于LOH阈值(85,000字节)的倍数是完全有意义的。 是时候分叉,分支, 创建问题并提出拉取请求了

幸运的是,我们可以在本地进行更改³,看看有什么好处。 通过PutObject上传同一图像的一百次统计:-

空闲的手

在等待对我的请求请求进行审查的同时,我决定查阅AWS S3文档,偶然发现了预签名URL的概念。 听起来很有趣! 创建上传器的V2:-

上载相同文件一百次时,我们看到它具有以下统计信息:-

那真是太棒了,而我们实际上要做的就是阅读文档! 好吧,这是不对的,您的好处是可以阅读包含所有内容的摘要文章。 您在此处看到的工作是在一周的时间内完成的,介于两次客户工作之间。

使用GetPreSignedURL的一个小缺点是,如果修改了GetPreSignedUrlRequest并且未相应地修改WebRequest,则AWS将返回HTTP 403 Forbidden (例如,删除WebRequest上的XAmzAclHeader)。 这是因为客户端哈希和服务器哈希不再匹配。

还有一件事

多亏了我的上一篇文章,我才知道什么是书呆子狙击 -我对自己做了很多事情。 在此阶段,我感到对其他东西可以省掉有点头晕,我一直在看着LOH上剩余的0.4 MB。 同样,dotTrace将我们指向代码路径的方向,导致向LOH分配了0.4 MB:-

AWS S3 .NET Client高内存使用率

耶克斯, 那看起来很严肃 安静地退后一步,尝试不同的策略; 我们知道一个预签名的URL看起来像这样:

我们应该能够自行生成该URL,因为AWS非常友好地发布了其签名过程 在这一点上,我承认我已经准备好接受失败了,只是将0.4 MB留给了LOH。 我真的不喜欢的代码里姆斯我正要写可能消除剩余的0.4 MB将是值得的。

直到我发现我想要的例子为止。 我的工作量大大减少了; V3诞生了:

V3只是一个实验,旨在了解可能的情况,因为所获得的收益和维护的代码量不大,这并不是我们在生产代码中实际使用的东西。 预签名URL的发现是这里的主要优势:-

同时,我的拉取请求已合并并发布在AWSSDK.Core的3.3.21.19版本中。 时间轴快速概述:-

  1. 2018–03–07 —在aws-sdk-net存储库上创建的问题
  2. 2018-03-13 —发送了拉取请求
  3. 2018-03-29 —合并请求请求
  4. 2018–03–29 —新版本的AWSSDK.Core已发布到NuGet

我喜欢开源。

TLDR-给我好东西

低于3.3.21.19的AWSSDK.Core版本导致在AWS S3 .NET客户端上每次PutObject调用的固定成本为0.3 MB。 此问题已在3.3.21.19及更高版本中得到纠正。 对于特别热的代码路径,值得探索在AWS S3 .NET客户端上使用GetPreSignedURL,因为在我们的上下文和用例中,LOH分配减少了98%。

找到我TwitterLinkedInGitHub

脚注

¹另一个原因可能是WinDbg仍然吓到我了。

²话虽如此, 最近的一次对话已经开始利用.NET Core的优点。

³确保不像个人那样构建发布版本-好的,是我

本来在发表 dev.to

AWS S3 .NET Client高内存使用率

From: https://hackernoon.com/its-not-your-code-vol-i-c06fac8784df