ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SOS 安装配置与托管堆诊断:从 OOM 到 dumpheap 实战

SOS 安装配置与托管堆诊断:从 OOM 到 dumpheap 实战 简介SourceOffsiteSOS是用于远程访问 VSS 配置库的版本管理工具在跨网段协作的配置管理场景中使用广泛。这份安装配置指南以 SOS v3.6 为例面向配置管理员、CM 工程师及需要在 Windows 环境下搭建 VSS 远程访问服务的开发人员帮助解决服务端部署、用户授权与客户端联通中的实际问题。整包仅一个 PDF 文件约 248KB体量轻巧适合随查随用。正文依次展开服务端安装与 Framework 1.1 依赖、端口规划1616 非加密端口、1617 加密端口、配置库创建与数据库别名绑定、用户添加、访问规则四字段写法优先级、用户名、IP/子网匹配、拒绝或加密控制、会话密钥生成与客户端导入、序列号替换、客户端连接验证以及项目目录创建与权限分配等环节并保留了作者实测的参数与注释可作为部署与排错时的对照手册。已有 164 人学习下载。1. 从一次托管堆 OOM 说起SOS 安装配置到底卡在哪一步线上一个 .NET 服务凌晨把容器内存吃到 limit被内核干掉日志里只剩一句 Killed。想看托管堆里谁在涨dotnet-counters 的实时曲线帮不上忙得拿一份转储文件慢慢翻。SOS 就是干这个的调试扩展它把 CLR 内部结构翻译成人能读的命令dumpheap 看对象分布、gcroot 追引用链、clrstack 看托管调用栈、pe 翻最后一次异常。而安装配置之所以值得单独写一篇是因为 SOS 不是一个能独立跑的程序它必须挂在 lldb 或 WinDbg 上工作。架构装错、版本跨了大版本、DAC 找不到任何一环出问题反馈给你的都只有一句 Failed to load data access module。下面按 Linux 与 Windows 两条线走给出可以直接复制的命令、参数取值以及把整套排错记录归类成一份可检索 PDF 的收尾做法。2. dotnet-sos 安装与调试器接入lldb、WinDbg 两条线的最小配置SOS 的部署分两层工具本体通过 dotnet tool 装扩展本体和调试器接线由dotnet-sos install完成。新手最容易漏掉的是第二层以为装完工具就能在 lldb 里敲 dumpheap结果提示命令不存在。这一章把两条线的接线方式、版本与架构对齐讲清楚后面遇到加载失败才有判断依据。2.1 dotnet-sos 安装命令与安装目录核对# 1. 装工具本体全局工具默认落在 ~/.dotnet/tools dotnet tool install --global dotnet-sos # 2. 安装 SOS 扩展本体并自动把当前机器的调试器接上 dotnet-sos install # 3. 交叉调试时指定架构取值 x86 / x64 / arm / arm64 dotnet-sos install --architecture arm64 # 4. 核对版本与落地位置 dotnet tool list --global | grep -i sos ls -l ~/.dotnet/sos--global决定工具装到用户级目录容器里如果以非 root 用户跑诊断注意这个目录的权限。dotnet-sos install做的事不止复制文件它会把与当前 runtime 匹配的 SOS 二进制释放到~/.dotnet/sosLinux 上追加 lldb 初始化项Windows 上写入用户级符号路径。--architecture默认跟随宿主架构只有当你在一台 x64 机器上分析 arm64 容器产出的转储时才需要显式指定装错架构的典型症状是插件能加载、命令能敲但读目标进程结构时直接报架构不支持。--diagnostics用于对接更早的诊断工具链日常排查用不到。2.2 lldb 侧确认 .lldbinit 里加载了 libsosplugin.so# 看 dotnet-sos install 写进去的内容 grep -n sos ~/.lldbinit正常应该能看到一行plugin load /home/user/.dotnet/sos/libsosplugin.so前面往往还有settings set target.process.stop-on-exec false。手工补上也等价比如默认 home 目录不在预期位置、或者你想固定用某个版本的 SOSsettings set target.process.stop-on-exec false plugin load /root/.dotnet/sos/libsosplugin.so接线的验证方式就是直接开一个转储看命令认不认lldb --core /dumps/core.10086 /usr/share/dotnet/dotnet (lldb) plugin load /root/.dotnet/sos/libsosplugin.so (lldb) clrstack (lldb) dumpheap -stat这里有个高频误区lldb 里的 SOS 命令不带!clrstack、dumpheap直接敲!clrstack是 WinDbg 的写法在 lldb 里只会得到 command not found。另外 lldb 的第二个参数必须是产生该转储的那个可执行文件framework-dependent 部署通常就是 dotnet 主程序路径给错会让 SOS 拿不到模块基址。2.3 WinDbg 侧.load sos.dll 与 _NT_SYMBOL_PATH:: 确认 dotnet-sos install 写入的用户级符号路径 set _NT_SYMBOL_PATH :: 手动加载 SOS 扩展 .load C:\Users\me\.dotnet\sos\sos.dll :: 当 sos.dll 与 coreclr.dll 同目录时的等价写法 .loadby sos coreclr !clrstack !dumpheap -stat -min 1000_NT_SYMBOL_PATH通常是srv*C:\symbols*https://msdl.microsoft.com/download/symbols这种形式dotnet-sos 会把%USERPROFILE%\.dotnet\sos追加进去让 WinDbg 能找到扩展。这里同样存在架构问题分析 32 位进程要装 x86 版 SOS靠.load硬指一个 x64 的 dll 会在加载阶段就失败。另外注意命令前缀WinDbg 下必须带!dumpheap -stat -min 1000的-min参数用来过滤掉单类型合计不足 1000 字节的噪声。2.4 版本与架构对齐一张表决定装哪个 SOS目标进程调试器安装方式关键注意点Linux x64 容器内 .NET 8 应用lldbdotnet-sos installSOS 版本与目标 runtime 同 band如 8.0.x 对 8.0.xLinux arm64 应用lldbx64 机器dotnet-sos install --architecture arm64必须显式指定默认装宿主架构的会失败Windows 服务WinDbgdotnet-sos install确认_NT_SYMBOL_PATH里含.dotnet\sos容器内不方便装工具dotnet-dump analyze无需装 SOSdotnet-dump 自带 SOS加载转储时自动释放跨大版本的 SOS 不是完全不能用但常见表现是命令能执行、字段却读不出来或者干脆卡在数据访问模块加载上。我一般把目标 runtime 的补丁号也一起对齐比如目标跑 8.0.6就装 8.0.x 的 dotnet-sos省掉一大半命令能敲但结果不对的来回。3. 转储采集与 SOS 命令族从 dotnet-dump collect 到 dumpheap -stat 的可复现链路装好 SOS 只是拿到了工具真正决定排查效率的是采集时机和命令选择。抓得太晚对象已经被 GC 回收抓得太早泄漏特征还没显现抓成 Full 级别几十 GB 的文件在分析机上根本打不开。这一节把三种采集入口、命令分组和一次完整的泄漏定位序列串起来。3.1 采集入口三选一在线抓、崩溃自动抓、内核转储方式触发时机关键命令或变量产物适用场景dotnet-dump collect人工发起dotnet-dump collect -p PID --type Heap.dmp进程还活着在线观察托管崩溃钩子未处理异常DOTNET_DbgEnableMiniDump1等自动转储偶发崩溃现场难复现内核 core_pattern被信号杀掉/proc/sys/kernel/core_patterncore.PIDOOM Killer 或 native 崩溃# A. 人工抓先抓托管堆级别体积可控、信息够用 dotnet tool install --global dotnet-dump dotnet-dump ps # 列出可诊断的 .NET 进程 dotnet-dump collect -p 12345 --type Heap -o /dumps/app_20240513_1015.dmp # B. 崩溃自动抓写进容器环境或 systemd 单元 export DOTNET_DbgEnableMiniDump1 export DOTNET_DbgMiniDumpType4 # 1Mini 2Heap 3Triage 4Full export DOTNET_DbgMiniDumpName/dumps/coredump.%p export DOTNET_CreateDumpDiagnostics1 # 抓取失败时打印原因 # C. 内核转储进程被 OOM Killer 干掉时唯一还能指望的现场 ulimit -c unlimited echo /dumps/core.%p /proc/sys/kernel/core_pattern cat /proc/sys/kernel/core_pattern--type是最该调的参数。Heap 含托管堆但不含全部 native 内存Mini 只有线程和少量栈Triage 在 Heap 基础上补更多诊断信息Full 才有完整地址空间和 native 栈代价是体积可能翻好几倍。DOTNET_DbgMiniDumpName支持%p进程号和%t时间戳两类占位符写死文件名在多进程场景下会互相覆盖。DOTNET_CreateDumpDiagnostics1建议默认打开它能把为什么没抓到转储的原因打进标准错误否则进程崩了、目录空空如也你没有任何线索。3.2 dotnet-dump analyze 里真正会用到的命令分组dotnet-dump analyze /dumps/app_20240513_1015.dmp clrthreads # 托管线程列表与锁持有情况 clrstack -all # 所有线程的托管栈 syncblk # 同步块看谁在等锁 dumpheap -stat # 按类型统计对象数与总大小 dumpheap -stat -min 1000 # 过滤小类型只看有分量的 dumpheap -type System.Byte[] dumpobj 00007f8c4a2b1c30 # 看对象字段值 gcroot 00007f8c4a2b1c30 # 追引用根路径 eeheap -gc # GC 堆分段与代际分布 pe # 最后一次托管异常分组命令看什么线程与死锁clrthreads、clrstack -all、syncblk谁持锁、谁在等、栈卡在哪个方法内存分布dumpheap -stat、eeheap -gc哪个类型在涨、各代占比是否失衡对象追责dumpobj、gcroot、dumparray字段实际取值、根路径、数组内容异常与栈pe、dumpstack崩溃点与调用链dumpheap -stat第一眼要看的是对象数量而不是总字节数。一个本该只有几千个实例的对象涨到几百万基本可以直接锁定再配合-min把零碎类型滤掉输出会干净很多。dotnet-dump analyze 里的命令同样不带!这一点和 lldb 一致。gcroot的输出里根通常集中在静态字段、线程栈和 Pinned 句柄三类静态字典缓存写错 key 是最常见的泄漏来源。3.3 一次托管堆泄漏的完整命令序列# 1. 间隔一段时间抓两份做差分比单点快照可靠得多 dotnet-dump collect -p 12345 --type Heap -o /dumps/a.dmp sleep 120 dotnet-dump collect -p 12345 --type Heap -o /dumps/b.dmp # 2. 非交互执行命令把统计结果落到文本方便比对 dotnet-dump analyze /dumps/a.dmp -c dumpheap -stat -min 1000 -c exit /dumps/a.txt dotnet-dump analyze /dumps/b.dmp -c dumpheap -stat -min 1000 -c exit /dumps/b.txt # 3. 抽出类型 - 对象数两时间点做差找涨得最多的类型 for f in a b; do grep -oE [0-9]\s[0-9]\s[A-Za-z0-9_.\[\]]$ /dumps/$f.txt \ | awk {print $3, $2} | sort /dumps/$f.count done join /dumps/a.count /dumps/b.count | awk {print $3-$2, $1} | sort -nr | head -20-c参数让 dotnet-dump analyze 以脚本方式跑命令最后一条必须是exit否则进程不退出、重定向文件会一直挂着。第 3 步的 awk 字段号随运行时版本略有差异先head /dumps/a.txt看一眼列排布再调别照抄。拿到增长最快的类型后回到交互模式用dumpheap -type 类型名取实例地址再gcroot逐层往上追通常两三层就能落到业务代码里的某个静态容器或未释放的订阅回调上。4. SOS 加载失败与符号缺失报错、参数与判断顺序大部分人卡住的地方不在命令本身而在 SOS 加载阶段。报错信息不区分根因全是同一句话所以需要一套固定的判断顺序先确认 SOS 本体在不在、再确认架构对不对、再看版本带不带得上、最后才处理符号和 DAC。按这个顺序走比反复重装工具省时间。4.1 四类典型报错与处理动作报错现象根因处理动作Failed to load data access module, 0x80004005SOS 与目标 CLR 版本不匹配或缺 libmscordaccore.so换同 band 的 dotnet-sos用 dotnet-symbol 下载 DAC必要时用setclrpath指定路径SOS does not support the current target architecture安装架构与转储架构不一致dotnet-sos install --architecture arch或换同架构分析机lldb 里plugin load报 cannot open shared object file路径写错、插件是别的架构、缺依赖库ls -l确认文件存在file看架构ldd看缺哪个 .sodumpheap -stat里满屏 UnknownFree 异常大符号缺失或 DAC 不匹配dotnet-symbol 补符号检查_NT_SYMBOL_PATH是否含 sos 目录判断顺序上有个细节值得强调plugin load成功不代表 SOS 和目标进程已经对上话真正的握手发生在第一条读取目标内存的命令比如clrstack执行时。所以别只看到插件加载成功就以为配置完了务必敲一条实际命令验证。4.2 用 dotnet-symbol 补齐符号与 DACdotnet tool install --global dotnet-symbol # --debugging 额外拉取 DAC、DBI、SOS 这些调试模块别只下 --symbols dotnet-symbol --debugging --output /dumps /dumps/coredump.12345 # 只下宿主相关模块离线机上避免拉一堆无关符号 dotnet-symbol --symbols --host-only --output /dumps/symbols /dumps/ ls /dumps/*.so /dumps/*.pdb 2/dev/null | head--symbols下载的是模块 PDB解决的是栈里只有地址没有函数名的问题--debugging下载的是 DAC、DBI、SOS 这些调试模块本体少了它lldb 会一直提示数据访问模块加载失败。这两个参数经常被混用实际排查里优先加--debugging。--output在离线环境很有用把符号和转储放到同一个目录SOS 会在转储同级目录查找配套模块路径对了就少一次手工setclrpath。内网机器无法直连符号源时提前在能出网的机器上跑一遍把产物一起拷过去。4.3 容器与权限ptrace、core_pattern 与只读根文件系统# 抓取容器内进程需要 ptrace 能力否则 attach 阶段就报权限错误 docker run --cap-addSYS_PTRACE --pidhost image # 宿主的 ptrace 限制1 表示只能跟踪自己的子进程 cat /proc/sys/kernel/yama/ptrace_scope sysctl -w kernel.yama.ptrace_scope0 # core_pattern 必须指向可写目录且 ulimit 放开 ulimit -c unlimited echo /dumps/core.%p /proc/sys/kernel/core_pattern注意kernel.yama.ptrace_scope调成 0 只是临时放开重启后失效需要通过系统配置持久化别只在排查时敲一次就不管。Kubernetes 环境下更常见的做法是用临时容器共享目标 Pod 的 pid namespace同时给 Pod 挂一个可写的转储目录——只读根文件系统下dotnet-sos install会因为无法写入~/.dotnet/sos直接失败这时候把 HOME 指到 emptyDir 或挂载卷或者干脆改用dotnet-dump analyze自带 SOS不落盘到用户目录是两条更省事的路。5. 把 SOS 排错记录归类导出成可检索 PDF字体嵌入与文本层校验排查记录散落在聊天记录和终端输出里下一个人遇到同样报错还得重来一遍。把每份记录整理成 Markdown再转成带书签、能全文检索的 PDF归档成本才真正降下来。转换链路里最容易踩的坑是中文乱码和文本层丢失前者是字体没嵌后者是排版时做了字体转曲——一旦转曲pdftotext抽不出文字关键词检索和后续用 PDF 编辑器二次编辑全部失效。归档型文档坚决不转曲只做嵌入。# 1. Markdown 手册转 PDF用 xelatex 引擎才能正确嵌入中文字体 pandoc sos-guide.md -o sos-guide.pdf \ --pdf-enginexelatex \ -V CJKmainfontNoto Sans CJK SC \ --toc --toc-depth3 \ -V geometry:margin2cm # 2. 校验字体是否以嵌入形式存在emb 列必须是 yes pdffonts sos-guide.pdf | head # 3. 校验文本层能否抽取这决定文档能不能被检索 pdftotext -f 1 -l 3 sos-guide.pdf - | head -20如果内容源是内网 Wiki 页面用无头浏览器直接打印成 PDF 更省事相当于一次网页到 PDF 的虚拟打印但要在打印参数里勾选输出背景图形否则表头底色和代码块高亮全丢。中文字体这一项在高版本容器镜像里经常缺pdffonts列出字体名为空或者 emb 列为 no就是没嵌进去换一个装了 CJK 字体的镜像重新生成即可。归类这一步用 pypdf 做把章节起止页登记成书签顺便写死元数据方便用文件名和标题字段双向检索from pypdf import PdfReader, PdfWriter reader PdfReader(sos-guide.pdf) writer PdfWriter() for page in reader.pages: writer.add_page(page) # 书签页码从 0 开始计数改章节顺序时记得同步改 writer.add_outline_item(SOS 安装与 lldb 配置, 1) writer.add_outline_item(转储采集与命令族, 4) writer.add_outline_item(加载失败排查, 9) writer.add_metadata({ /Title: SOS 安装配置指南-归类, /Keywords: SOS,dotnet-dump,dumpheap,gcroot, }) writer.write(SOS-安装配置指南-归类.pdf)归档命名建议带上分类与日期例如SOS-内存泄漏-20240513-v1.pdf分类名直接取自 dumpheap 报告里增长最快的那个类型比按日期堆在一起好找得多。最后一步是把抽取结果文本化让几十份手册变成可以被 grep 的语料# 批量抽取文本层再做关键词检索 for f in /docs/*.pdf; do pdftotext $f ${f%.pdf}.txt; done grep -l Failed to load data access module /docs/*.txt一条命令就能筛出所有踩过同一个 SOS 加载错误的案例再打开对应 PDF书签直接跳到排查步骤那一页。本文还有配套的精品资源点击获取
返回列表