• 使用 SOS 对 Linux 中运行的 .NET Core 进行问题诊断(实践篇)(转发)


    原文:

    https://www.cnblogs.com/blurhkh/p/14225222.html

    目录

    说明#

    • 本文主要描述 Linux 环境下 .NET Core 程序的问题分析方案,也会提及如何将 Linux 系统中保存好的 core dump 文件转移到其他位置进行分析,Mac 环境中未尝试成功,Windows 中推荐使用 WSL。
    • 将依次讲解如何在 .NET Core 2.x、.NET Core 3.x、.NET 5.x 中使用 SOS 命令。.NET Core 3.x 与 .NET 5.x 一致,以.NET 3.x 为例。
    • .NET Core 2.x 的例子中使用 plugin load /usr/share/dotnet/shared/Microsoft.NETCore.App/运行时版本号/libsosplugin.so 的方式加载 SOS 插件。.NET Core 3.x 开始提供了 dotnet-sos 来实现自动加载。且可以直接在.NET Core 2.x 环境中用 dotnet tool 安装到,后面也会讲到具体的操作。
    • 进行 dump 的 Linux 环境必须开启 SYS_PTRACE。

    准备一个方便的学习环境#

    为了方便我们的学习,我们可以准备一下 Linux 的开发测试环境,借助 VS Code 的 Remote Containers 功能可以很方便的构建出纯净的 Linux 测试环境。这里需要确保 Docker 运行正常。
    如果不需要通过此方式构建环境,可以直接跳到下一节。

    • 安装 VS Code 插件 Remote - Containers
    • 创建一个文件夹并用 VS Code 打开,在该文件夹下创建下列文件结构
    Copy
    工作目录
    └── .devcontainer
        ├── devcontainer.json
        ├── docker-compose.yml
        └── Dockerfile
    

    2.x 配置内容#

    devcontainer.json

    Copy
    {
    	"name": ".NET Core 2.x",
    	"dockerComposeFile": "docker-compose.yml",
    	"service": "dotnet-core-2.x", // 名字要和 docker-compose.yml 中定义的 service 名字一致
    	"workspaceFolder": "/workspace",
    	"settings": { 
    		"terminal.integrated.shell.linux": "/bin/bash"
    	},
    	"extensions": ["ms-dotnettools.csharp"] // 安装容器中 VS Code Server 的 C# 插件
    }
    

    docker-compose.yml

    Copy
    version: '3'
    services:
     dotnet-core-2.x:
       build:
         context: .
         dockerfile: Dockerfile
       
       volumes:
         # 把 VS Code 的工作目录挂载到容器的 workspace 目录下
         - .:/workspace:cached
    
       # 需要开启 SYS_PTRACE 的配置
       cap_add:
         - SYS_PTRACE
    
       # 避免容器主进程执行结束而退出
       command: /bin/sh -c "while sleep 1000; do :; done"
    

    Dockerfile

    Copy
    FROM microsoft/dotnet:2.1.300-sdk
    # 直接写入阿里源,方便 lldb 等工具的下载
    RUN echo "deb http://mirrors.aliyun.com/debian/ stretch main non-free contrib
    
    deb-src http://mirrors.aliyun.com/debian/ stretch main non-free contrib
    
    deb http://mirrors.aliyun.com/debian-security stretch/updates main
    
    deb-src http://mirrors.aliyun.com/debian-security stretch/updates main
    
    deb http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib
    
    deb-src http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib
    
    deb http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib
    
    deb-src http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib"
    > /etc/apt/sources.list
    # 安装在镜像内,避免下次用的时候重复安装
    RUN apt update && apt install -y lldb-3.9
    

    3.x 配置内容#

    devcontainer.json

    Copy
    {
    	"name": ".NET Core 3.x",
    	"dockerComposeFile": "docker-compose.yml",
    	"service": "dotnet-core-3.x",
    	"workspaceFolder": "/workspace",
    	"settings": { 
    		"terminal.integrated.shell.linux": "/bin/bash"
    	},
    	"extensions": ["ms-dotnettools.csharp"]
    }
    

    docker-compose.yml

    Copy
    version: '3'
    services:
     dotnet-core-3.x:
       build:
         context: .
         dockerfile: Dockerfile
       
       volumes:
         # 把 VS Code 的工作目录挂载到容器的 workspace 目录下
         - .:/workspace:cached
    
       # 后面需要使用 基于 ptrace 的 lldb,这里需要开启 SYS_PTRACE 的配置
       cap_add:
         - SYS_PTRACE
    
       # 避免容器主进程执行结束而退出
       command: /bin/sh -c "while sleep 1000; do :; done"
    

    Dockerfile

    Copy
    FROM mcr.microsoft.com/dotnet/sdk:3.1
    # 把所有后面可能会用到工具都提前装好
    RUN dotnet tool install --global dotnet-counters &&
    dotnet tool install -g dotnet-dump &&
    dotnet tool install -g dotnet-gcdump &&
    dotnet tool install --global dotnet-trace &&
    dotnet tool install -g dotnet-symbol &&
    dotnet tool install -g dotnet-sos
    # 将上述工具所在的文件夹添加到 PATH
    ENV PATH /root/.dotnet/tools:$PATH
    # 替换成阿里源
    RUN echo "deb http://mirrors.aliyun.com/debian/ buster main non-free contrib
    
    deb-src http://mirrors.aliyun.com/debian/ buster main non-free contrib
    
    deb http://mirrors.aliyun.com/debian-security buster/updates main
    
    deb-src http://mirrors.aliyun.com/debian-security buster/updates main
    
    deb http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib
    
    deb-src http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib
    
    deb http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib
    
    deb-src http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib"
    > /etc/apt/sources.list
    

    在完成了 Remote - Containers 插件的安装 并完成了上述三个文件的配置之后。
    直接通过 VS Code 左下角的按钮在自动构建的容器中打开工作目录。

    完成之后,我们就拥有了一个自由玩耍的空间了。
    可以直接在里面写代码或者把写好的代码拖到 VS Code 工作目录中。

    工具介绍#

    lldb sos plugin#

    lldb 是一个软件调试器,支持 C/C++ 的调试和 Linux core dump 文件的分析。
    微软提供了 lldb 的 SOS(Son of Strike) 插件,可以通过这个插件获取运行时的线程,托管堆中的对象等信息。
    官方推荐使用的 lldb 版本是 3.9 到 9。实测 3.8 版本有问题,会无法查看 thread 的 stack 信息。
    Ubuntu/Debian安装方式 apt install lldb-3.9

    .NET Core 2.x 版本中,SOS 插件直接在 .NET Core 的安装目录中可以找到,不强依赖 sdk,runtime 中也是自带的。

    Copy
    /usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.0/libsosplugin.so
    

    其中 2.1.0 是版本号,需根据实际的 dotnet runtime 版本号(通过 dotnet --info 获取信息)进行替换。
    一共有两种使用方式:

    1. attach 到进程上进行调试#

    Copy
    lldb-3.9 dotnet -p 进程号 -o "plugin load /usr/share/dotnet/shared/Microsoft.NETCore.App/运行时版本号/libsosplugin.so"
    

    注意:这种方式会停掉进程。如果是线上服务,使用请慎重,最好先下掉流量。


    等效 lldb-3.9 dotnet -p 进程号 再在lldb cli内执行 plugin load 插件路径

    2. 分析core dump文件#

    首先我们需要得到 dotnet 程序的 core dump 文件。创建 dump 文件的方式有很多。最简单可以使用 dotnet runtime 中自带的 createdump 工具。

    Copy
    /usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.0/createdump 进程id
    

    创建 dump 的同时,进程会短暂暂停,完成 dump 后恢复运行。文件的大小取决于应用所占内存的大小。这样我们就可以得到了 coredump 文件。


    针对线上环境,有条件的同学可以直接在线上环境内分析,如果你的容器配置不是很高,是在一个短暂存活的 k8s pod中,建议下到本地进行分析,如果文件过大,传输过程中建议先压缩。
    加载 dump 文件的方式如如下:
    lldb-3.9 dotnet -c dump文件路径 -o "plugin load 插件路径"

    SOS#

    无论使用上面哪种方式,接下来操作都是一样的,使用 lldb 的命令以及 sos 的扩展
    soshelp 可以看到所有支持的 sos 命令。点击跳转官方文档

    soshelp <functionname> 可以看到每种命令具体的使用方式

    使用 sos 完整命令名字 或者直接使用 别名


    案例分析#

    无论是采取的 attach 到进程的方式,还是分析 core dump 文件的方式。最后都会进入一样 lldb cli 界面。接下来伴随实际的案例介绍一个新朋友,sos 指令。

    CPU 占用过高#

    测试代码

    Copy
    [Route("api/[controller]")]
    public class TestController : ControllerBase
    {
        [HttpGet("highcpu/{milliseconds}")]
        public string HighCpu(int milliseconds)
        {
            var sw = Stopwatch.StartNew();
            while (true)
            {
                sw.Stop();
                if (sw.ElapsedMilliseconds > milliseconds) break;
                sw.Start();
            }
            return "success:highcpu";
        }
    }
    

    使用 ps 进行线上问题可能性排查。

    注意:这一步是一定要做的,否则后面没办法定位具有问题的线程。

    Copy
    ps [options] [--help]
    

    options:

    • a 显示现行终端机下的所有程序,包括其他用户的程序。
    • u 以用户为主的格式来显示程序状况。
    • x 显示所有程序,不以终端机来区分。
    • -T 以线程维度展示。

    精简版镜像可能没有 ps 工具。可自行安装,如 apt install procps

    实际进程可能比较多,可以加上 grep dotnet 进行过滤

    其中 ps aux -T | head -n1 是为了保留标题行
    关键列说明:

    • PID: 进程ID
    • SPID: 线程ID
    • %CPU: CPU使用率
    • TIME: 运行时间

    可以看到,我们的应用进程ID是 1069,问题线程ID为 1709,102%CPU。
    利用上文所述的两种方式之一进入 lldb cli。

    如果使用 createdump 的方式,一定要加上 -u 进行全量的dump,否则线程栈信息不全,影响问题分析。

    Copy
    /usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.0/createdump -u 1069
    


    1. clrthreads 指令查看概览托管线程的信息。


    2. thread select 线程编号 选中线程,或者使用简化指令 t 线程编号
    我们需要注意上图圈红的那两列,其中 OSID 是用 16进制 表示的操作系统的线程编号,1709(10进制)等于 6ad(16进制)。需要通过一次换算来在 clrthreads 的结果中匹配 ps 找到的线程。
    thread select 后面跟的参数是第一列,而非 ID 那一列。
    6ad 对应的第一列线程编号 21,所以执行 thread select 21 或者 t 21

    3. clrstack 查看选中线程的调用栈 栈帧确定线程执行的内容。

      • Child SP: Thread Stack Poiner
      • IP Call Site: Instruction Pointer Call Site
        从而,可以定位到问题代码

    内存泄漏#

    使用 attach 或者 core dump 方式进行分析。createdump 也需要全量。
    排查内存问题,我们需要用到 dumpheap 指令。

    Copy
    dumpheap [options] 
    

    常用 options:

    • -stat –只输出堆上所有类型对象的统计摘要,它们的数量和它们自身的大小(不含引用)。
    • -min 最小大小,单位 byte。
    • -max 最大大小,单位 byte。
    • -mt 根据 MethodTable 地址过滤对象。
    • -type 类型名和给定的字符串部分匹配的类型的所有实例对象。

    MethodTable 是 CLR 中维护类型方法信息等原数据的重要数据结构。和类型是一一对应的关系。

    测试代码

    Copy
    [Route("api/[controller]")]
    public class TestController : ControllerBase
    {
        private static ConcurrentBag<MemoryLeak> _cache = new ConcurrentBag<MemoryLeak>();
    
        [HttpGet("memoryleak/{count}")]
        public string MemoryLeak(int count)
        {
            for (int i = 0; i < count; i++)
            {
                _cache.Add(new MemoryLeak());
            }
            return "success:memoryleak";
        }
    }
    
    public class MemoryLeak
    {
        private byte[] _data;
        public MemoryLeak()
        {
            _data = new byte[1024];
        }
    }
    

    1. 分析什么类型的对象占的内存最大

    -stat 是为了只看摘要信息。
    占内存最大的是 MemoryLeak[] 类型的实例。
    如果你能够根据该类型定位到是哪块代码出了问题,那我们的故事就到此结束了。不是的话就要注意到这个线索 MemoryLeak 的 MethodTable 地址为 00007fb64b1e4488
    2. dumpheap -mt <address> 根据 MethodTable 找到有问题的对象的地址。取其中一个对象的地址。如 00007fb5d8042c68。

    3. gcroot <address> 找到可能存在内存泄漏的根

    如果能从上面的引用链上能找到能定位问题的地方,那故事也到此结束。如我们可以看到内存泄漏是发生在一个 Concurrent.ConcurrentBag<Test2.x.Controllers.MemoryLeak> 类型的容器上的。
    4. 寻找静态字段所在的类型(暂未解决)
    pinned handle 表示这是一个静态字段。那么怎么去定位这个静态字段所在的类呢。本人能力有限,仅找到了一篇 windbg 的老文章,暂时不知道 lldb 中如何操作。
    https://dzone.com/articles/pinpointing-static-gc-root-sos

    Monitor导致的死锁#

    测试代码

    Copy
    class Program
    {
        private static readonly object LockObj1 = new object();
        private static readonly object LockObj2 = new object();
    
        static void Main(string[] args)
        {
            Method1();
            Method2();
            Console.ReadLine();
        }
    
        static void Method1()
        {
            Task.Run(() =>
            {
                lock (LockObj1)
                {
                    Thread.Sleep(1000);
                    lock (LockObj2)
                    {
                        Console.WriteLine("Hello World Method1");
                    }
                }
            });
        }
        
        static void Method2()
        {
            Task.Run(() =>
            {
                lock (LockObj2)
                {
                    Thread.Sleep(1000);
                    lock (LockObj1)
                    {
                        Console.WriteLine("Hello World Method2");
                    }
                }
            });
        }
    }
    

    执行这段代码后没有任何结果输出。

    1. 利用 clrthreads,Lock Count 1 表示正在等待一个 Monitor 锁。这个数字也就是线程持有的同步块数量。除去第一个是 Console.ReadLine() 中的锁。另外两个标识着 Threadpool Worker 的的线程就是上面代码死锁的两个线程。

    2. 选中线程,用 clrstack 查看当前线程执行的内容从而定位到问题。

    .NET Core 3.x 的不同点#

    3.x 开始提供了一套诊断工具

    • dotnet-sos
      使用 lldb 时会自动加载 sos 插件, createdump 在 3.1 下依旧存在
    • dotnet-dump
      在不涉及本机调试的情况下收集和分析托管代码相关的 dump,可以运行在 lldb 无法正常运行的平台
    • dotnet-gcdump
      基于 EventPipe 收集实时 .NET 进程的 GC 信息
    • dotnet-counters
      基于 EventCounter API 发布的 Metrics 快速定位问题的临时性监控工具
    • dotnet-trace
      基于 EventPipe 收集 正在运行的进程中收集信息
    • dotnet-symbol
      在其他地方分析 dump 文件时,需要下载对应的 symbol 文件

    本文只介绍和 SOS 相关的部分。

    dotnet-sos#

    dotnet 安装目录中不再包含 libsosplugin.so。取而代之的是 dotnet-sos。
    安装完毕后,每次使用lldb都会自动加载sos 插件。
    也可以用于 .NET Core 2.x。
    安装方式

    Copy
    dotnet tool install –g dotnet-sos
    dotnet-sos install
    

    如果 dotnet-sos 安装目录的环境变量没有设置成功,需要到对应目录下进行安装
    用户home目录/.dotnet/tools/dotnet-sos install

    在新的sos插件中也增加了一些新的 sos 命令,例如 syncblk。

    分析之前的那个 Monitor 死锁的 core dump,得到持有同步块的线程 id

    dotnet-dump#

    dotnet-dump 的出现并不是为了完全取代上面一直在用的 lldb,它只能查看托管代码相关的信息。
    且不能用 .NET Core 2.x。

    Copy
    dotnet-dump ps
    

    查看 dotnet-dump 能够进行分析的进程

    Copy
    dotnet-dump collect [-p] [--type] [-o]
    
    • -p 进程ID

    • --type <Full|Heap|Mini> 指定转储类型,它确定从进程收集的信息的类型。 有三种类型:
      Full - 最大的转储,包含所有内存(包括模块映像)。
      Heap - 大型且相对全面的转储,其中包含模块列表、线程列表、所有堆栈、异常信息、句柄信息和除映射图像以外的所有内存。
      Mini - 小型转储,其中包含模块列表、线程列表、异常信息和所有堆栈。
      如果未指定,则 Full 为默认类型。

    • -o dump 保存路径,如果没有指定,默认当前路径

    Copy
    dotnet-dump analyze <dump_path>
    

    进入之后,一样可以用到之前提到的那些 sos 命令

    因为没有 lldb 的 thread select <tid> 命令可以切换线程,需要使用 setthread

    如何将 createdump 创建的 coredump 文件转移到其他位置分析#

    上面分析 coredump 文件的例子都是直接在现场分析的。但实际情况中,我们可能会选择将文件从服务器中保存下来,放到其他位置去分析,建议使用 Linux 环境或者 Windows WSL。

    1. 环境准备

    • 安装好dotnet,最好与分析的应用程序一致
    • 安装 lldb,3.9 到 9 版本
    • dotnet tool install –g dotnet-sos && dotnet-sos install 实现 sos 插件的自动加载
    • dotnet tool install -g dotnet-symbol 下载分析 coredump 所需的模块和符号
    • 应用的pdb文件

    2. 将应用的pdb文件放到和线上运行环境一样的目录下。若线上部署目录是/app,则也需要在当前机器的/app下放置相同的文件。若缺少此步骤,在使用clrstack 时,将看到不代码行号。如下图所示。

    3. lldb-3.9 dotnet -c <coredump path> 加载 dump 文件。即可开始分析。

    4. 如果在上一步执行 sos 失败,则需要先在 coredump 所在的文件夹执行 dotnet-symbol --host-only --debugging <dump file path> 下载需要的文件。

    如何将 dotnet-dump 创建的 coredump 文件转移到其他位置分析#

    1. 环境准备

    • 安装好dotnet,最好与分析的应用程序一致
    • dotnet tool install –g dotnet-dump
    • dotnet tool install -g dotnet-symbol 下载分析 coredump 所需的模块和符号
    • 应用的pdb文件

    2. 将应用的pdb文件放到和线上运行环境一样的目录下。

    3. dotnet-dump analyze <dump_path> 加载 dump 文件。即可开始分析。

    4. 如果在上一步执行 sos 失败,则需要先在 coredump 所在的文件夹执行 dotnet-symbol --host-only --debugging <dump file path> 下载需要的文件。

    作者: 黑洞视界

    出处:https://www.cnblogs.com/blurhkh/p/14225222.html

    版权:本文采用「署名-非商业性使用-相同方式共享 4.0 国际」知识共享许可协议进行许可。

  • 相关阅读:
    记第一场省选
    POJ 2083 Fractal 分形
    CodeForces 605A Sorting Railway Cars 思维
    FZU 1896 神奇的魔法数 dp
    FZU 1893 内存管理 模拟
    FZU 1894 志愿者选拔 单调队列
    FZU 1920 Left Mouse Button 简单搜索
    FZU 2086 餐厅点餐
    poj 2299 Ultra-QuickSort 逆序对模版题
    COMP9313 week4a MapReduce
  • 原文地址:https://www.cnblogs.com/panpanwelcome/p/14283807.html
Copyright © 2020-2023  润新知