**Engine-X
not en-jinks
. **
废话
Nginx, 我们可以说是一个Web Server, 不仅如此, 他还可以作为web reverse proxy. 关于反向代理以及一些缓存功能, 这里先不做涉及, 留在后面说. 不管怎么说, 我们还是先把HTTP协议复习一遍?
我们说过, HTTP Wed服务能够进行认证, 其中有基于IP的, 有基于用户的, 而这又分为基本认证(basic)和摘要认证(digest). 除了这个, 还有资源映射, 我们说过有Alias和DocumentRoot的映射. 另外,在现在各大站点的访问分析中, 一定会有PV和UV这两个重要的指标. PV就是指Page View, 而UV就是User View了. 值得注意的是, 一个页面中肯定有很多资源, 我们说过浏览器会开启多线程去请求这些资源, 但是对于一个域名的线程上限是存在的. 正因为此, 有些网站为了提高用户的浏览速度, 就会把资源放在不同域名的服务器上, 这样浏览器就会对每一个域名开启多线程, 对于整个网站来说相当于是数倍的速度提升.
在说Apache httpd的时候, 我们说过有一个叫做MPM的模块. 三种: prefork, worker, event.
现在来看一下web I/O模型相关.
I/O模型
显然, 我们先这样划分: 同步和异步 以及 阻塞和非阻塞(synchronous and asynchronous, block and nonblock)
对于异步IO, 他更多的关注的消息通知机制. 其实所谓IO, 就是存在一个调用方和一个提供服务方, 而调用方向服务方请求调用一个资源就是IO了. 主要是, 被调用方需要在自己的一方进行处理, 之后才会将结果返回到调用方, 那么问题就是, 调用方怎么知道他请求的资源已经被处理结束了呢? 所以这就是同步和异步的两种模式的区别, 同步就被调用方不会立即返回结果, 但是只要返回, 返回的就是最终结果; 而异步就不一样了, 被调用方会立即返回结果, 但是返回的结果只是消息, 不是最终结果. 当被调用方结束处理之后, 会通过状态, 通知机制来通知调用者, 或者通过回调函数的方式来处理结果.
接着我们再说说阻塞和非阻塞这两个家伙. 他们关注的东西其实是调用者等待被调用者返回调用结果的状态. 所谓阻塞, 调用者在结果返回之前, 会被挂起; 而这个时候调用者进入了一种不可中断的睡眠态, 只有当等到结果之后才能继续. 而非阻塞就相反了, 他不会被挂起, 也就是说调用不会阻塞自己.
要特别加一句: 阻塞非阻塞和同步异步关注的点根本就不是一个, 所以他们之间其实没有什么必然的关系.
这样的话, 我们就可以把IO模型分成5种:
- 阻塞IO
- 非阻塞IO
- IO多路复用
- (信号|事件)驱动IO
- 异步IO
接下来来解释一下他们. 以磁盘IO为例: read()操作
用户空间是没有权限读取磁盘的对不? 所以应用程序发起IO请求的时候, 分成两步骤: ①请求内核但是内核没有数据, 所以内核加载内容到内核内存中, ②可是进程无法读取内核内存, 所以再次将数据复制到进程内存中. 这里被称作IO的过程是第二步.
首先是阻塞IO:
在这种模型下, 进程挂起, 等到最终结果好了之后才继续. 很好理解, 接着来看非阻塞IO:
在这种模式下, 进程虽然不会挂起, 但是会进入到一种忙等待的状态, 被调用方不会进行通知, 为了知道结果是否处理完成, 进程会进行长轮询, 这样的状态其实也是很消耗资源的, 非阻塞不一定有阻塞的效率高. 根据情况, 这个时候可能阻塞了更好. 而且不仅如此, 看第二个阶段,其实还是阻塞的.
上面的两种都很好理解, 而且他们都很古老. 接着来看复用型IO.
这个IO模型是一个比较稳定的IO模型, 很多应用都是基于这种模式构建的. 到底什么叫多路复用? 可以这么理解: 前两种模型都是把请求发给内核, 但是现在我们在内核和调用者之间加进了一个代理 这个代理,能够将用户的请求发给内核, 这样的好处是, 用户进程如果有多个请求, 他可以在代理发送结束之后, 将请求发送过去, 尽管内核是阻塞的, 但是我们却可以通过这个代理实现多路的请求. 不太明白吗? 我们来看一段代码:
1 | #!/usr/bin/env python3 |
这里我们使用的select做的演示. select是最早的一个多路复用系统调用, 我们注册想要获得响应的socket/IO请求. 这个最早使用BSD实现的, 后来SysV模仿着搞出来了poll(), 他们两个其实没有什么太大的区别. select的最大限制1024个并发.
接着再来看看事件驱动式的IO:
这个也好理解, 就是当第一阶段OK了, 发送一个信号就行了,当然了第二阶段还是阻塞的. 看看图就能理解了.
但是事件驱动IO有一个显而易见的问题: 当内核向进程发送第二个请求的通知的时候, 如果进程此时堵塞在第一个请求的第二个阶段. 那么会怎么样? 对于这样的情况, 有这样的解决方法, 我们提出两种通知机制:
- 水平触发: 一直通知, 直到接受为止
- 边缘触发: 只通知一次, 如果没有回应, 就使用回调或者让调用者自己去取结果.
我们今天的主角(?) – Nginx就是用了事件驱动IO和下面的异步IO.
为了解决阻塞的问题, 最后一种:
异步IO. 用户进程只需要发出请求, 等到内核悄悄的根据进程的要求把一切都完成了之后, 发出信号通知就好了.
最后总结一下:
Nginx
Nginx是个俄罗斯人搞出来的, 这个程序使用了当时最新的网络编程技术, 所以使得它在一开始就很受欢迎, 直到现在也依然是全球前三的Web服务器.
Nginx依靠一个高性能的网络编程库 – libevent, 其中调用的就是epoll().
现在就先来聊聊Nginx的特性:
模块化设计(非DSO), 较好的扩展性.
有着高可靠的应用, 使用一个主进程(master), 该进程不接受任何请求, 要做的是读取配置文件生成子进程(工作进程worker) 这些worker进程有多个用途: 缓存 反代, …
低内存消耗: 10000+keep-alive模式的connection, Nginx仅仅需要2.5M内存来维持.
支持热部署: 不停机使得配置文件更新, 日志文件滚动, 甚至支持版本平滑升级
支持事件驱动, 支持async, 支持内存映射机制.
缓存打开的fd文件描述符
http,smtp,pop3协议的反向代理服务器
反向代理是什么?
原先我们访问服务器, 是直接访问. 这样有很多缺点, 比如服务器的压力会变的很大, 而且不是那么安全. 于是我们就想到在服务器和用户之间加上一个隔离层, 这个隔离自己也有Web服务, 但是却不提供任何内容, 他会将请求重新构建报文发向后面的真正的Web服务器, 同时对静态内容作缓存, 使得下次访问更快速, 这个缓存一般都是以键值对的形式存储在内存中, 所以很快.
支持缓存加速和负载均衡机制
支持fastcgi(fpm,LNMP), uWSGI(python)
过滤器, 图像大小调整, SSI.
支持url, rewrite, 路径别名, 基于IP和用户的访问控制
支持速率限制, 支持并发限制
来看看Nginx的架构特性:
一个Master进程生成多个Worker进程. 下层可以看到缓存manger和loader.
之前说过了Nginx是基于模块的, 那么核心模块是什么?
核心模块被叫做Standard HTTP modules, 除了核心模块, 还有Optional HTTP modules, Mail modules以及3rd party modules.
Nginx的安装
nginx在epel源中可以找到, 直接yum install就行.
在这里扯一句..不知道为什么, 阿里的yum源教育网总是很慢?? 反而是Fedora官方的源稳定速度快..
也可以使用rpm包, 也可以使用源码编译~ 这里我们来试试源码编译.
首先要确保开发环境包组已经安装, 接着nginx依靠一个pcre-devel
, 所以需要先把这个装了.
首先我们还是添加nginx用户和组:
1 | [root@VM-CentOS7 ~]# groupadd -r nginx |
接下来就开始配置了, 关于选项可以查看./configure --help
获得.
1 | [root@VM-CentOS7 nginx-1.12.1]# ./configure --prefix=/usr/local/nginx --conf-path=/etc/nginx/nginx.conf --user=nginx --group=nginx --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx/nginx.pid --lock-path=/var/lock/nginx.lock --with-http_ssl_module --with-http_stub_status_module --with-http_gzip_static_module --with-http_flv_module --with-http_mp4_module --http-client-body-temp-path=/var/tmp/nginx/client --http-proxy-temp-path=/var/tmp/nginx/proxy --http-fastcgi-temp-path=/var/tmp/nginx/fastcgi --http-uwsgi-temp-path=/var/tmp/nginx/uwsgi |
这里我们开启流媒体支持, ssl支持, 指定临时文件目录等等…
接着就是make && make install
了. 完成了之后我们需要手动创建那些临时目录:
1 | [root@VM-CentOS7 nginx-1.12.1]# mkdir -pv /var/tmp/nginx/{fastcgi,uwcgi,proxy} |
接着启动nginx~
1 | [root@VM-CentOS7 nginx-1.12.1]# PATH=/usr/local/nginx/sbin:$PATH |
可以看到有启动了两个进程, 一个主进程, 一个工作进程. 而且, 工作进程的用户是nginx, 只有主进程的用户才是root.
1 | [root@VM-CentOS7 nginx-1.12.1]# curl -I http://192.168.206.138 |
就是这么简单~ 接下来按照惯例, 我们来看看配置文件吧:
nginx的配置文件主要有下面的几个部分构成:
- main配置: 全局配置
- event配置: 定义event工作模型的相关特性
- http {}: 定义http协议的相关设定
要注意的是: nginx的配置指令需要使用分号作为结尾:
derective value1 [value2...]
配置同样支持使用变量: 首先会有很多内置变量, 接着还可以使用set指令进行自定义变量的声明.
set var_name value
我们先从主配置段开始, 主要分成四类:
用于调试,定位问题的, 正常运行的必备配置, 优化性能的配置, 事件相关的配置
Nginx的配置
首先来看主配置段的设置,
| Syntax: | user
user [group]; |
| Default: | user nobody nobody;
|
声明启动用户, 用户组可选. 由于我们在配置的时候已经指明了, 所以这个选项在我们的配置文件中已经被注释掉了:
1 | #user nobody; |
| Syntax: | pid file; |
| Default: | pid nginx.pid;
|
声明nginx守护进程的pid文件. 很好理解
| Syntax: | worker_rlimit_nofile number; |
| Default: | — |
就是所有worker进程的最大文件句柄数, 默认是1024来着. 用户能打开的(ulimit -n)
与此相关的还有一个:
| Syntax: | worker_rlimit_core size; |
| Default: | — |
这个是在说打开的文件大小峰值, 不过一般都不需要调整.
以上是我们说的正常运行的必备配置. 接着来看一下性能优化相关
| Syntax: | worker_processes number | auto; |
| Default: | worker_processes 1;
|
worker进程的数量, 通常 这个值略少于物理CPU的核心数.
| Syntax: | worker_cpu_affinity cpumask; worker_cpu_affinity auto [cpumask]; |
整个选项主要是为了将一个Nginx的进程绑定到某个CPU上, 这样的好处是, 可以提升命中该CPU的cache的命中率.而不会随着CPU切换导致缓存丢失
什么是cpumask啊, 如果你有三颗CPU, 那么他们的掩码就是00000001
, 00000010
, 00000100
, 如果还有第四颗那就是00001000
.
| Syntax: | timer_resolution interval; |
| Default: | — |
这个是说调整进程的计时器解析度, 什么玩意? 其实就是发起gettimeofday()
这么一个系统调用的频率, 我们知道发起系统调用就一定伴随着软中断. 通过降低这个, 就可以达到优化性能的要求.
| Syntax: | worker_priority number; |
| Default: | worker_priority 0;
|
听名字就知道是干什么的, 其实这个选项就是NICE值, 我们之前说过nice这个命令, 从-20到20.
接着我们看一下事件相关的设定
| Syntax: | accept_mutex on | off; |
| Default: | accept_mutex off;
|
mutex叫做互斥锁(?), 简单的说, 如果开启, worker就会进行轮流的响应, 也就是master调度用户请求负载均衡的到每一个worker上, workers序列化的响应. 但是如果你的系统支持 EPOLLEXCLUSIVE
(Linux 4.5, glibc 2.24) 就不需要开启这个选项了.
| Syntax: | accept_mutex_delay time; |
| Default: | accept_mutex_delay 500ms;
|
如果上面的互斥锁开启了, 那么这个选项指定的就是当请求轮到下一个worker, 但是该worker正忙,等到多少时间的选项, 如果超时选择下一个. 不是那么关键了
| Syntax: | lock_file file; |
| Default: | lock_file logs/nginx.lock;
|
这个就是上面的那个锁文件的路径.
| Syntax: | use method; |
| Default: | — |
用来指定使用的事件模型. 不过这个选项不推荐手动执行, 因为Nginx会选择最优化最有效的机制.
| Syntax: | worker_connections number; |
| Default: | worker_connections 512;
|
单个worker能处理的最大连接数. 这也是一个很重要的参数.
接着来看看用于调试, 定位问题的选项:
这个调试不是说调就调的, 首先你需要在编译的时候加上选项才可以:
1 | [root@VM-CentOS7 nginx-1.12.1]# ./configure --help | grep debug |
| Syntax: | daemon on | off; |
| Default: | daemon on;
|
很好理解了, 就是说是否作为后台进程. 有点像很多进程的-D -d区别. 反正就是前台后台了
| Syntax: | master_process on | off; |
| Default: | master_process on;
|
是否使用master-worker进程模型.
| Syntax: | error_log file [level]; |
| Default: | error_log logs/error.log error;
|
配置错误日志功能. 如果想要获得debug的级别, 需要在编译的时候加上–with-debug的选项.
接着我们稍微做一下修改:
1 | worker_processes 2; |
接着就是要使得nginx能够重读配置文件了, 这里我们可以向nginx发送信号:
1 | [root@VM-CentOS7 ~]# nginx -s reload |
接着查看ps aux:
1 | [root@VM-CentOS7 ~]# ps aux | grep nginx |
worker多了一个吧~
如果你提示nginx: command not found. 原因很简单了, 我们编译安装的嘛~所以需要添加到PATH里哦.
除了reload, 还有 quit, stop, reopen这些.
现在我们说说Nginx作为Web服务器的相关配置:
所有的相关配置都在http {}中(我们把他叫做上下文), 由http的核心模块 – ngx_http_core_module
引入. 这个模块的选项特别多, 而且引入了很多内嵌变量.
上下文里面还可以添加上下文, http其中会出现server这么个上下文, 而在server上下文中又会出现 location上下文, 一个server上下文有点类似httpd中的<VirtualHost>
, 这里的location类似httpd的<Location>
, 用于定义URL与本地文件系统的映射关系. 而localtion也可以不只有一个, 并且还可以搭配if条件判断语句使用.
我们来整体看一下配置框架 :
1 | http { |
框架就是这样了, 现在就这个框架看看常见的配置项目. 我们直接把默认的配置贴着说:
1 | server { |
我们在后面加上一个最小化的server:
1 | server { |
接着我们创建一下目录和index文件:
1 | [root@VM-CentOS7 ~]# mkdir -pv /vhost/web1/ |
这个时候, 我们可以在重新载入文件之前检查一下配置, 没有问题的话就重启吧.
1 | [root@VM-CentOS7 ~]# nginx -t |
这么简单的一个虚拟主机就建好了, 是不是超简单. 但是回过头来, 其实这些选项都很有说头:
1 | server { |
listen
首先: listen, 这里可以跟地址(加端口, 不加默认80), 也可以直接端口, 也可以直接写上Unix套接字. 不仅如此, 后面可以跟上超多的选项, 但是..应该用的不多吧.
server_name
接着: server_name, 这个选项可以使用多个值, 也可以使用通配符, 甚至可以使用正则表达式, 但是要写上~
来说明后面跟着正则表达式. 而匹配的顺序就是先做精确匹配, 接着做左侧通配符检查, 然后是右侧通配符检查, 再然后是正则表达式匹配检查, 最后使用default server, 没有的话就用第一个.
location
接下来我们说说**location
**这个是一个很重要的属性, 而且有些难以理解:
location其实有两种用法, 但是一般都只是用第一个:
1 | location [ = | ~ | ~* | ^~ ] uri { ... } |
location可以根据用户的请求URL的不同来匹配不同的配置项. 一个server中有多个location, 每一个location块客户已实现访问控制等等诸多功能. 由于可以存在多个, 他也有自己的优先级.
可以看到后面有很多不同的符号, 这些符号的意义不一. =
表示请求匹配检查, 一个字符不一样就不匹配. ~
正则表达式匹配, 区分大小写. ~*
正则表达式匹配, 不区分大小写. ^~
: URI的前半部分匹配, 不检查正则.
优先级最高的就是精确匹配(=), 接着依次是^~
, ~
, ~*
,以及最后的不携带符号的.
alias
接着我们说一说别名(alias), 这个玩意同样是为了实现路径映射的. 这里我们对root和alias做一个对比你就知道是区别是什么了.
1 | location /image { |
注意到我在写alias的时候在右边加上了/. 这就是alias所替换的地方, 而root替换的是左边的根.
index
在httpd的时候, 我们使用index来指定哪一个做默认主页, 但是在Nginx中, 这个事情有一个专门的模块在负责.
来看一下 http_index_module 其实很简单:
1 | location = / { |
先搜索左边的, 没有的话就继续找后面的.
error_page
接着再说说错误页面, 也很简单:
1 | error_page 404 /404.html; |
通过错误码和位置就可以了. 同时, 我们也可以重定向到别的位置. 不仅如此, 我们还可以覆盖状态码, 在状态码和位置的中加上=code
就行了(但是要求必须是自定义404页面才可以), 我们试一试:
1 | error_page 404 /404.html; |
于是就变成200了.
访问控制
接着我们扯扯 访问控制.
首先是基于IP的访问控制, 很简单的两个指令. allow和deny.我们只要在后面加上{主机IP|网络地址|ALL}就可以实现了. 这就是基于IP的访问控制. 接着看看基于用户的访问控制, 和httpd一样, Nginx也支持basic和digest两种形式的认证. 有趣的是, basic的密码文件建议使用htpasswd来创建. 对于basic认证, 需要指明两个选项:
1 | auth_basic "Only for admin."; |
我们使用htpasswd来创建那个密码文件:
1 | [root@VM-CentOS7 users]# htpasswd -c -m /etc/nginx/users/.htpasswd justin |
接着再次访问就可以了.
ssl
接着我们再试试把nginx设置成为ssl. 已经搞过好几次了, 这次还是直接走流程:
1 | [root@VM-CentOS7 ~]# cd /etc/pki/CA/ |
自签证书创建完成.
1 | [root@VM-CentOS7 CA]# cd /etc/nginx/ |
证书签署完成. 接下来就是配置nginx了:
1 | server { |
验证配置接着重载配置就行了 ( 当然了, 这是自签的. 所以浏览器肯定会提示不安全. 你也可以选择把自己加入进去~ )
rewrite
没有问题了, 接着来看看rewrite, 这个指令可以URL重写, 来看一个示例:
1 | rewrite ^/image/(.*\.jpg)$ /img/$1 break; |
这个指令会将: XXXX.com/image/1.jpg ==> XXXX.com/img/1.jpg
这种操作对于用户而言是透明的, 也就是说用户并不能发觉自己访问的地方是另外一个位置, 服务器悄悄的更换了URL.
前面的好说, 就是查找替换嘛, 主要说说最后面的那个flag.
- last : 就是说在更换了之后, 不再进行后面的规则匹配, 接着User Agent重发请求, 在从头开始执行前面的过程, 有可能会进入死循环.
- break : 一旦此规则重写完成就不会再被任何重写规则进行检查, 新请求也不会被当前location中的任何规则检查.
- redirect : 以302响应码返回新的URL.
- permanent : 以301响应码进行永久重定向.
一般情况下, redirect和permanent用的不多, 他们会直接改变用户请求的URL, 因为30X状态码嘛~
if
接着我们再来说说if条件判断, 可以在server和location中使用. 语法是:
1 | if (condition) { ... } |
先来看一下condition有哪些:
- 变量名, 空值和0起始的(包括0)认定为False, 否则均为True
- 以变量为操作数的比较表达式, 可以使用 =, !=类似的比较操作符
- 以正则表达式的模式匹配, 还是那两个
~
和~*
.也可以在前面加上!
取反. - 测试指定路径是否为文件, -f和!-f
- 测试指定路径是否为目录, -d和!-d
- 测试文件存在性, -e和!-e
- 检查文件是否有执行权限, -x和!-x
一个典型应用就像这样:
1 | if ($http_user_agent ~* IE) { |
防盗链
如何为站点加上防盗链呢? 其实防盗的原理就是根据请求的referer判断的, 因此我们只需要设定允许访问的来源就好了:
1 | location ~*(gif|jpeg|png|jpg)$ { |
定制日志格式
回过头来有说到了日志格式, 在httpd的时候我们说使用%符来说明, 但是现在我们使用的就是nginx的各个内建变量了.
1 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
以上就是对http相关的一些基本的设定选项了. 除了http, 还有一些关于连接的设定也很重要
(我知道我写的很乱…但是都试一试就行了~)
1 | keepalive_timeout #; # 长连接的超时时长 |
更多的选项以后再说道反向代理的时候再说.