(最終採用的是方法4)html
問題詳情見:.NET Core中遇到奇怪的線程死鎖問題:內存與線程數不停地增加git
看看在 Linux 與 Windows 上發生線程死鎖的後果。github
Linux:api
Microsoft.AspNetCore.Server.Kestrel.Internal.Networking.UvException: Error -24 EMFILE too many open files
Windows(1.3萬個線程):服務器
引起問題的代碼:併發
Task<IPAddress[]> task = System.Net.Dns.GetHostAddressesAsync(host); task.Wait(5000); var addresses = task.Result;
上面的代碼是在構造函數中調用的,只能同步調用,沒法異步調用。負載均衡
踩坑的條件:在必定數量的請求併發時纔出現,若是隻有不多的請求不會出現。因此,當咱們發佈時,將服務器從負載均衡上摘下來,結束進程,更新程序,在本機訪問後(host解析已完成)掛上負載均衡,問題不會出現。若是不從負載均衡上摘下來,直接結束 asp.net core 程序的進程,新啓動的進程就會出現這個問題。asp.net
接下來嘗試解決方法。異步
1)參考 Synchronously waiting for an async operation, and why does Wait() freeze the program here ,將上面的代碼改成:socket
var task = Task.Run(async () => { return await System.Net.Dns.GetHostAddressesAsync(host); }); task.Wait(5000); var addresses = task.Result;
死鎖問題依舊。
2)參考 System.Data.SqlClient 中的實現:
private static async Task<Socket> ConnectAsync(string serverName, int port) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await socket.ConnectAsync(serverName, port).ConfigureAwait(false); return socket; } // On unix we can't use the instance Socket methods that take multiple endpoints IPAddress[] addresses = await Dns.GetHostAddressesAsync(serverName).ConfigureAwait(false); return await ConnectAsync(addresses, port).ConfigureAwait(false); }
(注:SqlClient中在Windows上沒有調用Dns.GetHostAddressesAsync)
將 Dns.GetHostAddressesAsync 放在一個 async/await 代理方法中:
private static async Task<IPAddress[]> GetHostAddressesAsyncProxy(string host) { return await System.Net.Dns.GetHostAddressesAsync(host); }
死鎖依舊。
3)修改 System.Net.Dns 的源代碼,將異步方法
public static Task<IPAddress[]> GetHostAddressesAsync(string hostNameOrAddress) { NameResolutionPal.EnsureSocketsAreInitialized(); return Task<IPAddress[]>.Factory.FromAsync( (arg, requestCallback, stateObject) => BeginGetHostAddresses(arg, requestCallback, stateObject), asyncResult => EndGetHostAddresses(asyncResult), hostNameOrAddress, null); }
改成同步方法
public static Task<IPAddress[]> GetHostAddressesAsync(string hostNameOrAddress) { NameResolutionPal.EnsureSocketsAreInitialized(); return Task.FromResult<IPAddress[]>(GetHostEntry(hostNameOrAddress).AddressList); }
問題解決!
說明死鎖問題的確是因爲在構造函數中同步調用異步方法引發的。目前 System.Net.NameResolution 只提供了異步的 API 進行主機名的解析,上面的 GetHostEntry() 是同步方法,但只支持 netstandard2.0 ,目前 nuget.org 上的 System.Net.NameResolution 只支持到 netstandard 1.3 。
[備註]
---------------
修改 System.Net.Dns 的源代碼,生成程序集(System.Net.NameResolution)並更新至 asp.net core 程序中的方法:
1)在github上籤出corefx的源代碼
2)修改 System.Net.Dns 的源代碼
3)運行corefx文件夾中的init-tools.cmd命令
4)運行 MSBuild Command Prompt for VS2015 命令行,進入 corefx\src\System.Net.NameResolution\src 目錄,運行 msbuild System.Net.NameResolution.builds 命令,會在 corefx\bin\Windows_NT.AnyCPU.Debug\System.Net.NameResolution\netcore50 文件夾中生成對應的程序集 System.Net.NameResolution.dll 。
5)將上一步生成的 System.Net.NameResolution.dll 複製到 asp.net core 站點的文件夾替換已有的同名文件便可。
---------------
4)嘗試不修改 System.Net.Dns 的源代碼進行解決
同步的 System.Net.Dns.GetHostEntry(string hostNameOrAddress) 方法能夠解決問題,但它是爲 netstandard2.0 api 實現的,在基於 netstandard1.6 的程序中沒法直接調用,編譯不經過。實際的 System.Net.NameResolution.dll 程序集中已經包含了 GetHostEntry() 實現,雖然編譯時不讓調用,但咱們能夠在運行時調用,那運行時如何調用呢?「反射」閃亮登場,用反射改成下面的代碼:
var method = typeof(System.Net.Dns).GetMethod("GetHostEntry", BindingFlags.Public | BindingFlags.Static); var addresses = ((IPHostEntry)method.Invoke(null, new object[] { host })).AddressList;
但發現 NuGet 服務器上發佈的 System.Net.NameResolution 4.3.0 中並不包含 GetHostEntry() 這個方法。後來找到了另一個私有靜態方法 —— InternalGetHostByName() 。再後來發現 System.Net.DnsEndPoint ,使用它就不須要本身進行主機名的解析,但目前只支持 Windows 。
因而最終採起的方法是:Windows 平臺用 DnsEndPoint ,非 Windows 平臺用反射調用 System.Net.Dns.InternalGetHostByName() 方法。示例代碼以下:
private void ConnectWithTimeout(Socket socket, EndPoint endpoint, int timeout) { if (endpoint is DnsEndPoint && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { IPAddress[] addresses; var dnsEndPoint = ((DnsEndPoint)endpoint); var host = dnsEndPoint.Host; var method = typeof(System.Net.Dns).GetTypeInfo() .GetMethod("InternalGetHostByName", BindingFlags.NonPublic | BindingFlags.Static); if (method != null) { addresses = ((IPHostEntry)method.Invoke(null, new object[] { host, false })).AddressList; } else { Task<IPAddress[]> task = Dns.GetHostAddressesAsync(host); task.Wait(timeout); addresses = task.Result; } var address = addresses.FirstOrDefault(ip => ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); if (address == null) { throw new ArgumentException(String.Format("Could not resolve host '{0}'.", host)); } endpoint = new IPEndPoint(address, dnsEndPoint.Port); } 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); } } }
相關連接: