一 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\r\n" \
"Connection: keep-alive\r\n" \
"Content-Type: text/html; charset=utf-8\r\n" \
"Transfer-Encoding: chunked\r\n" \
"\r\n"
#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 \n " , usr_num );
}
 
static void dec_usr_num () {
usr_num -- ;
 
printf ( "~online user %d \n " , 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 \r\n <script>_('%s');</script> \r\n " , (( 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 ! \n " );
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 \n " );
free_res ( loop , w );
return ;
}
 
if ( r < 0 ) {
fprintf ( stderr , "read error \n " );
ev_io_stop ( EV_A_ w );
free_res ( loop , w );
return ;
}
 
if ( r == 0 ) {
fprintf ( stderr , "client disconnected. \n " );
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 \r\n %s \r\n " , ( 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 ! \n " );
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 \n " );
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 raw server.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 \t Bytes: %d \t Closed: %d \n " , 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 \t -> \t %s \n " , 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 ( " \n Chunks: %d \t Bytes: %d \t Closed: %d \n " , chunks_recvd , bytes_recvd , closed );
usleep ( SLEEP_MS * 1000 );
}
 
event_dispatch ();
return 0 ;
}
view raw client1.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万多个连接,是一个问题,如何克服,下一篇解决此问题,并且还会遇到文件句柄的受限问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值