3 min read

网络性能优化:TFO

今天大多数的web服务都是基于TCP协议对外提供交互,TCP协议是诞生在网络环境普遍很差的年代。传输时延由客户端和服务端之间往返时间(RTT)以及数据传输需要的往返次数决定。过去的几十年网络带宽有大幅增长,但传输时延还是受到光速的限制,所以谷歌公司在2011年的TCP FAST OPEN论文中介绍TCP协议的扩展——TCP FAST OPEN。

背景

TCP标准中只允许握手建立连接后进行数据传输,这就意味着在应用层数据交互之前有一个额外的RTT。

TCP Handshake
而这个额外RTT是传播时延的一部分。下图是谷歌公司统计的google.com请求中TCP握手占总请求时延的百分比。可以看到cold request(新TCP连接)的握手时延占比明显高出很多。
TCP Handshake Cast
这个问题的解决方案一个是应用层协议复用TCP,请求结束后连接不关闭,缓存给下次使用。但实际效果不理想,谷歌公司对一些大型CDN的研究表明,实际生产中每个TCP连接平均只有2.4个HTTP请求。

而另外一种解决方式就是在握手时期就进行数据传输,直接消除了额外的RTT。TCP标准中也是在握手第三阶段将数据包放入了SYN包中,所以应用层数据放入第一、二阶段SYN包中的设计理论上也是可行的。然而,这个想法的直接实现容易受到拒绝服务 (DoS) 攻击,并且可能面临重复或陈旧 SYN 的困难。谷歌的论文中提出了一种称为 TCP 快速打开 (TFO) 的新 TCP 机制,它可以在 TCP 的初始握手期间安全地交换数据。 TFO 的核心是一个安全 cookie,服务器使用它来验证启动 TFO 连接的客户端。

TCP Fast Open

实现

(被抓包程序代码在最后)

新TCP连接时,客户端发送带有 Fast Open Cookie Request TCP 选项的 SYN 数据包。

1

服务器通过在密钥下加密客户端的 IP 地址来生成 cookie。服务器使用 SYN-ACK 响应客户端,该 SYN-ACK 在 TCP 选项字段中包含生成的 Fast Open Cookie。

2

客户端缓存 cookie,以便将来 TFO 连接到同一服务器。

要使用从服务器接收到的TFO cookie,客户端执行以下步骤:

  1. 客户端发送带有缓存的 Fast Open cookie(作为 TCP 选项)以及应用程序数据的 SYN。
  2. 服务器通过解密并比较 IP 地址或通过重新加密 IP 地址并与接收到的 cookie 进行比较来验证 cookie。
    1. 如果 cookie 有效,服务器发送一个 SYN-ACK 确认 SYN 和数据。数据被传送到服务器应用程序。
    2. 否则,服务器丢弃数据,并发送仅确认 SYN 序列号的 SYN-ACK。连接通过常规的 3WHS 进行。
  3. 如果SYN包中的数据被接受,服务器可能会在收到客户端的第一个ACK之前向客户端发送额外的响应数据段。
  4. 客户端发送确认服务器 SYN 的 ACK。如果客户端的数据未被确认,则使用 ACK 重新传输。
  5. 然后连接像正常的 TCP 连接一样进行。

3
可以看到第二次TCP连接的握手第一步携带了上次服务端返回的cookie,并且携带了数据的。

TFO是TCP协议的experimental update,所以协议要求TCP实现默认必须禁止TFO,Linux中打开方式如下:(确保内核版本在3.17及以上)

在/etc/sysctl.conf文件中添加
net.ipv4.tcp_fastopen=3

Linux实现中,tcp_fastopen值如下

#define	TFO_CLIENT_ENABLE	1
#define	TFO_SERVER_ENABLE	2
#define	TFO_CLIENT_NO_COOKIE	4	/* Data in SYN w/o cookie option */

设置为3,则客户端服务端均开启TFO功能。

