【Azure App Service】应用服务中的SNAT (Source Network Address Translation 源网络地址转化)

简介: App Service 出站访问需经负载均衡器 SNAT 转换,每个实例默认仅约 128 个 SNAT 端口。高并发调用同一外部服务易耗尽端口,引发超时、5xx 错误。本文详解 SNAT 原理、耗尽原因、症状及连接复用等优化方案。

App Service 应用经常需要访问外部服务,比如 Azure SQL、Redis、Storage 或第三方 API。很多人会以为应用是直接从 worker 实例访问公网,但实际上并不是这样。


App Service 的 worker 实例运行在 scale unit / stamp 内部,通常没有直接分配公网 IP。它访问外部公网 endpoint 时,需要经过 stamp 的出站负载均衡器。这个负载均衡器会把 worker 的私网源地址和端口,转换成公网源地址和端口,这个过程就是 SNAT(Source Network Address Translation)


这篇文章主要整理 App Service 中排查 SNAT 问题时需要知道的几件事:

  1. SNAT 如何工作 ?
  2. SNAT 端口为什么会耗尽 ?
  3. 端口如何分配 ?
  4. 耗尽时有哪些症状 ?
  5. 以及应用应该如何优化连接使用 ?

 

1: SNAT 是怎么工作的

以 TCP 连接为例,一次出站访问大致是这样发生的:

负载均衡器会维护一条映射记录,例如:

字段 示例
协议 TCP
Worker 实例地址 10.0.5.60:51014
负载均衡器公网地址 13.76.245.72:12481
外部服务地址 52.189.232.180:80

 

注意:

  • 应用看到的是自己连到了外部服务
  • 外部服务看到的是负载均衡器的公网地址
  • 负载均衡器负责在两边之间做地址转换
  • 这个过程对应用和外部服务都是透明的

 

2: SNAT 端口耗尽

SNAT 端口的消耗和 TCP 五元组有关:

字段 含义
Protocol 协议,例如 TCP
Source IP 源 IP,SNAT 后是负载均衡器公网 IP
Source Port 源端口,也就是 SNAT 端口
Destination IP 外部目标 IP
Destination Port 外部目标端口

注意:

  • 如果多个 TCP 流访问的是同一个目标 IP、同一个目标端口、同一个协议,那么它们需要不同的源端口来区分。也就是说,高并发访问同一个外部服务时,SNAT 端口会很快被消耗
  • 反过来,如果多个流访问的是不同目标 IP 或不同端口,那么五元组本身已经不同,SNAT 端口就有机会被复用。


每个 IP 地址最多只能打开有限数量的端口。如果应用频繁打开和关闭连接,情况会更严重。因为 SNAT 端口关闭后不会马上释放:

关闭方式 SNAT 端口释放时间
正常 FIN/ACK 关闭 约 240 秒后释放
RST 重置 约 15 秒后释放
达到 idle timeout 按 idle timeout 释放

这意味着,如果一个 Web 应用每秒打开 1 条 HTTP 连接,调用后端服务后正常关闭,那么在 240 秒内可能累计占用约 240 个 SNAT 端口。

另一个例子是数据库连接池:如果一个繁忙站点的 SQL 连接池大小是 300,并且数据库查询执行较慢,那么这些连接可能持续占用约 300 个 SNAT 端口。

还有一种常见情况是队列触发的 Function App:如果压测一开始就把大量消息一次性灌入队列,Function 可能瞬间启动大量到 Storage 或其他外部服务的连接,很快耗尽 SNAT 端口。

 

3: SNAT 端口分配算法

为了避免某个站点耗尽整个 stamp 的 SNAT 端口并影响其他站点,Azure Load Balancer 需要对 SNAT 端口做分配控制。常见算法包括:

分配方式 端口数量
On-demand 算法 每实例基础 160 个,可按需尽力分配更多
新算法 每实例固定预分配 128 个

