楔子
相信很多人都用 Nginx 搭建过网站或者反向代理服务器,但其实这仅仅只是 Nginx 的一些最基本的用法,我们还需要熟悉 Nginx 底层的运行原理,比如:Nginx 的架构、进程模型,以及 Liunx 中的 CPU、内存、磁盘、网络等等要如何与 Nginx 配置文件中的指令相结合从而使得 Nginx 的性能达到最大化。那么下面我们就从零开始,一点一点深入挖掘 Nginx。
首先 Nginx 是一个高性能的 HTTP 和 反向代理 服务器,而且 Nginx 的作者是一个对操作系统有着深入研究的人,可以说他把 Nginx 已经优化到极致了,使得 Nginx 具备高性能和高吞吐量。Nginx 的异步非阻塞模型使得一个进程可以轻松处理上万个连接,而传统的 Apache 则是一个进程只能处理一个连接。
此外 Nginx 的模块化设计,使得它可以很容易被扩展,因此 Nginx 有着丰富的第三方模块;Nginx 可靠性高,可以持续不间断地提供服务;此外 Nginx 支持热部署,能够在不停止服务的情况下升级 Nginx;最重要的是 Nginx 遵循 BSD 协议,我们可以自由地进行二次开发(比如淘宝的 Tengine)。
Nginx 适用哪些场景
Nginx 可以应用于很多很多场景,但是最常见的场景有以下三个。
反向代理服务
提到反向代理,我们需要先了解一下正向代理,这里举个栗子介绍一下两者的区别:
假设你想找 B 借一样东西,但是 B 不同意,于是你拜托 A,然后 A 从 B 借、然后再交给你。这里的 A 就扮演了代理的角色,也是正向代理,因为真正找 B 借东西的是 A。如果 A 在找 B 借东西的时候没有说这是你想借的,那么 A 就是匿名代理,因为 B 不知道你的存在;如果 A 告诉了 B,其实是你拜托他来找 B 的,那么 A 就是透明代理,B 知道 A、但同时也知道你。其实我们平常挂的 威批恩 就是正向代理,假设你想访问谷歌但惨遭拒绝,于是你可以让 威批恩 去帮你访问,但是对于谷歌而言,向它发请求的是 威批恩、不是你。
至于反向代理也很简单,比如我们访问百度,其背后可能有千千万万台服务器为我们服务,我们并不知道具体是那一台。但是我们只需要知道反向代理服务器就好了,它会帮我们把请求转发到真实的服务器那里去。而 Nginx 就是一个非常好的反向代理服务器,可以对背后所有真实的服务器进行一个权衡,将请求转发到一个合适的服务器上,也就是所谓的负载均衡,因为如果某个真实的服务器很繁忙的话,那么就不会转发到它那里去了。再比如你找到老鸨,希望她能提供一个小姐姐为你上门服务,这个老鸨就是反向代理,她会将你的请求转发到某一个小姐姐那里去。
所以正向代理和反向代理都属于代理,而核心区别就在于代理的对象不同:正向代理 代理的是客户端,负责向服务端发送请求;反向代理 代理的是服务端,负责向客户端返回响应。
匿名代理:完全隐匿了被代理的机器,外界看到的只是代理服务器透明代理:顾名思义,它在传输过程中是透明开放的,外界既知道代理,也知道客户端正向代理:靠近客户端,代表客户端向服务器发送请求反向代理:靠近服务器端,代表服务器向客户端返回响应
并且 Nginx 还可以实现负载均衡、内容缓存、安全防护、数据处理等等。
负载均衡:把访问请求均匀分散到多台机器,实现访问集群化,不会让某台机器承受的负载过重内容缓存:暂存上下行的数据,减轻后端的压力,如果某些内容是不变的,那么可以暂时缓存起来,这样就可以加速响应了安全防护:隐匿 IP, 使用 WAF 等工具抵御网络攻击,保护被代理的机器数据处理:提供压缩、加密等额外的功能
所以对于任何一个大型网站,背后都会有成千上万台真实服务器,但用户并不会直接访问这些服务器;而是会访问反向代理服务器,反向代理服务器会将请求转发到真实的、提供服务的目标服务器上(怎么转发就涉及到了负载均衡),目标服务器处理完毕之后会返回内容给反向代理服务器,然后反向代理服务器再将内容返回给用户。因此此时反向代理服务器和目标服务器对外就相当于是一个整体,暴露给用户的就是反向代理服务器的地址,隐藏了真实服务器的 IP 地址。
静态资源服务
首先一个请求过来的时候会先经过 Nginx,然后 Nginx 会将请求转发给 uWSGI(以 Python 为例,Java 的话则是 Tomcat),然后 uWSGI 会再调用相应的 Web 服务。这里可能有人不清楚 WSGI、uWSGI、uwsgi、Nginx 之间的区别,先来区分一下:
WSGI 的全称是 Web Server Gateway Interface(Web 服务网关接口),它不是服务器、Python 模块、框架、API 或者任何软件,只是一种描述 Web 服务器和 Web 应用程序(使用 Web 框架,如 Django、Flask 编写的程序)进行通信的规范、协议。使用任何一个框架在编写完服务的时候都必须运行在 Web 服务器上,而这些框架本身自带了一个小型 Web 服务器,但只用于开发和测试。
uWSGI 是一个 Web 服务器,它实现了 WSGI、uwsgi、HTTP 等协议,所以我们把使用 Web 框架编写好的服务部署在 uWSGI 服务器上是可以直接对外提供服务的。
Nginx 同样是一个 Web 服务器,但它相比 uWSGI 可以提供更多的功能,比如上面说的反向代理、负载均衡、以及内容缓存,所以它对 HTTP 请求更加友好,而这些都是 uWSGI 所不具备、或者不擅长的。所以我们在将 Web 服务部署在 uWSGI 之后,还要在前面再搭一层 Nginx。此时 uWSGI 就不再暴露 HTTP 服务了,而是暴露 TCP 服务,因为它是和 Nginx 进行通信,Nginx 来对外暴露 HTTP 服务。
uwsgi 是 Nginx 和 uWSGI 通信所采用的协议,我们说 uWSGI 是和 Nginx 对接,Nginx 接收到用户请求时,如果是请求的是静态资源、那么会直接返回;请求的是动态资源,那么会将请求转发给 uWSGI,然后再由 uWSGI 调用相应的 Web 服务进行处理,处理完毕之后将结果交给 Nginx,Nginx 再返回给客户端。而 uWSGI 和 Nginx 之所以能交互,也正是因为它们都支持 uwsgi 协议,Nginx 中 HttpUwsgiModule 的作用就是与 uWSGI 服务器进行交互。
所以有些请求是访问服务器上的某个静态资源,比如用户就想查看某一张图片等等,那么直接把图片返回即可,该资源不涉及和其它组件的交互;而那些需要访问缓存、数据库等等则称之为动态资源。因此为了加快网站的解析速度,会把动态资源和静态资源交给不同的服务器来解析,从而降低单个服务器的压力,并且分工更明确,而返回静态资源的任务正好可以交给 Nginx。
Nginx 除了能将请求转发到目标服务器之外,还指向了一台存放静态资源(HTML、CSS、图片等等)的服务器。如果用户访问的是静态资源,那么nginx直接从静态资源的服务器将静态资源拿出来返回给用户即可;如果访问动态资源,然后再通过负载均衡转发到目标服务器。
API 服务
Nginx 除了可以访问静态资源,还可以访问 Redis、数据库。如果不涉及复杂的业务逻辑,那么从缓存、数据库中拿数据这一步也可以交给 Nginx 来做,而没有必要再经过应用服务。加上 Nginx 强大的并发性能,我们可以实现 Web 防火墙等功能。因此这要求 Nginx 提供的 API 服务也具备相应的业务处理功能,因此 Nginx 可以集成 JavaScript、Lua 等脚本语言,利用它们的功能来提供完整的 API 服务。
Nginx 的四个主要组成部分
Nginx 主要有以下四个部分组成:
Nginx 二进制可执行文件:由各个模块的源码、结合第三方组件共同编译所得到的文件,它是 Nginx 的主体nginx.conf 配置文件:控制 Nginx 的行为access.log 访问日志:记录每一条 HTTP 请求信息error.log 错误日志:记录错误信息、定位问题
这四个部分是相辅相成的,Nginx 二进制文件和 nginx.conf 配置文件定义了 Nginx 处理请求的方式;而如果我们需要对 Web 服务进行运营方面的分析,那么就需要分析 access.log,因为它记录了每一条请求的信息,通过这些请求信息便可以知道 "哪个的地区的哪个用户都访问了哪些页面";而当出现了错误,或者 Nginx 实际表现的行为和我们预期的行为不一致时,那么可以通过 error.log 来定位相关问题。
安装 Nginx
首先 Nginx 有以下几个种类:开源的 Nginx、收费的 Nginx Plus、淘宝的 Tengine、开源的 OpenResty、收费的 OpenResty。显然我们这里直接选择开源的 Nginx 即可。
然后是 Nginx 的下载,你可以使用 docker 安装、或者使用 yum 安装,但是我个人更建议的做法是通过源码编译的方式安装。因为 yum 在安装的时候不一定会把第三方模块也集成进去,而且任何一个软件,我们也都应该知道如何通过源码编译的方式去安装。
我们可以直接去 http://nginx.org/en/download.html 中下载源代码:
我们注意到上面有三个版本,mainline version 是正在主力开发的版本,stable version 是最新稳定版本,而 legacy version 则是以前的稳定的老版本。我们这里下载最新稳定版:
下载完毕之后进行解压,我们看到目录大概就是上面这样,然后我们来介绍一下这每个目录都是干什么的。
auto 目录
我们进入 auto 目录:
auto 目录里面有 4 个子目录,cc 是用于编译的,然后 lib 是一些额外的库,os 则是判断并适配操作系统的,至于其它的文件都是用来辅助 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 即可。
安装完毕之后,结尾会有一个 summary,告诉你一些关键信息。比如:nginx path prefix,告诉我们编译安装之后的目录为 /usr/local/nginx 中;nginx binary file,告诉我们编译安装之后的二进制文件为 /usr/local//nginx/sbin/nginx 等等。
但是注意:我们此时还没有编译、也没有安装,这一步只是相当于根据我们指定的参数生成了 Makefile、进行了一些依赖检查。
接下来执行 make 进行编译,编译之后进入到 objs 目录中。
通过 ngx_modules.c 可以查看都有哪些模块编译进去了,然后里面的 src 目录则是编译之后的中间文件。
最后回到主目录,执行 make install 安装即可,安装之后直接进入到 /usr/local 目录中会发现多出一个 nginx 目录。
这就是编译安装之后的结果,conf 目录存放配置文件,html 目录中就是两个标准 html 文件,logs 目录存放日志,sbin 目录存放启动文件(只有一个 nginx,通过该文件即可启动 Nginx 服务,我们说它是所有模块编译之后的最终结果)。可以看到编译安装的整个过程还是很简单的,并且安装之后 Nginx 的目录也很简洁。
Nginx 的命令行操作
然后我们来看一下 Nginx 支持的命令行操作,可以将 nginx 执行文件放到环境变量中,也可以直接进入到相应目录中操作。
查看 Nginx 版本信息
[root@iZ2ze3ik2oh85c6hanp0hmZ nginx]# sbin/nginx -v
nginx version: nginx/1.18.0
[root@iZ2ze3ik2oh85c6hanp0hmZ nginx]# sbin/nginx -V
nginx version: nginx/1.18.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
configure arguments:
[root@iZ2ze3ik2oh85c6hanp0hmZ nginx]#
帮助信息
sbin/nginx -? 或者 sbin/nginx -h
启动 nginx
sbin/nginx # 直接启动
sbin/nginx -g 配置指令 # 指定配置指令启动,不常用
sbin/nginx -c 配置文件 # 指定配置文件启动,最常用
sbin/nginx -p 运行目录 # 指定 Nginx 的运行目录,然后相应的日志文件就位记录在我们指定的目录中
关闭 nginx
Nginx 是通过信号来控制进程的,我们既可以使用 Linux 中自带的 kill 命令,也可以通过 nginx -s 的方式。
sbin/nginx -s stop # 立刻停止服务
sbin/nginx -s quit # 优雅的停止服务
sbin/nginx -s reload # 重新加载配置文件,如果配置文件修改了,那么无需重启 nginx,可以通过重新加载的方式;
sbin/nginx -s reopen # 新打开日志文件,重头开始记录(记得要先备份)
检测配置文件是否有误
当我们在修改配置文件之后,如果配置文件出错了,那么会导致 nginx 无法启动,这个时候可以通过 nginx -t 或者 nginx -T 来检测配置文件是否有语法错误。
启动 Nginx
下面我们来启动一下 Nginx 进行测试,这里我们就使用默认的配置文件,关于配置文件的内容后面会详细介绍。
sbin/nginx -c conf/nginx.conf
Nginx 默认监听 80 端口,我们打开浏览器测试一下:
我们 html 目录下有两个标准 html 文件,一个用于成功访问 Nginx 时返回,另一个用于出现 500 错误时返回,显然我们这里安装成功了。
另外可能会因为防火墙的原因,使得端口未对外开方,导致无法访问,这时候我们需要开放需要让外界访问的端口号。我这里使用的是阿里云,因此可以通过阿里提供的web页面的方式开放端口,如果通过命令的话该怎么做呢?
firewall-cmd --list-all:查看所有开放的端口号firewall-cmd --add-service=http --permanentfirewall-cmd --add-port=80/tcp --permanent:设置要开放的端口号firewall-cmd --reload:重启防火墙
初识 Nginx 配置文件
然后来看一下 Nginx 的配置文件,它是 Nginx 的核心,我们用 Nginx 主要就是在编写配置文件。首先 Nginx 的配置文件在 conf 目录下,名字为 nginx.conf,根据配置文件的内容,我们可以将整体分为三部分,分别是:
第一部分:全局块第二部分:events 块第三部分:http 块
当然 Nginx 支持的配置有很多,但是默认的配置文件中并没有全部写上,这里我们尽量介绍地详细一点。
Nginx 配置文件是有格式要求的,每一行配置都必须以分号结尾,# 表示注释。
第一部分:全局块
从配置文件开始到 events 块之间的内容,主要会设置一些影响 Nginx 服务器整体运行的配置指令,比如:配置运行 Nginx 服务器的用户(组)、允许生成的工作进程数量、日志存放路径和打印级别,以及进程 pid 的存放路径等等。
# Nginx 所属的用户和组
user root root;
# 工作进程的数量,这是 Nginx 并发处理服务的关键配置,该值越大,可以支持的并发处理处理也越多,但是会受到硬件、软件等设备的制约
# 通常等于 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 进程数相除,但是 nginx 分配请求并不是那么均匀,所以最好与 ulimit -n 的值保持一致
# 总之最好往大了写
worker_rlimit_nofile 204800;
以上就是全局块,可以看到配置的也确实是一些全局信息。
第二部分:events块
events 块主要设置 Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 worker process(工作进程)下的网络连接进行序列化、是否允许同时接收多个网络连接、选取哪种事件驱动模型来处理连接请求、每个 work process 可以同时支持的最大连接数。
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 {
# 设置 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 的时候使用
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表示监听的主机为本机,实际上这一部分也可以叫做 server 全局块,而下面是 location 块。
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 就是 html 目录下的文件,当没有指定 proxy_pass 的时候,就会到 html 目录下面找 index 选项指定的文件。
这里我们把 proxy_pass 这一项给去掉,然后让 nginx 访问我们自己指定的页面,假设访问 satori.html,那么显然此时的 location 就应该这么配。
location / {
root html;
index satori.html index.html index.htm;
}
配置中有三个 html 文件,会依次从左向右查找,哪个访问成功就返回哪个。但是 html 目录下还没有 satori.html 这个文件,而且我又把 html 目录里面配置文件给重命名了,导致 index 指令所指定的文件在 html 目录中都不存在,所以出现了403,表示资源访问不到。
下面我们就来写一个这样的文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 style="text-align: center">古明地さとりの避難小屋へようこそ</h1>
</body>
</html>
丢进去再次访问,会发现依旧报出了 403,这是因为 nginx 没有权限访问我们刚才创建的文件,因为我们是用 root 创建的。所以我们修改全局块,将 user 改成 root;或者不用 root,采用其它用户也行,只要它有权限读取我们刚才创建的 html 文件。
此时就访问成功了,所以如果不扣底层细节、直接用 Nginx 的话还是很简单的,主要就搞清楚配置文件怎么配置就可以了。假设我们希望访问 ip:8888 端口时,返回 /etc/redis.conf.txt 该怎么做呢?
server {
listen 8888;
server_name localhost;
location / {
root /etc;
index redis.conf.txt;
}
}
这里来测试一下:
访问成功了,这里我们访问的 redis.conf.txt,是因为 .conf 文件浏览器不会自动解析,而是会选择下载下来,尽管它是纯文本。但如果我们将后缀改成 .txt 的话,浏览器就会直接帮你解析了。
实现反向代理(二)
需求:访问不同的路径,跳转到不同的 url。比如当我输入 ip/satori 的时候跳转到 bilibili 页面,输入 ip/koishi 的时候跳转到知乎页面。显然此时就需要两个 location 了,因为这是两个不同的url。
# 这个原来的location,就还让它保持原样
location / {
root html;
index index.html index.htm;
}
# 有了 proxy_pass,可以不用写 root 和 index 指令了
location /satori {
proxy_pass http://www.bilibili.com;
}
location /koishi{
proxy_pass http://www.zhihu.com;
}
没有什么难度,我们来测试一下:
首先访问成功了,但是我们看到指定的 location 的路径参数(ip:port 或者 http://www.xxx.com 后面的那部分内容,我习惯称之为路径参数)会自动跟在要映射的 url 后面。就像我们要将 /satori 映射到 http://www.bilibili.com,但是当我们输入 /satori 的时候,这个 /satori 会跟在 http://www.bilibili.com 的后面,也就是 http://www.bilibili.com/satori ,同理知乎也是。但是如果我们访问的路径参数很多呢?
显然从图中我们可以看到,不管访问的 url 有多长,只要 location 和访问的 url 从头开始对比是相同的,比如这里的 /koishi 匹配上了,那么不管后面还跟着多少参数,都会加在 http://www.zhihu.com 的屁股后面。
问题来了,如果我们希望访问 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 的后面。
location ~ /satori/ {
proxy_pass http://www.bilibili.com;
}
此时就配好啦,访问 /a/b/c/satori/abc
只要路径有 /satori,那么就能匹配成功,然后访问的路径参数会自动全部跟在跳转的 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 application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 我们需要加上一个 upstream,然后给 upstream 起一个名字
# 假设就叫 my_server
upstream my_server {
# 写上你要转发的机器
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 {
server 1.1.1.1:80;
server 2.2.2.2:88;
fair;
}
动静分离
Nginx 动静分离简单来说,就是把动态和静态请求分开,不能简单理解成只是把动态页面和静态页面物理分离。严格来讲,是动态请求和静态请求分开,可以理解成使用 Nginx 处理静态页面,uWSGI 处理动态页面,动静分离从实现角度上大致分为两种:
纯粹把静态文件独立出来,放在一个单独服务器上,也是目前主流方案把动态和静态文件放在一起,通过nginx来分开
现在在我 /root/bg 目录下有很多图片,下面我们就进行配置,通过 Nginx 直接访问静态资源。我们希望当输入 /pic/1.png 的时候,就会访问 /root/bg 下面的 1.png。
server {
listen 80;
server_name localhost;
# 配置location
location /pic {
alias /root/bg;
# 当我们访问 /pic/1.png,那么会返回 /root/bg/1.png
# 而这个 autoindex 指的是当输入 /pic 的时候,会把 /root/bg 里面的内容全部列出来,像索引一样
# 可以通过点击的方式访问指定的静态资源
autoindex on;
}
}
我们看到这里出现了 alias,它和之前的 root 之间有什么区别呢?其实这两者的区别主要在于 Nginx 如何解释 location 后面的 url。对于上面来说,访问 /pic/1.png,那么得到的就是 /root/bg/1.png;如果将 alias 改成 root,那么访问 /pic/1.png,得到的就是 /root/bg/pic/1.png。所以对于 alias 而言,它不会把 location 后面的路径给带过去,但是 root 会。
并且 root 可以出现在 http、server、location、if 等很多块中,但是 alias 只能出现在 location 块中。所以对于 location 而言,如果访问的是根路径,那么就按照 Nginx 默认使用 root 即可,返回写死的文件,一般都是一个默认的 html 文件;如果访问的不是根路径,那么就使用 alias 来实现返回静态文件的效果,显然这是最正确的做法。
如果我们不希望输入 /pic 的时候显示所有的内容,那么只需要将 autoindex on 给注释掉即可。但如果我们又希望在输入 /pic 的时候返回一个默认的文件,那么可以通过 index xxx.jpg 的方式实现。
要是 index 和 autoindex 都指定了,那么优先 index;如果 index 指令指定的文件都找不到,那么再看 autoindex。
nginx 的进程结构
nginx 采用的是多进程的结构,那为什么不采用多线程呢?因为 nginx 设计之初就是为了高可用,并且运行在边缘节点上,通常是用来第一个接收用户的请求所需要的服务器。这就对服务器本身就更高的要求,需要具有足够的健壮性,而多线程共享同一片地址空间,容易出现内存越界导致段错误,因此 nginx 采用的是多进程的结构。
那么下面看看 nginx 的进程结构图:
里面的 Master Process 是主进程,但是它不直接处理请求,而是用来管理、监控其它组件,处理请求会交给子进程 Worker Process 来做;如果 Worker Process 挂掉了,那么会发送一个信号给 Master Process,这样的话Master Process 会重新启动一个 Worker Process。而且我们知道 nginx 是模块化设计的,如果要开发自己的第三方模块的话,那么不建议修改 Master Process,因为主程序一旦挂了整个服务就不可用了。
再比如我们修改了配置文件,Master Process 也会通知 Worker Process 去读取新的配置文件,从而实现热更新。当然还有 Cache Manager 和 Cache Loader,一个是用来管理缓存的,一个是用来加载缓存的;当请求被反向代理到后端服务时,后端服务会缓存一个结果,那么 Cache Loader 就可以将结果缓存起来,当然这一步是 Worker Process 来做的。
Master Process 下面的几个子进程是共享内存的,当然图中的 Worker Process 是4个,但是实际情况则不一定是4个。
Linux 的信号量管理机制
我们知道在 Linux 中,进程的管理是通过信号量进行管理的;每个进程会有一个 pid,我们使用 kill 命令即可给相应的进程发送各种各样的信号量。
我们看到 kill 命令可以发送非常多的信号量,我们在杀死一个进程的时候,一般会通过 ps 命令找到对应的pid,然后再使用 kill -9 pid 杀死该进程。而 -9 指的就是图中的 SIGKILL,它会强行终止掉该进程,而如果什么都不加的话则相当于 kill -15,该信号量指的是会慢慢关闭该进程。如果把 -15 想象成关机的话,-9 就相当于直接拔电源。
当然还有其它的信号量,比如:
SIGCHLD: 在Linux中主进程可以fork出很多个子进程, 如果子进程挂了那么会自动给主进程发送该信号, 让主进程知道该进程挂掉了, 从而允许主进程做一些其它的处理;SIGHUP: 让进程重新读取配置文件;