使用 TCP keepalive 提高 Azure Kubernetes 服务中的网络容错能力

在标准传输控制协议 (TCP) 连接中,当连接处于空闲状态时,对等机之间没有数据流。 因此,使用 TCP 与处理长时间运行的请求的服务器通信的应用程序或 API 请求可能必须依赖连接超时来了解是否已终止或失去连接。 本文说明如何使用 TCP keepalive 增强 Azure Kubernetes 服务 (AKS) 中托管的应用程序中的容错能力。

了解 TCP keepalive

多个 Azure 网络服务(例如 Azure 负载均衡器 (ALB))允许配置超时期限,在此期限过后,系统会终止空闲 TCP 连接。 当 TCP 连接保持空闲状态的时间超过网络服务上配置的超时持续时间时,任何在任一方向发送的后续 TCP 数据包都可能会被丢弃。 它们也可能会从网络服务收到 TCP 重置 (RST) 数据包,具体取决于服务上是否已启用 TCP 重置。

ALB 中的空闲超时功能旨在优化客户端和服务器应用程序的资源利用率。 此超时适用于 ALB 管理的入口和出口流量。 发生超时时,客户端和服务器应用程序可能会停止处理请求并释放与连接关联的资源。 这些资源随后可以重用于其他请求,提高应用程序的整体性能。

在 AKS 中,负载均衡器默认启用空闲时 TCP 重置,空闲超时期限为 30 分钟。 可以使用 az aks update 命令调整此超时期限。 以下示例将超时期限设置为 45 分钟。

az aks update \
    --resource-group myResourceGroup \
    --name myAKSCluster \
    --load-balancer-idle-timeout 45

在调整超时持续时间之前,请务必仔细地将其考虑清楚:

  • 持续时间太短可能会导致长时间运行的操作过早终止,从而导致请求失败和用户体验不佳。 它还可能导致频繁超时,从而增加错误率并使应用程序看起来不可靠。
  • 持续时间太长可能会使空闲连接长时间保持打开状态,耗尽服务器资源,从而降低可用于处理新请求的容量。 它还会延迟对服务器问题的检测,导致更长的停机时间和低效的负载均衡。

在 AKS 中,除了穿过 ALB 的北-南流量(流入量和流出量)之外,还有通常在群集网络上运行的东-西流量(Pod 到 Pod)。 这种情况下,超时期限由 kube-proxy TCP 设置和 Pod 的 TCP sysctl 设置定义。 默认情况下,kube-proxy 在 iptables 模式下运行,并使用 kube-proxy 规范中定义的默认 TCP 超时设置。 kube-proxy 的默认 TCP 超时设置如下:

  • CLOSE_WAIT 状态下的 TCP 连接的网络地址转换 (NAT) 超时为 1 小时。
  • 已建立的 TCP 连接的空闲超时为 24 小时。

在某些长时间运行的操作中,客户端和服务器都在 AKS 群集内,或者其中一个在 AKS 群集外,对于此类操作,你需要的超时的时间可能比为 Azure 负载均衡器或 Azure NAT 网关之类的网络服务配置的持续时间更长。 为了防止连接保持空闲状态的时间超过在网络服务上配置的持续时间,请考虑使用 TCP keepalive 功能。 此功能使服务器和客户端在等待响应时保持可用状态,允许你重试操作而不会遇到连接超时情况。

在 TCP 连接中,任一对等机都可以为其连接端请求 keepalive。 可以为客户端和/或服务器配置 keepalive,也可以两者都不配置它。 keepalive 机制遵循 RFC1122 中定义的标准规范。 keepalive 探测要么是一个空段,要么是一个仅包含 1 个字节的段。 其序列号比迄今为止从对等机收到的最大确认 (ACK) 号小一。 探测数据包模仿已收到的数据包。 作为响应,接收端会发送另一个 ACK​​ 数据包。 这是向发送方表明连接仍然处于活动状态。

RFC1122 规范规定,如果探测或 ACK 丢失,不会重新传输它们。 因此,对单个 keepalive 探测没有响应不一定意味着连接失效。 在这种情况下,发送方必须在终止连接之前尝试再发送探测几次。 为探测接收到 ACK 时,连接的空闲时间会重置,然后就会重复此过程。 使用 keepalive 探测,你可以配置以下参数来控制其行为。 在 AKS 中,基于 Linux 的节点具有以下默认 TCP keepalive 设置,这些设置与标准 Linux 操作系统相同:

  • Keepalive 时间(以秒为单位):发送第一个 keepalive 探测之前连接处于非活动状态的持续时间。 默认持续时间为 7200 秒或 2 小时。
  • Keepalive 间隔(以秒为单位):如果未收到确认,则为后续 keepalive 探测之间的间隔。 默认间隔为 75 秒。
  • Keepalive 探测数:连接被视为不可用之前未确认的探测的最大数量。 默认值为 9。

