.NET Core中遇到奇怪的線程死鎖問題:內存與線程數不停地增加 EnyimMemcached 死鎖問題 嘗試解決.NET Core Framework中Dns.GetHostAddressesAs

一個 asp.net core 站點,以前運行在Linux 服務器上,運行一段時間後有時站點會掛掉,在日誌中記錄不少「EMFILE too many open files」的錯誤:html

Microsoft.AspNetCore.Server.Kestrel.Internal.Networking.UvException: Error -24 EMFILE too many open files

後來將這個 asp.net 站點部署到 Windows 服務器的 IIS 上。運行一段時間後,發現其中一臺服務器出現503錯誤:git

HTTP Error 503.2 - Service Unavailable
The serverRuntime@appConcurrentRequestLimit setting is being exceeded.

登上服務器一看,該站點的進程佔用的內存居然有1.2G,而同一負載均衡中另一臺正常的服務器內存佔用只有40多M。而後看了一下進程中的線程數,驚呆了——居然有8000多個線程!而另一臺正常的服務器只有20多個線程。github

將這臺服務器從負載均衡上摘下來以後,出現了更加讓人驚呆的現象——在沒有請求的狀況下,這個 asp.net core 站點進程的內存佔用與線程數一直在增加。就像在代碼中寫了一個死循環,在循環中不停地建立線程。緩存

再後來內存增加到1.8G左右,線程數增加到1.3萬左右,並且還在持續增加。服務器

不只內存與線程數在增加,並且CPU也一直在波動,這但是在沒有任何請求的狀況下,誰在偷偷地幹活?併發

強制結束進程後恢復正常,但運行一段時間(一般是1天時間)後又會出現一樣的問題。很是奇怪!app

從目前分析的狀況看,罪魁禍首多是 EnyimMemcachedCore (支持.net core的memcached客戶端,是咱們從 EnyimMemcached 移植過來的),EnyimMemcachedCore 用到了 Socket 池,問題可能出在 Socket 池部分,源代碼在 github 上(EnyimMemcachedCore源代碼)。負載均衡

windbg分析進程dump文件顯示的線程狀況:asp.net

0:000> .load C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.0.1\sos.dll
0:000> !threads
ThreadCount:      8014
UnstartedThread:  0
BackgroundThread: 8013
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no

發現大量線程中存在 coreclr!Thread::DoAppropriateWaitWorker 這個操做:異步

!uniqstack
 # Child-SP          RetAddr           Call Site
00 00000056`ed5ad118 00007ffa`080e13ed ntdll!NtWaitForMultipleObjects+0xa
01 00000056`ed5ad120 00007ff9`f1dc885e KERNELBASE!WaitForMultipleObjectsEx+0xed
02 00000056`ed5ad400 00007ff9`f1dc8a0d coreclr!Thread::DoAppropriateWaitWorker+0xfe
03 00000056`ed5ad4b0 00007ff9`f1dca52f coreclr!Thread::DoAppropriateWait+0x7d
04 00000056`ed5ad530 00007ff9`f1e3b726 coreclr!CLREventBase::WaitEx+0x7f
05 00000056`ed5ad580 00007ff9`f1e3b636 coreclr!AwareLock::EnterEpilogHelper+0xca
06 00000056`ed5ad640 00007ff9`f1f92b18 coreclr!AwareLock::EnterEpilog+0x62
07 00000056`ed5ad6a0 00007ff9`f1f92131 coreclr!AwareLock::Contention+0x258
08 00000056`ed5ad760 00007ff9`92388e2b coreclr!JITutil_MonContention+0xb1

該問題還在進一步排查中。。。

[12月3日13:00更新]

今天排查後懷疑是 EnyimMemcached 中下面的代碼引發的:

private void ConnectWithTimeout(Socket socket, EndPoint endpoint, int timeout)
{       
    var completed = new AutoResetEvent(false);
    var args = new SocketAsyncEventArgs();
    args.RemoteEndPoint = endpoint;
    args.Completed += OnConnectCompleted;
    args.UserToken = completed;
    socket.ConnectAsync(args);
    if (!completed.WaitOne(timeout) || !socket.Connected)
    {
        using (socket)
        {
            throw new TimeoutException("Could not connect to " + endpoint);
        }
    } 
}

private void OnConnectCompleted(object sender, SocketAsyncEventArgs args)
{
    EventWaitHandle handle = (EventWaitHandle)args.UserToken;
    handle.Set();
}

已修改代碼以定位是否是上面的代碼引發的,要等待下次deadlock的發生。

[12月4日8:50更新]

終於能夠重現這個問題,在有負載的狀況下強制結束進程,詳見錄屏

[12月4日12:20更新]

終於定位到了引發問題的代碼:

Task<IPAddress[]> task = System.Net.Dns.GetHostAddressesAsync(host);
task.Wait(5000);
var addresses = task.Result;

這是上次解決 EnyimMemcached 死鎖問題 時埋下的坑,死鎖發生在有併發請求時進行主機名的解析,在強制結束進程時重現是由於dns解析緩存失效。

改成下面的代碼可解決死鎖問題:

Task<IPAddress[]> task = System.Net.Dns.GetHostAddressesAsync(host);
if (task.Wait(5000))
{
    var addresses = task.Result;
}

雖然死鎖問題解決了,但在併發請求下task.Wait(5000)返回false,沒法成功解析主機名。

問題的根源是在構造函數中用(且只能用)同步方式調用System.Net.Dns.GetHostAddressesAsync()異步方法。

最終解決方法見:嘗試解決.NET Core Framework中Dns.GetHostAddressesAsync()引發的線程死鎖

相關連接:

[12月3日16:10更新]

果真是上面的代碼引發的死鎖,改成下面的代碼後問題解決:

private void ConnectWithTimeout(Socket socket, EndPoint endpoint, int timeout)
{
    var task = socket.ConnectAsync(endpoint);
    if (!task.Wait(timeout))
    {
        using (socket)
        {
            throw new TimeoutException("Could not connect to " + endpoint);
        }
    }
}
相關文章
相關標籤/搜索