nginx开启reuse port后,据说benchmark能跑很多。那么为啥nginx能在reuseport开启的情况下性能提升不少呢?nginx使用reuseport需要注意哪些问题呢?
1.摘要
reuseport是在nginx 1.9.1里提供了支持,官方更是提供了篇幅介绍reuseport带来的好处,主要是benchmark的提升。
具体详情可见:https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/
我们这里想介绍一下,在nginx里是如何使用reuseport功能带来性能提升的
2.reuseport的原理
在3.9内核以前,为了支持多进程模型像haproxy,nginx等,大家不约而同的采用的fork的做法,即在父进程里,监听一个IP+port。
然后fork出N个子进程,子进程天然继承了父进程的listen socket的句柄,即可以执行accept操作了。
但因为是fork出来的,所以在kernel里,仍然是一个句柄,多个进程执行accept还是有竞争关系,所以nginx需要配置accept_mutex这样的开关
当开启reuseport后,每个监听地址将会有多个句柄,具体来说是一个worker一个,这样每个worker关心的listen socket就独立开了,自己搞定自己的事,避免了多进程的竞争。
3.reuseport在nginx的使用
通常情况下,使用reuseaddr都是启动多个进程,大家绑定相同的IP和port,然后就可以无限发挥reuseport的特性了。
但nginx毕竟还是采用了fork的模型。那么个是如何充分利用reuseport的呢?看代码吧。
在ngx_clone_listening里有这样的代码:
1 | for (n = 1; n < ccf->worker_processes; n++) { |
(注意是从1开始的,因为master会创建一个worker是0的listener)
也就是解析完配置文件后,会根据worker的个数fork出来多个listener对象,统一扔到数组里,那么啥时候打开监听呢?
ngx_init_cycle
—>ngx_open_listening_sockets
—>bind
—>listen
因为设置了reuseport,所以数组里塞进去的ip port重复的listener可以创建好。
比如有8个worker,会创建出8个listen socket。
那么剩下来的问题就是,如何让一个listen socket和worker进程绑定。
那么就看ngx_event_process_init,worker进程初始化event模块的时候,会调用这个函数。
1 | #if (NGX_HAVE_REUSEPORT) |
这里可以看出,我只把自己worker对应的listen socket加入到epoll里去。
4.reuseport在nginx使用中遇到的问题
先说现象:
nginx从reuseport升级为非reuseport,以及从多worker升级为少worker都会有大量性能下降。
这里还是需要介绍一下reuseport的升级的流程,好trick。
升级的时候,也就是-USR2的时候,old maste启动新master的时候,会把所有listen socket的句柄们放在新进程的环境变量里。如果reuseport,举监听80端口为例,如果开启了4个worker,那么环境变量则存了4个句柄,格式为
句柄id1;句柄id2;句柄id3;句柄id4。
新的master启动后会把这个4个句柄读出来,注意,这4个句柄在新进程里也是合法的,然后调用各种syscall获得这个句柄的信息
- ls[i].sockaddr (调用getsockname())
- ls[i].addr_text_max_len
- ls[i].addr_text
- ls[i].backlog
- ls[i].rcvbuf (调用getsockopt())
- ls[i].sndbuf (调用getsockopt())
- ls[i].accept_filter
- ls[i].deferred_accept
这个信息是放在old_cycle里的,然后加载配置文件,配置文件里也依然会监听80端口,这时新的cycle的listening数组里会有一个ngx_listening_t,但是在ngx_http_optimize_servers里会间接调用ngx_clone_listening,来clone出 worker个数的listen句柄,但这时候因为还没有调用listen函数,所以ls[i]的fd是空,肯定不会走后面的listen函数的,因为环境变量已经把老的句柄传递过来了,直接复用即可,而且如果不复用,重新listen的话会出问题的,因为老的句柄在内核有queue,确没人accept。
那么是哪里为新的ngx_listening_t赋值的呢?就是在init_cycle的后面1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27for (n = 0; n < cycle->listening.nelts; n++) {
for (i = 0; i < old_cycle->listening.nelts; i++) {
if (ls[i].ignore) {
continue;
}
if (ls[i].remain) {
continue;
}
if (ls[i].type != nls[n].type) {
continue;
}
if (ngx_cmp_sockaddr(nls[n].sockaddr, nls[n].socklen,
ls[i].sockaddr, ls[i].socklen, 1)
== NGX_OK)
{
nls[n].fd = ls[i].fd;
nls[n].previous = &ls[i];
ls[i].remain = 1;
if (ls[i].backlog != nls[n].backlog) {
nls[n].listen = 1;
}
...................
对比新的cycle和旧的cycle,如果监听的地址一样,就拿来复用,已经复用的remain会置为1,下一个相同地址的就不会复用了。比如老的cycle里因为reuseport,一个ip+80,开启了4个 listen句柄,新的也开启4个listen结构,在上面的二层循环里,就一次把这个4个句柄赋值给新的 ngx_listening_t的fd。
这里的remain名字起的真是烂啊,我觉得叫copied/inherited都可以。
5.结论
reuseport功能会给nginx的性能带来很大的提升。但是升级的时候由于老的master的延迟退出,会导致在老的master退出之前,性能骤降,这和本来的on the fly upgrade 风格实在是落差不小。
也许是我理解有误,知道的小伙伴可以mail我。qzzhou$126.com