15-03-工具-dotMemory与PerfView-生产环境内存分析
dotMemory 与 PerfView生产环境内存分析系列C# 与常用数据结构源码剖析 · 实战工具篇阅读时间约 90 分钟版本口径dotMemory2024.1、PerfView3.1.x、.NET 8。界面菜单、命令行参数与 EventPipe 支持会变化执行前以安装版本帮助和官方文档为准。平台口径PerfView 的完整能力以 Windows ETW 为中心跨平台.NET采集优先考虑dotnet-trace、dotnet-gcdump、dotnet-dump等再按数据格式导入/关联。安全原则生产快照、dump、事件参数和符号可能含凭据、用户数据、文件路径与业务对象必须按敏感生产数据治理。一、先定义问题内存大不等于泄漏严格地说托管内存泄漏是对象已经不再有业务用途却仍从某个 GC Root 可达因而不能回收。除此之外还有多种外观相似的问题高分配率对象很快死亡但分配和 GC 频繁堆快照未必很大。合理存活集增长缓存预热、连接数或会话真的增加。容量留存List/Dictionary/池保留历史峰值后备存储业务可能有意复用。非托管增长Native 库、图像、线程栈、内存映射不完全出现在托管堆对象大小中。allocator/RSS 不回落对象已死但运行时/OS 暂未把段归还操作系统。碎片和 pinning空闲总量存在却难以用于目标分配或压缩。终结器积压对象等待 finalizer暂时仍被运行时保留。所以“进程 Private Bytes 上涨”“Gen 2 大”“Dictionary 没 TrimExcess”都不是单独的泄漏证明。调查必须把业务负载、托管存活、根路径、分配流量与非托管指标关联起来。一个可证伪假设应写成“每轮请求结束并静置后RequestContext本应不可达若快照显示它们持续 surviving且唯一保留链是 Singleton event - delegate - context则事件订阅是泄漏根因。”二、工具分工回答不同问题问题dotMemoryPerfView/ETW其他常用工具哪些托管对象活着Heap snapshot、类型/实例GC Heap Snapshot/GC dump 视采集方式dotnet-gcdump、dotnet-dump/SOS谁保留对象retention paths、dominatorHeap graph 的 roots/pathsgcroot、专业 dump 工具何处持续分配memory traffic、timeline/调用栈模式依采集GC allocation ticks/stacks、Heap Allocdotnet-trace、EventPipeGC 暂停/代际行为timeline/GC 事件视模式GCStats、Events、CPU stacksdotnet-counters、traceCPU/锁/JIT 同时发生什么有限/关联视图PerfView CPU stacks、Thread Time、JITtrace、平台 profiler非托管/进程全貌部分指标需结合工具kernel/process events、stacksWindowsVMMap、WPR/WPA、平台工具dotMemory 更适合交互式浏览对象图、快照 diff、支配者和保留路径PerfView 更适合低层事件时间线、GCStats、CPU/分配栈与 ETW 关联。二者不是替代关系。先用低开销计数器确认时间窗口再选择最小必要采集。三、dotMemory快照、traffic、timeline 和对象图3.1 Heap snapshot 回答“此刻谁活着”快照通常会暂停/协调目标进程并遍历托管堆文件和开销与存活堆规模、运行时和采集模式相关。它展示类型数量、浅大小、保留关系等但不是完整时间线快照前已经分配又死亡的对象不会出现。生产上先在预生产测量暂停和磁盘需求选择低峰窗口并设置超时/回退。容器内还要确认 profiler attach 权限、诊断端口、只读文件系统和落盘空间。3.2 memory traffic 回答“期间分配和回收什么”traffic/分配跟踪用于识别短命对象和 allocation call stacks开销通常高于只看计数器且采样/插桩策略可能漏掉或估算部分事件。它适合短、可控、能复现的窗口而不是默认全天开启。若问题是“每秒大量临时字符串导致 GC”只抓事后快照可能看不到字符串traffic 或 PerfView allocation stacks 更合适。3.3 timeline 回答“何时发生”timeline 把内存、GC 和线程/事件放在时间轴上帮助将一次请求洪峰、场景切换或定时任务与堆增长联系起来。具体事件/视图取决于 attach 模式和平台。时间相关性仍不是因果需要回到对象路径和代码。3.4 retention paths“Paths to GC Roots”从目标实例向上找到 static、线程栈、handle、finalizer、delegate 等根路径。重点不是最短路径本身而是第一个本不该存在的业务边Static AppServices - Singleton EventBus - EventHandler invocation list - Scoped Handler target - RequestContext工具可能压缩/合并相似路径多个根路径同时存在时断掉一条不一定能回收。要看全部重要 retention path并验证修复后旧路径消失。3.5 dominator 与 retained size如果所有从根到对象 X 的路径都经过 D则 D 支配 X。D 的 retained size 近似表示“移除 D 后可能一并释放的对象图”适合找大缓存、会话根或文档树。支配不是业务罪责应用根对象自然支配大量合法状态共享对象可能不被某个候选单独支配retained size 也不等于进程 RSS 将立即下降。先判断生命周期是否合理再修复引用。Shallow size 只是对象本身不包含它指向的数组/字符串比较容器时同时看 backing storage 与 retained graph。四、快照 diff必须设计负载和静置阶段4.1 基线—负载—静置—复现推荐流程0. 预热 JIT、连接池、缓存和常规路径 1. Snapshot A稳定基线 2. 执行一轮固定可疑负载 3. 停止输入等待业务完成、定时清理和可接受 GC 周期 4. Snapshot B第一轮后 5. 再执行同量负载并静置 6. Snapshot C第二轮后 7. 比较 A-B 与 B-C 的 surviving/new/removed并查根路径为什么要第二轮缓存预热可能只在第一轮合理增长泄漏常随每轮近似单调累积。为什么要静置异步任务、队列和 finalizer 尚未完成时对象仍有合法用途。不应在生产强制GC.Collect只是为了“让图好看”。隔离实验可在明确目的下用强制 GC 判断可达性但必须注明它改变时序不能作为修复。4.2 diff 的常见误判类型数量上涨但请求并发也上涨需按已完成业务单位归一化。snapshot 本身、调试器或 profiler 引入对象。JIT/反射/serializer 缓存在早期轮次增长后稳定。字符串/类型元数据由运行时或框架全局缓存不能随业务立刻释放。基线和后续快照负载/GC 阶段不同。只看“New objects”没有看 survived 与 root。记录每个快照的业务计数、活动请求、队列深度、缓存 Count、线程数与采集时间点让 diff 可解释。五、PerfViewETW、EventPipe、GCStats 与 stacks5.1 Windows ETW 是主场PerfView 在 Windows 上使用 ETW 收集 CLR、kernel、process、CPU sample、allocation 等事件。Kernel stack/某些 system-wide provider 可能需要管理员权限生产服务身份、session 冲突、杀毒和 EDR 都可能影响采集。命令参数随版本变化先运行PerfView.exe /?和 Collect 对话框查看当前配置。不要复制一条未知版本命令直接在生产开启所有 provider。生产最小化策略是先只收 CLR GC/少量 process问题需要 CPU 根因再加 sampled profile需要 allocation stacks 才开启相应高开销事件限制时长、循环缓冲和文件大小。5.2 EventPipe 与跨平台.NET Core/.NET 5 有 EventPipe。跨 Linux/macOS 通常用dotnet-trace collect或诊断端口采集.nettrace再用 PerfView支持情况依版本或 Speedscope等分析。EventPipe 不是 ETW 的一比一替代provider 名、事件、kernel stacks、权限和时间戳能力不同。容器中进程 PID namespace、/tmp/diagnostic socket、ptrace/权限和 sidecar 采集需预演。禁用 diagnostics 的生产配置会阻止 attach不要为临时分析永久扩大容器权限。5.3 GCStatsGCStats 汇总 GC 次数、代、暂停、堆大小、分配率、提升和 CPU 等。它用于建立“时间花在何处”的证据而不是套固定健康阈值。实时游戏、批处理、低延迟 API和吞吐服务的预算不同Server/Workstation、容器 CPU、heap hard limit 也改变行为。关注组合模式allocation rate 高、Gen 0 频繁但存活低短命分配问题promotion/Gen 2 持续增长长寿命保留或缓存pause 与 pin/large object/heap expansion 同时出现进一步查事件和堆GC CPU 低但 RSS 高可能是合法存活、native 或 allocator 留存。不要只看平均暂停尾延迟和触发业务阶段更重要。5.4 Heap Snapshot / GC HeapPerfView 的 GC Heap/heap snapshot 可构建托管对象图和 type/roots 视图采集方式与运行时支持会影响是否触发 GC、是否完整、暂停多大。文件可能非常大。它适合与事件 trace 中的异常时间点关联但交互式对象图体验与 dotMemory 不同。dotnet-gcdump是跨平台类型/根图的另一选择通常比完整 dump 小但不包含所有进程状态。完整 dump 对 SOS 调试强大也最敏感、最占空间。5.5 CPU stacks 与 allocation stacksPerfView 的 CPU Stacks 是采样未出现不代表从未执行短函数和低频路径可能漏采百分比受采样窗口影响。分配 tick 也可能是采样/量化事件估算字节和调用次数要看事件机制。使用 Fold/Include/Exclude 聚焦进程和模块保存筛选表达式。优化前后用相同采集配置避免因采样率变化制造差异。六、符号、内联和调用栈可信度没有正确 PDB/符号栈可能只有模块地址、方法缺失或归因到调用者。生产符号服务器应按 build ID/version 匹配不把私有源码路径公开到不可信位置。NativeAOT、ReadyToRun、tiered JIT、dynamic methods 和内联都会影响栈解释。内联后分配栈可能显示调用点/内联框架而非直觉中的方法异步状态机会出现MoveNext要通过 state-machine 名与源码映射回 async 方法。闭包显示 display class事件显示 delegate target。验证符号记录进程模块版本、commit、PDB signature检查 PerfView Symbols 日志/解析状态只对已匹配符号下源码结论。符号下载需要网络时在隔离环境预缓存不让生产主机访问公共符号源。源码行可能因优化不精确。根因应以方法/对象/保留关系和可复现实验交叉证明不依赖单一行号。七、常见保留模式与证据7.1 静态集合与无界缓存// 反例线程安全不等于容量受控。 private static readonly ConcurrentDictionarystring, Result Cache new();证据Cache 或内部 Tables/Node 支配大量 ResultCount 随已完成唯一请求单调增长没有命中复用、过期或 size limit。修复要定义容量、TTL、淘汰、失败项和 value 大小不只是换容器。容量保留若有明确上界且命中收益合理就不是泄漏。验证峰值负载后静置是否回到设计水位。7.2 事件订阅长寿命 publisher 的 delegate invocation list 强引用短寿命 subscribersealed class ScreenController : IDisposable { private readonly EventBus _bus; public ScreenController(EventBus bus) { _bus bus; _bus.Changed OnChanged; } public void Dispose() _bus.Changed - OnChanged; private void OnChanged(object? sender, EventArgs e) { } }修复还要保证 Dispose 必被调用并处理重复 Dispose、异常构造和 publisher 先结束。弱事件不是免费替代会改变订阅语义和复杂度。7.3 ThreadLocal、线程静态和长寿命线程线程池/专用线程存活很久其 ThreadStatic/ThreadLocal 值会随线程保留。ThreadLocalT本身也可能追踪所有值取决于构造选项。根路径表现为 thread/static/ThreadLocal entries。使用 finally 清理、限制线程局部对象大小Dispose ThreadLocal不要把请求上下文放入永不清的线程静态。7.4 AsyncLocal 与 ExecutionContextAsyncLocal 值随 ExecutionContext 流动排队的 Task、timer callback 或长期异步链可能保留上下文对象。SuppressFlow 必须谨慎可能破坏日志/安全/文化上下文。通常先缩小 AsyncLocal value、在 scope 结束恢复旧值并找到为何 Task 长期未完成。7.5 LOH、大数组与池大数组、字符串或集合后备存储进入具体运行时的大对象策略阈值和行为按版本。频繁不同尺寸分配、pin 和长期大缓存会造成高峰/碎片。ArrayPool 降低分配却保留数组并带来敏感数据/所有权风险。快照里看到空 List 的大 Capacity 应问是否会复用是否超过设计峰值Trim 会不会紧接着扩容不是一律 Trim。7.6 pinningfixed、GCHandle pinned、互操作和某些 I/O 会固定对象。短 pin 可能合理长 pin/大量小 pin 可能限制移动和压缩。用 pinned handle/GC 事件、对象图和时间线确认不由碎片单独猜测。POH 也不会自动让内存无成本。7.7 finalizer queue对象创建快于 finalizer 消费、finalizer 阻塞或资源包装忘记 Dispose会形成可终结对象积压。SOSfinalizequeue、线程栈与事件能区分“等待终结”和普通根保留。修复 Dispose/安全句柄/终结器工作量绝不能从 finalizer 抛异常或执行无限阻塞。八、案例事件保留还是异步积压8.1 症状与竞争假设每次进入/退出功能页后PageViewModel数量上涨。假设 ASingleton event bus 未注销假设 B退出动画 Task 尚未完成并捕获 view model假设 C对象池按设计保留页面假设 Dsnapshot 时退出流程尚未静置。8.2 可复现实验预热后抓 A执行“进入—退出—等待退出 Task 和队列归零”一轮抓 B再一轮抓 C。业务日志记录活动页面数、未完成 Task、池 Count。diff 筛选 PageViewModel按创建代次/实例集看 survived。8.3 对象图判定若主要路径是Static Services - EventBus - delegate - PageViewModel且对象已退出支持 A。若路径是Task - state machine - this检查 Task 为什么未完成不能只把 state machine 字段置 null。若 Dominator 是有上限 PagePool 且数量稳定则可能是设计存活。8.4 修复与回归事件订阅改为可验证的生命周期 scope/IDisposable退出失败也 finally 注销。测试循环多轮后对象数稳定、旧订阅不再触发、重复 Dispose 安全。用弱引用只做辅助断言避免测试局部变量延长生命周期最终以快照 path 与功能测试为准。这个案例说明类型上涨只是线索retention path 才能区分事件、Task 与池。九、案例高分配率但没有存活泄漏API 的内存锯齿明显GCStats 显示分配率高但每次完整 GC 后存活集稳定。dotMemory snapshot 看不到大量临时 DTOtraffic/PerfView allocation stacks 指向 JSON 中间字符串和 LINQToList。调查应分开快照证明不是持续保留allocation stacks 找每请求临时对象CPU stacks/请求 trace 证明它影响预算。方案可能是流式 serializer、复用缓冲、减少中间 materialization但池化前先定义敏感数据清理与容量。回归比较相同请求与序列化字节记录每完成请求的分配、GC 尾暂停和峰值不使用无来源“健康阈值”。如果延迟本来满足 SLO复杂优化未必值得。十、生产采集的开销、权限与隐私10.1 分级采集L0已有监控RSS、GC heap、allocation rate、queue、request L1短时 counters/EventPipe GC 事件 L2CPU/sample allocation stacks、限定窗口 trace L3GC heap/gcdump L4完整 heap snapshot/process dump越往下通常暂停、CPU、磁盘和隐私风险越高。只有上一层无法回答问题才升级。生产变更走审批、维护窗口和终止条件先在同规模预生产演练。10.2 权限attach 常要求同用户/足够权限、诊断端口可用Windows ETW kernel event 可能要管理员容器可能需额外 capability 或共享 PID。只临时授予最小权限采集完撤销。不要以 root 长期运行应用来方便 profiler。10.3 数据隐私dump/heap 能含完整 string、byte[]、HTTP header、token、文档、密钥trace 参数、环境变量、命令行、路径和源码符号也可能泄露。采集前缩短窗口、禁用不必要 payload/provider落盘加密、限制 ACL、记录访问审计、设置短保留并安全删除。“脱敏 dump”通常不能靠搜索替换保证因为对象图、压缩数据和二进制缓冲仍含内容。优先在受控环境用合成负载复现生产数据只能在获批隔离分析区处理分析报告只导出聚合证据。10.4 稳定性保护预估最大文件与磁盘余量使用循环/大小限制避免 trace 写满系统盘。设置采集超时、CPU/延迟熔断和操作人员监控。多个 profiler 同时 attach 可能冲突或放大开销。十一、采样偏差与错误推理快照是某个时间点不能证明对象何时/哪里分配。allocation sampling 会低估小/短分配或按权重估算。CPU sampling 容易漏掉短路径wall-clock 等待与 CPU 不同。profiler 改变时序可能隐藏/放大竞态与 GC。forced GC 改变晋升和缓存行为。Debug/Development 构建、Tier 0、未预热和错误符号会扭曲栈。快照 A/B 若业务并发不同diff 不可比。retained size 大不代表对象应该释放。RSS 不降不代表托管对象仍可达。一条 gcroot 被修复后可能还有另一条保留路径。每个结论至少用两类证据交叉例如堆 path 业务生命周期测试allocation stack per-request counterGCStats timeline 请求事件。十二、可复现调查手册12.1 建立观测基线记录部署版本、运行时、GC 模式、容器限额、负载、RSS/Private Bytes、GC heap/代、allocation rate、线程/句柄、队列/缓存 Count 和 SLO。明确问题是增长、峰值、暂停还是 OOM。12.2 构造假设矩阵为静态缓存、事件、AsyncLocal、未完成 Task、pool、native、pin/finalizer 分别写“若为真应在工具里看到什么”。先选能最大区分假设的低开销采集。12.3 采集与证据保全记录准确开始/结束时间、命令、provider、权限、文件 hash、build ID 和业务事件。原始文件只进受控存储。trace 与 snapshot 时钟对齐。12.4 分析顺序counters/GCStats 判定分配、存活、暂停或 native 方向。CPU/allocation stacks 定位产生者。snapshot diff 定位 surviving 类型和实例。dominator/retention paths 找 owner。回到代码确认生命周期不变量。最小复现主动触发并消除路径。12.5 修复验收功能差分通过相同负载和静置下 B-C 不再线性增长旧对象 root path 消失allocation/暂停/峰值达到项目预算无敏感数据跨租户和无资源提前释放。保持一段 soak test避免只看一分钟。十三、命令与 SOS 的安全用法参数按安装版本帮助确认以下只是流程示例# 跨平台轻量起步查看运行时计数器。 dotnet-counters monitor --process-id PID System.Runtime # EventPipe traceprovider 与输出参数按当前 SDK 调整。 dotnet-trace collect --process-id PID --providers Microsoft-Windows-DotNETRuntime # gcdump捕获托管堆图先评估暂停和文件大小。 dotnet-gcdump collect --process-id PID --output heap.gcdump # 完整 dump 风险最高仅在受控授权环境。 dotnet-dump collect --process-id PID --output process.dmpSOS 常用方向包括dumpheap -stat看类型、dumpobj看字段、gcroot查根、finalizequeue看终结积压、eeheap -gc看 GC 段。命令名/参数因宿主和 SOS 版本变化在dotnet-dump analyze内用help核验。不要在报告粘贴对象地址当永久标识每个进程/快照地址都不同。导出类型、root path、业务 ID脱敏和截图/查询条件更可复核。十四、总结从“谁占内存”走到“谁不该保留它”dotMemory 擅长快照 diff、traffic/timeline、dominator 和 retention pathPerfView 擅长 Windows ETW/EventPipe 关联、GCStats、Heap/Allocation 和 CPU stacks。快照解释存活allocation trace 解释流量进程指标还要补足非托管与 allocator。泄漏的证据不是堆大而是已失去业务用途的对象在稳定负载/静置后持续存活并有不合理 GC Root 路径。静态缓存、事件、线程本地、AsyncLocal、LOH/pin 和 finalizer 各有不同图形与修复容量留存可能是设计不能一律 Trim。生产诊断必须控制 provider、时长、文件、权限和性能开销更要把 dump 当高敏数据。符号版本、采样偏差和 profiler effect 都写进结论边界。最终用可重复的基线—负载—静置—diff、功能回归和 soak test 验收而不是用无来源的“健康毫秒阈值”。下一篇术语表C# 与数据结构核心概念
上一篇/下一篇内容由系统自动关联
返回资讯列表 →