问题描述
当需要在应用中有大量的出站连接时候,就会涉及到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 建立出站连接,所以出站的端口就成了用于维护不同流的唯一标识符。 因为在网络流量的五元组中
- 目标 IP
- 目标端口
- 源 IP
- 源端口
- 协议
如访问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地址。
- 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