Keepalive 探测在 TCP 层进行管理。 启用后,对于请求者应用程序,探测可能会导致以下结果:

  • 正常操作:Keepalive 探测不影响请求者应用程序。
  • 对等机重启或崩溃(探测未确认):应用程序收到“连接超时”错误。
  • 对等机重启或崩溃(RESET RST 响应):应用程序收到“连接被对等机​​重置”错误。
  • 对等机主机出现网络问题:应用程序可能会收到“连接超时”错误或其他相关错误。

下一部分介绍如何更改群集和应用程序 Pod 的 sysctl 设置以设置 TCP keepalive。

在 AKS 上配置 TCP keepalive

使用 AKS,群集管理员可以调整节点的操作系统和 kubelet 参数,以满足其工作负荷的要求。 在设置群集或新节点池时,管理员可以启用与其工作负荷相关的 sysctl。 Kubernetes 将 sysctl 分为两类:安全和不安全。

安全 sysctl 是具有命名空间且在同一节点上的 Pod 之间得到适当隔离的 sysctl。 这种隔离意味着为一个 Pod 配置一个安全 sysctl 不会影响节点上的其他 Pod、节点的运行状况,也不会允许 Pod 超出其 CPU 或内存资源限制。 Kubernetes 默认启用安全 sysctl。 从 Kubernetes 1.29 开始,所有 TCP keepalive sysctl 都被视为安全 sysctl:

  • net.ipv4.tcp_keepalive_time
  • net.ipv4.tcp_fin_timeout
  • net.ipv4.tcp_keepalive_intvl
  • net.ipv4.tcp_keepalive_probes

若要详细了解安全 sysctl 和不安全 sysctl 及其配置,请参阅自定义 Azure Kubernetes 服务 (AKS) 节点池的节点配置。

注释

从 Kubernetes 1.29 开始,TCP keepalive sysctl 被视为安全 sysctl,默认情况下处于启用状态。 不需要在群集中显式启用它们。

可以通过在 Pod 定义中设置安全性上下文在所需的 Pod 中配置 TCP keepalive sysctl,如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: busybox-sysctls
spec:
  securityContext:
    sysctls:
      - name: "net.ipv4.tcp_keepalive_time"
        value: "45"
      - name: "net.ipv4.tcp_keepalive_probes"
        value: "5"
      - name: "net.ipv4.tcp_keepalive_intvl"
        value: "45"
  containers:
    - name: busybox
      image: busybox
      command: ["sleep", "3600"]

应用此规范可实现以下 TCP keepalive 行为:

  • net.ipv4.tcp_keepalive_time 将 keepalive 探测配置为在连接处于非活动状态 45 秒后发送。
  • net.ipv4.tcp_keepalive_probes 将操作系统配置为在认定连接不可用之前发送 5 个未确认的 keepalive 探测。
  • net.ipv4.tcp_keepalive_intvl 将分派两个 keepalive 探测之间的持续时间设置为 45 秒。

TCP keepalive sysctl 在 Linux 内核中具有命名空间,这意味着可以为节点上的每个 Pod 单独设置 sysctl。 使用这种隔离,你可以通过 Pod 的安全性上下文配置 keepalive 设置,该上下文适用于同一个 Pod 中的所有容器。

Pod 现在已准备好发送和响应 keepalive 探测了。 若要验证设置,可以在 Pod 上执行 sysctl 命令,如下所示:

kubectl exec -it busybox-sysctls -- sh -c "sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes"

执行该命令应会生成以下输出:

net.ipv4.tcp_keepalive_time = 45
net.ipv4.tcp_keepalive_intvl = 45
net.ipv4.tcp_keepalive_probes = 5

下一部分介绍如何才能确保应用程序在与客户端的连接上启用了 TCP keepalive。

在应用程序中配置 TCP keepalive

TCP 客户端应用程序应该启用 TCP keepalive,以便将 keepalive 探测发送到服务器。 大多数编程语言和框架都提供了在套接字连接上启用 TCP keepalive 的选项。 以下示例使用 Python 的 socket 库:

import socket
import sys

# Create a TCP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Enable TCP keepalive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

# Optional: Set TCP keepalive parameters (Linux specific).
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)    # Idle time before keepalive probes
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)   # Interval between keepalive probes
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)      # Number of keepalive probes

