【Azure 应用服务】App Service/Azure Function的出站连接过多而引起了SNAT端口耗尽,导致一些新的请求出现超时错误(Timeout)

问题描述

当需要在应用中有大量的出站连接时候,就会涉及到SNAT(源地址网络转换)耗尽的问题。而通过Azure App Service/Function的默认监控指标图表中,却没有可以直接查看到SNAT是否耗尽的问题(可以间接参考App Service Plan级中Metrics的 Socket Outbound All指标[截图见文末附录一],但是由于它是整个Plan下所有App Service的汇总数据,不能直接表明SNAT是否超过128的限制)。

 

这里所说的出站连接如:SQL数据库, Redis缓存以及其他的Restful API等等需要从App Service中向外发出的请求。当SNAT耗尽后,会出现以下一种或多种问题:

  • App Service的响应速度缓慢。
  • 间歇性 5xx 错误或“错误的网关”错误
  • 超时错误消息
  • 无法连接到外部终结点(例如 SQL DB,Redis,及其他API等)

问题分析

因为App Service是部署在云服务中,所以它也遵循着一个集群中很多实例(VM)通过负载均衡器的前端 IP 建立出站连接,所以出站的端口就成了用于维护不同流的唯一标识符。 因为在网络流量的五元组中

  1. 目标 IP
  2. 目标端口
  3. 源 IP
  4. 源端口
  5. 协议

如访问Redis服务(redistest01.redis.cache.chinacloudapi.cn 6380),目标IP为固定不变为RedisHost,而端口则固定为6380,源IP为当前App Service的出站IP,协议方式为Redis的序列化协议(RESP)。以上1,2,3,5都不可变的情况下,只有4 源端口可以改变,所以这里就需要使用SNAT。 

 

如App Service中一个示例(Worker Instance)发送TCP协议,介绍SNAT的工作流程:

1)App Service应用发送一个TCP包到外部IP地址,源地址和端口在TCP包中。

2)TCP包从App Service应用的工作实例上发送到SNAT负载均衡,SNAT改变了TCP包中的源地址为负载均衡器的公共IP地址和端口号。然后发送到外面目标IP地址。

AttributeValue
Protocol TCP
Worker instance IP address:port 10.0.5.60:51014
Load balancer IP address:port 13.76.245.72:12481
External endpoint IP address:port 52.189.232.180:80

3)外部服务接收到这个TCP包后,会原路回包,它会使用负载均衡器的公共IP地址和端口后作为目标IP和端口。

4)当负载均衡器收到外部服务的回包后,它将根据第2步中的映射关系,修改TCP包中的目标IP和端口。如此TCP回包就正确的回到了App Service的工作实例上。

 

负载均衡器的前端 IP 分配的每个公共 IP 都会为其后端池成员分配 64,000 个 SNAT 端口,后端池中大约有400多个实例,所以大约分配到每个实例的SNAT端口为64,000/400 =160(最大), 但是实际上分配的个数为128个。

 

间歇性连接问题的主要原因是在建立新的出站连接时遇到限制。 可以命中的限制包括:

  • TCP 连接数:可以建立的出站连接数有限制。 对出站连接的限制与使用的辅助角色的大小关联。
  • SNAT 端口: Azure 使用源网络地址转换 (SNAT) 和负载均衡器 (不向客户公开,) 与公共 IP 地址进行通信。 最初为 Azure 应用服务中的每个实例预分配了 128 个 SNAT 端口。 SNAT 端口限制会影响与相同地址和端口组合的打开连接。 如果应用与混合的地址/端口组合建立了连接,则不会用尽 SNAT 端口。 重复调用同一个地址/端口组合时,会用尽 SNAT 端口。 释放某个端口以后,即可根据需要重复使用该端口。 只有在等待 4 分钟后,Azure 网络负载均衡器才会从关闭的连接回收 SNAT 端口。

当应用程序或功能快速打开新的连接时,它们可能很快就会耗尽预分配的配额(128 个端口)。 然后,应用程序或功能会一直受到阻止,直到通过动态分配额外的 SNAT 端口或者通过重复使用回收的 SNAT 端口提供了新的 SNAT 端口为止。 如果你的应用程序用完了 SNAT 端口,则会出现间歇性的出站连接问题。

 

解决办法

避免 SNAT 端口问题意味着需要避免对同一主机和端口反复创建新连接。 连接池是解决该问题的更显而易见的方法之一。

如短时间无法修改代码,基于App Service, Azure Function的易扩展的特性,可以增加实例个数来及时缓解SNAT的受限问题。当每增加一个实例,SNAT端口即可增加128个。

 

建立连接池的方式一:HttpClientFactory 建立 HTTP 连接池

尽管 HttpClient 实现了 IDisposable 接口,但它是为重复使用而设计的。 关闭 HttpClient 的实例使套接字在 TIME_WAIT 一小段时间内处于打开状态。 如果经常使用创建和处置对象的代码路径 HttpClient ,应用可能会耗尽可用的套接字。 ASP.NET Core 2.1 中引入了HttpClientFactory作为此问题的解决方案。 它处理池 HTTP 连接以优化性能和可靠性。

建议:

  • 不要 直接创建和释放 HttpClient 实例。
  • 请 使用 HttpClientFactory 来检索 HttpClient 实例。 

示例部分

1)以下代码即可复现SNAT快速耗尽的情况(没有及时释放Response资源)

public string Index(string url)
{
    var request = HttpWebRequest.Create(url);
    request.GetResponse();

    return "OK";
}

可以修改为:

public string Fin(string url)
{
    var request = HttpWebRequest.Create(url);
    var response = request.GetResponse();
    response.Close();

    return "OK";
}

 

2)下面使用Using来释放httpclient对象,但是也是用以导致SNAT耗尽的问题。

public async Task<string> Client(string url)
{
    using (var client = new HttpClient())
    {
        await client.GetAsync(url);
    }

    return "OK";
}

可以考虑重用HttpClient来缓解SNAT耗尽问题

private static Lazy<HttpClient> _client = new Lazy<HttpClient>();

public async Task<string> ReuseClient(string url)
{
    var client = _client.Value;
    await client.GetAsync(url);
    return "OK";
}

 

附录一:应用服务计划中查看全部的出站Socket连接

 

 

 

参考资料

使用 SNAT 进行出站连接(负载均衡器默认端口分配)https://docs.microsoft.com/zh-cn/azure/load-balancer/load-balancer-outbound-connections#default-port-allocation

排查 Azure 应用服务中的间歇性出站连接错误:https://docs.microsoft.com/zh-cn/azure/app-service/troubleshoot-intermittent-outbound-connection-errors#avoiding-the-problem

SNAT with App Service,介绍Azure App Service中SNAT的原理,问题,及如何避免?:https://www.cnblogs.com/lulight/articles/13543209.html

posted @ 2021-04-24 17:08  路边两盏灯  阅读(355)  评论(0编辑  收藏  举报