最流行的web服务器nginx

楔子

相信很多人都用 Nginx 搭建过反向代理服务器,但其实这仅仅只是 Nginx 的一些最基本的用法,我们还需要熟悉 Nginx 底层的运行原理,比如:Nginx 的架构、进程模型,以及 Liunx 中的 CPU、内存、磁盘、网络等等要如何与 Nginx 配置文件中的指令相结合从而使得 Nginx 的性能达到最大化。那么下面我们就从零开始,一点一点深入挖掘 Nginx。

首先 Nginx 是一个高性能的 HTTP 和反向代理服务器,而且 Nginx 的作者是一个对操作系统有着深入研究的人,可以说他把 Nginx 已经优化到极致了,使得 Nginx 具备高性能和高吞吐量。当然 Nginx 还有很多其它优点:

  • Nginx 的模块化设计,使得它可以很容易被扩展,因此 Nginx 有着丰富的第三方模块;
  • Nginx 可靠性高,可以持续不间断地提供服务;
  • Nginx 支持热部署,能够在不停止服务的情况下升级 Nginx;
  • Nginx 遵循 BSD 协议,可以自由地进行二次开发(比如淘宝的 Tengine)。

安装 Nginx

Nginx 现在分为四种:Nginx 开源版Nginx plus 商业版OpenRestyTengine,这里我们直接选择开源版本即可。然后是 Nginx 的安装,可以使用 Docker 安装、也可以使用 yum 安装,但我个人更建议的做法是通过源码编译的方式安装。因为 yum 在安装的时候不一定会把第三方模块也集成进去,而且任何一个软件,我们也都应该知道如何通过源码编译的方式去安装。

下面进入 Nginx 官网将源码下载下来。

我们注意到上面有三个版本,Mainline version 是正在主力开发的版本,Stable version 是稳定的最新版本,而 Legacy versions 则是稳定的老版本。这里下载稳定的最新版本,也就是 1.24.0。

