zoukankan      html  css  js  c++  java
  • 100万并发连接服务器笔记之准备篇

    前言

    测试一个非常简单服务器如何达到100万(1M=1024K连接)的并发连接,并且这些连接一旦连接上服务器,就不会断开,一直连着。 
    环境受限,没有服务器,刚开始都是在自己的DELL笔记本上测试,凭借16G内存,和优秀的vmware workstation虚拟机配合,另外还得外借别人虚拟机使用,最终还得搭上两台2G内存的台式机(安装centos),最终才完成1M并发连接任务。

    • 测试程序也很简陋,一个C语言所写服务器程序,没有任何业务存在,收到请求后发送一些头部,不断开连接
    • 测试端程序也是使用C语言所写,发送请求,然后等待接收数据,仅此而已
    • 服务器端/测试端内存都受限(8G不够使用),要想完成1024K的目标,需要放弃一些东西,诸如业务不是那么完整
    • 一台分配10G内存Centos服务器,两台分配6G内存Centos测试端,两台2G内存Centos测试端
    • 假如热心的您可以提供丰富的服务器资源,那就再好不过了。
    • 理论上200万的并发连接(IO密集型),加上业务,40G-50G的内存大概能够保证

    说明

    以前也做过类似的工作,量不大,没记录下来,一些压力测试和调优,随着时间流逝,早已忘记。这次是从零开始,基本上所有过程都会记录,一步一步,每一步都会遇到问题,并且给出相关解决问题的方法,最终完成目标。 
    为了方便,服务器端程序和客户端测试程序,都是使用C语言,不用像JAVA一样需要预先指定内存,感觉麻烦。使用较为原始的语言来写,可以避免不必要的调优工作。这中间,可能会穿插Java代码的思考方式。

    可能需要懂点Linux,C,Java,假如您有更好的做法,或者建议,请直接告知,谢谢。

    Linux系统

    测试端和服务器端都选用较为熟悉的64位Centos 6.4,32位系统最多支持4G内存,太受限。IO密集型应用,对CPU要求不是很高。另外服务器确保安装上gcc,那就可以开工了。 
    所有端系统一旦安装完之后,默认不做任何设置。

    服务器端程序

    服务器端程序依赖libev框架,需要提前编译,然后存放到相应位置。下面是具体服务器端代码: 

    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <err.h>
     
    #include <unistd.h>
     
    #include "../include/ev.h"
     
    #define HTMLFILE_RESPONSE_HEADER
    "HTTP/1.1 200 OK "
    "Connection: keep-alive "
    "Content-Type: text/html; charset=utf-8 "
    "Transfer-Encoding: chunked "
    " "
    #define HTMLFILE_RESPONSE_FIRST
    "<html><head><title>htmlfile chunked example</title><script>var _ = function (msg) { document.getElementById('div').innerHTML = msg; };</script></head><body><div id="div"></div> "
     
    static int server_port = 8000;
     
    struct ev_loop *loop;
    typedef struct {
    int fd;
    ev_io ev_read;
    } client_t;
     
    ev_io ev_accept;
     
    static int usr_num;
    static void incr_usr_num() {
    usr_num ++;
    printf("online user %d ", usr_num);
    }
     
    static void dec_usr_num() {
    usr_num --;
     
    printf("~online user %d ", usr_num);
    }
     
    static void free_res(struct ev_loop *loop, ev_io *ws);
     
    int setnonblock(int fd) {
    int flags = fcntl(fd, F_GETFL);
    if (flags < 0)
    return flags;
     
    flags |= O_NONBLOCK;
    if (fcntl(fd, F_SETFL, flags) < 0)
    return -1;
     
    return 0;
    }
     
    static int format_message(const char *ori_message, char *target_message) {
    return sprintf(target_message, "%X <script>_('%s');</script> ", ((int)strlen(ori_message) + 23), ori_message);
    }
     
    static void write_ori(client_t *client, char *msg) {
    if (client == NULL) {
    fprintf(stderr, "the client is NULL ! ");
    return;
    }
     
    write(client->fd, msg, strlen(msg));
    }
     
    static void write_body(client_t *client, char *msg) {
    char body_msg[strlen(msg) + 100];
    format_message(msg, body_msg);
     
    write_ori(client, body_msg);
    }
     
    static void read_cb(struct ev_loop *loop, ev_io *w, int revents) {
    client_t *client = w->data;
    int r = 0;
    char rbuff[1024];
    if (revents & EV_READ) {
    r = read(client->fd, &rbuff, 1024);
    }
     
    if (EV_ERROR & revents) {
    fprintf(stderr, "error event in read ");
    free_res(loop, w);
    return ;
    }
     
    if (r < 0) {
    fprintf(stderr, "read error ");
    ev_io_stop(EV_A_ w);
    free_res(loop, w);
    return;
    }
     
    if (r == 0) {
    fprintf(stderr, "client disconnected. ");
    ev_io_stop(EV_A_ w);
    free_res(loop, w);
    return;
    }
     
    write_ori(client, HTMLFILE_RESPONSE_HEADER);
     
    char target_message[strlen(HTMLFILE_RESPONSE_FIRST) + 20];
    sprintf(target_message, "%X %s ", (int)strlen(HTMLFILE_RESPONSE_FIRST), HTMLFILE_RESPONSE_FIRST);
     
    write_ori(client, target_message);
    incr_usr_num();
    }
     
    static void accept_cb(struct ev_loop *loop, ev_io *w, int revents) {
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int client_fd = accept(w->fd, (struct sockaddr *) &client_addr, &client_len);
    if (client_fd == -1) {
    fprintf(stderr, "the client_fd is NULL ! ");
    return;
    }
     
    client_t *client = malloc(sizeof(client_t));
    client->fd = client_fd;
    if (setnonblock(client->fd) < 0)
    err(1, "failed to set client socket to non-blocking");
     
    client->ev_read.data = client;
     
    ev_io_init(&client->ev_read, read_cb, client->fd, EV_READ);
    ev_io_start(loop, &client->ev_read);
    }
     
    int main(int argc, char const *argv[]) {
    int ch;
    while ((ch = getopt(argc, argv, "p:")) != -1) {
    switch (ch) {
    case 'p':
    server_port = atoi(optarg);
    break;
    }
    }
     
    printf("start free -m is ");
    system("free -m");
    loop = ev_default_loop(0);
    struct sockaddr_in listen_addr;
    int reuseaddr_on = 1;
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0)
    err(1, "listen failed");
    if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr_on, sizeof(reuseaddr_on)) == -1)
    err(1, "setsockopt failed");
     
    memset(&listen_addr, 0, sizeof(listen_addr));
    listen_addr.sin_family = AF_INET;
    listen_addr.sin_addr.s_addr = INADDR_ANY;
    listen_addr.sin_port = htons(server_port);
     
    if (bind(listen_fd, (struct sockaddr *) &listen_addr, sizeof(listen_addr)) < 0)
    err(1, "bind failed");
    if (listen(listen_fd, 5) < 0)
    err(1, "listen failed");
    if (setnonblock(listen_fd) < 0)
    err(1, "failed to set server socket to non-blocking");
     
    ev_io_init(&ev_accept, accept_cb, listen_fd, EV_READ);
    ev_io_start(loop, &ev_accept);
    ev_loop(loop, 0);
     
    return 0;
    }
     
    static void free_res(struct ev_loop *loop, ev_io *w) {
    dec_usr_num();
    client_t *client = w->data;
    if (client == NULL) {
    fprintf(stderr, "the client is NULL !!!!!!");
    return;
    }
     
    ev_io_stop(loop, &client->ev_read);
     
    close(client->fd);
     
    free(client);
    }
    view rawserver.c hosted with ❤ by GitHub

    编译

    gcc server.c -o server ../include/libev.a -lm
    

    运行

    ./server -p 8000
    

    在源码中默认指定了8000端口,可以通过-p进行指定新的端口。 开启了8000端口进行监听请求,http协议处理类似于htmlfile chunked块编码传输。

    测试服务器端程序

    测试程序使用libevent框架,因其使用简单,提供丰富易用接口,但需要提前下载,手动安装:

    wget https://github.com/downloads/libevent/libevent/libevent-2.0.21-stable.tar.gz
    tar xvf libevent-2.0.21-stable.tar.gz
    cd libevent-2.0.21-stable
    ./configure --prefix=/usr
    make
    make install
    

    注意make和make install需要root用户。

    测试端程序

    client1.c 源码:

    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
    #include <sys/types.h>
    #include <sys/time.h>
    #include <sys/queue.h>
    #include <stdlib.h>
    #include <err.h>
    #include <event.h>
    #include <evhttp.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <time.h>
    #include <pthread.h>
     
    #define BUFSIZE 4096
    #define NUMCONNS 62000
    #define SERVERADDR "192.168.190.133"
    #define SERVERPORT 8000
    #define SLEEP_MS 10
     
    char buf[BUFSIZE];
     
    int bytes_recvd = 0;
    int chunks_recvd = 0;
    int closed = 0;
    int connected = 0;
     
    void chunkcb(struct evhttp_request *req, void *arg) {
    int s = evbuffer_remove( req->input_buffer, &buf, BUFSIZE );
    bytes_recvd += s;
    chunks_recvd++;
    if (connected >= NUMCONNS && chunks_recvd % 10000 == 0)
    printf(">Chunks: %d Bytes: %d Closed: %d ", chunks_recvd, bytes_recvd, closed);
    }
     
    void reqcb(struct evhttp_request *req, void *arg) {
    closed++;
    }
     
    int main(int argc, char **argv) {
    event_init();
    struct evhttp *evhttp_connection;
    struct evhttp_request *evhttp_request;
    char path[32]; // eg: "/test/123"
    int i;
    for (i = 1; i <= NUMCONNS; i++) {
    evhttp_connection = evhttp_connection_new(SERVERADDR, SERVERPORT);
    evhttp_set_timeout(evhttp_connection, 864000); // 10 day timeout
    evhttp_request = evhttp_request_new(reqcb, NULL);
    evhttp_request->chunk_cb = chunkcb;
    sprintf(&path, "/test/%d", ++connected);
    if (i % 100 == 0) printf("Req: %s -> %s ", SERVERADDR, &path);
    evhttp_make_request( evhttp_connection, evhttp_request, EVHTTP_REQ_GET, path );
    evhttp_connection_set_timeout(evhttp_request->evcon, 864000);
    event_loop( EVLOOP_NONBLOCK );
    if ( connected % 200 == 0 )
    printf(" Chunks: %d Bytes: %d Closed: %d ", chunks_recvd, bytes_recvd, closed);
    usleep(SLEEP_MS * 1000);
    }
     
    event_dispatch();
    return 0;
    }
    view rawclient1.c hosted with ❤ by GitHub

    备注:这部分代码参考了A Million-user Comet Application with Mochiweb, Part 3 ,根据需要有所修改。

    编译

    gcc -o client1 client1.c -levent
    

    运行

    ./client1
    

    可能在64位系统会遇到找不到libevent-2.0.so.5情况,需要建立一个软连接

    ln -s /usr/lib/libevent-2.0.so.5 /lib64/libevent-2.0.so.5
    

    即可自动连接IP地址为192.168.190.133:8000的服务器端应用。

    第一个遇到的问题:文件句柄受限

    测试端程序输出

    看看测试端程序client1输出的错误信息:

    Chunks: 798 Bytes: 402990 Closed: 0
    Req: 192.168.190.133 -/test/900
    Req: 192.168.190.133 -/test/1000
    Chunks: 998 Bytes: 503990 Closed: 0
    [warn] socket: Too many open files
    [warn] socket: Too many open files
    [warn] socket: Too many open files
    

    服务器端程序输出

    服务器端最后一条日志为

    online user 1018
    

    两边都遇到了文件句柄打开的情况。 
    在服务器端查看已经连接,并且端口号为8000的所有连接数量:

    netstat -nat|grep -i "8000"|wc -l 
    1019
    

    但与服务器端输出数量对不上,增加所有已经建立连接的选项:

    netstat -nat|grep -i "8000"|grep ESTABLISHED|wc -l 
    1018
    

    那么剩下的一条数据到底是什么呢?

    netstat -nat|grep -i "8000"|grep -v ESTABLISHED
    tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 

    也就是server.c监听的端口,数量上对的上。

    在测试服务器端,查看测试进程打开的文件句柄数量

    lsof -n|grep client1|wc -l
    1032
    

    再次执行

    ulimit -n
    1024
    

    也是就是client1应用程序共打开了1032个文件句柄,而不是1024,为什么? 
    把当前进程所有打开的文件句柄保存到文件中,慢慢研究 lsof -n|grep client1 > testconnfinfo.txt

    导出的文件可以参考: https://gist.github.com/yongboy/5260773
    除了第一行,我特意添加上供友善阅读的头部列定义,也就是1032行信息,但是需要注意头部:

    
    COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
    client1 3088 yongboy cwd DIR 253,0 4096 800747 /home/yongboy/workspace/c_socket.io_server/test
    client1 3088 yongboy rtd DIR 253,0 4096 2 /test_conn
    client1 3088 yongboy txt REG 253,0 9697 799991 /home/yongboy/workspace/c_socket.io_server/test/test_conn_1
    client1 3088 yongboy mem REG 253,0 156872 50404 /lib64/ld-2.12.so
    client1 3088 yongboy mem REG 253,0 1922152 78887 /lib64/libc-2.12.so
    client1 3088 yongboy mem REG 253,0 145720 76555 /lib64/libpthread-2.12.so
    client1 3088 yongboy mem REG 253,0 47064 69491 /lib64/librt-2.12.so
    client1 3088 yongboy mem REG 253,0 968730 26292 /usr/lib/libevent-2.0.so.5.1.9
    client1 3088 yongboy 0u CHR 136,2 0t0 5 /dev/pts/2
    client1 3088 yongboy 1u CHR 136,2 0t0 5 /dev/pts/2
    client1 3088 yongboy 2u CHR 136,2 0t0 5 /dev/pts/2
    client1 3088 yongboy 3u REG 0,9 0 4032 anon_inode
    client1 3088 yongboy 4u unix 0xffff88007c82f3c0 0t0 79883 socket
    client1 3088 yongboy 5u unix 0xffff880037c34380 0t0 79884 socket
    client1 3088 yongboy 6u IPv4 79885 0t0 TCP 192.168.190.134:58693->192.168.190.133:irdmi (ESTABLISHED)
    client1 3088 yongboy 7u IPv4 79889 0t0 TCP 192.168.190.134:58694->192.168.190.133:irdmi (ESTABLISHED)
    client1 3088 yongboy 8u IPv4 79891 0t0 TCP 192.168.190.134:58695->192.168.190.133:irdmi (ESTABLISHED)
    client1 3088 yongboy 9u IPv4 79893 0t0 TCP 192.168.190.134:58696->192.168.190.133:irdmi (ESTABLISHED)
    

    可以看到文件句柄是从0u开始,0u上面的8个(5个mem + 3个启动)进程,1032 - 8 = 1024个文件句柄,这样就和系统限制的值吻合了。

    root用户编辑/etc/security/limits.conf文件添加:

    * soft nofile 1048576
    * hard nofile 1048576
    
    • soft是一个警告值,而hard则是一个真正意义的阀值,超过就会报错。
    • soft 指的是当前系统生效的设置值。hard 表明系统中所能设定的最大值
    • nofile - 打开文件的最大数目
    • 星号表示针对所有用户,若仅针对某个用户登录ID,请替换星号

    注意: 
    1024K x 1024 = 1048576K = 1M,1百万多一点。

    备注:测试端和服务器端都需要作此设置,保存退出,然后reboot即可生效。

    第一个问题,就这样克服了。再次运行 /client1测试程序,就不会出现受打开文件句柄的限制。但大概在测试端打开对外28200个端口时,会出现程序异常,直接退出。

    段错误

    这个也是程序没有处理端口不够用的异常,但可以通过增加端口进行解决。

    备注: 但测试端单机最多只能打开6万多个连接,是一个问题,如何克服,下一篇解决此问题,并且还会遇到文件句柄的受限问题。

  • 相关阅读:
    【前端积累】Awesome初识
    【Python系列】Python3获取控制台输入
    【linux系列】Centos下安装mysql数据库
    Intellij 部署项目java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener
    【大数据系列】节点的退役和服役[datanode,yarn]
    【大数据系列】使用api修改hadoop的副本数和块大小
    【规范】alibaba编码规范阅读
    【大数据系列】hadoop上传文件报错_COPYING_ could only be replicated to 0 nodes
    【分布式系列之ActiveMq】ActiveMq入门示例
    类的生命周期
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13318388.html
Copyright © 2011-2022 走看看