• 尝试解决在构造函数中同步调用Dns.GetHostAddressesAsync()引起的线程死锁


    (最终采用的是方法4

    问题详情见:.NET Core中遇到奇怪的线程死锁问题:内存与线程数不停地增长

    看看在 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 程序的进程,新启动的进程就会出现这个问题。

    接下来尝试解决方法。

    1)参考 Synchronously waiting for an async operation, and why does Wait() freeze the program here ,将上面的代码改为:

    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 命令行,进入 corefxsrcSystem.Net.NameResolutionsrc 目录,运行 msbuild System.Net.NameResolution.builds 命令,会在 corefxinWindows_NT.AnyCPU.DebugSystem.Net.NameResolution etcore50 文件夹中生成对应的程序集 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);
            }
        }
    }

    相关链接:

    在同步方法中调用异步方法时如何避免死锁问题

  • 相关阅读:
    索引跳跃式扫描(INDEX SKIP SCAN)
    Oracle参数Arraysize设置对于逻辑读的影响分析
    Oracle的SQL优化思路
    通过SID查找历史执行的SQL语句
    expdp/impdp数据泵分区表导入太慢了。添加不检查元数据参数提高效率:ACCESS_METHOD=DIRECT_PATH
    Oracle不能并行直接添加主键的方法:先建唯一索引后建主键
    ORA-20011 问题处理
    安装grid时找不到ASM共享磁盘
    jmeter-测试计划
    jmeter解压后启动jmeter.bat报错:Not able to find java executable or version
  • 原文地址:https://www.cnblogs.com/dudu/p/6131448.html
Copyright © 2020-2023  润新知