Nginx 是一个遵循主从架构的 Web 服务器,可以用作反向代理、负载均衡器、邮件代理和 HTTP 缓存。 哇!复杂的术语和混乱的定义,里面充斥着大量令人困惑的词语,对吧?不用担心,这篇文章可以帮大家先了解 Nginx 的基本架构和术语,然后我们将安装并创建 Nginx 配置。
Nginx 是一个神奇的 Web 服务器。
简单来说,Web 服务器就像个中间人。比如你想访问 dev.to,输入地址 https://dev.to
,你的浏览器就会找出 https://dev.to
的 Web 服务器地址,然后将其定向到后台服务器,后台服务器会把响应返回给客户端。
代理 vs 反向代理
Nginx 的基本功能是代理,所以现在就需要了解什么是代理和反向代理。
Proxy
目前国内无法访问 google,但是我们有时说挂个代理,然后就能顺利访问,而这种代理模式就是正向代理。假如我们在香港有一台服务器,这台服务器是能访问 google 的,而国内无法直接访问谷歌,但是可以访问香港的服务器。每次我们请求香港服务器,香港服务器拿到我们请求以后,再去访问 google 服务器,google 服务器把响应返回给香港服务器,香港服务器再把响应返回给我们。这样我们就能顺利的访问 google 了。
但是如果过多的客户端使用代理,导致代理服务器频繁请求 google,而 google 可能认为代理服务器是爬虫,会做一些反扒机制,这样客户端就无法正常访问,所以有时候代理服务器会告诉 google 我是一台代理服务器。
好的,我们有一个或多个客户端、一个中间 Web 服务器 A(在这种情况下,我们称它为代理)和一个真正提供服务的服务器 B。这其中最主要的事情是B 服务器不知道哪个客户端正在请求。是不是有点困惑?让我用一张示意图来解释一下。
一般来说代理分为三种,即透明代理,匿名代理和高匿名代理。
- 透明代理,代理服务器暴露了客户端真实的信息。
- 匿名代理,隐藏了客户端信息,但是会声明自己是代理服务器。
- 高匿名代理,隐藏了客户端信息,也不会声明自己是代理服务器,目标服务器不知道是否使用了代理,更不知道客户端真实信息
Reverse Proxy
例如淘宝,每天访问量很大,不可能只用单个服务器处理所有业务,于是出现了分布式部署。也就是通过部署多台服务器来解决访问人数限制的问题。
客户端请求 taobao.com,DNS 服务器把域名解析到 nginx 服务器上(简单的这么理解),nginx 服务器接收到之后,按照一定的规则(比如:轮询调度 Round-Robin)分发给了后端的业务处理服务器进行处理了。
反向请求的来源也就是客户端是明确的,但是请求的具体由哪台服务器处理并不明确,nginx 扮演的就是一个反向代理角色。
反向代理隐藏了具体处理业务的服务器信息。
负载均衡
可恶,又是一个新词,但是这个词比较容易理解,因为它是“反向代理”本身的一个实际应用。
我们先说说基本的区别。在负载均衡中,必须要有两个或者更多的后台服务器;但在反向代理设置中,这不是必须的,它甚至可以只跟单台后台服务器一起使用。
让我们从幕后看一下,如果我们有大量来自客户端的请求,这个负载均衡器会检查每个后台服务器的状态并分配请求的负载,然后将响应更快地发送给客户端,目的就是保障每台服务器的会比较平均的处理请求,而不会是“旱的旱死,涝的涝死”,分担了服务器压力,避免了服务器崩溃的情况。
NGINX 配置
代理静态资源
在这里,我们让 NGINX 监听 5000
端口,并指向 /nginx-demo/
文件夹下的静态资源。
此时我们通过 curl -k http://localohost:5000
就可以访问到 /nginx-demo/
下的静态文件,默认为 /nginx-demo/index.html
http {
server {
listen 5000;
root /path/to/nginx-demo/;
}
}
events {}
添加 events {}
是必须的,因为对于 NGINX 架构来讲,它通常被用来表示 Worker
的数量。
Reverse Proxy
NGINX 反向代理主要通过 proxy_pass
来配置,将你项目的开发机地址填写到 proxy_pass
后面,正常的格式为 proxy_pass URL
即可
server {
listen 80;
location / {
proxy_pass http://10.10.10.10:20186;
}
}
现在我们通过 curl -k http://localhost/hello
访问某个服务,NGINX 会把该请求转发到 http://10.10.10.10:20186/hello
然后拿到结果,然后返回。
同时多个服务我们可以通过反向代理合并到一个端口中,比如现在服务器上有 app1 处理部分请求,app2 处理另外一部分请求,同时我们知道 Node.js http server 端口号不能重叠,所以我们可以让 app1 server 监听 1000 端口,app2 监听 2000 端口,然后通过 NGINX 反向代理到同一个端口
server {
listen 80;
location /app1/ { # 处理 http://localhost/app1/xxxx
proxy_pass http://localhost:1000;
}
location /app2/ { # 处理 http://localhost/app2/xxxx
proxy_pass http://localhost:2000;
}
}
具体转发到 app1,还是 app2 是根据请求的 url 决定的。url 匹配规则可以采用正则表达式,全等,前缀,后缀等等,后面会继续讲解
HTTPS
NGINX 同样可以简单的创建 https 服务,比如我们有内部 http 服务,通过反向代理的方式可以转换成“对外”的 HTTPS 服务,通过监听 443 端口以及指定 https 证书文件和私钥文件的地址(服务器上的绝对路径),可以简单的创建 https 服务
curl -k https://sylvenas.xyz/a/xxx 转发到 http://localhost:1000
curl -k https://sylvenas.xyz/b/xxx 转发到 http://localhost:2000
server {
listen 443 ssl;
server_name sylvenas.xyz;
ssl_certificate /Users/sylvenas/Documents/feature/nginx/final.crt;
ssl_certificate_key /Users/sylvenas/Documents/feature/nginx/site.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location /a/ {
proxy_pass http://localhost:1000;
}
location /b/ {
proxy_pass http://localhost:2000;
}
}
location 匹配规则
语法规则很简单,一个 location 关键字,后面跟着可选的修饰符(=, ~, *, ^),后面是要匹配的字符(uri),花括号中是要执行的操作。
修饰符
= 表示精确匹配。只有请求的 url 路径与后面的字符串完全相等时,才会命中。
~ 表示该规则是使用正则定义的,区分大小写。
* 表示该规则是使用正则定义的,不区分大小写。
^ 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。
匹配过程
对请求的 url 序列化。例如,对 %xx
等字符进行解码,去除 url 中多个相连的/
,解析 url 中的.
,..
等。这一步是匹配的前置工作。
location 有两种表示形式,一种是使用前缀字符,一种是使用正则。如果是正则的话,前面有 ~
或 ~\*
修饰符。
正则也是前缀字符的一种,这里只是单独拿出来说罢了,就是为了区分正则和普通的前缀字符
具体的匹配过程如下:
- 首先先检查使用前缀字符定义的 location,选择最长匹配的项并记录下来。
- 如果找到了精确匹配的 location,也就是使用了
=
修饰符的 location,结束查找,使用它的配置。 - 然后按顺序查找使用正则定义的 location,如果匹配则停止查找,使用它定义的配置。
- 如果没有匹配的正则 location,则使用前面记录的最长匹配前缀字符 location。
基于以上的匹配过程,我们可以得到以下两点启示:
使用正则定义的 location 在配置文件中出现的顺序很重要。因为找到第一个匹配的正则后,查找就停止了,后面定义的正则就是再匹配也没有机会了。
使用精确匹配可以提高查找的速度。例如经常请求 /
的话,可以使用 =
来定义 location。
实例
假如我们有下面的一段配置文件:
location = / {
[ configuration A ]
}
location / {
[ configuration B ]
}
location /user/ {
[ configuration C ]
}
location ^~ /images/ {
[ configuration D ]
}
location ~* \.(gif|jpg|jpeg)$ {
[ configuration E ]
}
-
请求
/
精准匹配 A,不再往下查找。 -
请求
/index.html
匹配 B。首先查找匹配的前缀字符,找到最长匹配是配置 B,接着又按照顺序查找匹配的正则。结果没有找到,因此使用先前标记的最长匹配,即配置 B。 -
请求
/user/index.html
匹配 C。首先找到最长匹配 C,由于后面没有匹配的正则,所以使用最长匹配 C。 -
请求
/user/1.jpg
匹配 E。首先进行前缀字符的查找,找到最长匹配项 C,继续进行正则查找,找到匹配项 E。因此使用 E。 -
请求
/images/1.jpg
匹配 D。首先进行前缀字符的查找,找到最长匹配 D。但是,特殊的是它使用了^~
修饰符,不再进行接下来的正则的匹配查找,因此使用 D。这里,如果没有前面的修饰符,其实最终的匹配是 E。大家可以想一想为什么。 -
请求
/documents/about.html
匹配 B。因为 B 表示任何以/
开头的 URL 都匹配。在上面的配置中,只有 B 能满足,所以匹配 B。
负载均衡
NGINX 通过 upstream
模块实现负载均衡
worker_processes 1;
events {
worker_connections 1024;
}
http {
upstream firstdemo {
server 39.106.145.33;
server 47.93.6.93;
}
server {
listen 8080;
location / {
proxy_pass http://firstdemo;
}
}
}
- worker_processes
工作进程数,和CPU核数相同
- worker_connections
每个进程允许的最大连接数
- upstream 模块
负载均衡就靠它 语法格式:upstream name {} 里面写的两个server分别对应着不同的服务器
- server 模块
实现反向代理 listen 监督端口号 location / {}访问根路径 proxy_pass http://firstdemo,代理到 firstdemo 里两个服务器上
ip_hash
当用户第一次访问到其中一台服务器后,下次再访问的时候就直接访问该台服务器就好了,不用总变化了。那么就发挥了 ip_hash
的威力了
upstream firstdemo {
ip_hash;
server 39.106.145.33;
server 47.93.6.93;
}
ip_hash
它的作用是如果第一次访问该服务器后就记录,之后再访问都是该服务器了,这样比如第一次访问是服务器A,那之后再访问也会分配为服务器A访问了。
常用命令
- 启动
sudo nginx
- 重启
sudo nginx -s reopen
- 重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s reload
- 强制停止Nginx服务
nginx -s stop
- 优雅地停止Nginx服务(即处理完所有请求后再停止服务)
nginx -s stop
- 检测配置文件是否有语法错误,然后退出
nginx -t
- 杀死所有nginx进程
killall nginx