上传到服务器之后解压,目录结构如图所示,我们解释一下每个目录是做什么的。

  • auto 目录里面有几个子目录,auto/cc 用于编译,auto/lib 包含一些额外的库,auto/os 用于判断并适配操作系统。至于 auto 目录里的其它文件则是用来辅助 configure 脚本在执行的时候判定 Nginx 支持哪些模块,以及当前的操作系统有什么特性可以供 Nginx 使用。
  • conf 目录里面提供了配置文件的模板,后续直接拷贝过来进行修改即可。
  • contrib 目录提供了两个 perl 脚本和一个 vim 工具,我们在 Linux 上打开配置文件的时候默认是没有语法高亮的,而如果将 contrib/vim/* 拷贝到 ~/.vim 之后再打开的话,会发现文件具备了语法高亮效果。
  • html 目录提供了两个标准的 html 文件,当 Nginx 成功启动时显示的 html,以及发生 500 错误时的 html。
  • man 目录则提供了一些 Linux 的帮助手册。
  • src 目录应该无需解释,它是 Nginx 源代码所在的目录。

除了上面说的目录之外,我们看到 Nginx 主目录中还有几个文件。

  • CHANGES:记录 Nginx 每个版本所做的修改。
  • CHANGES.ru:Nginx 作者是俄罗斯人,所以还提供了一个俄语版本。
  • configure:编译之前的必备动作,生成 Makefile、检查依赖等等,在执行 configure 的时候可以指定参数来对安装进行控制。当执行完 configure 之后就可以使用 make 进行编译了,编译之后再使用 make install 进行安装。
  • LICENSE: 相关许可证明。
  • README: README 文件。

接下来将目光放在 configure 文件上,看看它支持哪些选项,可以通过 ./configure --help 查看。虽然支持的选项非常多,但是不需要全部都用,可以根据自己的情况进行选择,并且每个选项都有相应的注释。

./configure --prefix=/usr/local/nginx

这里只指定安装的目录(其实默认也是安装在 /usr/local/nginx 中),其余选项直接使用默认的。如果不出意外的话,执行之后会报错,因为缺少依赖,我们需要执行:

yum install gcc gcc-c++ make automake autoconf libtool pcre* zlib openssl openssl-devel

将相关依赖安装之后,再执行 ./configure --prefix=/usr/local/nginx 即可。

安装完毕之后,结尾会有一个 Configuration summary,告诉我们一些相关信息。比如 nginx path prefix 表示编译安装后的目录,nginx binary file 表示安装后的二进制文件等等。但需要注意:我们此时还没有编译、也没有安装,这一步只是相当于根据指定的参数生成了 Makefile、进行了一些依赖检查。

接下来执行 make && make install 进行编译安装,这个过程需要花费一些时间。等安装完毕之后,/usr/local 中会多出一个 nginx 目录。

这就是编译安装之后的结果,conf 目录存放配置文件,html 目录包含两个标准的 html 文件,logs 目录存放日志,sbin 目录存放启动文件(只有一个 nginx,通过该文件即可启动 Nginx 服务,它是所有模块编译之后的最终结果)。可以看到编译安装的整个过程还是很简单的,并且安装之后 Nginx 的目录也很简洁。

至于安装之前的 nginx-1.24.0 源码目录就可以直接删掉了。

然后看看 Nginx 支持的命令行操作,为了操作方便,建议先配置一下环境变量。


查看 Nginx 版本信息

 

帮助信息

输入 nginx -? 或者 nginx -h 即可。

 

启动 Nginx

  • nginx:直接启动,会使用默认的配置文件。
  • nginx -g 配置指令:指定配置指令启动,不常用。
  • nginx -c 配置文件:指定配置文件启动,最常用。
  • nginx -p 运行目录:指定 Nginx 的运行目录,然后相应的日志文件就会记录在指定的目录中。

 

关闭 Nginx

Nginx 是通过信号来控制进程的,我们既可以使用 Linux 中自带的 kill 命令,也可以通过 nginx -s 的方式。

  • nginx -s stop:立刻停止服务。
  • nginx -s quit:优雅地停止服务,在退出前会完成已经接受的连接请求。
  • nginx -s reload:重新加载配置文件,如果配置文件修改了,那么无需重启 Nginx,可以通过重新加载的方式。
  • nginx -s reopen:新打开日志文件,重头开始记录(记得要先备份)。

 

检测配置文件是否有误

如果修改配置文件出错,那么会导致 Nginx 无法启动,这个时候可以通过 nginx -t 或者 nginx -T 来检测配置文件是否有语法错误。

检测结果正常,因为我们目前还没有对配置文件做修改。


下面我们来启动一下 Nginx,直接命令行输入 nginx 即可。

html 目录下有两个标准的 html 文件,一个用于成功访问 Nginx 时返回,另一个用于出现 500 错误时返回,显然我们这里安装成功了。

另外可能因为防火墙的原因,会使得端口未对外开方,导致无法访问,这时我们需要开放让外界访问的端口号。我这里使用的是腾讯云服务器,因此可以通过腾讯云提供的 Web 页面的方式开放端口,如果通过命令的话该怎么做呢?

  • systemctl status firewalld:查看防火墙状态
  • firewall-cmd --list-all:查看所有开放的端口号
  • firewall-cmd --add-service=http --permanent
  • firewall-cmd --add-port=80/tcp --permanent:设置要开放的端口号
  • firewall-cmd --reload:重启防火墙

Nginx 配置文件解析

Nginx 配置文件是 Nginx 的核心,我们用 Nginx 主要就是在编写配置文件。首先 Nginx 的配置文件在 conf 目录下,名字为 nginx.conf,根据配置文件的内容,我们可以将整体分为三部分。

  • 第一部分:全局块
  • 第二部分:events 块
  • 第三部分:http 块

当然 Nginx 支持的配置有很多,但默认的配置文件中并没有全部写上,这里我们尽量介绍地详细一点。

Nginx 配置文件是有格式要求的,每一行配置都要以分号结尾,# 表示注释。

第一部分:全局块

从配置文件开始到 events 块之间的内容,主要会设置一些影响 Nginx 服务器整体运行的配置指令。

# 指定 Nginx 的启动用户
user root;  

# 工作进程(Worker)的数量,Nginx 是 Master Worker 多进程架构,当执行 nginx 命令时,会启动一个 Master 进程
# Master 进程验证配置文件 nginx.conf 是否正常,如果正常,那么启动子进程处理请求
# 所以 Master 不直接处理请求,而是用来管理、监控其它组件,处理请求会交给子进程 Worker 来做
# 如果 Worker 挂掉了,那么会发送一个信号给 Master,这样的话 Master 会重新启动一个 Worker
# 而该参数便是指定工作进程、也就是 Worker 的数量,通常等于 CPU 数量或者 2 倍的 CPU 数量
worker_processes  16;

# 错误日志的存放路径,以及打印的日志级别
error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;

# pid(进程标识符)的存放路径
pid        logs/nginx.pid;

# 指定一个 nginx 进程最多可以打开多少个文件描述符
# 理论值应该是最多打开文件数(ulimit - n)与 Nginx 进程数相除,但多个进程分配请求并不是那么均匀,所以最好与 ulimit -n 的值保持一致
# 总之最好往大了写
worker_rlimit_nofile 204800;

以上就是全局块,可以看到配置的也确实是一些全局信息。

第二部分:events 块

events 块主要设置 Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 Worker(工作进程)下的网络连接进行序列化、是否允许同时接收多个网络连接、选取哪种事件驱动模型来处理连接请求、每个 Worker 可以同时支持的最大连接数。

events {
    # 事件驱动模型,Linux 指定为 epoll,Unix 指定为 Kqueue,Windows 不指定
    use epoll;
    
    # 每个工作进程支持的最大连接数,这部分配置对 Nginx 的性能影响较大,在实际中应该灵活配置
    # 理论上每台 Nginx 服务器的最大连接数为 worker_processes * worker_connections
    worker_connections 204800;
    
    # 每个 TCP 连接最多可以保持多长时间
    keepalive_timeout 60;
    
    # 客户端请求头部的缓冲区大小,这个可以根据你的系统分页大小来设置
    # 一般一个请求头的大小不会超过 1k,不过由于系统分页都要大于 1k,所以这里设置为分页大小
    # 分页大小可以用命令 getconf PAGESIZE 取得,在我当前机器上是 4096
    # 不过也有 client_header_buffer_size 超过 4k 的情况,但 client_header_buffer_size 必须设置为 系统分页大小 的整数倍
    client_header_buffer_size 4k;
        
    # 为打开的文件指定缓存,max 表示缓存的文件数量,inactive 表示文件经过多长时间没被请求后删除缓存
    open_file_cache max=65535 inactive=60s;
    
    # 多长时间检查一次缓存的有效信息
    open_file_cache_valid 80s;
    
    # open_file_cache 指令中的 inactive 时间内文件的最少使用次数
    # 如果超过这个数字,文件描述符一直是在缓存中打开的,如果有一个文件在 inactive 时间内一次没被使用,它将被移除。
    open_file_cache_min_uses 1;
}

如果你打开默认配置文件的话,会发现 events 块里面只有一个指令 worker_connections,其它指令也是支持的,只不过没有写在默认的配置文件中。很多指令如果不配置的话,那么会使用默认的,Nginx 会根据当前平台进行选择。

第三部分:http 块

http 块算是 Nginx 中配置最为频繁的部分,代理、缓存、日志定义以及我们说的负载均衡、动静分离等绝大部分功能都是在这里面进行配置的,另外 http 块也可以分为 http 全局块和 server 块。


http 全局块

http 全局块配置的指令包括文件的引入,MIME-TYPE 定义,日志自定义,连接超时时间,单链接请求数上限等等。

http {
    # 在 conf 目录中还有很多其它文件,可以通过 include 导入进来
    # 这里就是设置 mime 类型,类型由 mime.type 文件定义
    include       mime.types;
    
    # 响应体的 Content-Type
    default_type  application/octet-stream;

    # 日志格式
    # $remote_addr 与 $http_x_forwarded_for 用来记录客户端的 ip 地址
    # $remote_user:用来记录客户端用户名称
    # $time_local: 用来记录访问时间与时区
    # $request: 用来记录请求的 url 与 http 协议
    # $status: 用来记录请求状态,成功是 200
    # $body_bytes_sent:记录发送给客户端的文件主体内容大小
    # $http_referer:用来记录是从哪个页面跳转过来的
    # $http_user_agent:记录客户浏览器的相关信息
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    # 通常 Web 服务器放在反向代理的后面,这样就不能获取到客户端的 IP 地址了,通过 $remote_addr 拿到的 IP 地址是反向代理服务器的 IP 地址    
    # 因此反向代理服务器在转发请求的 http 头信息中,可以增加 x_forwarded_for 信息,用来记录真实客户端的 IP 地址和请求的服务器地址
    
    # 该指令用于指定日志文件的存放路径
    access_log  logs/access.log  main;
    
    # 和 events 块中的 client_header_buffer_size 类似
    client_header_buffer_size 4k;
    
    # 客户请求头缓冲大小,Nginx 默认会用 client_header_buffer_size 这个 buffer 来读取 header 值
    # 如果 header 过大,它会使用 large_client_header_buffers 来读取
    large_client_header_buffers 8 128k;
    
    # 和 events 块中的 open_file_cache、open_file_cache_valid、open_file_cache_min_uses 类似
    open_file_cache max=102400 inactive=20s;
    open_file_cache_valid 80s;
    open_file_cache_min_uses 2;
    # 默认值为 off,表示是否记录 cache 错误
    open_file_cache_errors on
    
    # 请求体的最大值
    client_max_body_size 300m;
	
    # 指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,如果反向代理的是普通应用,应该设为 on
    # 如果代理的是下载相关的会导致磁盘 IO 重负载的应用,那么可设置为 off,以平衡磁盘与网络 IO 处理速度,降低系统 uptime
    sendfile        on;
    
    # 允许或禁止使用 socket 的 TCP_CORK 选项,仅在 sendfile 为 on 的时候使用
    tcp_nopush     on;
	
    # 每个 TCP 连接最多可以保持多长时间
    keepalive_timeout  65;
    
    # 后端服务器连接的超时时间,发起握手等候响应的超时时间
    proxy_connect_timeout 90; 
    
    # 连接成功后等待后端服务器响应的时间,其实已经进入后端的队列之中等候处理(也可以说是后端服务器处理请求的时间)
    proxy_read_timeout 180;
    
    # 后端服务器数据回传时间,就是在规定时间之内后端服务器必须传完所有的数据
    proxy_send_timeout 180;
    
    # 设置从被代理服务器读取的第一部分响应的缓冲区大小,通常情况下这部分应答中包含一个小的应答头
    # 默认情况下这个值的大小为指令 proxy_buffers 中指定的一个缓冲区的大小,不过可以将其设置为更小
    proxy_buffer_size 256k;
    
    # 设置用于读取响应(来自被代理服务器)的缓冲区数目和大小,默认情况也为分页大小,根据操作系统的不同可能是 4k 或者 8k
    proxy_buffers 4 256k;
    
    # 使 nginx 阻止 HTTP 应答代码为 400 或者更高的应答。
    proxy_intercept_errors on;
    
    # Nginx 分配给请求数据的 Buffer 大小,如果请求的数据小于 client_body_buffer_size 直接将数据先在内存中存储
    # 如果请求的值大于 client_body_buffer_size 小于 client_max_body_size,就会将数据先存储到临时文件中
    # 通过 client_body_temp 指定,默认是 /tmp,所以配置的 client_body_temp 路径,一定要让启动 Nginx 的用户有读写权限
    # 否则,当传输的数据大于 client_body_buffer_size,会写入临时文件失败并报错
    client_body_buffer_size 512k;
    # 传输的数据大于 client_max_body_size,一定是传不成功的,小于 client_body_buffer_size 直接在内存中高效存储
    # 如果大于 client_body_buffer_size 小于 client_max_body_size 会存储在临时文件中,临时文件一定要有权限
    # 如果追求效率,就让 client_max_body_size 和 client_body_buffer_size 保持一致,这样就不会存储在临时文件中,而是直接存储在内存里

    # 开启 gzip 压缩,默认是被注释掉的
    gzip  on;
    
    # 用于负载均衡,后面介绍
    upstream ... {
    
    }

http 块是由 http 全局块和 server 块组成的,而 server 块位于 http 内部。这里先来简单总结一下 nginx.conf 的结构:

# 全局块
user  nobody;
worker_processes  1;
pid        logs/nginx.pid;
... ...;

# events 块
events {
    worker_connections  1024;
    ... ...;
}

# http 块
http {
    # http 全局块
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    ... ...;
    ... ...;
    #gzip  on;
    
    upstream {
    
    }
    
    # server 块,可以配置多个,位于 http 块中
    server {
        listen       80;
        server_name  localhost;
        ... ...;
    }
    server {
        listen       8080;
        server_name  localhost;
        ... ...;        
    }
    server {
        listen       8081;
        server_name  localhost;
        ... ...;        
    }
}

所以整体结构还是比较清晰的,尽管每个块里面的内容有很多,但并不是每一个都需要,很多指令直接采用默认值即可。最后来介绍一下 server 块。

server 块

server 块可以说是我们改动最频繁的部分了,里面支持的操作非常多,因为大部分都是在 server 块里面配置的。事实上其它的块我们只需要根据配置文件做简单修改即可,但是 server 块比较特殊,所以我们通过几个例子来介绍 server 块是如何使用的。

Nginx 配置实例

接下来我们将只修改 server 块,像全局块、events 块我们直接采用默认值即可,工作中则具体根据业务需求修改。

实现反向代理(一)

需求:当我打开浏览器,输入服务器的 IP 的时候,会自动跳转到百度页面。

listen 表示监听 80 端口,当然也可以改成别的,只不过在用浏览器访问的时候就需要显式地指定要访问的端口了。server_name 表示监听的域名或主机名,如果你没有配置域名,那么使用 localhost 即可。location / 表示当你访问的路径是 / 的时候,进行相应的处理,我们很多时候都只是用 Nginx 做一层转发,而实现转发则需要配置 location。

我们在 location 块中只需要加上一行 proxy_pass http://www.baidu.com 即可,由于该机器监听的是 80 端口,而浏览器默认访问 80 端口,那么当我们直接输入 IP 地址访问该机器的时候,会自动转发到百度。如果你尝试失败了,那么想想是不是忘记 nginx -s reload 了。

如果不指定 proxy_pass 这一行,那么就会来到 nginx 的指定页面,root html 表示 Nginx 主目录下的 html 目录,index index.html index.htm 表示目录下的文件。当没有指定 proxy_pass 时,就会到 root 指定的目录中查找 index 指定的文件。

这里我们把 proxy_pass 这一项给去掉,然后让 nginx 访问我们自己指定的页面,先创建一个 /root/satori.html。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 style="text-align: center">古明地さとりの避難小屋へようこそ</h1>
</body>
</html>

然后别忘记赋予权限,因为 Nginx 默认使用 nobody 用户启动,如果没有权限或者资源不存在,那么会抛出 403 错误。接下来修改 nginx.conf:

location / {
    # 指定静态文件的根目录
    root   /root;
    # index 可以同时指定多个文件,如果第一个不存在,那么会去找下一个
    index  satori.html index.html index.htm;
}

nginx -s reload 之后,我们再来访问一下。

此时就访问成功了,所以如果不扣底层细节,直接用 Nginx 的话还是很简单的,主要搞清楚配置文件怎么配就可以了。假设我们希望访问 IP:8888 时,返回 /usr/local/nginx/conf/nginx.conf 该怎么做呢?

# 可以配置很多个 server 快,因此 Nginx 可以监听多个端口
server {
    listen 8888;
    server_name localhost;
    
    location / {
        root   /usr/local/nginx/conf;
        index  nginx.conf;
        # .conf 后缀的文件会被浏览器当成二进制文件,直接下载下来
        # 因此告诉浏览器,返回的内容是纯文本格式的,直接展示在页面上即可
        default_type text/plain;
        # 文本使用 UTF-8 编码,不然中文会出现乱码
        charset utf-8;
    }
}

nginx -s reload 之后,访问 8888 端口。

结果没有问题,一个 http 块可以配置多个 server 块,每个 server 块监听一个端口,并且可以配置多个 location 块。并且 default_type 和 charset 等指令可以放在单独的 location 中,也可以放在整个 server 中(会作用于所有的 location)。

实现反向代理(二)

需求:访问不同的路径,跳转到不同的 url。比如输入 IP/satori 的时候跳转到 bilibili 页面,输入 IP/koishi 的时候跳转到知乎页面。显然此时就需要两个 location 了,因为这是两个不同的url。

server {
    listen       80;
    server_name  localhost;
    # 这个原来的 location,就还让它保持原样
    location / {
        root   html;
        index index.html index.htm;
    }
    # 有了 proxy_pass,可以不用写 root 和 index 指令
    location /satori {
        proxy_pass https://www.bilibili.com;
    }
    location /koishi{
        proxy_pass https://www.zhihu.com;
    }
}    

没有什么难度,我们来测试一下:

首先访问成功了,但是我们看到路径参数会自动跟在要映射的 URL 后面,即使路径参数有多个也是如此。

从图中可以看到,不管访问的 URL 有多长,只要 location 是访问的 URL 的路径参数的前缀,那么就算匹配上了。

  • location /satori
  • 访问的 URL 的路径参数 /satori/a/b/c/d/e/111/222

显然 location /satori 是访问的 URL 的路径参数的前缀,所以此时匹配成功,然后会将访问的 URL 的路径参数追加在 proxy_pass 指定的 URL 的屁股后面。但如果 proxy_pass 指定的 URL 也包含了路径参数,情况会稍有不同。

server {
    listen       80;
    server_name  localhost;

    location / {
        root   html;
        index index.html index.htm;
    }

    location /satori {
        # 包含路径参数
        proxy_pass https://www.bilibili.com/v/dance;
    }
    location /koishi{
        proxy_pass https://www.zhihu.com;
    }
}  

此时访问 /satori/a/b/c,因为可以和 location /satori 匹配上,那么会跳转到 https://www.bilibili.com/v/dance/a/b/c 页面,但此时的路径中不包含 satori。因为 proxy_pass 指定的 URL 带有路径参数,因此公共的前缀会被去掉。

import requests

res = requests.get("http://82.157.146.194/satori/a/b/c")
# 我们看到 satori 被去掉了
print(res.request.url)
"""
https://www.bilibili.com/v/dance/a/b/c
"""

res = requests.get("http://82.157.146.194/koishi/a/b/c")
# location /koishi 内部的 proxy_pass 指定的 URL 没有路径参数
print(res.request.url)
"""
https://www.zhihu.com/koishi/a/b/c
"""

总结:如果 location 是访问的 URL 的路径参数的前缀,那么两者匹配成功,此时请求会被转发到 proxy_pass 指定的 URL 中。如果 proxy_pass URL 没有路径参数,那么访问的 URL 的路径参数会被完整追加到 proxy_pass URL 中;如果 proxy_pass URL 包含了路径参数,那么访问的 URL 的路径参数中去除 location 的部分会被追加到 proxy_pass URL 中。

为了更好地理解,我们再举个例子。

import requests

# location /koishi,然后 proxy_pass 是 http://www.koishi.com
# /koishi 是 /koishi001/a/b/c 的前缀,两者能匹配上
res = requests.get("http://82.157.146.194/koishi001/a/b/c")
# 最终访问的 URL 是 http://www.koishi.com/koishi001/a/b/c
print(res.request.url)
"""
https://www.zhihu.com/koishi001/a/b/c
"""


# location /satori,然后 proxy_pass 是 http://www.bilibili.com/v/dance
# /satori 是 /satori001/a/b/c 的前缀,两者能匹配上
res = requests.get("http://82.157.146.194/satori001/a/b/c")
# 但 proxy_pass 带有路径参数,所以 location 会被去掉,留下的是 001/a/b/c
# 然后追加在 proxy_pass URL 后面,因此最终访问的 URL 是 https://www.bilibili.com/v/dance001/a/b/c
print(res.request.url)
"""
https://www.bilibili.com/v/dance001/a/b/c
"""

当然上面的 location 只有一个路径参数,而有多个也是可以的,规则不变。


问题来了,如果我们希望访问 IP 跳转到 bilibili,访问 IP:8888 跳转到 baidu 该怎么做呢?显然此时不再是添加 location 能解决的问题了,因为它们的端口都不一样了。但解决办法仍然简单,增加一个 server 块即可,我们说一个 http 块里面可以包含一个 http 全局块和多个 server 块。也就是说,Nginx 可以同时监听多个端口。

server {
    listen       80;
    server_name  localhost;
    location / {
        proxy_pass http://www.bilibili.com;
    }
}

server {
    listen 8888;
    server_name localhost;
    location / {
        proxy_pass http://www.baidu.com;
    }
}

Nginx 可以监听多个端口,如果我们只输入 IP、或者 IP:80,那么会被转发到 bilibili;如果输入 IP:8888,那么会被转发到 baidu。尽管 location 相同,但它们是在不同的端口下,因此不会相互影响。

实现反向代理(三)

需求:只要访问的 URL 中包含了 satori,就跳转到 bilibili。也就是说 url 可以是 IP/satori、IP/aaa/satori、ip/aaa/bbb/c/satori/aa,只要路径参数中包含了 satori,那么就跳转到 bilibili,怎么做呢?

这种做法显然要使用正则来实现,对于上面的需求,location 使用正则简直不要太好配,直接 ~ /satori 即可。此时匹配上之后,所有的路径参数同样会跟在跳转的 URL 的后面。

server {
    listen       80;
    server_name  localhost;
    location ~ /satori {
        # 跳转的 URL
        proxy_pass http://www.bilibili.com;
    }
}

配置完成,重启加载配置文件,然后访问 IP/a/b/c/satori/d/e。

只要路径有 satori,那么就能匹配成功,然后访问的路径参数会自动全部跟在跳转的 URL 的屁股后面。那如果 proxy_pass URL 包含路径参数呢?答案是如果使用了正则表达式,那么 proxy_pass URL 不允许包含路径参数。

下面简单介绍一下 Nginx 中的正则,主要体现在 location 中:

  • =:表示 url 不包含正则表达式,要求访问的 url 和 location 指定的 url 完全一致
  • ~:url 包含正则表达式,并且区分大小写
  • ~*:url 包含正则表达式,并且不区分大小写
  • ^~:url 不包含正则表达式,要求 Nginx 找到请求的 url 和 location 指定的 url 相似度最高的 location,然后用该 location 进行请求处理

当然 ^ 跟在路径前面表示这个以此为开头,同理还有 $ 跟在路径后面表示以此为结尾。

负载均衡

需求:假设有两台服务器,我们需要实现当访问 IP:90/satori 的时候,能够把请求转发到不同的服务器当中去,也就是实现负载均衡的效果。而实现这个功能需要使用 upstream,我们在介绍 http 全局块的时候说过 upstream 用于负载均衡,但是怎么使用没有说,那么现在就来介绍一下。

http {
    include       mime.types;
    # 之前我们介绍过 default_type,它可以放在 http 全局块中,作用所有的 server 下的所有 location
    # 也可以放在某个 server 中,作用指定 server 下的所有 location
    # 当然也可以放在某个 location 中,只针对所在的 location,如果都指定了,那么优先级从内到外依次降低
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    # 使用 upstream,然后给 upstream 起一个名字,假设就叫 my_server
    upstream my_server {
    	# 写上你要转发的机器的 IP 地址或主机名
    	server 39.23.48.129:5555;
    	server 39.35.137.238:6666;
    }
    
    server {
        # 监听 90 端口
        listen 90; 
        # 主机依旧是本地
        server_name localhost;
    
        # 配置 location
        location /satori {
    	    # 这里将 proxy_pass 改成 http://my_server 即可,这个 my_server 就是给 upstream 起的名字
            proxy_pass http://my_server;
        }
    }
}    

当我们访问 IP:90/satori 的时候,就会将请求转发到 http://my_server 中,也就是 upstream my_server 中指定的目标服务器。

但随着互联网信息的爆炸式增长,负载均衡(Load Balance)早已不再是一个陌生的话题,就是把负载分摊到不同的存储单元,既保证服务的可用性,又保证服务足够快。但问题是负载均衡只是平摊负载吗?如果有的机器性能好,那么它的负载是不是应该要多一些呢?所以 Nginx 在负载均衡方面有如下策略。


轮询(默认)

每个请求按照时间顺序逐一分配到不同的后端服务器,如果后端服务器宕机了,会自动剔除。

 

weight(权重)

权重越高,被分配的请求越多,权重与请求数成正比。默认是 1,比如 server 39.23.48.129:5555 weight=10,此时 39.23.48.129:5555 这台服务器的权重就是 10。

 

ip_hash

每个请求按访问 IP 的 hash 结果分配,这样每个访客固定访问一个后端服务器,可以解决 Session 的问题。

upstream my_server {
    ip_hash;
    server 39.23.48.129:5555;
    server 39.35.137.238:6666;
}

 

fair

按照服务器的响应时间来分配,响应时间短的优先分配。

upstream my_server {
    fair;
    server 1.1.1.1:80;
    server 2.2.2.2:88;
}

动静分离

Nginx 的动静分离简单来说,就是把动态请求和静态请求分开,但不能理解成只是把动态页面和静态页面物理分离。严格来讲,是动态请求和静态请求分开,可以理解成使用 Nginx 处理静态页面,uWSGI 处理动态页面,动静分离从实现角度上大致分为两种:

  • 纯粹把静态文件独立出来,放在一个单独服务器上,也是目前主流方案
  • 把动态和静态文件放在一起,通过 Nginx 来分开

在 /etc/ssh 目录下有一些文件,下面我们就进行配置,通过 Nginx 直接访问静态资源。我们希望当输入 /file/xxx 的时候,就会访问 /etc/ssh 下面的 xxx。

server {
    listen 80; 
    server_name localhost;
    
    # 配置 location
    location /file {
        # 当我们访问 /file/xxx,会返回 /etc/ssh/xxx
    	alias /etc/ssh;    	
    	# 而这个 autoindex 指的是当输入 /file 的时候,是否把 /etc/ssh 里面的内容全部列出来,像索引一样
    	# 可以通过点击的方式访问指定的静态资源
    	autoindex on;
    }
}

我们输入 IP/file 查看一下。

点击之后即可访问指定的文件,当然也可以直接输入 URL。然后这里出现了 alias,它和之前的 root 有什么区别呢?其实这两者的区别主要在于 Nginx 如何解释 location 后面的 url。对于上面来说,访问 /file/xxx,那么得到的就是 /etc/ssh/xxx;如果将 alias 改成 root,那么访问 /file/xxx,得到的就是 /etc/ssh/file/xxx。所以对于 alias 而言,它不会把 location 后面的路径给带过去,但是 root 会。

并且 root 可以出现在 http、server、location、if 等很多块中,但是 alias 只能出现在 location 块中。所以对于 location 而言,如果访问的是根路径,那么就按照 Nginx 默认使用 root 即可,返回写死的文件,一般都是一个默认的 html 文件;如果访问的不是根路径,那么就使用 alias 来实现返回静态文件的效果,显然这是最正确的做法。

另外如果我们不希望输入 /file 的时候显示所有的内容,那么只需要将 autoindex on 给注释掉即可。但如果我们又希望在输入 /file 的时候返回一个默认的文件,那么可以通过 index xxx 的方式实现。

要是 index 和 autoindex 都指定了,那么优先 index;如果 index 指定的文件都找不到,那么再看 autoindex。

server_name

假设你的服务器绑定了一个域名,就叫 example.com 吧,现在需要实现以下需求:

  • 访问 www.example.com 跳转到百度
  • 访问 bilibili.example.com 跳转到哔哩哔哩
  • 访问 zhihu.example.com 跳转到知乎
server {
    listen 80; 
    server_name www.example.com;
    
    location / {
    	proxy_pass http://www.baidu.com;
    }
}

server {
    listen 80; 
    server_name bilibili.example.com;
    
    location / {
    	proxy_pass http://www.bilibili.com
    }
}

server {
    listen 80; 
    server_name zhihu.example.com;
    
    location / {
    	proxy_pass http://www.zhihu.com
    }
}

这里指定了多个 server,监听的端口是一样的,但 server_name 不一样,也就是说如果有多个 server,那么 list 和 server_name 组合起来必须是唯一的。因此这三个 server 虽然 listen 一样,但 server_name 不一样,所以没有问题。根据 server_name 的不同,会转发到不同的地址,比如:

  • 访问 bilibili.example.com/v/dance 会跳转到 www.bilibili.com/v/dance

当然啦,上面是属于精确匹配,但 server_name 也可以指定通配符。

server {
    listen 80; 
    # 所有以 .mail.example.com 结尾的域名会匹配成功,比如 google.mail.example.com
    # server_name 可以指定多个,比如我们希望 mail.example.com 本身也能匹配上
    server_name *.mail.example.com mail.example.com;
    
    location / {
    	proxy_pass http://www.baidu.com;
    }
}

server {
    listen 80; 
    # 还可以使用正则表达式,比如匹配以 www. 开头、.example.com 结尾的域名
    server_name ~^www\..+\.example\.com?;
    
    location / {
    	proxy_pass http://www.bilibili.com;
    }
}

注意:通配符只能在 server_name 的起始处或结尾处使用一个星号,并且星号与其它字符之间用点号分隔,所以 www.*.example.com 就是非法的。然后是正则,如果使用正则,那么必须以 ~ 开头,否则会被认为是一个普通的名字。并且开头和结尾需要 ^ 和 $ 作为锚点,尽管语法上不是必须的,但逻辑上必须这么做,然后域名中的点号记得转义。

由于我这里没有域名,具体就不演示了。

posted @ 2019-06-30 21:39  古明地盆  阅读(1239)  评论(2编辑  收藏  举报