青岛建站通,网站开发多语言切换思路,做网站需要哪些参考文献,wordpress企业主题哪个好在 .NET 9 中#xff0c;我们默认启用了 DATAS。但 .NET 9 并不是长期支持#xff08;LTS#xff09;版本#xff0c;因此很多人会在升级到 .NET 10 时首次获得 DATAS。这是一个很艰难的决定#xff0c;因为 GC 功能通常是不需要用户干预的 —— 但 DATAS 有些不一样。这也…在 .NET 9 中我们默认启用了 DATAS。但 .NET 9 并不是长期支持LTS版本因此很多人会在升级到 .NET 10 时首次获得 DATAS。这是一个很艰难的决定因为 GC 功能通常是不需要用户干预的 —— 但 DATAS 有些不一样。这也是为什么本文标题是“做准备”而不是单纯的“新功能介绍”。如果你在使用 Server GC你可能会注意到相比以往的运行时升级性能特征差异更为明显。内存使用可能会显著不同很可能更小—— 这未必是你想要的。这取决于这种取舍对你来说是否明显以及它是否符合你的优化目标。我建议你至少快速查看一下应用性能指标看看是否对这种变化满意。很多人会绝对欢迎此变化 —— 但如果你不是其中之一不必慌张。我建议继续阅读看是否可以简单地关闭 DATAS或者稍微调优让它对你有好处。我将介绍我们通常如何决定添加哪些性能功能为什么 DATAS 与典型 GC 功能有很大不同以及自我上次撰写 DATAS 文章以来引入的调优变化。我还会分享两个我在首方场景中调优 DATAS 的实例。如果你主要是想知道 DATAS 不适用的场景以帮助判断是否要关闭它可以直接跳到相关部分。术语表#在深入内容前先列出本文中使用的缩写GC垃圾回收器负责管理应用程序的内存分配与释放DATAS动态适应应用程序大小Dynamic Adaptation To Application SizesTCP吞吐成本百分比Throughput Cost Percentage—— 测量 GC 开销包括 GC 暂停和分配等待BCD通过 DATAS 计算的预算Budget Computed via DATAS—— 代际 0 分配预算的上限LDS活动数据大小Live Data Size即应用程序在最强 GC 下占用的内存大小UOH用户旧代堆User Old Heap旧代中用户代码分配的部分包括 LOH 和 POHLOH大对象堆Large Object Heap用于存储 ≥85,000 字节的对象可通过 GCLOHThreshold 配置修改POH固定对象堆Pinned Object Heap专门用于存储在分配时标记为固定的对象的堆区域添加 GC 性能功能的一般策略#大多数 GC 性能功能 —— 无论是新的 GC 类型、新的机制还是优化现有机制 —— 通常在你升级到新的运行时版本时自动启用。我们不要求用户采取操作因为这些功能旨在改善广泛的场景。这也是我们选择实现它们的原因我们分析许多场景以找出常见问题确定解决它们的方法然后优先设计并实现可带来最大影响的功能。当然任何性能变化都有引入回退的风险 —— 对于一个拥有数百万用户的框架你几乎可以肯定会让某些人退步。这种退步在微基准测试中尤其明显因为微基准测试的行为高度极端甚至细微变化都会让结果大幅波动。一个近期的例子是我们改变了处理 UOH即 LOH POH代的可用区域的方式。我们从基于预算的裁剪策略改为基于“年龄”的策略因为它整体更稳健这样我们不会快速释放内存再重新提交或在长时间后仍保留大量可用区域因为一直没有消耗几乎所有的 UOH 预算。但这会完全改变一个原本在一次 GC.Collect() 后主内存降到很低值的微基准测试使其必须调用 3 次 GC.Collect()因为我们需等待 UOH 可用区域在两次 gen2 GC 中“变老”第三次才会把它放入释放列表。但对于 DATAS我们知道它天生并不适用于广泛场景。正如我在上篇博客文章中所述DATAS 针对两类特定场景。我在这里重申在受内存限制的环境中运行的突发性负载。DATAS 旨在当应用不需要那么多内存时收缩堆大小而在需要更多时扩展堆大小。这对运行在有内存限制的容器中的应用尤其重要。使用 Server GC 的小型负载 —— 例如有人想试试一个小型 asp.net core 应用在 .NET 中的体验DATAS 旨在提供与小型应用实际需求更加匹配的堆大小。关于第 1 点我需要进一步解释。突发性负载非常常见。一个处理请求的应用在某个时间段的用户数自然可能远超一天中其他时间段。但关键在于随后的动作 —— 如果在非高峰时释放了内存你会如何利用这些内存事实证明有时人们并没有真正的计划 —— 他们只是希望看到内存占用下降但并不打算使用这些内存。而有的团队也不需要降低内存占用因为他们已经为应用预留了全部内存。我最近就和一个客户交流当我问他们“如果 DATAS 为你释放了内存你会用来做什么”时对方回答“这是个好问题我们从没想过。”对于那些希望利用释放内存的人一个常见的方法是使用编排环境。DATAS 让此场景更稳健因为堆大小更可预测从而帮助设定合理的内存限制。例如在 k8s 中你可以为非高峰和高峰负载分别确定合适的 request 和 limit 值更好利用 HPA。我还见过有团队安排任务在机器/虚拟机有空闲内存时运行 —— 这通常更复杂而且这些团队通常配有专门的性能工程师但控制力更强。另外也有很多团队拥有专用机器集群想在高峰时尽最大可能提高吞吐量。他们不愿容忍任何形式的性能下降。他们显然不是 DATAS 的目标用户因为 DATAS 几乎总会降低其吞吐量 —— 性能问题很少是“全有或全无”我会在下文讨论如何决定是否该关闭 DATAS。所有这些因素让我们很难将 DATAS 设为默认开启因为我们知道有很多团队不愿牺牲吞吐量或者不会利用释放的内存。我将在下文详细讨论如果你想比较性能差异并判断 DATAS 是否适用或者看到内存减少后想到利用这些空闲内存该如何分析。DATAS 与传统 Server GC 的性能差异#DATAS 是一个我花了比其它任何 GC 功能更多时间向同事解释的功能——由于它是高度显性的用户可感知特性自然比我添加的几乎任何其它 GC 功能收到更多的提问。而且围绕它存在许多误解。有些人认为 DATAS 只影响启动阶段有些人假设它只是“内存减少 x%吞吐量降低 y%”还有些人期待它能“神奇地减少内存占用而不会带来其他性能差异”好吧“神奇地”是我加上的 等等。要正确理解它的不同我们需要先理解两者的策略差异。首先也是最重要的Server GC 并不会根据应用大小进行自适应——这从来就不是它的设计目标。Server GC 主要观察每一代的存活率并根据这一指标来决定何时进行 GC当然还有其他因素影响 GC 的触发时机但存活率是其中最重要的因素之一。在我上一篇关于 DATAS 的文章中我谈到了堆的数量会显著影响堆大小尤其是在分配了大量临时数据的负载场景下。由于 Server GC 会创建与进程可使用核心数相同数量的堆这意味着同一个应用在不同核心数的机器上运行或者同一台机器上限制可用核心数量运行都会表现出非常不同的堆大小。而 DATAS 的目标则是适应应用的大小这意味着即使核心数差异很大你的堆大小也应该是相近的。因此没有什么“DATAS 会比 Server GC 减少 X% 的内存占用”的固定结论。如果我们看 asp.net 基准测试的“最大堆大小”指标可以明显看到 Server GC 在 28 核机器28c和 12 核机器12c上的行为差异——细心的读者会注意到图中的颜色顺序并不一致。比如在 MultipleQueriesPlatform 场景中最大堆大小在 12c 情况下比 28c 更大。仔细查看数据可以发现在 12c 情况下最大堆大小其实发生在测试一开始——(Heap size (before) 指在某次 GC 之前的堆大小即 GC 还未来得及缩减堆时的大小。因此 “最大堆大小” 是此指标的最大值)这是因为在刚开始阶段28c 配置下由于有 28 个堆在第一次 GC 发生前进行了更多的内存分配。于是第一次 GC 后观察到的存活率较小从而使 gen0 的分配预算budget比 12c 更小。12c 很快就进入了稳定状态并且堆大小显著低于 28c。在稳定状态下这些基准测试在 28c 下的堆大小始终高于 12c。这说明了两点如果只测量“最大堆大小”很容易被非稳定状态的行为影响堆大小会因为测试运行的机器不同而有非常大的变化。需要注意的是这些效应在小型基准测试里会被放大但其原理同样适用于真实应用。使用 DATAS 时我们看到的情况是——28c 与 12c 的最大堆大小非常接近这正是 DATAS 的设计目的——它适应应用的大小。如果我使用 Workstation GC需要关注 DATAS 吗#答案取决于你使用 Workstation GC 的原因。如果你使用 Workstation GC 是因为你的工作负载根本不需要 Server GC例如应用是单线程的或分配压力很小你完全可以接受只有一个线程做垃圾回收那么 Workstation GC 不仅足够而且就是正确的选择。但如果你使用它只是因为 Server GC 内存占用太大而改用 Workstation GC 来限制内存占用那么你可能会觉得 DATAS 很有吸引力因为它既可以限制内存占用也可以让更多的 GC 线程参与回收从而减少 GC 暂停时间。DATAS 是如何工作的#如果你理解了 DATAS 的工作原理就会自然地得出下面这些建议帮助你判断它是否适合你的场景。你也可以跳过这一部分但我个人更喜欢去理解事物的运作机制而不是单纯记下一些经验规则。这样我能自己得出结论而不是机械地照搬配置。在上一篇博客中我提到过当时.NET 8的 DATAS 一些实现细节并指出它很可能会发生重大改变 —— 事实确实如此在设计和实现上都有较大改动。我们在 .NET 8 中的实现更多是功能性验证几乎没有花时间在优化调优上而主要的调优工作是在 .NET 8 之后进行的。DATAS 的目标是根据应用规模即 LDSLive Data Size存活数据大小进行自适应调整。因此需要有一种方法去适配它。由于 .NET GC 是代际垃圾回收它并不会频繁回收整个堆。而且大多数完整 GC 都是后台 GC并且不会进行压缩因此我们可以近似地通过旧年代对象占用的空间即总大小减去碎片来估计 LDS。在做性能分析时另一个方便的数值是查看一次完整 GC 时的晋升大小promoted size。在上一篇博客中我提到过 conserve memory 配置是 DATAS 实现的一部分 —— 这一点没有变化。但是 conserve memory 只影响何时触发完整 GC。对于分配非常频繁的应用除非它们主要分配的是临时 UOH大对象堆对象否则大部分触发的都是瞬时代ephemeralGC。而瞬时代的大小在小堆场景中可能占据整个堆的一大部分。在尝试不同方法之后我最终确定了一个兼顾适应应用大小与保持合理性能的策略包含两个核心部分引入了一个概念——DATAS 计算的预算BCD, Budget Computed via DATAS它是基于应用规模计算出来的 gen0 最大预算上限。这个值可以近似 gen0 的代大小考虑到有对象会被固定实际 gen0 的大小可能会略有不同。在上述预算上限之内如果能保持合理性能我们还会进一步减少内存占用。我们用目标吞吐量成本百分比TCP, Throughput Cost Percentage来定义“合理性能”。TCP 考虑了 GC 暂停时间以及分配线程等待时间不过在稳定状态下使用 GC 暂停时间百分比近似 TCP 已足够。目标是在可能的情况下将 TCP 控制在这个目标值左右。这意味着当负载减轻时我们会缩小 gen0 预算从而使 gen0 在下一次 GC 前的大小更小最终导致堆大小变小。默认目标 TCP 为 2%可以通过 GCDTargetTCP 配置进行修改。我们来看两个例子看看这如何在不同场景下体现出来。为了简化说明我忽略了后台 GC并用 GC 暂停时间百分比近似 TCP。场景 A —— 一个电商应用将完整商品目录存储在内存中并在整个进程生命周期保持不变这就是我们的 LDS。进程开始处理请求每个请求都会分配一些内存并在请求完成后释放。在高峰时段它同时处理大量请求。这时我们达到了最大的预算 BCD。假设这个预算是 1GB这意味着每分配 1GB 就会发生一次 GC。如果用 GC 暂停时间百分比近似 TCP假设每秒分配 1GB会进行一次 GC暂停时间为 20ms。那么 GC 时间占比为 2%正好等于目标 TCP。在非高峰时段同时请求减少假设每秒只分配 ~200MB如果仍用 1GB 预算就会每 5 秒一次 GC此时 GC 时间占比为 (20ms / 5s 0.4%)远低于 2%。为了达到目标 TCP我们希望减少预算更早触发 GC。如果预算减少到 200MB并假设 GC 暂停时间仍为 20ms实际上可能会更短因为存活率少那么 TCP 再次达到 2%。在这种情况下非高峰时段的堆大小减少了约 800MB。根据总堆大小这会是非常显著的提升。场景 B —— 基于场景 A但我们增加了一个缓存该缓存是 LDS 的一部分并在轻负载时缩小因为不需缓存过多。由于 LDS 变小BCD 也会变小此时 gen0 预算会进一步减少再次体现了 DATAS“根据规模自适应”的特性。同时conserve memory 机制仍然生效它会相应调节旧年代的预算和大小。注意到在以上例子中我们完全没有提到堆的数量这是 DATAS 自己处理的事情因此你无需手动指定。以前有客户会通过 GCHeapCount 配置来指定 Server GC 堆的数量。而 DATAS 更加稳健可以在需要时利用更多堆通常意味着更短的单次暂停时间并在 LDS 下降时减少堆大小而无需你自己设置。DATAS 有专门的事件来表示实际的 TCP 和 LDS但获取这些数据需要通过 TraceEvent 库编程获取。对于几乎所有性能分析使用上述近似值已经足够。在哪些情况下不适合使用 DATAS#如果你读过前面的内容下面这些判断应该很容易理解。如果你不需要释放的内存 这很明显 —— 如果释放的内存对你毫无用途就没必要改动任何东西。你可以通过 GCDynamicAdaptationMode 配置关闭 DATAS。 我遇到过一些内部团队他们的进程运行在专用机器上不会运行其他程序因此无需额外空闲内存。他们会关闭 DATAS。但如果他们将来希望在非高峰期利用这部分内存可以再启用。如果启动性能至关重要 DATAS 启动时总是从 1 个堆开始因为我们无法预测你的负载压力且 DATAS 优化的是大小所以初始堆数最小。如果你的应用启动性能非常重要DATAS 会导致在扩展到多堆的过程中有性能回退。如果你不能接受任何吞吐量回退 包括启动吞吐量。对于不关心启动的场景可根据情况选择是否用 DATAS。例如如果 Server GC 的 GC 暂停时间占比为 1%你可以设置 GCDTargetTCP 为 1。如果之前限制堆数DATAS 可能会带来性能提升因为暂停时间会更短。如果适应应用大小对你有帮助DATAS 可能是更好的选择。但如第 1 点所述如果你完全用不到释放出来的内存就没必要花时间。如果你的场景主要发生 gen2 GC 如果你的场景几乎总是 gen2 GC这几乎总是因为大量分配临时大对象DATAS 并未在这种情况上进行深入调优。如果你试用 DATAS 后不满意可以关闭它。如果有足够理由花时间调优也可参考后面的调优部分进行尝试。如果需要如何调优 DATAS#我在一些内部重要负载上试过 DATAS总体效果很好。但在少数场景下默认参数效果一般稍微调整一两个配置就能让它工作得很好。客户案例 1#这是一个运行在专用机器上的服务器应用。但他们正计划将其容器化因此使用 DATAS 的确有一定价值。启用 DATAS 后他们观察到吞吐量下降了 6.8%同时工作集减少了 10%。目前他们已经禁用了 DATAS —— 我会解释我是如何调试的并确定在他们未来想重新启用 DATAS 时应使用的配置。由于 DATAS 会根据 LDS 限制最大的 gen0 预算我们需要查看是否触及了这个上限。最简单的方法是分别在启用 DATAS 和未启用 DATAS 的情况下捕获 GC 跟踪。如果你发现触发的 GC 次数更多那么很可能就是达到了这个限制。你可以用 “% Pause Time” 列来近似计算 TCP用 “Gen0 Alloc MB” 列来近似计算 gen0 预算。你需要找到 % 暂停时间最高的阶段并查看此时是否触发了更多的 GC。对于这个特定客户下面是他们的一些 GC 摘录我已裁剪了 GCStats 视图的列——未启用 DATASGC index Trigger reason Gen % pause time Gen0 Alloc (MB) Promoted (MB)7017 AllocSmall 0N 0.7 4,243.75 382.8557018 AllocSmall 1N 1.2 4,157.85 1,074.827019 AllocSmall 0N 0.8 4,218.46 484.2767020 AllocSmall 1N 2.0 4,249.86 1,072.567021 AllocSmall 0N 1.5 4,258.12 453.5347022 AllocSmall 1N 1.8 4,244.21 1,026.417023 AllocSmall 0N 1.0 4,254.77 461.7027024 AllocSmall 1N 1.4 4,239.38 992.2437025 AllocSmall 0N 1.0 4,252.54 465.9047026 AllocSmall 1N 2.5 4,252.47 1,153.607027 AllocSmall 0N 1.7 4,216.14 442.2337028 AllocSmall 2B 0.3 0 15,039.207029 AllocSmall 0N 0.6 4,166.23 411.2387030 AllocSmall 1N 1.0 4,104.28 681.4307031 AllocSmall 0N 1.4 4,229.11 582.2567032 AllocSmall 1N 1.1 4,222.06 963.8177033 AllocSmall 0N 1.5 4,248.45 463.5557034 AllocSmall 1N 1.1 4,230.40 889.2867035 AllocSmall 0N 0.8 4,255.81 467.8547036 AllocSmall 1N 1.4 4,254.73 926.1037037 AllocSmall 0N 2.3 4,220.31 448.9187038 AllocSmall 1N 1.2 4,249.19 963.297启用 DATASGC index Trigger reason Gen % pause time Gen0 Alloc (MB) Promoted (MB)17632 AllocSmall 0N 2.6 1,645.46 236.15517633 AllocSmall 1N 1.9 1,637.37 430.24417634 AllocSmall 0N 1.4 1,648.58 228.61117635 AllocSmall 1N 1.8 1,633.46 461.74117636 AllocSmall 0N 3.8 1,644.98 257.46117637 AllocSmall 1N 2.6 1,646.77 492.17617638 AllocSmall 0N 1.5 1,650.46 217.60417639 AllocSmall 1N 2.2 1,652.98 446.63417640 AllocSmall 0N 2.0 1,647.49 176.04717641 AllocSmall 1N 2.2 1,638.71 495.13717642 AllocSmall 0N 1.3 1,643.52 194.35317643 AllocSmall 1N 4.1 1,589.32 451.10017644 AllocSmall 0N 2.8 1,645.70 220.34317645 AllocSmall 1N 2.4 1,644.41 479.15917646 AllocSmall 0N 1.1 1,642.08 229.87717647 AllocSmall 1N 1.2 1,638.72 436.05117648 AllocSmall 0N 1.2 1,653.15 158.11517649 AllocSmall 1N 1.5 1,648.69 487.92317650 AllocSmall 0N 1.6 1,649.91 211.39117651 AllocSmall 1N 5.2 1,624.07 412.57017652 AllocSmall 0N 1.9 1,644.00 213.89517653 AllocSmall 2B 0.3 0 14,936.54比较他们在 GC 中的 gen0 预算和 % 暂停时间 —指标 无 DATAS 有 DATAS 无 DATAS / 有 DATASgen0 预算 (GB) 4.22 1.64 2.6% 暂停时间 1.2 2.1 0.6因此无 DATAS 时的 gen0 预算是有 DATAS 时的 2.6 倍。另一个有用的观察是 % 暂停时间几乎正好等于目标 TCP——2%。这表明从 DATAS 的角度来看它完全按设计工作。但无 DATAS 时我们有 2.6 倍的预算自然触发 GC 的频率降低% 暂停时间从 2.1 降到了 1.2。如果我们想启用 DATAS 且在这一阶段不降低吞吐量就需要让 DATAS 使用更大的 gen0 预算。要做到这一点我们必须理解 DATAS 是如何确定 BCD 的。既然是适配内存大小我们希望将大小乘以一个系数。但这个乘数不能是常量因为当内存很小时乘数应该非常大——如果 LDS 只有 2MB对于小型应用这是完全可能的我们不希望每分配 0.2MB 就触发 GC——这样开销太高。假设我们希望在触发 GC 前允许分配 20MB这意味着乘数是 10。但如果 LDS 是 20GB我们也不希望分配 200GB 才 GC这意味着乘数要小得多。这就意味着需要一个幂函数同时在最小值和最大值之间进行限制 ——m constant / sqrt(LDS);// max_m 默认值是 10m min (max_m, m);// min_m 默认值是 0.1m max (min_m, m);幂函数的实际公式是 —m (20 - conserve_memory) / sqrt (LDS / 1000 / 1000);可以简化为 —m (20 - conserve_memory) * 1000 / sqrt (LDS);m (20 - 5) * 1000 / sqrt (LDS);m 15000 / sqrt (LDS);因此常量是 15000或者如果以 MB 为单位可以说常量是 15。以下是不同 LDS 值的一些示例 —LDS (MB) m m 限制后 BCD (MB)1 15.00 10.00 105 6.71 6.71 3410 4.74 4.74 4750 2.12 2.12 106100 1.50 1.50 150500 0.67 0.67 3351,000 0.47 0.47 4745,000 0.21 0.21 1,06110,000 0.15 0.15 1,50015,000 0.12 0.12 1,83730,000 0.09 0.10 3,00050,000 0.07 0.10 5,000https://gist.github.com/Maoni0/15064a505db2d06189a875d4b7e9e211/raw/2153a4f57c1d5c30401dd796573e553bb8f4cb36/bcd.csv这个常量、max_m 和 min_m 都可以通过配置调整请查阅配置页面的详细说明。现在很明显 DATAS 是如何得出 gen0 预算以及我们如何调整它的。如果我们想让预算接近无 DATAS 时的数值应使用 GCDGen0GrowthPercent 配置将常量增加到 2.6 倍并使用 GCDGen0GrowthMinFactor 配置提高 min_m使其不被限制为 0.1——不需要非常精确只要确保它不是限制因素即可。在此案例中如果用 15GB 近似 LDSgen2 GC 的 “Promoted (mb)” 列都显示约 15GB而无 DATAS 时 gen0 预算是 4.22GB那么 min_m 应该设为 (4.22 / 15 0.28)。我们可以将 min_m 设置为 300这就相当于 LDS 的 0.3。客户案例 2#这是客户在预备服务器上的一个 asp.net 应用代表了他们的一个关键场景。我用压测工具生成了可变的工作负载。团队已经在使用一些 GC 配置GCHeapCount 设置为 2使用 2 个堆。通过 GCNoAffinitize 配置关闭了线程亲和性。如果指定了 GCHeapCountDATAS 会被禁用因为它告诉 GC 不要修改堆数量。而修改堆数量是 DATAS 调整性能的关键机制之一所以这是关闭 DATAS 的信号。由于该进程与其他许多进程共存于同一台机器在没有 DATAS 之前他们选择使用 2 个堆来限制内存使用同时保持合理的吞吐量。但这种方式不够灵活——当负载升高时2 个堆的吞吐量会下降并且 GC 暂停时间会明显增加因为只有 2 个 GC 线程运行收集。另外他们可以手动调整堆数量但这样工作量很大且服务器 GC 对于减少内存使用并不积极当负载较轻时可能会出现堆过大的情况。我将演示使用 DATAS 如何让这个过程更稳健。当我提高负载时可以看到 GC 中的 % 暂停时间很高——这在只有 2 个堆时并不意外。所以我通过删除 GCHeapCount 配置启用了 DATAS我保留了 GCNoAffinitize 配置因为我仍然希望 GC 线程不进行亲和绑定。我发现即使有 BCDGC 中的 % 暂停时间仍然很高因为我们仍频繁触发 GC。于是我决定用 GCDGen0GrowthPercent 配置将 BCD 增加到默认值的 2 倍我不需要用 GCDGen0GrowthMinFactor因为 2 倍仍在 max_m/min_m 的限制范围内。这样该进程就表现得更理想具有以下特点GC 的 % 暂停时间显著降低。使用默认 DATAS 时 % 暂停时间也相当低且堆大小明显更小。根据优化目标这可能正是你想要的效果。DATAS 可以通过较小的预算和更多的 GC 线程来完成收集工作。但对于该客户来说这样的 % 暂停时间会影响吞吐量他们不希望它这么高。我也可以让 DATAS 使用更小的目标 TCP但在此情况下默认 TCP 已足够。单次 GC 暂停时间显著缩短因为有更多的 GC 线程在运行收集任务。当负载变轻并发客户端线程数从 200 降到 100时堆也会变小。同时我们依然保持较低的 GC % 暂停时间和单次 GC 暂停时间。希望这些对你的 DATAS 调优有所帮助如果你有需要的话。DATAS 事件#我预计大多数用户都不需要查看这些事件所以我会简要说明。前面提到的那些近似值应该已经足够。对于少数希望出于某种原因进行详细分析的人DATAS 会触发一个事件该事件准确表示了我们讨论过的指标。需要注意的是我们仅在程序中使用这些事件因此它们不会显示在 PerfView 的 Events 视图中在那里面你只能看到 GC/DynamicTraceEvent 的事件名而不是该事件的各个字段。请参考这篇博客文章了解如何在程序中从跟踪中获取 GC 信息列表以 TraceGC 对象的形式。LDS 和 TCP 会在 SizeAdaptationTuning 事件中体现假设你有一个类型为 TraceGC 的 gc 对象 —// LDSgc.DynamicEvents().SizeAdaptationTuning?.TotalSOHStableSize// TCPgc.DynamicEvents().SizeAdaptationTuning?.TcpToConsider该事件不会在每次 GC 都触发因为我们只是每隔几个 GC 才检查是否需要调整 DATAS 的调优参数。译者文章总结1. DATAS 核心思想#DATAS Dynamic Adaptation To Application Sizes动态适配应用大小目标让 GC 内存预算特别是 Server GC随着应用实时 Live Data SizeLDS变化而调整好处内存 峰值需求下降后 可以收缩堆大小处理容器 / 内存限制环境更稳健适配不同机器核心数的情况下堆大小更一致机制计算 BCDGen0 Budget 上限公式m (20 - conserveMemory) * 1000 / sqrt(LDS)m clamp(min_m, max_m, m)BCD LDS × mBCD * (GCDGen0GrowthPercent / 100)保持 TCPGC 暂停时间占比 接近目标值默认 2%2. DATAS 适用场景#容器/内存限制环境中的突发型业务高峰分配大低峰分配少需要收缩内存小型 Server GC 应用例如测试 ASP.NET Core 小应用由于 Server GC 默认堆太大DATAS 可以自动缩减3. 不适用的场景可考虑禁用#无法利用释放的内存如专用机器只跑单进程GCDynamicAdaptationMode0吞吐量完全优先不能容忍回退启动性能极其关键DATAS 启动只有 1 heap → 多 heap 需要时间主要是 Gen2 GC 场景常见于大量临时大对象分配业务自己固定堆数GCHeapCount 会直接禁用 DATAS4. 关键可调参数表DATAS 相关#配置项 默认值 影响范围 / 对应指标 公式关系 适用场景 调优方向GCDGen0GrowthPercent 100%常数15 改变 BCD 常数部分直接增加/减少 gen0 最大分配预算 BCD × (GCDGen0GrowthPercent / 100) % Pause Time 高于目标 TCP 且预算太小导致频繁 GC 提高 → 减少 GC 次数、提高吞吐降低 → 更频繁 GC减少内存占用GCDGen0GrowthMinFactor 0.1 控制 min_m预算乘数下限防止 LDS 较小时预算过低 clamp m 下限m ≥ min_m最终 BCD LDS × m LDS 较小但不希望 GC 过频 提高 → 给小 LDS 场景更多预算降低 → 更积极收缩内存GCDGen0GrowthMaxFactor 10 控制 max_m预算乘数上限 clamp m 上限m ≤ max_m最终 BCD LDS × m LDS 极小时预算过大浪费内存 降低 → 限制预算上限GCDTargetTCP 0.022% 目标 TCPGC 暂停时间占比 调整 GC 频率以接近目标 TCP 对暂停时间敏感的低延迟业务 降低 → 减少暂停提高 → 容忍更多暂停换取更小内存conserveMemory 5 影响 BCD 的基数 (20 - conserveMemory) m (20 - conserveMemory) × 1000 / sqrt(LDS) 对内存敏感容器环境 提高 → 缩小预算更积极收缩内存降低 → 增加预算减少 GC 次数GCDynamicAdaptationMode 1 是否启用 DATAS 1启用0禁用 不希望内存缩放或要固定吞吐量 设为 0 → 回退为传统 Server GCGCHeapCount 默认核心数 固定堆数禁用 DATAS 堆自适应机制 如果设置则 DATAS 禁用 需要固定堆数测试或控制内存 不设置 → 让 DATAS 自动调整堆数GCNoAffinitize false 控制 GC 线程是否绑定固定 CPU 核心 与 DATAS 共存不影响内存大小但影响线程调度 多进程共用 CPU 场景 true → 让 GC 线程自由调度5. 调优步骤建议#收集 GC 性能数据% Pause Time近似 TCPGen0 Alloc (MB)近似 BCDPromoted (MB)近似 LDS判断瓶颈原因如果 GC 触发太频繁 → 提高 GCDGen0GrowthPercent 或 GCDGen0GrowthMinFactor如果内存占用过高 → 提高 conserveMemory 或降低 GCDGen0GrowthPercent调整 TCP吞吐优先 → 调低 GCDTargetTCP减少 GC 次数内存优先 → 调高 GCDTargetTCP更频繁 GC 收缩内存验证回归高峰 低峰数据对比确保在不同负载下曲线符合预期C# .NET 交流群#相信大家在开发中经常会遇到一些性能问题苦于没有有效的工具去发现性能瓶颈或者是发现瓶颈以后不知道该如何优化。之前一直有读者朋友询问有没有技术交流群但是由于各种原因一直都没创建现在很高兴的在这里宣布我创建了一个专门交流.NET 性能优化经验的群组主题包括但不限于如何找到.NET 性能瓶颈如使用 APM、dotnet tools 等工具.NET 框架底层原理的实现如垃圾回收器、JIT 等等如何编写高性能的.NET 代码哪些地方存在性能陷阱