Libevent是一个轻量级高效的开源高性能网络库,很多公司都基于该网络库进行开发,我之前参与过的几个的项目客户端的网络底层以及服务端的网络库都是基于该网络库的开发,并且取得良好的性能,并未遇到明显的性能瓶颈。我相信很多大公司也是跟我一样做出相同明智的选择。因此,基于该库进行一系列的扩展也是理所当然。前几天我们客户提出了需求,需要服务器提供http服务,以便接受各种http的请求,这种http请求有其特殊性,一般是短链接,瞬间流量较大,需要抵抗瞬间峰值流量。因为我们服务器是基于libevent的开发,很自然想到了利用libevent的原始库进行支持,因为他底层是支持http服务器的一些接口,很自然写起来代码。传统的代码还是类似的写法,都是基于单线程的,意思是说,一个线程监听在ip以及对外开放的http端口上。伪代码如下:
short http_port = 8081;
char *http_addr = "127.0.0.1";
if (argc > 1)
{
strcpy(filename,argv[1]);
printf("Using %s\n",filename);
}
else
{
strcpy(filename,DEFAULT_FILE);
}
struct event_base * base = event_base_new();
struct evhttp * http_server = evhttp_new(base);
if(!http_server)
{
return -1;
}
int ret = evhttp_bind_socket(http_server,http_addr,http_port);
if(ret!=0)
{
return -1;
}
evhttp_set_gencb(http_server, generic_handler, NULL);
read_file();
load_file(base);
printf("http server start OK! \n");
event_base_dispatch(base);
evhttp_free(http_server);
以上基本上能满足很多初级用户需求,也是在网上能找到最多的处理方法,但是并不能得到大多数用户满意,因为跑在一台32核,带宽千兆的一台服务器上,只有一个线程接受前端大量的http请求,是一种浪费,也不能给发薪水的老板交代。所以很自然我想到要提高性能,以便抵抗更大的http请求的压力。我的设想是这样的,前端多个线程监听http的端口上,接受各种http的请求,一旦有新的http请求来了,其中一个线程立即接受处理,并且解析请求的性质以及请求的合法性。处理好之后,立即交给一个任务线程池,待任务线程处理好任务之后,立即返回给原线程处理以reply给http请求的客户端。如下图所示:
之所以这样设计的原因是因为
1. 多个线程监听在这个端口上,可以达到同时能处理多个http请求,而不像单线程的条件,同时只能处理一个http请求
2. 当一个线程处理http请求时候,解析数值,交出任务给任务处理线程之后,马上可以接着处理新的http请求,这样加大了http请求的处理性能
这两点是我认为可以大幅提高处理http请求性能的关键所在,所以势必要修改老的逻辑处理方式,我认为做到我说的目的,有两个难点
但是我的做法有两点比较麻烦
第一:如何利用多线程监听同一个端口上,而不至于出现线程错误。
第二:如何将线程交给任务线程处理,并且当任务处理好之后,能轻松交回给原先http请求事件触发线程处理,而不至于给别的线程,任何别的线程处理,必然会导致内部线程冲突紊乱,直接导致崩溃。
我们换了一个思考方式,我们事先创建好一个tcp的对应的fd,并且监听在http服务器的ip和端口上,该tcp的fd是我们的关键。截取了部分代码:
evutil_socket_t sock_fd = ::socket(AF_INET,SOCK_STREAM, 0); //创建tcp的fd
evutil_make_socket_nonblocking(fd);//设置非阻塞
InetAddress addr(ip, port); //设置监听地址和端口
Socket::bind(sock_fd, addr.get_sock_addr() );//bind刚刚创建的fd以及地址
Socket::listen(sock_fd, 10);//创建监听
监听sock_fd之后,我们就可以创建多个evhttp来接受同一个sock_fd的各种http请求,并且每一个evhttp从属于一个线程
struct evhttp* httpd = evhttp_new(loop->base);//创建新的evhttp于当前线程相关,即唯一一个线程处理所有的evhttp线程事件
evhttp_accept_socket(httpd, sock_fd);//将evhttp接受sock_fd的各种请求
evhttp_set_allowed_methods(httpd,EVHTTP_REQ_GET|EVHTTP_REQ_PUT|EVHTTP_REQ_DELETE);//接受各种http类型的请求
evhttp_set_gencb(httpd, default_generic_handler, this);//设置http请求的回调函数
接下来的工作就是如何将多个evhttp绑定到不同的线程上,以及将事件线程分发到任务线程以及返回给原事件现场,我这里使用的是muduo库的的runinloop系列函数,可以很轻松的将线程交付给另外一个线程去执行。并且在抛出任务的时候,带上原线程的线程信息,以方便一会能找回到原线程上。
在这种条件下我做了性能测试,利用jmeter做的性能测试,机器配置并不是很好,基本上接受是在20w+每秒的http请求,满足我们的需求是毫无压力。