Azure SNAT

这篇文章由 Pedro Perez 提供。

Azure 的网络基础架构和通常的本地网络是完全不同的,因为在后台运行的是不同的软件抽象层。我今天想要讨论的是这些软件层中的其中一层,以及为什么在对您的应用程序进行网络故障排错时需要考虑到这一点。

在云端对于我们及我们的客户最大的挑战可能就是可扩展性。我们的工程师已经将 Azure 设计成适用于超大规模并同时满足技术要求最为苛刻的用户的需求。可想而知,这增加了系统的复杂性。例如,会有多个 IP 地址与每个虚拟机相关联。在非 Azure 资源管理(非 ARM)模型的场景下,当您在云服务中部署虚拟机时,虚拟机获取一个 IP 地址,这个 IP 被称为动态 IP (DIP),该 IP 在 Azure 外不可路由。云服务获取一个被称为虚拟 IP(VIP)的 IP 地址,这是一个可路由的公共 IP 地址。当发送出站流量时,云服务中的每一个虚拟机都隐藏在 VIP 之后,并且只能通过在 VIP 上创建映射到该指定虚拟机的终结点来进行访问。

类似于传统的本地网络,VIP 可以被定义为 NAT IP 地址。VIP 的最大特殊性是在相同的云服务中所有虚拟机共享该 VIP。您可以利用云服务中的终结点很容易地控制端口的数据流量重定向到特定的虚拟机,但是对于出站流量该如何转换?

源 NAT

这就是源 NAT(以下用 SNAT 代替)发挥作用之处。任何云服务的出站流量(从云服务中的一台虚拟机传出并且不传入到相同云服务的另一台虚拟机的流量)将会通过 NAT 层,这时将会应用到SNAT。顾名思义,SNAT 只更改源信息:源 IP 地址和源端口。

源 IP 地址转换

源 IP 地址从原始的 DIP 转变为云服务的 VIP,所以流量能够被路由。这是一个多对一的映射,所有云服务中的虚拟机都映射到该云服务的 VIP。这里我们会面临一个挑战:如果在相同云服务中的两个虚拟机创建了到相同目标的出站连接,并且也使用相同的源端口,将会发生什么状况?记住,系统是通过源 IP、源端口、目标 IP和目标端口的4元组来区分不同的 TCP(或 UDP)连接的。

更改任何目标信息将导致断开连接,并且由于只有一个源 IP( VIP),同样也无法更改。因此我们需要改变源端口。

源端口转换

Azure 在 VIP 中为虚拟机的连接预分配了 160 个源端口。这种预分配是为了加快建立新的通信,160 个端口的限制是为了节省资源。初始端口是从该大范围的端口中随机进行选择, 然后对其以及随后的 159 个端口进行预分配。经验表明,只要在开发应用程序时在网络设计部分遵循最佳实践,这样的设计就是十分有效的。

Azure 将把一个出站连接的源端口转换为 160 个预分配端口中的第一个可用端口。

有一个问题是,如果我们同时使用 160 个端口将会发生什么?如果所有端口都未被释放,则系统会尽最大努力分配更多的端口;只要有一个端口被释放了,它将再次可供使用。

SNAT 表

所有这些转换必须进行存储,以便在数据流入流出时对端口转换进行跟踪。这些用于存储的地方被称为 SNAT 表,它和其他网络产品如防火墙或路由器中的 NAT 表具有相似的概念。

系统会保存原始的5元组(源 IP、源端口、目标 IP、目标端口、协议 – TCP/UDP)和转换后的 5 元组,其中源 IP 、源端口已被转换为 VIP 和一个预分配的端口。

删除 SNAT 表中记录

