1. 程式人生 > >嘗試解決在建構函式中同步呼叫Dns.GetHostAddressesAsync()引起的執行緒死鎖

嘗試解決在建構函式中同步呼叫Dns.GetHostAddressesAsync()引起的執行緒死鎖

(最終採用的是方法4

看看在 Linux 與 Windows 上發生執行緒死鎖的後果。

Linux:

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 程式的程序,新啟動的程序就會出現這個問題。

接下來嘗試解決方法。

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);
        }
    }
}

相關連結: