三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

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

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

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 :SNAT with App Service | 4lowTheRabbit.github.io


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

← 返回列表