Ubuntu 환경에서 .NET Core의 profiling 및 postmortem debugging

Ubuntu(16.04) 환경에서 .NET Core 애플리케이션에서 대한 CPU Profiling은 기존의 Windows 환경에서 사용했던 PerfView라는 툴을 이용할 수 있다.

CPU profiling을 위해서 우선 perfcollect 라는 툴을 Ubuntu 환경에 설치해야 한다.

 curl -OL https://aka.ms/perfcollect
sudo chmod +x perfcollect
sudo ./perfcollect install

설치가 완료 되면, 다음과 같은 순서로 CPU sampling을 할 수 있다.

애플리케이션을 수행할 terminal 창에서 아래를 수행한다.

 export COMPlus_PerfMapEnabled=1
export COMPlus_EnableEventLog=1

이후에 perfcollect를 수행할 terminal 창을 하나 더 연후에 아래를 수행하여 CPU sampling을 시작한다.

 sudo ./perfcollect collect hicputrace

수행과 더불어 sampling은 시작된다.

이후에는 애플리케이션 수행창에서 애플리케이션을 수행한다. 그리고 일정 시간이 흐른 후에 perfcollect가 호출되었던 창에서 ctrl+c를 눌러 중지하면, 현 directory에 hicputrace.trace.zip 파일이 생성된다. 해당 trace 파일은 windows 환경에서 perfview (https://aka.ms/perfview) 툴을 이용하여 볼 수 있다.

Perfview.exe 가 위치한 폴더에 Ubuntu에서 수집된 trace 파일을 위치하고 perfview를 실행하면 아래와 같은 화면을 볼 수 있다.

이후에 해당 zip파일을 click 하면, 아래와 같이 profile 정보를 볼 수 있다.

특히, 보고자 하는 것이 .NET core 애플리케이션의 CPU 점유 call stack을 확인하는 것이므로, CallTree 메뉴에서 dotnet process를 check 하고 tree를 확장하면, CPU를 점유하고 있는 call stack을 볼 수 있다.

아쉽게도 PerfCollect 툴은 현재 Memory Profiling은 제공하지 않는 다. 만일, Memory Profiling과 같은 정보를 추출하려면 core dump를 통해서 .NET managed memory사용량을 확인할 수 있다. 그 방법에 대해서는 다음과 같다.

먼저, Core dump를 수집하는 방법은 여러가지가 있으나, dump의 size를 고려해 볼 때, 아래의 방법을 생각해 볼 수 있다. 우선 애플리케이션의 수행에 앞서서 아래의 명령을 수행하여 충분한 size의 덤프를 생성할 수 있도록 한다.

 ulimit -c Unlimited

그리고, 애플리케이션을 수행하여 메모리가 충분히 누수가 되는 시점에 새로운 terminal 창을 오픈하여 아래와 같이 애플리케이션을 중지시키면 해당 시점에 덤프가 떨어진다.

 sudo kill -4 <pid>

덤프는 애플리케이션 수행시점의 현재폴더 혹은 /var/crash 폴더에서 확인할 수 있다.
core덤프를 수집하는 일반적인 방법은 다음과 같다. Gdb를 이용하는 방법이다.

 sudo apt-get install gdb
sudo gdb

이후에 ps -efH 를 이용하여 pid값을 얻은 후에 gdb를 attach 한다.

 attach <pid>

Gdb가 attach 된 이후에 적절한 시점에 generate-core-file 명령을 통해서 core file을 얻을 수 있다.

 generate-core-file <core file path>

이후 덤프분석을 위해 해당 덤프를 lldb 디버거를 통해서 오픈 할 수 있다. Gdb에서 사용할 수 있는 .NET Core plugin이 존재하지 않기 때문에 lldb 디버거를 사용해야 한다.

아래는 “core” 라는 이름의 core file을 lldb 디버거에서 오픈한다.

그리고, libsosplugin.so 파일을 load 한다.

 plugin load /usr/share/dotnet/shared/Microsoft.NETCore.App/2.0.0/libsosplugin.so
setclrpath /usr/share/dotnet/shared/Microsoft.NETCore.App/2.0.0

먼저, Libsosplugin.so 에서 제공하는 “eeheap –gc” 명령을 통해서 해당 프로세스가 사용하고 있는 전체 managed memory의 size를 알 수 있다.

“dumpheap –stat” 명령은 해당 managed memory 영역내에서 개별적으로 메모리를 사용하는 오브젝트들의 정보를 알 수 있다.

메모리 누수에 대해서 검토한다면, count와 TotalSize가 큰 오브젝트들이 사실은 관심대상이다. 아래를 보면, System.Byte[]가 그 중 많은 메모리를 점유하고 있다.

System.Byte[]의 MethodTable의 정보는 00007f8138ba1210 이며, 10565개의 동일한 타입의 오브젝트가 존재하는 것을 알 수 있다.  “dumpheap -mt” 명령어는 System.Byte[]타입의 오브젝트들을 나열해준다.

 dumpheap –mt 00007f8138ba1210

상위의 명령을 수행하면, <MT> <Address> <Size> 의 배열로 나열된 정보를 확인할 수 있다. 그리고, “dumpobj” 명령은 “dumpheap -mt” 명령의 결과값에 존재하는 address 값을 parameter로 사용하여 개별적인 오브젝트의 정보를 출력한다.

사실 중요한 것은 해당 오브젝트를 사용하는 코드이다. 그 정보를 확인할 수 있는 명령이 gcroot 인데, 아래와 같이 덤프에서는 정상적으로 출력이 되지 않는다.

하지만, lldb(3.6)를 문제가 발생하고 있는 애플리케이션에 직접 붙여서 확인한다면, 아래와 같은 정보를 확인할 수 있다.

memleakdemo.Program.ManagedLeak 메소드 안에 존재하는 ArrayList가 System.Byte[]를 참조하고 있다. 그러므로, ArrayList의 Size 가 얼마나 큰지, 그리고, 언제 Byte[]를 release 하는 지 등을 검토함으로써 Memory 문제여부를 isolation할 수 있을 것 같다.

 // memleakdemo.Program.ManagedLeak (System.Object)
private static void ManagedLeak(object s)
{
    State state = (State)s;
    ArrayList list = new ArrayList();

    for (int i = 0; i < state._iterations; i++)
    {
        if (i % 100 == 0)
            Console.WriteLine(string.Format("Allocated: {0}", state._size));

            System.Threading.Thread.Sleep(10);
            list.Add(new byte[state._size]);
    }
}

그러기 위해서는 결국 얼마나 많은 오브젝트를 살펴보느냐가 필요한데, 이것보다는 이러한 부분을 직관적으로 살펴볼 수 있는 memory profiler가 제공되면 좋을 듯 하다.