李乐
引子
咱们为什么须要学习Nginx呢?高性能,高稳固,优雅的模块化编程等就不提了,就说一个理由:Nginx是目前最受欢迎的web服务器,据统计,寰球均匀每3个网站,就有一个应用Nginx。如果你不懂Nginx,日常很多工作可能都无奈发展。
比方,最近看到过这么一句话:"Nginx master过程接管到客户端申请,转发给worker过程解决"。如果你不懂Nginx,就有可能也闹出相似的笑话。
比方,当你须要搭建一套webserver时,可能根本的一些配置都一头雾水。location匹配规定你分明吗,正则匹配、最大前缀匹配和准确匹配的程序以及优先级你能记得住吗?反向代理proxy_pass以及fastcgi_pass你晓得怎么配置吗?限流负载平衡等基本功能又怎么配置呢?一大堆配置有时真的让人脑仁疼。
比方,线上环境Nginx曾呈现"no live upstreams"。顾名思义,Nginx认为所有上游节点都挂掉了,此时Nginx间接向客户端返回502,而不会申请上游节点。这时候你该想想,Nginx是根据什么判断上游节点都挂掉了?呈现这种谬误后,Nginx又是怎么复原的呢?
再比方,线上环境高峰期Nginx还呈现过这种谬误:"Resource temporarily unavailable",对应的错误码是EAGAIN。非阻塞读写socket遇到EAGAIN不是通常稍等再尝试吗?其实通过源码里的一句正文就能霎时明确了:"Linux returns EAGAIN instead of ECONNREFUSED for unix sockets if listen queue is full"。原来如此,Nginx和FPM之间是通过域套接字建设连贯的,监听队列满了,零碎间接返回的是EAGAIN,而不是咱们平时理解的ECONNREFUSED;而Nginx在发动连贯connect时,如果返回EAGAIN间接完结申请返回502。
本篇文章旨在让你对Nginx可能有个零碎的意识,理解其外围性能的实现思路,以及如何切入Nginx源码的学习。在遇到问题时,至多让你直到能够去哪寻找你想要的答案。次要波及以下几个方面:
- 如何开始Nginx源码的学习;
- 模块化编程;
- master与worker过程模型;
- Nginx事件驱动模型;
- HTTP解决流程之11个阶段;
- location匹配规定;
- upstream与负载平衡;
- proxy_pass;
- fastcgi_pass与FPM;
- 限流;
- 案例剖析:502问题剖析。
如何开始Nginx源码的学习
作为一名初学者,如何去上手浏览Nginx源码呢?这还不简略,从main办法动手,一行一行看呗。如何你这么做了,也保持了一段时间,我给你点个赞,至多我是做不到的。不过,我置信大部分人是保持不了几天的。数十万行Nginx源码,岂是短时间就能钻研透的?如果长时间都没有获得显著功效,大多数人都会抉择放弃吧。
我个别是怎么浏览源码的呢?
1)入手GDB,入手GDB,入手GDB;重要的话说三遍;
逻辑比拟艰涩,各种判断分支太简单,回调handler不晓得是什么。GDB调试其实真的很简略,b打断点,p命令看变量名称,bt命令看调用栈,c继续执行至下一个断点,n执行到下一行。笔者个别罕用的也就这几个命令。
2)带着问题去浏览,最好能带着答案去浏览。
带着问题去浏览,就有了一条主线,只须要关注你须要关注的。比方Nginx是一个事件驱动程序,那么第一个问题就是去摸索他的事件循环,事件循环中无非就是通过epoll_wait()期待事件的产生,而后执行事件回调handler。其余逻辑都可不用过多关注。
有了答案,你就能更容易的切入到源码中,去摸索他的实现思路。去哪里寻找答案呢?官网的开发者指南就比拟具体的介绍了Nginx诸多性能的实现细节 ,包含代码布局介绍,根本数据结构介绍,事件驱动模型介绍,
HTTP解决流程基本概念介绍等等。通过这些介绍咱们就能失去一些问题的答案。比方,事件循环的切入点是ngx_process_events_and_timers()函数,那你是不是就能在这个函数打断点,跟踪事件循环执行链路(http://nginx.org/en/docs/dev/...)。
配置不明确,也能够查看官网文档 ,留神配置是依照模块分类的。咱们以配置"keepalive_timeout"为例,官网文档有很分明的介绍,http://nginx.org/en/docs/http... 。
英文难以浏览怎么办?还是尽量浏览官网文档吧,更新及时,准确性高。或者,Nginx作为目前应用最宽泛的web服务器,网络上相干博客文档也是十分多的,搜寻"Nginx 事件循环",立即就能失去你想要的答案。不过须要留神的是,网络上找到的答案,无奈保障正确性,最好本人验证下。
3)从点,到线,再到面,再到点
同样的以事件循环为例,你从官网文档或者博客失去切入点是函数ngx_process_events_and_timers(),只有这一点信息怎么办?全局搜寻代码,查看该函数的调用链路,比方我通过understand能够很容易得出其调用链,如下图:
从一个切入点,你就能失去一条执行链路;一直的去摸索新的问题,逐渐你就能把握了整个零碎。
待你对整个零碎有了肯定理解,还须要再度回归到具体的点上。毕竟,第一阶段浏览源码时,因为整体的把握度不够,跳过了很多实现细节。
比方,事件构造体ngx_event_s包含数十个标识类字段,当初都没有深究具体含意;比方,当我配置了N多个location匹配规定时,Nginx是从头到尾一个个遍历匹配吗?效率是不是长处低呢?比方,Nginx的多过程模型,master过程是如何治理以及监控work过程的,Nginx的平滑降级又是怎么实现的?过程间通信以及信号处理你理解吗?再比方,Nginx通过锁来解决多个worker的惊群效应,那么锁的实现原理是什么呢?
模块化编程
在学习Nginx源码之前,最好理解一下其模块块编程思维。一个模块实现一个小小的性能,所有模块组合成了弱小的Nginx。比方下表几个功能模块:
模称 | 性能 |
---|---|
ngx_epoll_module | 基于epoll的事件处理模块 |
ngx_http_limit_req_module | 按申请qps限流 |
ngx_http_proxy_module | 按HTTP协定转发申请到上游 |
ngx_http_fastcgi_module | 按fastcgi协定转发申请到上游 |
ngx_http_upstream_ip_hash_module | iphsh负载平衡 |
…… | …… |
Nginx模块被划分为几大类,比方外围模块NGX_CORE_MODULE,事件模块NGX_EVENT_MODULE,HTTP模块NGX_HTTP_MODULE。构造体ngx_module_s定义了Nginx模块,咱们重点须要关注这几个(类)字段:
- 钩子函数,比方init_master/exit_master在master过程启动/退出时回调;init_process/exit_process在work过程启动/退出时回调;init_module在模块初始化时候调用;
- 指令数组commands,其定义了该模块能够解析哪些配置;这个很好了解,性能由各个模块实现,与性能对应的配置也应该由各个模块解析解决;
- 模块上下文ctx,查看源码的话你会发现其类型为void*,那是因为不同类型的模块ctx定义不一样。ctx构造通常都定义了配置创立以及初始化回调;另外,事件模块还会定义事件处理(增加事件,批改事件,删除事件,事件循环)回调;HTTP模块还定义了HTTP解决流程的回调,这些回调会在HTTP流程 11个执行阶段调用。
总结一句话,Nginx的框架已定义,主流程已知,各个模块只须要实现并注册流程中的回调即可,Nginx会在适合的机会执行这些回调。
最初再来一副脑图简略列一下重点常识,还须要读者去进一步钻研摸索:
master/worker过程模型
在解说master/worker过程模型之前,咱们先思考这么一个问题:如果配置worker_processes=1(work过程数目),执行上面几条命令后,Nginx还是否失常提供服务。
//master_pid即master过程id,work_pid即work过程pidkill master_pidkill -9 master_pidkill work_pidkill -9 work_pid
留神,kill默认 发送SIGTERM(15)信号,用于告诉过程须要被敞开,指标过程能够捕捉该信号并做相应清理工作后退出;kill - 9示意强制杀死该过程,信号不能被捕捉或疏忽,同时接管该信号的过程在收到这个信号时不能执行任何清理。
好了,请短暂的思考一分钟,或者本人入手验证下。上面咱们揭晓答案:
------ 试验一:kill master_pid -----------#ps aux | grep nginxroot 2314 0.0 0.0 25540 1472 ? Ss Aug18 0:00 nginx: master process /nginx/nginx-1.15.0/output/sbin/nginx-c /nginx/nginx-1.15.0/conf/nginx.confnobody 15243 0.0 0.0 31876 5880 ? S 22:40 0:00 nginx: worker process#kill 2314#curl http://127.0.0.1/test -H "Host:proxypass.test.com"curl: (7) Failed to connect to 127.0.0.1 port 80: Connection refused#ps aux | grep nginx//空,没有过程输入------ 试验二:kill -9 master_pid -----------# ps aux | grep nginxroot 15911 0.0 0.0 20544 680 ? Ss 22:50 0:00 nginx: master process /nginx/nginx-1.15.0/output/sbin/nginx-c /nginx/nginx-1.15.0/conf/nginx.confnobody 15914 0.0 0.0 26880 5156 ? S 22:50 0:00 nginx: worker processkill -9 15911# curl http://127.0.0.1/test -H "Host:proxypass.test.com"hello world#ps aux | grep nginxnobody 15914 0.0 0.0 26880 5652 ? S 22:50 0:00 nginx: worker process------ 试验三:kill work_pid -----------# ps aux | grep nginxroot 15995 0.0 0.0 20544 676 ? Ss 22:52 0:00 nginx: master process /nginx/nginx-1.15.0/output/sbin/nginx-c /nginx/nginx-1.15.0/conf/nginx.confnobody 15997 0.0 0.0 26880 5152 ? S 22:52 0:00 nginx: worker process# kill 15997# curl http://127.0.0.1/test -H "Host:proxypass.test.com"hello world# ps aux | grep nginxroot 15995 0.0 0.0 20544 860 ? Ss 22:52 0:00 nginx: master process /nginx/nginx-1.15.0/output/sbin/nginx-c /nginx/nginx-1.15.0/conf/nginx.confnobody 16021 0.0 0.0 26880 5648 ? S 22:52 0:00 nginx: worker process------ 试验四:kill -9 work_pid -----------# ps aux | grep nginxroot 15995 0.0 0.0 20544 860 ? Ss 22:52 0:00 nginx: master process /nginx/nginx-1.15.0/output/sbin/nginx-c /nginx/nginx-1.15.0/conf/nginx.confnobody 16021 0.0 0.0 26880 5648 ? S 22:52 0:00 nginx: worker process# kill -9 16021# curl http://127.0.0.1/test -H "Host:proxypass.test.com"hello world# ps aux | grep nginxroot 15995 0.0 0.0 20544 860 ? Ss 22:52 0:00 nginx: master process /nginx/nginx-1.15.0/output/sbin/nginx-c /nginx/nginx-1.15.0/conf/nginx.confnobody 16076 0.0 0.0 26880 5648 ? S 22:54 0:00 nginx: worker process
- 试验一:kill master_pid,后果是Connection refused,只因master和work过程都退出了,没有过程监听80端口;kill的是master过程,为什么work过程也退出了呢?其实是master过程让work过程退出的。
- 试验二:kill -9 master_pid,Nginx还能失常提供HTTP服务,此时master过程退出,work过程还在;到这里就能阐明,HTTP申请由work过程解决,与master过程无关;另外,为什么这里master过程没有让work过程退出呢?这就是kill -9的特点了,master过程不能捕捉该信号做清理工作;
- 试验三:kill work_pid,发现Nginx还是能失常提供HTTP服务,而且work过程居然还健在?再认真看看,kill之后的work过程是16021,之前是15997;原来是Nginx启动了新的work过程,其实是master过程在监听到work过程退出后,会拉起新的work过程;
- 试验四:kill -9 work_pid同试验三。
通过下面的四个试验,咱们能够初步失去以下论断:1)master过程在监听到work过程退出后,会拉起新的过程,而master过程在退出时(kill形式),会销毁所有work过程;其实就一句话,master过程次要用于治理work过程。2)work过程用于接管并解决HTTP申请。
上面咱们将简要介绍master/work过程解决流程。
master过程被fork后,会执行主处理函数ngx_master_process_cycle函数,次要工作如下图所示:
留神咱们下面的形容『master过程被fork后』,被谁fork呢?其实这是规范的daemon守护过程启动形式:两次fork+setsid。
能够看到,master过程在主循环中期待信号的达到,信号处理函数为ngx_signal_handler。另外须要分明的是,子过程在退出时,会向父过程发送信号SIGCHLD;master过程就是通过该信号来监听work过程的异样退出,从而拉起新的work过程。
最初还有一个问题,master过程在接管到退出信号时,如何告知work过程退出呢?这里能够应用信号吗?其实这里是通过socketpair向work过程发送音讯的。至于为什么不必信号呢,就和work过程事件处理框架无关了。
最初总结下,操作Nginx时,罕用的信号如下列表:
信号 | Nginx内置shell | 阐明 |
---|---|---|
SIGUSR1 | nginx -s reopen | 从新关上文件,可配合mv实现日志切割 |
SIGHUP | nginx -s reload | 从新加载配置 |
SIGQUIT | nginx -s quit | 平滑退出 |
SIGTERM | nginx -s stop | 强制退出 |
SIGUSR2 + SIGWINCH + SIGQUIT | 无 | 平滑降级二进制程序 |
work过程用于接管并解决HTTP申请,主函数为ngx_worker_process_cycle。须要留神,master过程创立socket并启动监听,work过程只是将listen_fd退出到本人的监听列表(epoll_ctl)。问题来了,如果多个work过程同时监听listen_fd的可读事件,新的连贯申请达到时,Linux内核会唤醒哪个work过程并交由其解决呢?这就是所谓的『惊群』效应了。
Nginx是通过『锁』实现的,work过程在监听listen_fd的可读事件之前,须要获取到锁;没有锁,work过程会将listen_fd从本人的监听列表中移除。读者能够浏览函数ngx_process_events_and_timers(事件循环主函数),
加锁函数为为ngx_trylock_accept_mutex,基于共享内存 + 原子操作实现。
在加锁的时候,Nginx还有一些负载平衡策略;每个work过程启动的时候,都会初始化若干ngx_connection_t构造,连接数可通过worker_connections配置,如果以后work过程的闲暇连接数小于总数的1/8,则会在近几次事件循环中不获取锁。
新版本Linux内核曾经解决了惊群效应,不须要加锁了;是否加锁也能够通过accept_mutex配置,官网解释如下:
There is no need to enable accept_mutex on systems that support the EPOLLEXCLUSIVE flag (1.11.3) or when using reuseport.
好了,到这里你对master/worker过程模型也有了肯定理解了,咱们能够简略画个示意图:
最初,脑图奉上:
Nginx事件驱动模型
Nginx作为webserver,就是接管客户端申请,转发到上游,再转发上游响应后果给客户端,其中必然随同着大量的IO交互,没有一个高效的IO复用模型如何能行?另外在期待客户端申请,期待上游响应时,通常还随同着一些超时事件(工夫事件)。
咱们所说的事件驱动,就是指IO事件以及工夫事件驱动,没有事件时候服务通常会阻塞期待事件的产生。不止是Nginx,个别事件驱动程序都是如下相似的套路:
for { //查找最近的工夫事件timer //epoll_wait期待IO事件的产生(最大等待时间timer) //解决IO事件 //解决工夫事件}
1)查找最近的工夫事件:个别必定存在N多个工夫事件,那么这些工夫事件最好是依照触发工夫排好序的,不然每次都须要遍历。通常会抉择红黑树或者最小堆实现。
2)期待IO事件的产生:目前都是基于IO多路复用模型,比方Linux零碎应用epoll实现;对于epoll,最好钻研下其红黑树+双向链表+程度触发/边缘触发。epoll的应用还是非常简单的,就3个API:
//创立epoll对象int epoll_create(int size);//往epoll增加须要监听的fd(这时候就依赖红黑树了)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//阻塞期待事件产生,最大期待timeout工夫int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
上一大节讲过,worker过程的主函数是ngx_worker_process_cycle,事件循环主函数是ngx_process_events_and_timers,从这两个函数中很容易找到其事件循环逻辑。
static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data){ for ( ;; ) { //事件循环 ngx_process_events_and_timers(cycle); //信号处理,如reopen,quit等 }}void ngx_process_events_and_timers(ngx_cycle_t *cycle){ //查找最近的工夫事件 timer = ngx_event_find_timer(); //抢锁,胜利后才监听listen_fd //(参照函数ngx_enable_accept_events,基于epoll_ctl将listen_fd退出epoll) ngx_trylock_accept_mutex(cycle) //阻塞期待IO事件的产生,底层基于epoll_wait实现 (void) ngx_process_events(cycle, timer, flags); //accept解决连贯事件 ngx_event_process_posted(cycle, &ngx_posted_accept_events); //解决工夫事件 ngx_event_expire_timers(); //解决一般读写事件 ngx_event_process_posted(cycle, &ngx_posted_events);}
事件循环咱们有肯定理解了,还须要关注几个外围构造体,比方连贯对象ngx_connection_t,事件对象ngx_event_t。worker过程在启动时刻,就会创立若干个连贯对象以及事件对象,创立的数目可通过worker_connections配置。连贯对象与事件对象示意图如下:
最初,脑图奉上:
HTTP解决流程之11个阶段
Nginx解决客户端连贯申请事件的handler是ngx_event_accept,同时对于listen_fd,还设置有连贯初始化函数ngx_http_init_connection,该函数开始了HTTP申请的解析工作:
void ngx_http_init_connection(ngx_connection_t *c){ c->read = ngx_http_wait_request_handler; c->write->handler = ngx_http_empty_handler; ngx_add_timer(rev, c->listening->post_accept_timeout);}
解析HTTP申请的解决流程如下图所示:
上面就开始解决HTTP申请了。Nginx将HTTP申请解决流程分为11个阶段,绝大多数HTTP模块都会将本人的handler增加到某个阶段(将handler增加到全局惟一的数组phases中),nginx解决HTTP申请时会挨个调用每个阶段的handler。须要留神的是其中有4个阶段不能增加自定义handler。11个阶段定义如下:
typedef enum { NGX_HTTP_POST_READ_PHASE = 0, NGX_HTTP_SERVER_REWRITE_PHASE, //server块中配置了rewrite指令,重写url NGX_HTTP_FIND_CONFIG_PHASE, //查找匹配的location配置;不能自定义handler; NGX_HTTP_REWRITE_PHASE, //location块中配置了rewrite指令,重写url NGX_HTTP_POST_REWRITE_PHASE, //查看是否产生了url重写,如果有,从新回到FIND_CONFIG阶段;不能自定义handler; NGX_HTTP_PREACCESS_PHASE, //访问控制,比方限流模块会注册handler到此阶段 NGX_HTTP_ACCESS_PHASE, //拜访权限管制,比方基于ip黑白名单的权限管制,基于用户名明码的权限管制等 NGX_HTTP_POST_ACCESS_PHASE, //依据拜访权限管制阶段做相应解决;不能自定义handler; NGX_HTTP_TRY_FILES_PHASE, //只有配置了try_files指令,才会有此阶段;不能自定义handler; NGX_HTTP_CONTENT_PHASE, //内容产生阶段,返回响应给客户端 NGX_HTTP_LOG_PHASE //日志记录} ngx_http_phases;
- NGX_HTTP_POST_READ_PHASE:第一个阶段,ngx_http_realip_module模块会注册handler到该阶段(nginx作为代理服务器时有用,后端以此获取客户端原始IP),而该模块默认不会开启,须要通过--with-http_realip_module启动;
- NGX_HTTP_SERVER_REWRITE_PHASE:server块中配置了rewrite指令时,该阶段会重写url;
- NGX_HTTP_FIND_CONFIG_PHASE:查找匹配的location配置;该阶段不能自定义handler;
- NGX_HTTP_REWRITE_PHASE:location块中配置了rewrite指令时,该阶段会重写url;
- NGX_HTTP_POST_REWRITE_PHASE:该阶段会查看是否产生了url重写,如果有,从新回到FIND_CONFIG阶段,否则间接进入下一个阶段;该阶段不能自定义handler;
- NGX_HTTP_PREACCESS_PHASE:访问控制,比方限流模块ngx_http_limit_req_module会注册handler到该阶段;
- NGX_HTTP_ACCESS_PHASE:拜访权限管制,比方基于ip黑白名单的权限管制,基于用户名明码的权限管制等;
- NGX_HTTP_POST_ACCESS_PHASE:该阶段会依据拜访权限管制阶段做相应解决,不能自定义handler;
- NGX_HTTP_TRY_FILES_PHASE:只有配置了try_files指令,才会有此阶段,不能自定义handler;
- NGX_HTTP_CONTENT_PHASE:内容产生阶段,用于产生响应后果;ngx_http_fastcgi_module模块就处于该阶段;
- NGX_HTTP_LOG_PHASE:该阶段会记录日志;
HTTP模块通常都有回调postconfiguration,用于注册本模块的handler到某个解决阶段,Nginx在解析实现http配置块后,会遍历所有HTTP模块并注册handler到相应阶段,后续解决HTTP申请时只需遍历执行该所有handler即可。
留神,这里的图是二维数组,后续还会将其转化微一维数组,便于遍历执行所有handler。
在这11个阶段里,咱们须要重点关注内容产生阶段NGX_HTTP_CONTENT_PHASE,这是HTTP申请解决的第10个阶段,用于产生响应后果;个别状况有3个模块注册handler到此阶段:ngx_http_static_module、ngx_http_autoindex_module和ngx_http_index_module。
然而当咱们配置了proxy_pass和fastcgi_pass时,执行流程会有所不同。此时会设置申请的回调content_handler,当Nginx执行到内容产生阶段时,如果content_handler不为空,则执行此回调,不再执行其余handler。
ngx_int_t ngx_http_core_content_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph){ if (r->content_handler) { //如果申请对象的content_handler字段不为空,则调用 r->write_event_handler = ngx_http_request_empty_handler; ngx_http_finalize_request(r, r->content_handler(r)); return NGX_OK; } rc = ph->handler(r); //否则执行内容产生阶段handler}
HTTP解决流程就简略介绍到这里,还有很多细节须要读者持续摸索。最初,脑图奉上。
location匹配规定
location用于匹配特定的申请URI,配置解析见文档http://nginx.org/en/docs/http...。根本语法为:
location [=|~|~*|^~] uri{……}location @name { ... }
其中:
- “=”用于定义准确匹配规定,申请URI与配置的uri模式齐全匹配能力失效;
- “ ~ ”和“ ~* ”别离定义辨别大小写的正则匹配规定和不辨别大小写的正则匹配规定,正则匹配胜利时,立刻完结location查找过程;
- “^~”用于定义最大前缀匹配规定,该类型location即便匹配胜利也不会完结location查找过程,仍然会查找匹配长度更长的location。另外,只蕴含uri的location仍然为最大前缀匹配。
- “@”用于定义命令location,此类型location不能匹配惯例客户端申请,只能用于外部申请重定向。
以“ ^~ ”开始的匹配模式与只蕴含uri的匹配模式都示意最大前缀匹配规定,这两者有什么区别呢?以“ ^~ ”开始的location在匹配胜利时,不会再执行后续的正则匹配,间接抉择该location配置。只蕴含uri的location在匹配胜利时,仍然会执行后续的正则匹配,只有当正则匹配不胜利时,才会抉择该location;否则,会抉择正则类型location。
另外,通常应用“location /”用于定义通用匹配,任何未匹配到其余location的申请都会匹配到。如果某申请URI未匹配到任何location,Nginx会返回404。
每个虚构server都能够配置多个location块;客户端申请达到时,须要遍历所有location,检测申请URI是否与location配置相匹配。那么当location配置数目较多时,匹配效率如何保障?遍历形式显然是不可行的。解析实现location配置时,多个location的存储构造是双向链表,该构造须要进行再解决,优化为查找匹配效率更高的构造。
思考下,正则匹配只能一一遍历,没有更优的查找匹配算法或数据结构;因而,所有正则匹配的location不须要非凡解决,只是从双向链表中裁剪进去,另外存储在regex_locations数组。
双向链表中最初只剩下准确匹配与最大前缀匹配,该类型location查找只能基于字符串匹配。Nginx将残余的这些location存储为树形构造(三叉树),每个节点node都有三个子节点,left、tree和right。left小于node;right大于node;tree与node前缀雷同,且tree节点的uri长度大于node节点的uri长度。三叉树的转换过程由函数ngx_http_init_static_location_trees实现,这里不做过多介绍。三叉树结构相似于下图所示:
location匹配过程处于HTTP解决流程第3阶段NGX_HTTP_FIND_CONFIG_PHASE,location匹配逻辑由函数ngx_http_core_find_location实现,有趣味的读者能够查看学习。
最初,脑图奉上:
upstream与负载平衡
Nginx反向代理是其最重要最罕用的性能:Nginx转发客户端申请到上游服务,接手上游服务响应并转发给客户端。而upstream使得Nginx能够成为一台反向代理服务器,并且upstream还提供了负载平衡能力,能够将申请依照某种策略平均的散发到上游服务。
提到upstream,就不得不提一个很重要的构造体ngx_http_upstream_t,该构造次要包含Nginx与上游的连贯对象,读写事件,缓冲区buffer,申请创立/申请解决等回调函数,等等,如下所示:
struct ngx_http_upstream_s { //读写事件处理handler ngx_http_upstream_handler_pt read_event_handler; ngx_http_upstream_handler_pt write_event_handler; //该构造封装了连贯获取/开释handler,不同负载平衡策略对应不同handler ngx_peer_connection_t peer; //各种缓冲区buffer //回调handler:创立申请,解决上游返回头等的回调。 //proxypass与fastcgipass都实现了这些回调 ngx_int_t (*create_request)(ngx_http_request_t *r); ngx_int_t (*reinit_request)(ngx_http_request_t *r); ngx_int_t (*process_header)(ngx_http_request_t *r); void (*abort_request)(ngx_http_request_t *r); void (*finalize_request)(ngx_http_request_t *r, ngx_int_t rc); ngx_int_t (*rewrite_redirect)(ngx_http_request_t *r, ngx_table_elt_t *h, size_t prefix); ngx_int_t (*rewrite_cookie)(ngx_http_request_t *r, ngx_table_elt_t *h);};
upstream流程次要蕴含的步骤以及处理函数如下表:
步骤 | 处理函数 |
---|---|
初始化upstream | ngx_http_upstream_create()、 ngx_http_upstream_init() |
与上游建设连贯 | ngx_http_upstream_connect() |
发送申请到上游 | ngx_http_upstream_send_request() |
解决上游响应头 | ngx_http_upstream_process_header() |
解决上游响应体(边接管边转发) | ngx_http_upstream_send_response() |
完结申请 | ngx_http_upstream_finalize_reques() |
可能的重试 | ngx_http_upstream_next() |
针对upstream解决流程,咱们须要思考以下几个问题:
- proxypass与fastcgipass解决回调是在什么时候设置的?咱们已proxypass为例,在配置proxy_pass指令后,内容产生阶段处理函数为ngx_http_proxy_handler;该处理函数开启了upstream的主流程,创立并初始化upstream,同时设置了申请解决回调。
- 负载平衡对应的解决回调是在什么时候确定的?同样的,也是在解析配置文件的时候,依据不同的配置指令,初始化不同的负载平衡策略。
默认的负载平衡策略为加权轮询,其初始化函数为ngx_http_upstream_init_round_robin;咱们也能够通过配置指令设置指定的负载平衡策略,如下表:
指令 | 初始化函数 |
---|---|
默认 | ngx_http_upstream_init_round_robin |
hash key | ngx_http_upstream_init_hash |
ip_hash | ngx_http_upstream_init_ip_hash |
least_conn | ngx_http_upstream_init_least_conn |
- Nginx在转发申请包体到上游服务时候,
是须要接管到残缺的申请体之后转发,还是能够边接管边转发呢?能够通过指令proxy_request_buffering配置:
Syntax: proxy_request_buffering on | off;Default: proxy_request_buffering on;Context: http, server, locationThis directive appeared in version 1.7.11.When buffering is enabled, the entire request body is read from the client before sending the request to a proxied server.When buffering is disabled, the request body is sent to the proxied server immediately as it is received. In this case, the request cannot be passed to the next server if nginx already started sending the request body.
这里要特地留神,当proxy_request_buffering设置为off时,如果申请转发给上游出错(或者上游处理错误等),该申请将不能再转发给其余上游进行重试。因为Nginx没有缓存申请体。
- Nginx解决完上游响应头部后,就能够开始将返回后果转发给客户端,并不需要等到残缺接管上游响应体,只须要边承受响应体,边返回给客户端即可。具体的逻辑能够参考函数ngx_http_upstream_send_response()。
- Nginx在某个上游服务器不可用的状况下,能够从新抉择一个上游服务器,并建设连贯,将申请转发给新的上游服务器。具体在这几个阶段呈现谬误时,都能够进行重试:1)连贯upstream失败;2)发送申请到上游服务器失败;3)解决上游响应头失败。须要留神,如果Nginx曾经开始解决上游响应体,此时呈现谬误,则会间接完结这次与上游服务器的交互,并完结这次申请,不会再抉择新的上游服务器重试;这里是因为Nginx可能曾经发送了局部数据到客户端。
另外还须要关注这两个配置,用于配置在什么谬误下重试,以及重试次数:
Syntax: proxy_next_upstream_tries number;Default: proxy_next_upstream_tries 0;Context: http, server, locationThis directive appeared in version 1.7.5.Limits the number of possible tries for passing a request to the next server. The 0 value turns off this limitation.Syntax: proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 | http_403 | http_404 | http_429 | non_idempotent | off ...;Default: proxy_next_upstream error timeout;Context: http, server, locationSpecifies in which cases a request should be passed to the next server:
- 线上环境,Nginx与上游服务器通常建设的是长连贯,与长连贯严密相干的有三个配置:1)keepalive connections,限度最大闲暇连接数(不是最大可建设的连接数),高并发状况下理论建设的连接数可能比这多;2)keepalive_requests number,Nginx版本需高于1.15.3,每个连贯上最多可解决申请数;3)keepalive_timeout timeout,Nginx版本需高于1.15.3,闲暇连贯超时工夫。
长连贯由模块ngx_http_upstream_keepalive_module实现,配置后会替换下面介绍的负载平衡初始化函数为ngx_http_upstream_init_keepalive_peer。
- 将Nginx作为反向代理服务器时,还须要留神与上游服务器交互的一些超时配置;默认的超时工夫是60秒,对于大部分业务来说都是过长的。次如:
Syntax: proxy_connect_timeout time;Default: proxy_connect_timeout 60s;Syntax: proxy_send_timeout time;Default: proxy_send_timeout 60s;Syntax: proxy_read_timeout time;Default: proxy_read_timeout 60s;
已经线上环境就看到过一起十分不合理的配置,超时工夫都是60秒,重试次数proxy_next_upstream_tries不限度。业务高峰期呈现突发慢申请,解决工夫超过60秒,网关记录HTTP状态504;同时又抉择下一台上游服务器重试,直到遍历完所有的上游服务器为止。最终这个申请的响应工夫为900秒!
- 最初不得不提的是Nginx的上游服务器剔除机制;当同一台上游服务器失败次数过多时,Nginx会短暂认为该上游服务器不可用,在肯定工夫内不会再将申请转发到该上游服务器。该策略由两个配置决定(详情:http://nginx.org/en/docs/http...):
Syntax: server address [parameters];Default: —Context: upstreammax_fails=numbersets the number of unsuccessful attempts to communicate with the server that should happen in the duration set by the fail_timeout parameter to consider the server unavailable for a duration also set by the fail_timeout parameter. By default, the number of unsuccessful attempts is set to 1. The zero value disables the accounting of attempts. What is considered an unsuccessful attempt is defined by the proxy_next_upstream, fastcgi_next_upstream, uwsgi_next_upstream, scgi_next_upstream, memcached_next_upstream, and grpc_next_upstream directives.fail_timeout=timesets the time during which the specified number of unsuccessful attempts to communicate with the server should happen to consider the server unavailable;and the period of time the server will be considered unavailable.By default, the parameter is set to 10 seconds.
须要特地留神,一旦Nginx认为所有上游服务器都不可用,在接管到客户端申请时,会间接返回502,不会尝试申请任何一台上游服务起。此时Nginx会记录日志"no live upstreams"。所以,线上环境,最好将非核心或者一些慢接口隔离到不同的upstream,以防这些接口的谬误影响其余外围接口。
upstream的主流程以及一些留神点下面也介绍了,对于想理解一些实现细节的,能够参照上面的脑图,去摸索源码:
fastcgi_pass与FPM
当咱们配置了fastcgi_pass指令后,Nginx会将申请转发给上游FPM解决。fastcgi协定用于Nginx与FPM之间的交互,不同于HTTP协定(以"\r\n"作为分隔符解析),fastcgi协定在发送申请之前,先发送固定构造的头部信息,蕴含该申请数据的类型以及长度等等。
fastcgi协定音讯头定义如下:
typedef struct { u_char version; //FastCGI协定版本 u_char type; //音讯类型 u_char request_id_hi; //申请ID u_char request_id_lo; u_char content_length_hi; //内容长度 u_char content_length_lo; u_char padding_length; //内容填充长度 u_char reserved; //保留} ngx_http_fastcgi_header_t;
Nginx对fastcgi音讯类型定义如下:
#define NGX_HTTP_FASTCGI_BEGIN_REQUEST 1#define NGX_HTTP_FASTCGI_ABORT_REQUEST 2#define NGX_HTTP_FASTCGI_END_REQUEST 3#define NGX_HTTP_FASTCGI_PARAMS 4#define NGX_HTTP_FASTCGI_STDIN 5#define NGX_HTTP_FASTCGI_STDOUT 6#define NGX_HTTP_FASTCGI_STDERR 7#define NGX_HTTP_FASTCGI_DATA 8
个别状况下,最先发送的是BEGIN_REQUEST类型的音讯,而后是PARAMS和STDIN类型的音讯;当FastCGI响应解决完后,将发送STDOUT和STDERR类型的音讯,最初以END_REQUEST示意申请的完结。
fastcgi协定的申请与响应构造示意图如下:
配置fastcgi_pass指令后,会设置内容产生阶段处理函数为ngx_http_fastcgi_handler;函数ngx_http_fastcgi_create_request创立fastcgi申请;
函数ngx_http_fastcgi_process_record解析FPM的响应后果。
咱们能够GDB调试打印输出/输出数据,便于更好的了解fastcgi协定。增加断点如下:
Num Type Disp Enb Address What1 breakpoint keep y 0x0000000000418f05 in ngx_process_events_and_timers at src/event/ngx_event.c:203 inf 3, 2, 1 breakpoint already hit 17 times2 breakpoint keep y 0x000000000045b7fa in ngx_http_fastcgi_create_request at src/http/modules/ngx_http_fastcgi_module.c:735 inf 3, 2, 1 breakpoint already hit 4 times3 breakpoint keep y 0x000000000045c2af in ngx_http_fastcgi_create_request at src/http/modules/ngx_http_fastcgi_module.c:1190 inf 3, 2, 1 breakpoint already hit 4 times4 breakpoint keep y 0x000000000045a573 in ngx_http_fastcgi_process_record at src/http/modules/ngx_http_fastcgi_module.c:2145 inf 3, 2, 1 breakpoint already hit 1 time
执行到ngx_http_fastcgi_create_request函数结尾(断点3),打印r->upstream->request_bufs三个buf:
(gdb) p *r->upstream->request_bufs->buf->pos@1000$18 =\001\001\000\001\000\b\000\000 //8字节头部,type=1(BEGIN_REQUEST)\000\001\000\000\000\000\000\000 //8字节BEGIN_REQUEST数据包\001\004\000\001\002\222\006\000 //8字节头部,type=4(PARAMS),数据内容长度=2*256+146=658(不是8字节整数倍,须要填充6个字节)\017\025SCRIPT_FILENAME/home/xiaoju/test.php //key-value,格局为:keylen+valuelen+key+value\f\000QUERY_STRING\016\004REQUEST_METHODPOST\f!CONTENT_TYPEapplication/x-www-form-urlencoded\016\002CONTENT_LENGTH19\v\tSCRIPT_NAME/test.php\v\nREQUEST_URI//test.php\f\tDOCUMENT_URI/test.php\r\fDOCUMENT_ROOT/home/xiaoju\017\bSERVER_PROTOCOLHTTP/1.1\021\aGATEWAY_INTERFACECGI/1.1\017\vSERVER_SOFTWAREnginx/1.6.2\v\tREMOTE_ADDR127.0.0.1\v\005REMOTE_PORT54276\v\tSERVER_ADDR127.0.0.1\v\002SERVER_PORT80\v\tSERVER_NAMElocalhost\017\003REDIRECT_STATUS200\017dHTTP_USER_AGENTcurl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2\t\tHTTP_HOSTlocalhost\v\003HTTP_ACCEPT*/*\023\002HTTP_CONTENT_LENGTH19\021!HTTP_CONTENT_TYPEapplication/x-www-form-urlencoded\000\000\000\000\000\000 //6字节内容填充\001\004\000\001\000\000\000\000 //8字节头部,type=4(PARAMS),示意PARAMS申请完结\001\005\000\001\000\023\005\000 //8字节头部,type=5(STDIN),申请体数据长度19个字节 (gdb) p *r->upstream->request_bufs->next->buf->pos@20$19 = "name=hello&gender=1" //HTTP申请体,长度19字节,需填充5个字节 (gdb) p *r->upstream->request_bufs->next->next->buf->pos@20$20 =\000\000\000\000\000 //5字节填充\001\005\000\001\000\000\000 //8字节头部,type=5(STDIN),示意STDIN申请完结
执行到办法ngx_http_fastcgi_process_record,打印读入申请数据:
p *f->pos@1000$26 =\001\006\000\001\000\377\001\000 //8字节头部,type=6(STDOUT),返回数据长度为255字节(须要填充1个字节)Set-Cookie: PHPSESSID=3h9lmb2mvp6qlk1rg11id3akd3; path=/\r\n //返回数据内容,以换行符分隔Expires: Thu, 19 Nov 1981 08:52:00 GMT\r\nCache-Control: no-store, no-cache, must-revalidate\r\nPragma: no-cache\r\nContent-type: text/html; charset=UTF-8\r\n\r\n{\"ret-name\":\"ret-hello\",\"ret-gender\":\"ret-1\"}\000\001\003\000\001\000\b\000\000 //8字节头部,type=3(END_REQUEST),示意fastcgi申请完结,数据长度为8\000\000\000\000\000\000\000\000 //8字节END_REQUEST数据
proxy_pass
当咱们配置了proxy_pass指令后,Nginx会将申请转发给上游HTTP服务解决;此时设置内容产生阶段处理函数为ngx_http_proxy_handler。
这里咱们简略介绍下长连贯的配置以及注意事项。上面配置使得Nginx与上游HTTP服务放弃长连贯:
upstream proxypass.test.com{ server 127.0.0.1:8080; keepalive 10;}server { listen 80; server_name proxypass.test.com ; access_log /home/nginx/logs/proxypass.test.com_access.log main; location / { proxy_pass http://proxypass.test.com; proxy_http_version 1.1; proxy_set_header Connection "keep-alive"; }}
留神upstream配置块里的配置指令keepalive,官网文档如下:
Syntax: keepalive connections;This directive appeared in version 1.1.4.The connections parameter sets the maximum number of idle keepalive connections to upstream servers that are preserved in the cache of each worker process.It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open. The connections parameter should be set to a number small enough to let upstream servers process new incoming connections as well.
每个work都有长连贯缓存池,而keepalive配置的就是缓存池忠最大闲暇连贯的数目,留神是闲暇连贯,并不是限度Nginx与上游HTTP建设的连贯总数目。
proxy_http_version配置应用1.1版本HTTP协定;proxy_set_header配置HTTP头部Connection为keep-alive(Nginx默认Connection:close,即便应用的是1.1版本HTTP协定)。
在应用长连贯时,肯定要特地留神;如果上游被动敞开连贯,而此时恰好Nginx发动申请,可能会呈现502(线上已经呈现过偶发502景象),而且呈现502的概率较低,现场难以捕捉。
长连贯上游为什么会被动敞开连贯呢?比方,在Golang服务中,通常会配置IdleTimeout,当长连贯长时间闲暇时后,Golang会被动敞开该长连贯。如上面的抓包实例,前两次申请专用同一个TCP连贯,然而当长时间没有新的申请达到时,Golang会被动敞开该长连贯。如果此时恰好Nginx发动新的申请,就有可能造成异常情况。
//建设连贯10:24:16.240952 IP 127.0.0.1.31451 > 127.0.0.1.8080: Flags [S], seq 388101088, win 43690, length 010:24:16.240973 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [S.], seq 347995396, ack 388101089, win 43690, length 010:24:16.240994 IP 127.0.0.1.31451 > 127.0.0.1.8080: Flags [.], ack 1, win 86, length 0//第一次申请10:24:16.241052 IP 127.0.0.1.31451 >127.0.0.1.8080: Flags [P.], seq 1:111, ack 1, win 86, length 110: HTTP: GET /test HTTP/1.110:24:16.241061 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [.], ack 111, win 86, length 010:24:17.242332 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [P.], seq 1:129, ack 111, win 86, length 128: HTTP: HTTP/1.1 200 OK10:24:17.242371 IP 127.0.0.1.31451 > 127.0.0.1.8080: Flags [.], ack 129, win 88, length 0//隔几秒第二次申请,没有新建连贯10:24:24.536885 IP 127.0.0.1.31451 > 127.0.0.1.8080: Flags [P.], seq 111:221, ack 129, win 88, length 110: HTTP: GET /test HTTP/1.110:24:24.536914 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [.], ack 221, win 86, length 010:24:25.537928 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [P.], seq 129:257, ack 221, win 86, length 128: HTTP: HTTP/1.1 200 OK10:24:25.537957 IP 127.0.0.1.31451 > 127.0.0.1.8080: Flags [.], ack 257, win 90, length 0//上游被动断开长连贯10:25:25.538408 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [F.], seq 257, ack 221, win 86, length 010:25:25.538760 IP 127.0.0.1.31451 > 127.0.0.1.8080: Flags [F.], seq 221, ack 258, win 90, length 010:25:25.538792 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [.], ack 222, win 86, length 0
Nginx在与上游建设长连贯时,也有一个配置,用于设置长连贯超时工夫:
Syntax: keepalive_timeout timeout;Default: keepalive_timeout 60s;Context: upstreamThis directive appeared in version 1.15.3.Sets a timeout during which an idle keepalive connection to an upstream server will stay open.
须要留神,这是配置在upstream配置块的,而且必须Nginx版本高于1.15.3。
http/server/location配置块还有一个配置keepalive_timeout,其配置的是与客户端的长连贯超时工夫:
Syntax: keepalive_timeout timeout [header_timeout];Default: keepalive_timeout 75s;Context: http, server, locationThe first parameter sets a timeout during which a keep-alive client connection will stay open on the server side. The zero value disables keep-alive client connections.
限流
限流的目标是通过对并发拜访/申请进行限速来爱护零碎,一旦达到限度速率则能够拒绝服务(定向到谬误页)、排队期待(秒杀)、或者降级(返回兜底数据或默认数据)。通常限流策略有:限度刹时并发数(如Nginx的ngx_http_limit_conn_module模块,用来限度刹时并发连接数)、限度工夫窗口内的均匀速率(如Nginx的ngx_http_limit_req_module模块,用来限度每秒的均匀速率)。另外还能够依据网络连接数、网络流量、CPU或内存负载等来限流。
罕用的限流算法有计数器(简略粗犷,升级版是滑动窗口算法)漏桶算法,以及令牌桶算法。上面简略介绍令牌桶算法。如下图所示,令牌桶是一个寄存固定容量令牌的桶,依照固定速率r往桶里增加令牌;桶中最多寄存b个令牌,当桶满时,新增加的令牌被抛弃;当一个申请达到时,会尝试从桶中获取令牌;如果有,则持续解决申请;如果没有则排队期待或者间接抛弃。如下图:
限流模块会注册handler到HTTP解决流程的NGX_HTTP_PREACCESS_PHASE阶段。如ngx_http_limit_req_module模块注册ngx_http_limit_req_handler;函数ngx_http_limit_req_handler执行限流算法,判断是否超出配置的限流速率,从而进行抛弃或者排队或者通过。
ngx_http_limit_req_module模块提供以下配置指令,供用户配置限流策略:
//每个配置指令次要蕴含两个字段:名称,解析配置的解决办法static ngx_command_t ngx_http_limit_req_commands[] = { //个别用法:limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; //$binary_remote_addr示意近程客户端IP; //zone配置一个存储空间(须要调配空间记录每个客户端的拜访速率,超时空间限度应用lru算法淘汰;留神此空间是在共享内存调配的,所有worker过程都能拜访) //rate示意限度速率,此例为1qps { ngx_string("limit_req_zone"), ngx_http_limit_req_zone, }, //用法:limit_req zone=one burst=5 nodelay; //zone指定应用哪一个共享空间 //超出此速率的申请是间接抛弃吗?burst配置用于解决突发流量,示意最大排队申请数目,当客户端申请速率超过限流速率时,申请会排队期待;而超出burst的才会被间接回绝; //nodelay必须与burst一起应用;此时排队期待的申请会被优先解决;否则如果这些申请仍然依照限流速度解决,可能等到服务器解决实现后,客户端早已超时 { ngx_string("limit_req"), ngx_http_limit_req, }, //当申请被限流时,日志记录级别;用法:limit_req_log_level info | notice | warn | error; { ngx_string("limit_req_log_level"), ngx_conf_set_enum_slot, }, //当申请被限流时,给客户端返回的状态码;用法:limit_req_status 503 { ngx_string("limit_req_status"), ngx_conf_set_num_slot, },};
Nginx限流算法依赖两个数据结构:红黑树和LRU队列。红黑树提供高效率的增删改查,LRU队列用于实现数据淘汰。
咱们假如限度每个客户端IP($binary_remote_addr)申请速率。当用户第一次申请时,会新增一条记录,并以客户端IP地址的hash值作为key存储在红黑树中,同时存储在LRU队列中;当用户再次申请时,会从红黑树中查找这条记录并更新,同时挪动记录到LRU队列首部。存储空间(limit_req_zone配置)不够时,会淘汰记录,每次都是从尾部删除。
上面咱们通过两个试验进一步了解Nginx限流策略的配置以及限流景象。
- 试验一:
配置限速1qps,容许申请被排队解决,配置burst=5,即最多容许5个申请排队期待解决。
http{ limit_req_zone $binary_remote_addr zone=test:10m rate=1r/s; server { listen 80; server_name localhost; location / { limit_req zone=test burst=5; root html; index index.html index.htm; }}
应用ab并发发动10个申请,ab -n 10 -c 10 http://xxxxx;
查看服务端access日志;依据日志显示第一个申请被解决,2到5四个申请回绝,6到10五个申请被解决;为什么会是这样的后果呢?
xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:49 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:50 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:51 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:52 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [22/Sep/2018:23:41:53 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
查看ngx_http_log_module,注册handler到NGX_HTTP_LOG_PHASE阶段(HTTP申请解决最初一个阶段);因而理论状况应该是这样的:10个申请同时达到,第一个申请达到间接被解决,第2到6个申请达到,排队提早解决(每秒解决一个);第7到10个申请被间接回绝,因而先打印access日志;
ab统计的响应工夫见上面,最小响应工夫87ms,最大响应工夫5128ms,均匀响应工夫为1609ms:
min mean[+/-sd] median maxConnect: 41 44 1.7 44 46Processing: 46 1566 1916.6 1093 5084Waiting: 46 1565 1916.7 1092 5084Total: 87 1609 1916.2 1135 5128
- 试验二
试验一配置burst后,尽管突发申请会被排队解决,然而响应工夫过长,客户端可能早已超时;因而增加配置nodelay,使得Nginx紧急解决期待申请,以减小响应工夫:
http{ limit_req_zone $binary_remote_addr zone=test:10m rate=1r/s; server { listen 80; server_name localhost; location / { limit_req zone=test burst=5 nodelay; root html; index index.html index.htm; }}
同样ab发动申请,查看服务端access日志;第一个申请间接解决,第2到6个五个申请排队解决(配置nodelay,Nginx紧急解决),第7到10四个申请被回绝:
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
ab统计的响应工夫见上面,最小响应工夫85ms,最大响应工夫92ms,均匀响应工夫为88ms:
min mean[+/-sd] median maxConnect: 42 43 0.5 43 43Processing: 43 46 2.4 47 49Waiting: 42 45 2.5 46 49Total: 85 88 2.8 90 92
ngx_http_limit_conn_module限流模块比较简单,这里不再介绍。
最初,脑图奉上:
案例剖析:502问题介绍
生产环境通常存在各种各样的异样HTTP状态码,比方499,504,502,500,400,408等,每个状态码的含意还是须要分明。这里咱们简略介绍下最常见的502问题排查。
502的含意是NGX_HTTP_BAD_GATEWAY,即网关谬误。通常的起因是上游没有监听,或者上游被动断开连接。在呈现502时候,通常Nginx都会记录谬误日志;比方:
2020/11/02 21:35:24 [error] 20921#0: *40 upstream prematurely closed connection while reading response header from upstream, client: 127.0.0.1, server: proxypass.test.com, request: "GET /test HTTP/1.1", upstream: "http://127.0.0.1:8080/test", host: "proxypass.test.com"
通过日志霎时就明确了,在Nginx期待上游返回后果时候,上游被动敞开连贯了。上游为什么会被动敞开连贯呢?这起因就简单了,比方上游是golang服务时,如果配置了WriteTimeout=3秒,当申请解决工夫超过3秒时,Golang服务会被动敞开连贯。如上面抓包实例:
//三次握手建设连贯21:57:13.586604 IP 127.0.0.1.31519 > 127.0.0.1.8080: Flags [S], seq 574987506, win 43690, length 021:57:13.586627 IP 127.0.0.1.8080 > 127.0.0.1.31519: Flags [S.], seq 3599212930, ack 574987507, win 43690, length 021:57:13.586649 IP 127.0.0.1.31519 > 127.0.0.1.8080: Flags [.], ack 1, win 86, length 0//发送申请21:57:13.586735 IP 127.0.0.1.31519 > 127.0.0.1.8080: Flags [P.], seq 1:111, ack 1, win 86, length 110: HTTP: GET /test HTTP/1.121:57:13.586743 IP 127.0.0.1.8080 > 127.0.0.1.31519: Flags [.], ack 111, win 86, length 0//申请解决5秒超时;没有响应,上游间接断开连接21:57:18.587918 IP 127.0.0.1.8080 > 127.0.0.1.31519: Flags [F.], seq 1, ack 111, win 86, length 021:57:18.588169 IP 127.0.0.1.31519 > 127.0.0.1.8080: Flags [F.], seq 111, ack 2, win 86, length 021:57:18.588184 IP 127.0.0.1.8080 > 127.0.0.1.31519: Flags [.], ack 112, win 86, length 0
再比方Nginx谬误日志:
connect() to unix:/tmp/php-fcgi.sock failed (11: Resource temporarily unavailable) while connecting to upstream
表明Nginx在通过域套接字连贯上游FPM过程时,返回EAGAIN(11);查看Nginx源码中的正文:
Linux returns EAGAIN instead of ECONNREFUSED for unix sockets if listen queue is full
Linux域套接字在队列溢出时,接管到连贯申请会返回EAGAIN,而此时Nginx间接完结该申请,并返回502。
总结
想一篇文章齐全介绍分明Nginx源码是不可能的,因而本文针对Nginx局部知识点,做了一个概括介绍,次要通知读者一些根本的设计思维,以及能够去源码哪里寻找你想要的答案。当然,限于篇幅以及能力问题,还有很多知识点或者细节是没有介绍到的。联合这些脑图,剩下的就须要读者本人去摸索钻研了。