Nginx 作为一款高性能的代理软件,在工作中常被用做负载均衡器、正向反向代理。最近在看了一个关于 Nginx 的视频,老师讲得很不错。笔者也记录了一下整套课程的一些重点和自己的理解。
笔者学习习惯是先实战后理论,所以本文主要偏实战,希望读者可以通过本文快速搭建 Nginx,并尝试使用它的特性。关于 Nginx 的运行原理、核心模块、源码分析等内容可能要等下次了(其实是笔者学艺不精,暂时看不太懂),大家感兴趣可以先看看其他优质文章。
准备工作
Nginx 是什么这里就不赘述了,相信大家都用过,Nginx 目前的发行版本如下:
- Nginx 开源版
- Nginx Plus:F5 基于 Nginx 开源版开发的商业版本
- Openresty:国人开发,整合了 lua 模块
- Tengine:淘宝基于开源版改造,经过双十一验证
本文主要讲述开源版的 Nginx 和 Openresty 开源版
快速安装
大家到 nginx.com 下载一下源码包,解压编译安装
# 安装依赖
$ yum install -y pcre pcre-devel
$ yum install -y zlib zlib-devel
# 解压编译
$ tar -zxf nginx-1.21.6.tar.gz
$ ./configure --prefix=/usr/local/nginx
$ make && make install
编写一下 service 文件,方便操作
$ cat <> /usr/lib/systemd/system/nginx.service
[Unit]
Description=nginx - web server
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
ExecQuit=/usr/local/nginx/sbin/nginx -s quit
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
$ systemctl daemon-reload
$ systemctl start nginx.service
$ ystemctl enable nginx.service
# 以后重启 nginx 只需要
$ systemctl restart nginx
# 重新加载配置
$ systemctl reload nginx
配置文件在 conf/nginx.conf
,简化一下
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
有没有发现我的配置文件比较工整,以前是在 SFTP 上记事本编辑,缩进需要手动调整特别麻烦,后面直接用 Vscode 的 nginx-formatter 格式化(后面会跟大家说怎么安装)。
nginx-formatter 通过先用特殊符号替换双引号中的内容避免误操作,通过正则表达式的方式去除空白字符,项目链接:
- nginx-config-formatter:https://github.com/slomkowski...
- nginxbeautifier:https://github.com/vasilevich...
对于最小配置的一些说明:
- worker_processes:Nginx 基于多进程 React 模型,启动时会启动一个 Master 和 N 个 Worker,这里配置 Worker 个数,一般设置为 CPU 逻辑核心数 + 1
- worker_connections:Worker 是真正处理请求的进程,这个配置主要描述一个 Worker 可以处理的连接上限
- include:包含其他 nginx 配置文件
- mine.type:配置了资源后缀名和响应体的 Content-Type 的映射关系
- sendfile:是否开启零拷贝,只有在磁盘的内容会触发 sendfile
- server:定义一个服务
- listen:监听端口
- server_name:可以配置主机名或者域名
- location:匹配的 URL 后缀,root 指的是文件系统路径,index 是欢迎页
- root:定义根目录
- index:欢迎页
保存后,使用 curl 命令简单测试一下是否成功
$ curl localhost
curl 是一个简单的 HTTP 请求工具,常用的几个参数如下:
- -H "Conten-Type: application/josn": 添加请求头
- -I:显示响应头
- -e:设置 Reference
- -X 设置请求方式
- --proxy:设置代理
- -sSL:跟踪重定向
详细文档:https://itbilu.com/linux/man/...
测试环境搭建
为了更好的测试,这里就直接使用 Flask(容器运行) 模拟上游服务,宿主机作为 Nginx,网络拓扑如下:
这张图是使用虚拟画板画的:https://board.oktangle.com/
$ mkdir ~/flask-demo
$ cd ~/flask-demo
创建app.py
from flask import Flask
from flask import render_template
from flask import request
import socket
app = Flask(__name__)
@app.route("/")
def index():
return render_template('index.html', data={
'hostname': socket.gethostname(),
'num': request.args.get('num', 'None')
})
创建 templates 目录,新建 index.html 文件,内容如下:
Nginx 实战
当前服务器 hostname: {{ data.hostname }}
从 URL 中获取 参数 num: {{ data.num }}
本来想输出 IP 的,但是没找到 flask 从 IP 报文获取源地址的 API,拿主机名代替一下
回到主目录,创建 Dockerfile
FROM python:3.8-slim-buster
WORKDIR /python-docker
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=80"]
创建依赖文件 requirements.txt
Flask==2.0.3
最终目录结构如下:
│ app.py
│ Dockerfile
│ requirements.txt
│
├─static
│ └─img
│ demo.png
│
├─templates
└─ index.html
在 Linux 中使用下面命令进行测试环境搭建:
$ docker build -t flask-demo:0.1 .
$ docker network create --driver bridge --subnet 172.20.0.0/24 --gateway 172.20.0.1 flask-cluster
# 启动三个镜像
$ for i in $(seq 1 3); do docker run --name flask-demo-$i --hostname flask-demo-$i --restart=always --network flask-cluster --ip 172.20.0.1$i -d flask-demo:0.1; done;
# 打开 nginx 端口
$ firewall-cmd --add-port=80/tcp --permanent
$ firewall-cmd --reload
# 下面命令用于删除容器
for i in `seq 1 3`; do docker rm -f flask-demo-$i; done
VsCode配置
使用 Vsocde 中的插件会有更好的学习体验,安装下面插件,Remote SSH 连接虚拟机或远程主机,以后修改配置就非常方便了。
- nginxbeautifier:Nginx 配置文件格式化工具(感兴趣可以阅读一下代码,300多行)
- vscode-nginx-conf:VSCode 配置文件代码补全、连接到官方文档插件
- luaHelper:Lua 脚本语法补全、高亮插件
- Remote SSH:连接远程主机插件
Nginx 核心功能
Nginx 的核心功能主要围绕着 http - server 模块进行描述。在 server 模块下,server_name 可以配置主机名和域名,Nginx 在解析 HTTP 报文中的 header —— Host
来判断进入哪一个 server 块。这里可以使用通配符,优先级自上而下。基于此可以实现
多租户二级域名:
比如
xxx.domain.com
,ServerName 配置*.domain.com
,后端服务器解析 Http 报文中的 Host,根据 xxx 展示对应用户信息,当然这样子不太安全。- 短网站:这个需要和后面介绍 URLRewrite 配合使用
我们可以自定义一个 Server 实现 HTTP DNS,但这需要我们学习到后面 lua 的时候才好实现。
HTTPDNS:由于 DNS 污染、本地 DNS 服务不够智能、DNS 根据自己而非客户返回最近服务器错误等问题,我们可以自己基于 HTTP 协议搭建 DNS 服务器,但这需要客户端自己实现发起解析的逻辑,但是浏览器不支持 HttpDNS,解决方式是本地启动一个 DNS 服务器结合 FakeIP 解决,这个可以参考我之前写的 Clash 代理工具解析。
server_name
配置方式:
完整配置
server_name domain1.com domain2.com;
正则配置
server_name domain1.~^[0-9]+\.domain.com;
通配符配置
server_name domain1.*
配置完 server_name
后,接着就要编写匹配规则 location
了,目前 server 已经匹配 URL 上的 host 部分,剩下的 uri 则由 location 进行匹配。你可以选择让 Nginx 帮助你将请求转发出去(正反向代理),也可以让 Nginx 返回本地文件(资源服务)。
正反向代理
为了保证服务器的安全性,暴露统一网络入口,我们会使用 Nginx 作为服务的入口,所有流量都会经过这个入口进入上游服务。
这种方式称为反向代理,客户端不知道服务端的具体地址,只关注与代理服务器的通讯。
正向代理则更常用于跨网段通讯,比如机房 A 中的主机不能访问机房 B 的主机,需要通过能够同时于机房 A、机房 B 中主机通讯的代理服务器作为跳板进行跨机房访问。
其实正向代理和反向代理没有区别,只是通常情况下反向代理是对请求发起方无感,正向代理对发起者有感(甚至需要发起者自己去代理中配置)
这里的 Nginx 也被称为网关,由于网关数量比较少,容易出现瓶颈,比如下载大文件的场景,Response 都要经过网关。LVS 的 DR 模式开放上游服务器的 OUTPUT 防火墙,使得入栈经过网关,出栈由上游服务直接返回客户端。
反向代理配置
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://flask;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
upstream flask {
server 172.20.0.11:80;
server 172.20.0.12:80;
server 172.20.0.13:80;
}
proxy_pass:会将请求内容转发到 upstream 上有服务上
正向代理配置
正向代理其实就是 HTTP 报文写着需要访问的目标地址(header 中的 hsot 和 uri),但将报文发给了代理服务器。所以如果你希望 Nginx 充当代理,只需要进行下面的配置
location / {
proxy_pass $scheme://$host$request_uri;
resolver 8.8.8.8;
}
这是基于七层代理,四层需要引入 stream 模块,七层正向代理只支持 HTTP 协议,如果需要支持 HTTPS 的话需要使用下面模块
- ngx_http_proxy_connect_module:https://github.com/chobits/ng...
负载均衡
在负载不断增加的情况下,我们很有可能对上游服务进行扩容,即增加更多的节点。这时候就需要 Nginx 负责将请求尽量均匀的分摊到上游服务上。Nginx 提供了好几种算法:
Round Robin 轮询
雨露均沾,一人一下。但很多时候物理机或者虚拟机的配置不一样,我们可以为资源较多的主机配置更高的权重。
upstream flask {
server 172.20.0.11:80 weight=10;
server 172.20.0.12:80 weight=2;
server 172.20.0.13:80 weight=1;
}
这里会大概以 10 : 2 : 1 分配流量,处理 weight 还有几个可以设置的参数:
- down:不参与调度
- backup:除了 backup 以外的服务器都无法提高服务时被调度
这两个一般不会使用,比如 backup,我们试图希望服务出现故障后备用机器可以顶替,但如果是因为代码逻辑不正确导致的宕机,理论上备用机器上的代码也是有问题的,所以切换后很可鞥也会宕机。
ip_hash
轮询算法有个问题就是每次请求必须是无状态的,即不能使用单个服务器的 session 暂存数据。ip_hash 算法根据客户端 IP 进行散列,所以每次请求都会打到同一个服务器,但该算法的问题在于:
- 由于手机移动过程中 IP 地址会发生改变,如果上游服务使用了 session 会导致失效
- 可能出现大部分 IP 地址散列到同一台上游服务
- 对于内网用户数量较多,比如考试系统、ERP 系统,出口 IP 地址只有几个,容易导致流量倾斜
least_conn
最少连接,即将流量分给流量最小的主机。这个也不常用,之所以会有流量倾斜很可能是服务权重具备差异。
url_hash
根据 URL 进行散列,适合资源定位,比如多个文件散落在不同服务中,可以通过 URL 中的文件名进行 hash 找到目标节点。缺点在于:基于 url_hash 上游服务只能部署在一台主机上
fair
基于响应时间的调度策略,需要依赖第三方插件,这种方式不常用,因为响应时间和很多因素有关系,会导致交换机过热流量倾斜。
综上,我们大多数情况下都会使用 RR 轮询,如果上游服务是有状态的,可以选择 hash,更特殊的如果系统正在进行灰度发布,可以自己编写 lua 脚本动态调整。
资源服务
如果你选择让 Nginx 作为资源服务器,比如你希望将前端静态资源、图片前置到 Nginx 服务器,上游服务只负责业务处理,那么你很能需要进行动静分离的配置
server {
location / {
proxy_pass http://127.0.0.1:8080;
root html;
index index.html index.htm;
}
location /css {
root /usr/local/nginx/static;
index index.html index.htm;
}
location /images {
root /usr/local/nginx/static;
index index.html index.htm;
}
location /js {
root /usr/local/nginx/static;
index index.html index.htm;
}
}
我们也可以使用正则匹配的方式
location ~*/[js|css|images] {
root /usr/local/nginx/static;
index index.html index.htm;
}
对于 location 的书写有通用模式、精准模式和正则模式:
- / 通用匹配,任何请求都会匹配到。
- = 精准匹配,不是以指定模式开头
- ~ 正则匹配,区分大小写
- ~* 正则匹配,不区分大小写
- ^~ 非正则匹配,匹配以指定模式开头的location
在同一个配置文件中也有一些规定:
- 多个正则按书写顺序进行匹配,fallthrough
- 非正则也是按书写顺序匹配,找到匹配度最高的规则执行
- 正则模式优先级高于非正则,优先级顺序为 “=”匹配 > “^~”匹配 > 正则匹配 > 普通
比如我们可以将 Flask 中的图片删除,在 Nginx 中进行下面配置
$ docker exec flask-demo-1 rm /python-docker/static/img/demo.png
$ docker exec flask-demo-2 rm /python-docker/static/img/demo.png
$ docker exec flask-demo-3 rm /python-docker/static/img/demo.png
编辑配置文件
location ~*/static {
root flask-demo;
index index.html index.htm;
}
将图片上传到 $NGINX_DIR/html/flask-demo/static/img
中。这里也可以使用 alias。
root 和 alias 的区别:
- root:设置根目录,nginx 会在根目录,按照 URL 的层级寻找文件,root 保留匹配上的部分进行搜索
alias:和 root 类似,但会舍弃匹配上的部分进行搜索
location ^~ /static { alias flask-demo/static/; }
使用 alias 时,当 nginx 匹配上 location 后,会舍弃匹配上的文本,比如上面配置的
/static
,当访问http://localhost/static/img/demo.png
时,会保留/img/demo.png
,如果配置为/static/
则保留下img/demo.png
,保留下来的部分会拼接到`alias
后面,所以这里 alias 最后加了 /,如果 location 保留了 /,这里就不要加。如果填写相对路径,父目录是 NGINX_DIR
URI 重写
现在我们已经可以完整配置 Nginx 了,但有一些场景我们需要对 URI 进行重写:
- 防盗链:有时候我们不希望我们的图片、JS 被其他网站直接引用
- 隐藏 URL:让用户看不出请求地址,防止恶意访问
- 添加文件后缀:比如在连接后添加 .html 后缀,搜索引擎的蜘蛛会识别并收集里面的内容
只需要在 location
中添加配置
rewrite ^/([0-9]+).html$ /?num=$1 break;
# break 表示使用当前规则
# last 表示匹配下一条规则,如果没有下一条则使用当前规则重写
# redirect 表示301临时重定向,浏览器会重新访问重写后的 URL
# permanent 永久重定向,301 和 302 主要是给搜索引擎的蜘蛛识别
比如
location / {
rewrite ^/([0-9]+).html$ /?num=$1 break;
proxy_pass http://flask;
}
访问 localhost/10086.html
,响应体的 html 中可以读取到 arg,正因为进行了 URI 重写
防盗链
Gitee 图床之前在别的网站引用会返回一个 Gitee 的图标,防止其他网站滥用图床,这里使用到了防盗链。当我们使用浏览去访问一个网页后,再次向服务器获取图片、js 等资源时会自动带上 Referer 头。
在 Nginx 中我们可以进行配置,检测 Referer是否合法
valid_referers none | blocked | server_names | strings ....;
- none:允许没有携带 Referer 头的请求
- blocked:请求头中存在 Referer字段,但值不是以"http(s)://"开头的字符串
我们可以为 demo.png 设置防盗链
location ~* ^.+\.*..(jpeg|gif|png|jpg) {
valid_referers 192.168.80.154;
if ($invalid_referer) {
rewirte ^/ /invalid.png
}
}
location /static {
valid_referers 192.168.80.154;
if ($invalid_referer) {
return 403;
}
root flask-demo;
index index.html index.htm;
}
location = /invalid.png {
root flask-demo;
}
尝试用浏览器访问一下 192.168.80.154/static/demo.png
会发现返回的是 invalid.png
高可用配置
单台 Nginx 作为网关容易出现单点故障,所以需要为其准备一台从机进行 backup。使用 VIP + keepalived 的方式,主节点的 keepalived 通过进程状态或者脚本监控 Nginx 服务,如果 Nginx 宕机则主动放弃 Master 角色,或者从机的 keepalived 发现主节点发送 VRRP Report 超时也会升级为 Master。
- keepalived 官网:https://www.keepalived.org/in...
- VRRP 详解:https://cshihong.github.io/20...
- keepalived 进行服务监控:https://www.wumingx.com/linux...
- keepalived 脑裂问题:https://www.zhihu.com/questio...
简单概括一下,keepalived 基于 VRRP 协议,VRRP 协议能够将多个设备虚拟成一台设备对外暴露服务。VRRP 协议中存在 Master 和 Backup,Master 会不断通过组播方式通告当前持有的虚拟 IP,并响应局域网内的 ARP 请求。Backup 则维护定时器,只要在一定时间内接收不到 Master 的通告报文,或是 Master 的优先级小于当前 Backup 的优先级(相等则比较 IP 地址大小,大者优先)则升级为 Master,旧 Master 接收到通告也会降级为 Backup。
这里会有几个问题:
如果不通畅,Backup定时器超时升级为 Master 后受到旧 Master 的通告,发现自己优先级不高又回到 Backup,造成频繁切换。可以设置一下切换延迟,延迟时间设置需要权衡。
按照 keepalived 并进行配置
$ yum -y install openssl-devel
$ ./configure --prefix=/usr/local/keepalived
$ make && make install
$ vim etc/keepalived/keepalived.conf
# Master 节点配置
global_defs {
router_id node1
}
vrrp_instance nginx-demo {
state MASTER
interface ens33
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass admin
}
virtual_ipaddress {
192.168.80.220 # 虚IP
}
}
# Backup 节点配置
global_defs {
router_id node2
}
vrrp_instance nginx-demo {
state BACKUP
interface ens33
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass admin
}
virtual_ipaddress {
192.168.80.220 # 虚IP
}
}
$ systemctl start keepalived
Https 配置
https 是二十一世纪最伟大的发明之一,通告非对称加密,公钥加密私钥解密、私钥加密公钥解密、公钥加密公钥无法解密、CA 机构认证等方式保证通讯内容不被篡改、不被伪造、不被窃听
整个 https 原理这里就不赘述了,这里有一点想说的是,比如我们的服务器和域名在阿里云上购买,向 CA 申请证书时需要认证当前服务器和当前域名都是同一个用户的,阿里云使用 DNS 自动检测的方式,也就是让用户修改 DNS 记录到一个指定地址,CA 认证服务器访问看看是否成功(这一步也是自动的)。除了这种方式,还可以让用户自己上传一个文件到服务器中并暴露访问地址,CA 通过访问该地址确认服务器的归属人。
Nginx 上配置也比较简单,将证书和私钥上传并配置即可
server {
# 服务器端口使用443,开启ssl, 这里ssl就是上面安装的ssl模块
listen 443 ssl;
# 域名,多个以空格分开
server_name hack520.com www.hack520.com;
# ssl证书地址
ssl_certificate /usr/local/nginx/cert/ssl.pem; # pem文件的路径
ssl_certificate_key /usr/local/nginx/cert/ssl.key; # key文件的路径
# ssl验证相关配置
ssl_session_timeout 5m; #缓存有效期
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; #加密算法
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #安全链接可选的加密协议
ssl_prefer_server_ciphers on; #使用服务器端的首选算法
location / {
root html;
index index.html index.htm;
}
}
server {
listen 80;
server_name hack520.com www.hack520.com;
return 301 https://$server_name$request_uri;
}
以上就是实战上篇所有内容啦,希望大家能够通过本文快速配置 Nginx 并使用,下篇会介绍一下高级模块、二次开发、缓存等功能,敬请期待!