这里的 160 个 SNAT 端口 可以用一个粗略的容量分摊思路来理解。一个典型的 App Service stamp 可能有 5 个出站 IP,每个 IP 理论上有大约 65536 个端口。如果这些端口要被 stamp 内大约 2000 个实例共享,那么每个实例能稳定分到的端口数量大约是:


5 × 65536 ÷ 2000 ≈ 163.84  取整后就接近 160 个端口 / 实例。


4: SNAT 端口耗尽时的症状

当 SNAT 端口耗尽时,应用常见表现包括:

  • 连接外部 endpoint 变慢;
  • 请求长时间 pending;
  • 最终出现 socket timeout;
  • Application Insights 里能看到依赖调用失败。

如果启用了 Application Insights dependency tracking,也可能看到外部依赖调用失败。

 

5: 如何解决 App Service 的 SNAT 端口耗尽

总体方向是:先减少不必要的连接占用,再考虑扩展。


具体建议:

  1. 复用连接:不要每次请求都 new 一个 HttpClient
  2. 使用连接池:数据库、HTTP 客户端都应合理复用连接;
  3. 控制连接池大小:连接池不是越大越好,过大的池会持续占用端口;
  4. 降低重试强度:失败时疯狂重试会进一步放大端口占用;
  5. 让后端尽快响应:后端越慢,连接存活越久,SNAT 端口占用越久;
  6. 横向扩容 App Service Plan:SNAT 端口按实例分配,实例变多,总可用端口也会增加;
  7. 使用 App Service Environment:ASE 的实例池更小,worker 实例通常可以获得更多 SNAT 端口;
  8. 压测要贴近真实流量:负载测试应以稳定速度投喂数据,而不是一开始就把所有消息一次性灌入队列。

 


示例代码及优化

下面的代码可以复现 SNAT 端口耗尽问题:

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


下面这种写法也会造成 SNAT 端口泄漏,因为每次调用都会创建新的 HttpClient

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


 

可以改成复用同一个 HttpClient

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


 

常见问题(FAQ):

Q:我能自己看到 App Service 的 SNAT 端口分配指标吗?

A:一般情况下,这个指标不直接公开。日常设计时不要依赖“实际能拿到多少端口”,而应按每实例 128 个的保守值来控制。

 

Q:为什么不能直接根据 SNAT 端口指标做自动扩缩容?

A:因为很多 SNAT 问题本质是连接没有复用。如果代码持续浪费连接,单纯扩容只是把问题摊开,并不一定治本。应该先优化连接复用和后端响应,再考虑扩容。

 

Q:SNAT 耗尽和 TCP Connections 耗尽有什么区别?

A:TCP Connections 是 worker 实例层面的连接计数,SNAT 是出站负载均衡器上的公网源端口资源。前者统计所有 TCP 连接,后者只和外部网络流量有关。二者有关联,但不能互相替代。

 

Q:多个 WebJob 共用同一个 App Service Plan,怎么判断谁占用了最多连接?

A:如果没有按进程维度的连接指标,可以把部分 WebJob 移到另一个 App Service Plan,通过隔离法观察问题是否缓解,逐步定位高连接消耗的任务。

 

 


 

参考资料

SNAT with App Service :https://4lowtherabbit.github.io/blogs/2019/10/SNAT/



当在复杂的环境中面临问题,格物之道需:浊而静之徐清,安以动之徐生。 云中,恰是如此!

相关文章
|
2月前
|
人工智能 监控 网络协议
【App Service】常规排查 App Service 启动 Application Insights 无数据的步骤 (.NET版本)
本文详解Application Insights在Azure App Service中无日志数据的三大原因及排查方法:1)网络连通性(验证到AI端点的443端口访问);2)w3wp.exe进程是否成功加载AI模块;3)DLL冲突(检查并移除重复的Microsoft.ApplicationInsights等组件)。
155 10
|
2月前
|
NoSQL 网络协议 Cloud Native
【Azure Redis】云原生环境下的 Redis 超时之谜:为什么 15 分钟后应用才恢复?
云原生中Redis短暂不可用后应用持续超时15分钟?问题不在Redis,而在Linux TCP默认重传机制(tcp_retries2=15)与长连接模型的错位。需三管齐下:调低内核重传次数、客户端显式配置超时与自动重连、应用层引入断路器与弹性重试。
217 20
|
2月前
|
网络协议 虚拟化 Docker
【Azure Developer】.NET Aspire 启动报错:listen tcp bind: An attempt was made to access a socket in a way forbidden by its access permissions
.NET Aspire在Windows启动时因Hyper-V端口保留机制,导致DCP代理无法绑定53209等端口(报错“访问被拒绝”)。虽端口未被占用,但已被系统保留。推荐方案:修改launchSettings.json,将服务端口改为7xxx等安全范围;或临时重启winnat服务、永久排除指定端口。
441 21
|
2月前
|
前端开发 应用服务中间件 Linux
【Azure App Service】PHP页面上传文件413错误的解决方案
在使用 Azure App Service(Linux + PHP) 部署 Web 应用时,如果上传文件大于1MB,就会遇到 HTTP 413(Request Entity Too Large) 错误。 # 问题解答 ### 一、HTTP 413 错误的本质含义 413 Request Entity Too Large 是标准 HTTP 状态码,表示: > 客户端提交的请求体(Request Body)大小超过了服务器当前允许的最大限制。 在 Azure App Service(Linux)环境中,这个错误并不一定来自前端网关(Frontend),而更常见的来源是 App...
918 13
|
8月前
|
Java 开发工具
【Azure Storage Account】Java Code访问Storage Account File Share的上传和下载代码示例
本文介绍如何使用Java通过azure-storage-file-share SDK实现Azure文件共享的上传下载。包含依赖引入、客户端创建及完整示例代码,助你快速集成Azure File Share功能。
529 6
|
9月前
|
Java API 开发工具
【Azure Developer】Java代码实现获取Azure 资源的指标数据却报错 "invalid time interval input"
在使用 Java 调用虚拟机 API 获取指标数据时,因本地时区设置非 UTC,导致时间格式解析错误。解决方法是在代码中手动指定时区为 UTC,使用 `ZoneOffset.ofHours(0)` 并结合 `withOffsetSameInstant` 方法进行时区转换,从而避免因时区差异引发的时间格式问题。
398 4
|
10月前
|
API C++
【Azure 环境】VS Code登录China Azure(Function)报错 An error occurred while signing in: invalid_request - AADSTS65002
An error occurred while signing in: invalid_request - AADSTS65002: Consent between first party application 'c27c220f-ce2f-4904-927d-333864217eeb' and first party resource '797f4846-ba00-4fd7-ba43-dac1f8f63013' must be configured via preauthorization - applications owned and operated by Microsoft mus
467 13
|
5月前
|
NoSQL 网络协议 Java
【Azure Redis】客户端应用使用 Azure Redis Cluster 报错 java.security.cert.CertificateException: No subject alternative names matching IP address xxx.xxx.xxx.xxx found
使用Lettuce连接Azure Redis集群时,因SSL证书仅含域名不支持IP地址,导致“CertificateException”错误。通过自定义`MappingSocketAddressResolver`,将IP映射为域名进行证书验证,结合`ClientResources`配置实现安全连接,最终成功访问Redis集群并执行操作。
261 2
|
5月前
|
测试技术 Windows
【Azure 环境】在Windows环境中使用OpenSSL生成自签名证书链步骤分享
本文详解如何使用OpenSSL生成自签名证书链,包括根CA、中间CA和服务器证书,并最终打包为包含私钥的PFX文件,适用于内部系统或测试环境部署,步骤清晰,可复制即用。
703 3
|
7月前
|
开发框架 Java .NET
【App Service】Java应用上传文件功能部署在App Service Windows上报错 413 Payload Too Large
Java应用在Azure App Service(Windows)部署时,因IIS默认限制上传文件不超过30MB,导致大文件上传出现413错误。虽无web.config文件,可通过Kudu工具手动创建并配置maxAllowedContentLength和maxRequestLength,突破限制,实现大文件上传。
373 3