# Connect to the server
server_address = ('server.example.com', 12345)
print(f'Connecting to {server_address[0]} port {server_address[1]}')
sock.connect(server_address)

try:
    # Send and receive data
    message = 'This is a test message.'
    print(f'Sending: {message}')
    sock.sendall(message.encode())

    # Wait for a response
    data = sock.recv(1024)
    print(f'Received: {data.decode()}')

finally:
    print('Closing connection')
    sock.close()

在本示例中:

  • 应用程序启用 TCP keepalive。
  • keepalive 探测在连接处于非活动状态 60 秒后发送。
  • keepalive 探测以 10 秒的间隔发送。
  • 如果连续 5 个探测失败,则会关闭连接。

注释

请注意,通过 kubelet 中的 sysctl 设置的任何系统级 TCP keepalive 配置都会被应用程序的 TCP keepalive 设置替代。 为了在多个应用程序中保持一致的 keepalive 行为,请在 kubelet 级别设置 keepalive 参数。 然后,请在套接字上启用 keepalive 选项,而不在应用程序内指定单个参数,以便将系统级 keepalive 参数用于应用程序。 只有在绝对必要的情况下,才允许单个应用程序对系统级参数值执行替代操作,如上一示例所示。

如果使用 .NET,以下代码会生成与上一 Python 示例相同的结果:

static async Task Main()
{
    using SocketsHttpHandler handler = new SocketsHttpHandler();

    handler.ConnectCallback = async (ctx, ct) =>
    {
        var s = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
        try
        {
            s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
            s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime,60);
            s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 10);
            s.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 5);
            await s.ConnectAsync(ctx.DnsEndPoint, ct);
            return new NetworkStream(s, ownsSocket: true);
        }
        catch
        {
            s.Dispose();
            throw;
        }
    };

    // Create an HttpClient object
    using HttpClient client = new HttpClient(handler);

    // Call asynchronous network methods in a try/catch block to handle exceptions
    try
    {
        HttpResponseMessage response = await client.GetAsync("<service url>");

        response.EnsureSuccessStatusCode();

        string responseBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"Read {responseBody.Length} characters");
    }
    catch (HttpRequestException e)
    {
        Console.WriteLine("\nException Caught!");
        Console.WriteLine($"Message: {e.Message} ");
    }
}

有关详细信息,请参阅 ConnectCallback 处理程序。

HTTP/2 keepalive

如果使用基于 HTTP/2 的通信协议(例如 gRPC),则 TCP keepalive 设置不会影响应用程序。 HTTP/2 遵循 RFC7540 规范,该规范要求客户端向服务器发送 PING 帧,并且要求服务器立即使用 PING ACK 帧进行回复。 HTTP/2 在网络堆栈的第 7 层运行,并受益于在第 4 层运行的 TCP 提供的数据传送保证。 由于每个 HTTP/2 请求都保证会得到响应,因此 HTTP/2 传输所需的唯一 keepalive 配置就是超时设置。 如果在配置的超时期限之前未收到 PING ACK,则连接会断开。

当应用程序使用 HTTP/2 传输时,服务器负责支持 keepalive 并定义其行为。 客户端的 keepalive 设置必须与服务器的设置兼容。 例如,如果客户端发送 PING 帧的频率超过服务器允许的频率,则服务器会使用 HTTP/2 GOAWAY 帧响应终止连接。

对于 gRPC 应用程序,客户端和服务器可以自定义 keepalive 设置和默认值,例如 PING 帧之间的间隔、通道可以存在的最长时间等。 有关可配置选项和特定于语言的示例(演示使用 keepalive 的客户端和服务器应用程序)的完整列表,请参阅 gRPC keepalive 配置规范。

最佳做法

虽然 keepalive 探测可以提高应用程序的容错能力,但也会消耗更多的带宽,这可能会影响网络容量并导致额外的费用。 此外,在移动设备上,网络活动的增加可能会影响电池使用时间。 因此,请务必遵循以下最佳做法:

  • 自定义参数:根据应用程序要求和网络状况调整 keepalive 设置。
  • 应用程序级别的 keepalive:对于加密连接(例如 TLS/SSL),请考虑在应用程序层实施 keepalive 机制,以确保探测通过安全通道发送。
  • 监视和日志记录:实施日志记录以监视由 keepalive 引起的连接关闭情况,以便进行故障排除。
  • 回退机制:对应用程序进行设计以妥善处理断开连接的情况,包括重试逻辑和故障转移策略。