c100m 问题

互联网诞生以来,如何让一台服务器服务更多的用户就成为了软件工程师一直试图解决的难题。c10k1 指的是如何让一台服务器同时服务 10k 个用户的连接。工程师们发明了一种又一种技术方案来挑战性能的极限,Event driven IO,Async IO,Erlang,等等。Whatsapp 用 Erlang 在 24 核心的机器上支持了 2百万 个连接,MigratoryData 用 12 核心的机器,Java 语言,支持了 1千万的连接。虽然技术进步,硬件也在进步,这项性能挑战来到了 c100m,一亿个连接……

一亿个连接是什么概念呢?就算微信这种超大体量的用户,只需要几台机器就可以提供接入服务了。

本文小试牛刀,用 Linux 网络栈来尝试建立和保持尽可能多的连接。但是止步于 TCP 连接的建立,不做数据传输,所以除了好玩,没有实际意义。

实验针对的是主动发起连接的一侧,这样比较好控制速率。

实验的环境是,client 端用一个程序发起 TCP 连接建立,server 端用上一篇博文介绍的 XDP bounce 程序来回复 SYN-ACK 包2,假装建立好连接。然后我们观察 client 端最多可以建立多少连接。仅仅是建立连接而已,不会做数据发送。

Client 端的代码如下,代码来自 c1000k3,稍微做了修改,支持了并发,这样建立连接速度快一些。

/*****************************************************
 * The TCP socket client to help you to test if your
 * OS supports c1000k(1 million connections).
 * @author: ideawu
 * @link: http://www.ideawu.com/
 *****************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <sys/wait.h>

#define LOCAL_PORTS 10000
#define DST_PORTS 1000

int child_process(const char *ip, int dest_port){
    printf("create %d connections to port=%d\n", LOCAL_PORTS, dest_port);
	struct sockaddr_in addr;
	int opt = 1;
	int bufsize;
	socklen_t optlen;
	int connections = 0;

	memset(&addr, sizeof(addr), 0);
	addr.sin_family = AF_INET;
	inet_pton(AF_INET, ip, &addr.sin_addr);

	char tmp_data[10];
    for (int conns=0; conns<LOCAL_PORTS; conns++){
        int port = dest_port;
        //printf("connect to %s:%d\n", ip, port);

        addr.sin_port = htons((short)port);

        int sock;
        if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){
            goto sock_err;
        }
        if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){
            goto sock_err;
        }

        connections ++;

        if(connections % 1000 == 999){
            //printf("press Enter to continue: ");
            //getchar();
            printf("connections to dest_port %d: %d, fd: %d\n", dest_port, connections, sock);
        }
        usleep(1 * 1000);

        bufsize = 0;
        setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
        setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
    }
    
    sleep(3 * 60 * 60);

	return 0;
sock_err:
	printf("connections: %d\n", connections);
	printf("error: %s\n", strerror(errno));
	return 0;
}

int main(int argc, char **argv){
    if(argc <= 2){
        printf("Usage: %s ip port\n", argv[0]);
        exit(0);
    }

    const char *ip = argv[1];
    int base_port = atoi(argv[2]);

    for (int i = 0; i < DST_PORTS; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            child_process(ip, base_port+i);
            exit(0); 
        } else if (pid < 0) {
            perror("fork failed");
            exit(1);
        }
    }

    for (int i = 0; i < DST_PORTS; i++) {
        wait(NULL);
    }

    return 0;
}

代码的逻辑是:fork 1000 个进程,每一个进程对一个 dest port 建立 10000 个连接,总共是1千万个连接数。

实验需要两台机器对着发,因为 server 端使用的是 XDP,性能很高,所以 1核心的机器就足够了。

Client 端我用的 8 核心的机器。

需要进行配置的内容

  1. 修改 ulimit,这样程序才能用更多的 fd:ulimit -n 1048576;
  2. 修改 TCP keepalive,keepalive 会消耗额外的资源:sudo sysctl -w net.ipv4.tcp_keepalive_time=720000
  3. 如果内存不够,通过 swap 来扩展;
  4. 使用 cgroups 限制程序能用的 CPU,防止 ssh 都连不上:
mkdir /sys/fs/cgroup/testlimit
# 8 cores max
echo "2000000 100000" > /sys/fs/cgroup/testlimit/cpu.max
echo $$ > /sys/fs/cgroup/testlimit/cgroup.procs

几分钟就可以跑到 1千万连接,轻轻松松。

下面可以修改参数来尝试 1亿连接数:

#define LOCAL_PORTS 50000
#define DST_PORTS 2000

还是用上面的环境,跑到3千万连接的时候虚拟机崩溃了,只提示 fatal error。

我又去 DigitalOcean 开了台虚拟机,看能不能跑到更高。

这次跑到 5千万连接机器又崩溃了。

约5千万连接数

到之类就没办法继续研究是什么原因了,可能是虚拟化的问题,如果折腾一下物理机环境,说不定可以跑到1亿。

另外我发现一个有趣的地方,如果 abort client 程序,大量连接的 fd 需要 kernel 去回收,会造成所有的 CPU 100% kernel state,机器几乎卡死了,直到连接回收完毕。这部分好像是用 cgroups 限制不住的。所以,如果在不可信的共享的执行环境,通过建立大量的连接再退出进程,说不定有可能恶意挤兑其他租户的资源?

  1. https://en.wikipedia.org/wiki/C10k_problem ↩︎
  2. XDP 实现所有的 TCP 端口都接受 TCP 建立连接 ↩︎
  3. https://github.com/ideawu/c1000k ↩︎


Leave a comment

您的邮箱地址不会被公开。 必填项已用 * 标注