与任何其他 NAT 表相同,您无法永远存储这些转换记录,表中均有删除记录的规则,其中最明显的是:

  • 如果连接在 FIN、ACK 状态下被关闭,则我们会等待几分钟(2 x MSL(Maximum Segment Lifetime)- http://www.rfc-editor.org/rfc/rfc793.txt)再删除该记录。
  • 如果连接在 RST 状态下被关闭,则我们会立刻删除该记录。

此时,我相信您已经发现了一个问题。如果配对方消失(例如,程序崩溃或停止响应),那我们如何判断是否应当删除某个 UDP 连接或 TCP 连接呢?

在这种情况下,我们硬性规定了一个超时值。每当一个连接中的数据包经过 SNAT,我们在该连接上启动一个 4 分钟的倒计时。如果在另一个数据包到达之前计时器归零了,则我们从 SNAT 表中删除该记录,因为我们认为这个连接已经结束。这是非常重要的一点:如果您的应用程序保持一个空闲连接超过 4 分钟,那么这条记录将会从SNAT 表中删除。大多数应用程序不会对丢失掉那些他们认为仍处于活跃状态的连接进行处理,所以明智的管理连接的生命周期,以及不让连接进入空闲状态是需要慎重考虑的。

长期闲置的连接被认为是有害的

给个例子更容易说明这个复杂的状况。下面举例说明上述情况会导致什么样的错误,以及为什么不应该保持闲置的 TCP 连接。以下展示了一个在客户端(云服务中的虚拟机)中活跃的 HTTP 连接、SNAT 以及服务器端的连接表:

客户端

源 IP 源端口 目标 IP 目标端口 TCP 状态
客户端 DIP 12345 服务器端 VIP 80 建立连接

源端口是由客户端操作系统随机选择的。

SNAT 表

源 IP 源端口 目标 IP 目标端口
DIP-> VIP 12345 -> 54321 服务器端 IP 80

DIP 被转换为 VIP 并且源端口被转换为160 个预分配端口中的第一个可用的端口。

服务器端

源 IP 源端口 目标 IP 目标端口 TCP 状态
客户端 VIP 54321 服务器端 IP 80 建立连接

服务器端不知道客户端的 DIP 或者源端口,因为这些都被 SNAT 隐藏在 VIP 之后。

目前为止,一切正常。

现在设想连接已经闲置超过了 4 分钟,表中将变成什么状态?

客户端

源 IP 源端口 目标 IP 目标端口 TCP 状态
客户端 DIP 12345 服务器端 VIP 80 建立连接

此处没有变化。客户端为可能的更多数据建立好连接准备,但是没有数据从服务器端传来。

SNAT 表

源 IP 源端口 目标 IP 目标端口
已删除 已删除 已删除 已删除

此处发生了什么?

由于该连接已闲置超过了 4 分钟,SNAT 表中记录已过期,所以这条记录被删除。

服务器端

源 IP 源端口 目标 IP 目标端口 TCP 状态
客户端 VIP 54321 服务器端 IP 80 建立连接

不出所料,在服务器端也没有任何变化。 它返回了客户端请求的所有数据,并且在该 TCP 连接上等待新的请求超过 4 分钟。

这样问题就出现了。假设客户端恢复其操作并决定使用相同的连接从服务器端请求更多数据。不幸的是这将不能实现,Azure 将会在 SNAT 层丢弃这部分流量,因为这些数据包不符合以下任何标准:

  • 属于现有连接(不满足该条件,因为它已经过期,所以已被删除!)
  • SYN 数据包(对于新连接而言)(不满足该条件,因为它不是一个 SYN 数据包!)

这意味着在这个多元组上尝试的连接都将失败。好,这是个问题,不过并不是无法解决的,对吧?客户端将建立一个新的 TCP 连接(SYN 数据包将通过 SNAT)并在其中发送 HTTP 请求。这没问题,但是有些情况下我们可能会面临 SNAT 记录到期这一后果。

如果客户端与同一个服务器以及端口(服务器 IP : 80)建立新的连接并且足够快的在 160 个分配好的端口中循环(或者更快),但并没有显式地关闭它们, 端口 54321 就可以再次使用(记住:端口 12345 --> 54321 的转换已过期),我们将可以轻易的在原始的 160 个端口中随意使用。端口 54321 终将会与 12345 以外的源端口进行对应转换从而再次被使用,但对于相同的源和目标 IP 地址(以及相同的目标端口!),它将会是如下状态:

客户端

源 IP 源端口 目标 IP 目标端口 TCP 状态
客户端 DIP 12346 服务器端 VIP 80 SYN 已发送

客户端决定建立新的连接,所以它会发送一个 SYN 数据包,在服务器端 IP : 80 建立一个新的连接。

SNAT 表

源 IP 源端口 目标 IP 目标端口
DIP-> VIP 12346 -> 54321 服务器端 IP 80

Azure 发现这个数据包在表中没有匹配记录,会将其作为 SYN 数据包接收(即新连接)。将端口 12346 转换为 54321 ,因为端口 54321 在 160 个分配好的端口中又是第一个可用端口。

服务器端

源 IP 源端口 目标 IP 目标端口 TCP 状态
客户端 VIP 54321 服务端 IP 80 建立连接

服务器端已经有一个建立好的连接,所以当它从客户端 VIP : 54321 接收到一个 SYN 数据包时将忽略并且丢弃它。此时,我们已经有了两个断开的连接:被闲置 4 分钟以上的原连接和这个新连接。

避免这个在很多不同平台上都会出现的问题的最好方式,是在应用层面设置一个合理的 keep-alive 值(SO_KEEPALIVE 套接字选项) 。应该考虑每 30 秒或 1 分钟通过空闲连接发送一个数据包,因为它会在 Azure 和本地防火墙中重置每一个用于统计空闲时间的计时器。

需要快速的解决方法?

在 Azure 中您还有另一个快速的解决方法。对于出站流量(同样对于入站流量)您可以不使用 VIP,而是指定一个实例级 IP ,称为 PIP。PIP 只分配给一个实例, 因此不需要使用 SNAT 以响应不同虚拟机的请求。它仍然通过负载均衡(SLB),但并不应用 SNAT ,也没有 SNAT 表,您可以随意的保持您的空闲连接直到 SLB 终结它们(Azure 负载均衡器的可配置空闲超时),但这将是另一种情况。

结束之前,我们应该承认这种设计的另一个长期存在的问题。由于 Azure 以每批 160 个来分配出站端口,有可能会发生创建一批新的 160 个端口不够快,从而导致出站连接的尝试失败的情况。我们通常只在非常高的负荷情况下才会看到(几乎总是在负载测试阶段)此类情况,但如果您真的遇到了这种情况,请尝试使用 PIP 这个解决方案。