Nginx 实战核心知识点整理(上)

Nginx 作为一款高性能的代理软件,在工作中常被用做负载均衡器、正向反向代理。最近在看了一个关于 Nginx 的视频,老师讲得很不错。笔者也记录了一下整套课程的一些重点和自己的理解。

笔者学习习惯是先实战后理论,所以本文主要偏实战,希望读者可以通过本文快速搭建 Nginx,并尝试使用它的特性。关于 Nginx 的运行原理、核心模块、源码分析等内容可能要等下次了(其实是笔者学艺不精,暂时看不太懂),大家感兴趣可以先看看其他优质文章。

思维导图
Nginx 实战核心知识点整理(上)_第1张图片
那我们开始吧!

准备工作

Nginx 是什么这里就不赘述了,相信大家都用过,Nginx 目前的发行版本如下:

  1. Nginx 开源版
  2. Nginx Plus:F5 基于 Nginx 开源版开发的商业版本
  3. Openresty:国人开发,整合了 lua 模块
  4. 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 通过先用特殊符号替换双引号中的内容避免误操作,通过正则表达式的方式去除空白字符,项目链接:

  1. nginx-config-formatter:https://github.com/slomkowski...
  2. 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,网络拓扑如下:
Nginx 实战核心知识点整理(上)_第2张图片

这张图是使用虚拟画板画的: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 块。这里可以使用通配符,优先级自上而下。基于此可以实现

  1. 多租户二级域名:

    比如 xxx.domain.com,ServerName 配置 *.domain.com,后端服务器解析 Http 报文中的 Host,根据 xxx 展示对应用户信息,当然这样子不太安全。

  2. 短网站:这个需要和后面介绍 URLRewrite 配合使用

我们可以自定义一个 Server 实现 HTTP DNS,但这需要我们学习到后面 lua 的时候才好实现。

HTTPDNS:由于 DNS 污染、本地 DNS 服务不够智能、DNS 根据自己而非客户返回最近服务器错误等问题,我们可以自己基于 HTTP 协议搭建 DNS 服务器,但这需要客户端自己实现发起解析的逻辑,但是浏览器不支持 HttpDNS,解决方式是本地启动一个 DNS 服务器结合 FakeIP 解决,这个可以参考我之前写的 Clash 代理工具解析。

server_name 配置方式:

  1. 完整配置

    server_name  domain1.com domain2.com;
  2. 正则配置

    server_name  domain1.~^[0-9]+\.domain.com;
  3. 通配符配置

    server_name  domain1.*

配置完 server_name 后,接着就要编写匹配规则 location 了,目前 server 已经匹配 URL 上的 host 部分,剩下的 uri 则由 location 进行匹配。你可以选择让 Nginx 帮助你将请求转发出去(正反向代理),也可以让 Nginx 返回本地文件(资源服务)。

正反向代理

为了保证服务器的安全性,暴露统一网络入口,我们会使用 Nginx 作为服务的入口,所有流量都会经过这个入口进入上游服务。

Nginx 实战核心知识点整理(上)_第3张图片

这种方式称为反向代理,客户端不知道服务端的具体地址,只关注与代理服务器的通讯。

正向代理则更常用于跨网段通讯,比如机房 A 中的主机不能访问机房 B 的主机,需要通过能够同时于机房 A、机房 B 中主机通讯的代理服务器作为跳板进行跨机房访问。

Nginx 实战核心知识点整理(上)_第4张图片

其实正向代理和反向代理没有区别,只是通常情况下反向代理是对请求发起方无感,正向代理对发起者有感(甚至需要发起者自己去代理中配置)

这里的 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 的话需要使用下面模块

负载均衡

在负载不断增加的情况下,我们很有可能对上游服务进行扩容,即增加更多的节点。这时候就需要 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 还有几个可以设置的参数:

  1. down:不参与调度
  2. backup:除了 backup 以外的服务器都无法提高服务时被调度

这两个一般不会使用,比如 backup,我们试图希望服务出现故障后备用机器可以顶替,但如果是因为代码逻辑不正确导致的宕机,理论上备用机器上的代码也是有问题的,所以切换后很可鞥也会宕机。

ip_hash
轮询算法有个问题就是每次请求必须是无状态的,即不能使用单个服务器的 session 暂存数据。ip_hash 算法根据客户端 IP 进行散列,所以每次请求都会打到同一个服务器,但该算法的问题在于:

  1. 由于手机移动过程中 IP 地址会发生改变,如果上游服务使用了 session 会导致失效
  2. 可能出现大部分 IP 地址散列到同一台上游服务
  3. 对于内网用户数量较多,比如考试系统、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

在同一个配置文件中也有一些规定:

  1. 多个正则按书写顺序进行匹配,fallthrough
  2. 非正则也是按书写顺序匹配,找到匹配度最高的规则执行
  3. 正则模式优先级高于非正则,优先级顺序为 “=”匹配 > “^~”匹配 > 正则匹配 > 普通

比如我们可以将 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 进行重写:

  1. 防盗链:有时候我们不希望我们的图片、JS 被其他网站直接引用
  2. 隐藏 URL:让用户看不出请求地址,防止恶意访问
  3. 添加文件后缀:比如在连接后添加 .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 基于 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 并使用,下篇会介绍一下高级模块、二次开发、缓存等功能,敬请期待!

你可能感兴趣的:(nginx负载均衡反向代理)