共计 31141 个字符,预计需要花费 78 分钟才能阅读完成。
李乐
引子
咱们为什么须要学习 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 过程 pid
kill master_pid
kill -9 master_pid
kill work_pid
kill -9 work_pid
留神,kill 默认 发送 SIGTERM(15)信号,用于告诉过程须要被敞开,指标过程能够捕捉该信号并做相应清理工作后退出;kill – 9 示意强制杀死该过程,信号不能被捕捉或疏忽,同时接管该信号的过程在收到这个信号时不能执行任何清理。
好了,请短暂的思考一分钟,或者本人入手验证下。上面咱们揭晓答案:
------ 试验一:kill master_pid -----------
#ps aux | grep nginx
root 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.conf
nobody 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 nginx
root 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.conf
nobody 15914 0.0 0.0 26880 5156 ? S 22:50 0:00 nginx: worker process
kill -9 15911
# curl http://127.0.0.1/test -H "Host:proxypass.test.com"
hello world
#ps aux | grep nginx
nobody 15914 0.0 0.0 26880 5652 ? S 22:50 0:00 nginx: worker process
------ 试验三:kill work_pid -----------
# ps aux | grep nginx
root 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.conf
nobody 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 nginx
root 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.conf
nobody 16021 0.0 0.0 26880 5648 ? S 22:52 0:00 nginx: worker process
------ 试验四:kill -9 work_pid -----------
# ps aux | grep nginx
root 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.conf
nobody 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 nginx
root 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.conf
nobody 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, location
This 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, location
This 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, location
Specifies 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: upstream
max_fails=number
sets 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=time
sets 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 What
1 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 times
2 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 times
3 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 times
4 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\n
Cache-Control: no-store, no-cache, must-revalidate\r\n
Pragma: no-cache\r\n
Content-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 0
10:24:16.240973 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [S.], seq 347995396, ack 388101089, win 43690, length 0
10: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.1
10:24:16.241061 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [.], ack 111, win 86, length 0
10: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 OK
10: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.1
10:24:24.536914 IP 127.0.0.1.8080 > 127.0.0.1.31451: Flags [.], ack 221, win 86, length 0
10: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 OK
10: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 0
10:25:25.538760 IP 127.0.0.1.31451 > 127.0.0.1.8080: Flags [F.], seq 221, ack 258, win 90, length 0
10: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: upstream
This 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, location
The 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 max
Connect: 41 44 1.7 44 46
Processing: 46 1566 1916.6 1093 5084
Waiting: 46 1565 1916.7 1092 5084
Total: 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 max
Connect: 42 43 0.5 43 43
Processing: 43 46 2.4 47 49
Waiting: 42 45 2.5 46 49
Total: 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 0
21:57:13.586627 IP 127.0.0.1.8080 > 127.0.0.1.31519: Flags [S.], seq 3599212930, ack 574987507, win 43690, length 0
21: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.1
21: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 0
21:57:18.588169 IP 127.0.0.1.31519 > 127.0.0.1.8080: Flags [F.], seq 111, ack 2, win 86, length 0
21: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 局部知识点,做了一个概括介绍,次要通知读者一些根本的设计思维,以及能够去源码哪里寻找你想要的答案。当然,限于篇幅以及能力问题,还有很多知识点或者细节是没有介绍到的。联合这些脑图,剩下的就须要读者本人去摸索钻研了。