最后是谷歌公司针对TFO做的对比数据,可以看到TFO机制明显缩短了传输时延,并且RTT越大的情况下越明显。

result

广域网TFO可用性

由于中间路由器、交换机等设备可能不支持,导致TFO在互联网环境下可能失败,从而弱化到标准的TCP握手,甚至导致更恶劣的重传,不过从Anna Maria Mandalari博士的测试数据来看,只有2.18%的SYN数据包会被直接丢弃,引起重传。 下面是博士团队在2015年对18个国家、22个ISP环境下进行TFO的测试数据

result

附录

server.c

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int Listen() {
    int rc;
    struct addrinfo *listp, *p, hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    if ((rc = getaddrinfo("0.0.0.0", "8080", &hints, &listp)) != 0) {
        printf("getaddrinfo: %s\n", gai_strerror(rc));
        return -1;
    }

    int ln;
    socklen_t ai_addrlen;
    struct sockaddr ai_addr;

    for (p = listp; p; p = listp->ai_next) {
        if ((ln = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
            continue;
        }
        if ((rc = bind(ln, p->ai_addr, p->ai_addrlen)) == -1) {
            printf("bind: %d %s\n", errno, strerror(errno));
            close(ln);
            return -1;
        }
        int qlen = 5;
        setsockopt(ln, p->ai_protocol, 23, &qlen, sizeof(qlen));
        if ((rc = listen(ln, 128)) == -1) {
            printf("listen: %d %s\n", errno, strerror(errno));
            close(ln);
            return -1;
        }

        ai_addr = *p->ai_addr;
        ai_addrlen = p->ai_addrlen;
    }
    freeaddrinfo(listp);
    return ln;
}

void request(int client) {
    char buf[1024];
    recv(client, buf, 4, 0);
    printf("%s\n", buf);
}

void response(int client) {
    char buf[1024];

    sprintf(buf, "pong");
    send(client, buf, strlen(buf), 0);
}

int main(int argc, char **argv) {
    int ln = Listen();
    if (ln == -1) {
        return -1;
    }
    for (;;) {
        struct sockaddr ai_addr;
        socklen_t ai_addrlen;
        int client = accept(ln, &ai_addr, &ai_addrlen);
        if (client == -1) {
            printf("accept: %d %s\n", errno, strerror(errno));
            break;
        }
        struct sockaddr_in addr;
        socklen_t addrlen = sizeof(addr);
        if (getpeername(client, (struct sockaddr *)&addr, &addrlen) == -1) {
            printf("get remote addr: %d %s\n", errno,
                   strerror(errno));
            break;
        }
        int port = ntohs(addr.sin_port);
        char *ip = inet_ntoa(addr.sin_addr);
        printf("client addr: %s:%d\n", ip, port);

        request(client);
        response(client);
        close(client);
    }
    return 0;
}

client.c

#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

void request(int client) {
    char buf[1024];

    sprintf(buf, "ping");
    send(client, buf, strlen(buf), 0);
}

void response(int client) {
    char buf[1024];
    recv(client, buf, 4, 0);
    printf("%s\n", buf);
}

int main() {
    struct sockaddr_in serv_addr;
    struct hostent *server;

    // 第一次
    int client = socket(AF_INET, SOCK_STREAM, 0);
    server = gethostbyname("localhost");

    bzero((char *)&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr,
          server->h_length);
    serv_addr.sin_port = htons(8080);

    sendto(client, "ping", 4, MSG_FASTOPEN /*MSG_FASTOPEN*/,
           (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    response(client);
    close(client);

    // 第二次
    client = socket(AF_INET, SOCK_STREAM, 0);
    server = gethostbyname("localhost");

    bzero((char *)&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr,
          server->h_length);
    serv_addr.sin_port = htons(8080);

    sendto(client, "ping", 4, MSG_FASTOPEN /*MSG_FASTOPEN*/,
           (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    response(client);
    close(client);
}