Nginx 学习笔记
记录学习
Nginx
过程中的一些基础知识, 踩坑, 以及原理调优
基础
简介
Nginx
通常作为代理服务器. Nginx
有一个master
进程和若干个worker
进程. master
进程负责读取和处理配置configuration
并维护worker
进程的运行情况. worker
负责处理具体的某个请求.
Nginx
的配置文件通常为nginx.conf
,通常放置在/usr/local/nginx/conf
, /etc/nginx
, /usr/local/ect/nginx
下面。下图即为目录结构。
Nginx
支持以下负载均衡机制
- round-robin/轮询:到应用服务器的请求以round-robin的方式被分发
-
least-connect/最少连接
:下一个请求被分配到活动连接数量最少的服务器 - ip-hash/IP散列: 使用hash算法来决定下一个服务器请求要选择哪一个服务器
根据不同的负载均衡机制,需要在nginx
的对应upstream模块中配置不同least_conn
, ip_hash
。
http {
upstream myapp1 {
//ip_hash, least_conn;
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://myapp1;
}
}
}
命令
-
nginx -s quit
: 退出关闭nginx
-
sudo /etc/init.d/nginx restart
更新了nginx
配置之后重启 -
sudo openssl req -x509 -nodes -days 36500 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt
创建SSL
证书.
调优
Sendfile (静态文件服务优化)
sendfile on
默认为off,指定是否使用sendfile系统调用来传输文件,sendfile
系统调用在两个文件描述符之间直接传输数据(在内核中进行),从而避免了数据在内核缓冲区和用户缓冲区之间的拷贝,操作高效,称为零拷贝
在传统的文件传输方式(read、write/send方式),具体流程细节如下:
- 调用read函数,文件数据拷贝到内核缓冲区
- read函数返回,数据从内核缓冲区拷贝到用户缓冲区
- 调用write/send函数,将数据从用户缓冲区拷贝到内核socket缓冲区
- 数据从内核socket缓冲区拷贝到协议引擎中
在这个过程当中,文件数据实际上是经过了四次拷贝操作: 硬盘—>内核缓冲区—>用户缓冲区—>内核socket缓冲区—>协议引擎
sendfile系统调用则提供了一种减少拷贝次数,提升文件传输性能的方法。
- sendfile系统调用利用DMA引擎将文件数据拷贝到内核缓冲区,之后数据被拷贝到内核socket缓冲区中
- DMA引擎将数据从内核socket缓冲区拷贝到协议引擎中
这里没有用户态和内核态之间的切换,也没有内核缓冲区和用户缓冲区之间的拷贝,大大提升了传输性能。 这个过程数据经历的拷贝操作如下: 硬盘—>内核缓冲区—>内核socket缓冲区—>协议引擎
sendfile 是将 in_fd 的内容发送到 out_fd 。而 in_fd 不能是 socket , 也就是只能文件句柄。 所以当 Nginx 是一个静态文件服务器的时候,开启 SENDFILE 配置项能大大提高 Nginx 的性能。 作为反向代理,没有什么用。
配置
配置https, 同时接受http
midas.conf
server {
# the port your site will be served on
listen 80;
listen 443 ssl;
# the domain name it will serve for
server_name midas46.com; # substitute by your FQDN and machine's IP address
charset utf-8;
#Max upload size
client_max_body_size 75M; # adjust to taste
# Django media
location /media {
alias /var/www/path/to/your/project/media; # your Django project's media files
}
# location /assets {
# alias /var/www/path/to/your/project/static; # your Django project's static files
# }
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
# Finally, send all non-media requests to the Django server.
location / {
proxy_pass http://0.0.0.0:7000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
问题
1. Nginx如何处理一个请求?
stackoverflow-how-does-nginx-handle-http-requests
首先Nginx
在生产环境部署使用的多为多进程模式,会有一个master进程,和多个worker进程。master进程用来接收来自外界的信号,向各个worker进程发送信号,监控worker进程的运行状态,当worker进程出现异常时,master进程则会重启进程。而最基本的网络事件,则放在了worker进程中来处理。一个请求只能在一个nginx worker中处理,通常来说nginx worker进程的数量与cpu核心数一致(过多的nginx worker进程会导致竞争cpu资源,从而带来不必要的上下文切换,并且nginx为了更多的利用多核特性,提供了cpu亲缘性的绑定)。
在处理Http请求方面,Nginx使用reactor模型。是一个单进程事件驱动模型,事件驱动的主循环会一直等待直到OS触发了可读,可写事件。事件触发之后,会调用相应的事件处理函数,来具体处理请求。
首先Nginx部署运行为多进程模型,而worker进程之间又是平等的,每个进程,处理请求的机会也相等,那么当一个请求过来时,是如何做的请求被一个worker所处理呢?首先,每个worker进程都是从master进程fork过来的,在master进程里面,先建立好需要listen的socket(listenfd)之后,再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变成可读。为了保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex, 抢到互斥锁的那个进程注册listenfd读事件,在读事件例调用accept接收该连接,然后进行读取请求,解析请求,处理请求,返回响应。
对于Nginx整个事件处理机制,伪代码如下,和redis有点类似,都包含了时间事件,和网络事件(文件事件),且时间事件并不是fork一个线程的实现,而是在整个事件处理机制中。下面是nginx事件处理模型的伪代码。epoll_wait
函数在调用的时候可以设置一个超时时间timeout
,因此nginx借助这个超时时间来实现定时器,也就是时间事件。nginx中的定时器是放在一颗维护定时器的红黑树里. (注意:redis事件循环使用一个链表维护时间事件,redis时间事件数量较小,且作者antirez
也说明了如果使用skip-list会在时间复杂度上面更优, 这里与nginx事件机制有异曲同工之妙),每次进入epoll_wait之前,会从该红黑树里拿到所有定时器事件的最小时间,在此基础上会计算epoll_wait的timeout。也就是说如果没有网络事件产生,epoll就会超时,然后会检查定时器是否超时,然后处理所有的超时事件,并将其状态置为超时,然后再继续处理网络事件。
while (true) {
for t in run_tasks:
t.handler();
update_time(&now);
timeout = ETERNITY;
// 处理时间事件
for t in wait_tasks: /* sorted already */
if (t.time <= now) {
t.timeout_handler();
} else {
// 计算epoll-wait的超时时间
timeout = t.time - now;
break;
}
// 处理网络事件,epoll调用
nevents = poll_function(events, timeout);
// 返回触发的文件描述
for i in nevents:
task t;
if (events[i].type == READ) {
// 注册读的回调函数
t.handler = read_handler;
} else { /* events[i].type == WRITE */
// 注册写回调函数
t.handler = write_handler;
}
// 执行回调函数
run_tasks_add(t);
}
2. keepalive支持
http1.0和http1.1(默认)支持长连接。实现Http/1.0 keepalive连接的客户端可以通过包含Connection:Keep-Alive
首部请求将一条连接保存在打开的状态。如果服务端最后决定是keepalive打开,那么在响应的http头里面,也会包含Connection:Keep-Alive
, 否则就是Connection: Close
. 如果是connection: close
, 则nginx在响应完数据之后,会主动关闭连接。所以,对于请求量比较大的nginx来说,关掉keep-alive会产生比较多的socket time-wait状态。
keepalive
的本质其实是在一个TCP socket上面,复用多个Http连接的请求。要实现这个功能的前提条件是,必须得确定请求头与响应体的长度。
- 对于Http1.0协议,响应头中
content-length
即可以知道body的长度,接收完后,就表示这个请求完成了。而如果没有content-length
头(流式数据),则客户端会一直接收数据,直到服务器主动断开连接,才表示body接收完毕 - 对于Http1.1协议,如果响应头中的Transfer-encoding为chunked传输,则表示body是流式输出,body会被分成多个块,每块的开始会标识出当前块的长度,此时,body不需要通过content-length来指定,客户端会接收数据直到服务端主动断开连接
3. Transfer-encoding: chunked 分块编码
分块编码,主要用于处理流式数据,允许服务器把主体body逐块发送,并表明每块的大小。分块编码非常简单,由起始的Http响应首部块开始,随后是一系列的分块。每个分块包含一个长度值(hex),和该块的数据。具体如下图所示,最后一个块有一个特定,长度值为,表面此次流式数据传输结束。
4. Nginx限流是如何工作的
Nginx流量限制主要使用漏桶算法,具体实现也是FIFO的实现,所有请求都会放到FIFO的队列中。以10qps为例,即Nginx会限制每个请求100milliseconds.如果下一个请求到来的时间小于前一次请求的100milliseconds,则直接拒绝此次请求,或者进行一次请求排队。
Nginx限流主要是单机的一种解决方案,在微服务的体系中还是推荐使用redis限流方案。
具体在Nginx的配置如下:
- binary_remote_addr: 为二进制客户端ip,相比于remote_addr字符串会节省更多的空间。
- zone: 共享内存空间,在nginx worker进程共享,用来存储ip
- rate: 速率
- burst: 支持突增流量的队列长度
- nodelay: 对于burst的流量,直接通过
- delay: 对于突增的流量,会delay到的一定速率进行执行
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /login/ {
limit_req zone=mylimit burst=20 nodelay/delay 8;
proxy_pass http://my_upstream;
}
}
Reference
- 官方教程
- Tengine nginx
- Nginx