Nginx指北

  1. 1. Nginx指北
    1. 1.1. 安装部署
    2. 1.2. 基本操作
    3. 1.3. 配置
      1. 1.3.1. 文件结构
      2. 1.3.2. 各个块的作用
      3. 1.3.3. 基本指令
        1. 1.3.3.1. 全局
        2. 1.3.3.2. event块
        3. 1.3.3.3. http块
        4. 1.3.3.4. server块
        5. 1.3.3.5. location块
        6. 1.3.3.6. 小结
      4. 1.3.4. 高级应用
        1. 1.3.4.1. 代理与反向代理
        2. 1.3.4.2. 负载均衡
        3. 1.3.4.3. HTTPS
        4. 1.3.4.4. 防盗链
        5. 1.3.4.5. 限制请求(限流)
      5. 1.3.5. 总结
    4. 1.4. 附录
      1. 1.4.1. 事件驱动模型
      2. 1.4.2. MIME
      3. 1.4.3. 内置变量
      4. 1.4.4. Gzip
    5. 1.5. 参考资料

Nginx指北

从零点五开始的Nginx学习经历

Nginx毕竟是个工具类应用,故笔者强烈建议读者在观看本博文的同时同步操作您的Nginx。唯有多用方能熟悉并掌握。

同时请注意,附录的存在是为了在不影响主体的情况下,增加对相关知识的认知,或者作为一种附加项,或者手册。笔者建议初学者在学习过程中,除查询手册等必要举措外,不要受到附录的影响。它并不影响你对Nginx的学习之旅。

安装部署

一般通过yum/apt-get/pacman等包管理器安装即可。

在安装完成后,通过systemctl start nginx.service启动Nginx。亦可通过Nginx自带工具启动,参考基本操作

不同Linux发行版系统工具可能不同,如init.d等,具体请单独查询。

而后向防火墙中添加HTTP服务。不然直接访问流量会被防火墙干掉。

1
2
3
# CentOS
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --reload

在浏览器中输入地址,返回Nginx默认页面,表示成功。

在ContOS中,Nginx默认配置文件在/etc/nginx/目录下。

一般为/etc/nginx/, /usr/local/nginx/conf, /user/local/etc/nginx/三个之一

基本操作

Nginx自带命令行工具,输入nginx -h查看相关功能。若为源码编译安装,则运行sbin目录下的二进制文件sbin/nginx -h

功能 命令 备注
启动 nginx 效果等同
systemctl start nginx.service
测试配置文件 nginx -t -c filename连用可指配置文件路径
发送信号 nginx -s signal 用于停止、重启Nginx

Nginx信号:

信号 作用 命令
stop 快速停止Nginx服务 nginx -s stop
quit 平缓停止Nginx服务 nginx -s quit
reload 平滑重启(使用新配置文件并缓慢停止原进程) nginx -s reload
reopen 重新打开日志文件,用于分割日志 nginx -s reopen

关于stop与quit的区别:

  • stop

    立即断开所有连接,停止工作

  • quit

    将当前正在处理的连接完成,但不再接受新链接。待所有链接完成后,停止工作。

配置

Nginx的精髓就在于配置文件的书写,同时也是一大难点。下面首先介绍配置文件的相关基本概念,以对Nginx配置有一个大致的了解。而后详细展开包括反向代理,负载均衡等高级功能的配置。(不学语法写个锤子应用)

文件结构

Nginx主配置文件为配置目录下的nginx.conf文件.

下述为默认Nginx配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
worker_connections 1024;
}

http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

include /etc/nginx/mime.types;
default_type application/octet-stream;

# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {
}

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}

同C语言一般(Nginx就是C写的),Nginx主配置文件一般一行一个语句,以分号结尾。#开头表示注释。大括号括起的部分称为一个“”。块中语句和C一样,仅在对应块中生效。

因此,可以看出nginx.conf文件的基本结构并不复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
...
events {
...
}
http {
...
server {
...
location [PATTERN]{
...
}
}
}

...对应相应区域的“全局”语句。

一个http块中可有多个server块,location同理。同一配置块中的“平级”配置,一般不取决于书写顺序,无次序关系。相同语句在不同块中出现,则取“就近原则”,以最近的块为准(同CSS)。

各个块的作用

  • 全局块

    影响Nginx服务器整体运行的配置指令。如pid存放路径,日志管理等.

    与具体业务无关,不关心Nginx的用途。

  • events块

    影响Nginx服务器与用户的网络连接。如最大连接数,事件驱动模型(详见附录)等。

    对性能影响较大。

  • http块

    影响核心业务功能。代理、缓存、日志等绝大多数功能置于该功能块下。

  • server块

    影响虚拟主机配置

    虚拟主机技术,简单来说,就是可以在一台主机上跑多个互联网服务,如HTTP,FTP,EMAIL等而不会互相干扰,且不用为每个服务单独配置一个Nginx。

  • location块

    基于对Nginx服务器接受到的请求URI(如“/url”)进行匹配,对特定请求进行处理。

基本指令

以下命令斜体表示根据具体需求进行修改,方括号表示可选项,|表示多选一。若无特殊说明,相关指令仅用于对应块。

重要指令位于http及其内部块内。全局与event块,若无特殊性能需求,一般默认即可。

全局

    user user [group];

用于配置运行Nginx服务器的用户和组。非指定用户/组内用户不可启动,关闭,重启Nginx服务。若希望所有用户都可以起停Nginx(相信你不会想这样做),不写该语句或者将user设置为nobody

    worker_processes number | auto;

配置worker process数。其为Nginx实现高并发的关键之所在。理论上,值越大,支持并发量越高。实际受硬件、资源限制。一般设置为auto,自动检测并配置。附上nginx进程图。可见笔者电脑自动配置了4个worker processes。

worker process
    pid location;
    error_log location;

master process的pid和错误日志的指定位置。一般无需修改。其中error_log的location支持stderr和文件两种形式。

    include file_path

引入配置文件。一般用于精简主配置文件,使其结构清晰。此命令可用于任何块。

event块

    accept_mutex on | off;

默认on。用于解决惊群现象(一个网络连接的到来同时唤醒多个睡眠进程,从而产生争抢),提高性能。

    multi_accept on | off;

默认off。用于设置是否允许一个worker process同时接受多个网络连接。

    use select | poll | kqueue | epoll | rtsig | eventport;

事件驱动模型选择。详情请参考附录。一般可不写该语句,默认即可。

    worker_connections number;

配置一个worker process同时开启的最大连接数。当multi_accept为on时才生效。 同时,由于Linux中网络Socket操作本质为IO操作,故number不可超过操作系统支持的最大文件句柄数量(最多能同时打开多少个文件)。

http块

默认配置中的mime参考附录

    access_log path  [format [buffer=size]];
    log_format name  format;

自定义服务日志。其中access_log中的format可以通过log_format指定。此举在于精简access_log语句,同时将一种format格式用于多个日志记录语句,减少冗余(类似变量定义)。

其中值得注意的是,foramt采用大量的内置变量。正是这些内置变量,使得Nginx简洁,且强大。format相关内置变量见附录中的内置变量

    sendfile on | off;
    sendfile_max_chunk size;

配置是否运行上传文件以及单次请求的最大数据量。\(size == 0\)表示无限制。

    keepalive_timeout time_duration [header_timeout];
    keepalive_requests number;

单次连接相关配置。注意到HTTP,FTP等是基于TCP,而TCP是面向连接的。因此通过设置time_duration可以限制单次TCP连接的时长,而header_timeout是在应答的报文头部设置超时时间。这个值是可以被浏览器识别的。

keepalive的设置除了可以限制连接外,也可以起到复用连接的作用,使客户端能够通过单次连接发送复数次请求。keepalive_requests就是限制单次TCP连接建立成功后,允许接受的请求次数。默认100.

可用于http, server块。

server块

    listen address[:port] [default_server] [rcvbuf=size] [sndbuf=size] [ssl];
    listen port [default_server] [rcvbuf=size] [sndbuf=size] [ssl];

配置网络监听。有三种,上面写出的两种由上至下分别为监听IP地址监听端口。第三种是监听UNIX Domain Socket,不常用,一般不管。

其中,address若为IPv6,则需要使用[]括起来,如[fe80::1]。

其中rcvbuf和sndbuf为监听socket的缓冲区大小。一般可不用显式配置

ssl为HTTPS相关,详见高级应用中的HTTPS服务

default_server为将该虚拟主机设置为address:port的默认主机。

前文有提及,一个HTTP块中可以包括多个server块,且“平级”块之间无次序关系,一般不取决于书写顺序(location块中有更清晰的体现)。同时,server块对应的是虚拟主机,彼此之间是逻辑分离的。故,当多个虚拟主机同时监听相同的address:port且无其他区别时(如下一条指令的server_name),选取default_server。若无其他区别且未设置default_server时,按书写顺序。

default server

ATTENTION: 如果监听非80端口一定要记得开放相关端口,否则请求会一直返回错误503

    server_name name;

配置主机名。一个server块对应一个虚拟主机。不同虚拟主机之间便可用过server_name区分。一般情况下,设置为该虚拟主机提供的服务对应的域名。如本站域名为xylonx.com,则设置为server_name xylonx.com;

server name

与DNS对应即可。

通过设置不同的server_name,可以实现多个虚拟主机绑定同一个端口且逻辑分离。如在一台主机上配置多个服务,对应设置DNS后,全部绑定在80端口——80为TCP默认端口,直接输入域名便是访问80端口,若要访问其他端口,需主动输入,如xylonx.com:8080(举个例子,本站没开8080,别访问了)——通过server_name区分。

若服务开在其他端口,而不想访问时手动输入端口号,则可采用反向代理,使对应虚拟主机监听80端口,而后将请求转发至相应端口。具体请参考高级应用中的反向代理

server_name的设置支持多个主机名,不同主机名之间通过空格隔开。同时支持通配符(如*.xylonx.com)与正则表达式(如~^www\.xylonx\.com$),其中正则表达式需要使用 ~ 开始作为标记。

当多台虚拟主机匹配成功,按照以下优先级选取主机:

  • 精准匹配
  • 通配符在开始时匹配server_name成功
  • 通配符在结尾时匹配server_name成功
  • 正则表达式匹配server_name成功

同一优先级则取决于书写顺序。

E.g.

there are several server_names:

  • xylonx.com
  • *.xylonx.com
  • www.xylonx.*
  • ~^\w+\.xylonx\.\w+$

request: www.xylonx.com

meets: *.xylonx.com, www.xylonx.*, ~^\w+\.xylonx\.\w+$

pass to: *.xylonx.com

    root path;

配置请求根目录。Web服务器在接受到网络请求后,会首先在指定的路径中寻找请求的资源。而root便是配置这个根目录使用的。可用于http, server, location块。

    error_page code [=[response]] uri

设置错误页面。

code为要处理的HTTP错误代码,如最常见的404NOT FOUND。

response为将code指定的错误代码转化为另一错误代码。如error_page 404=301 /301.html

uri为对应错误页面的路径(如/40x.html)或网址(如http://somewebsite.com/40x.html)。

可用于server, location块。

location块

    location [ = | ^~ ] uri {}

location的语法结构。其中匹配包括匹配方法和匹配URI两个部分。

一个server块中可以包含多个location,用以对不同的请求URI指定不同的资源路径。

URI的写法有两种:路径名(如/some/path/resource.html中的/some/path)和正则表达式,其中正则表达式需要以~或~*开头标识。~大小写敏感,~*忽略大小写。

location块的重点便在于匹配优先级

为方便说明,名称作如下规定:客户端发起请求的资源路径称之为URI, location中uri参数的路径名写法称之为prefix string, 正则表达式写法称之为regular string

  1. 将URI与所有的prefix string匹配
  2. 存储当前最长匹配的prefrx string. 如对URI/home/xylonx/path/to/xxx.html,prefix string/home/xylonx/home/更长,所以选取prefix string/home/xylonx
  3. 将URI与所有的regular string匹配
  4. 一旦发现有regular string匹配上了,停止匹配并选取该location块
  5. 否则选取之前存储的最长prefix string块

由此可见,Nginx的location匹配给予regular string即正则表达式更高的优先级

但是可以通过=, ^~前缀手动调整优先级。

  • =

    作用对象: prefix string

    作用:当URI与该prefix string精准匹配上了,则立即停止并选取改location块。即跳过对regular string即正则表达式的匹配。

    如URI/error/404.html, prefix string/error不叫精准匹配,/error/404.html才是精准匹配。

  • ^~

    作用对象: prefix string

    作用:与=类似,但不是精准匹配。当被^~标识的prefix string为最长匹配的prefix string,则立即停止并选取该location块,跳过正则匹配阶段。

可以看出,=, ^~的目的都是通过跳过正则匹配,从而提高prefix string的优先级。

    index file;

设置网站的默认首页。一般有两个作用:

  • 在发出请求后可以不用写首页地址。如本站首页地址为/index.html,故设置index index.html,因此当直接访问xylonx.com/时,会自动定位到xylonx.com/index.html
  • 根据请求内容的不同,设置不同的首页。如针对URI/resource//user/,可以分别设置两个不同的location块,从而为其配置不同的首页。

可以设置于server块和location块。

    autoindex [ on | off ];
    autoindex_exact_size [on | off];
    autoindex_localtime [on | off];

设置Nginx的自动列目录配置。

autoindex_exact_size设置列举文件索引时,显示文件大小。默认on

autoindex_localtime开启本地时间显示文件更新时间。默认为off(GMT时间)。

小结

以上所有便是将Nginx作为一个Web服务器所需要掌握的最简单,最基本的功能和指令了。基本上,与默认的Nginx配置文件无大区别。仅有少数添加,如autoindex.

但也仅限于此。这些很难满足当今的需求。因为它的资源均是静态,而当今互联网应用都是以动态加载(如ajax)为主。也就是说,仅仅将Nginx作为Web服务器,并不能满足如今Web应用的大部分需求。但是也有少数例外,如本站搭建所用的hexo技术,便是纯静态网站。

此处,容笔者插点题外话,讲述一下Web应用的一些相关历史。

早期,一个Web应用是依托于LAMP / LNMP, 其中,L指Linux,A和N分别指Apache和Nginx两种Web服务器,M指MySQL,一款开源数据库,P指PHP, Perl, Python等脚本语言。

整个应用搭建在Linux系统这个平台上,由Apache / Nginx等Web服务器处理连接和资源路径配置,数据存储于MySQL数据库中,通过PHP等脚本语言处理表单等用户提交的数据。

但是经历了几十年的发展,如今的Web应用与当初截然不同。Web服务器已经内嵌于各语言框架内。Java的Spring boot,Golang的Echo,Python的Django,等等,都是单独启一个Web服务器,监听端口,处理请求,资源映射,连接数据库,通过ORM框架执行CRUD操作,等等。曾经的LAMP如今已经高度集成于HTTP框架当中。

使用过Spring Boot,Echo等现代化的HTTP框架的读者,都应该了解,在启动服务时,需要绑定某个端口。且这个端口不能重复。一旦重复便会报错。

因此,这些由现代HTTP框架所编写的Web应用,本身便应该被视作为一个独立服务。

从而再次提及了虚拟主机这个概念。我们可以将每一个由现代HTTP框架所编写的Web应用视作一个虚拟主机,它监听母机的一个端口并处理请求。

既然如此,Nginx的位置在哪呢?

Nginx的作用更多地转移到了对请求的处理上,而非资源的映射。它的优势在体积小,对高并发的处理能力强,以及负载均衡,反向代理等强大的模块。

笔者认为,可以将Nginx视作一个主机连接互联网的大门,一个Web“中间件”,它接受来自互联网的请求,但并不“处理”,而是将其转发给对应的虚拟主机处理。一切都被包装在了port 80,因此也就自然地避开了访问网址时还需带端口号的不优雅的做法。

那么学习Nginx作为一款Web服务器是否是无意义的呢?并不是。因为Nginx首先是一个Web服务器,其次才承载着众多高级的功能。它作为一款Web服务器的功能是它作为一个“中间件”的基础。只有掌握了Nginx作为一个Web服务器的作用,才能更好地学习和掌握其作为一个Web“中间件”。

高级应用

本阶段涉及Nginx的一些高级功能,包括反向代理,负载均衡等。

在学习这些高级应用的过程中,应该时刻思考这些功能的本质是什么。应该注意到Nginx对请求的处理很多时候是基于对HTTP报文的处理。

PS:本文将gzip相关知识点转移至附录部分,因为其仅为在传输过程前进行压缩,从而加快速度减少流量。并无理解上的难度。

代理与反向代理

首先需要了解代理反向代理的概念。

代理服务器是一种作为客户端与资源提供者之间的中间服务器。允许客户端与资源拥有者之间建立非直接的连接

如下图所示,客户端(Client)A并不是直接向资源提供者(Resource Owner)C发起请求,而是首先通过与一个代理服务器(Proxy Server)B建立连接,而后该代理服务器B通过A发送来的报文向C请求资源,而后将该资源返回给A。

此时,服务端只知道代理服务器的IP,而不知道客户端的真实IP

proxy

但是网络请求是“双边”的,如果将报文的目的IP设置为代理服务器,那么代理服务器又如何能够得知该报文的真正目的地呢?

答案是通过特殊的代理协议,如HAProxy。一般来说,代理协议是作为某个协议的一种补充部分。如HTTP协议,方法为添加首部字段。


反向代理与代理类似。反向代理是接受来自客户端的请求,从其关系的一组或多组后端服务器上获取资源,并返回给客户端。

此时,客户端只知道代理服务器IP,而不知道服务器的真实IP

如下图所示,客户端(Client)D通过因特网访问的“资源拥有者(Resource Owner)”F实际为其设置的反向代理服务器(Reverse Proxy Server)E,改服务器通过请求,选取一系列后端服务器中的一个获取资源,然后返回给客户端D。

reverse proxy

由此可以看出,代理与反向代理本质上是类似的。都是起到一个“通道”的作用,从而掩盖真实的IP。

代理更多的是针对请求方而言,即请求方通过代理发送自己的请求。而反向代理更多的则是针对接受方而言,即接受方通过反向代理接收请求并处理

概念理清后, 来看看Nginx中如何配置代理与反向代理。

但是请注意,Nginx最初是被设计为一款反向代理服务器的。故最好不要将其作为(正向)代理使用。这并不值得,有更好的方案

故笔者在此并不描述如何将Nginx作为(正向)代理使用。读者若有兴趣,可自行查询相关文档。

  • 反向代理

Nginx执行反向代理的流程:当Nginx反向代理服务器获取到一个请求后,它会将这个请求发送给一个特定的服务器(它所“掩盖”的许多后端服务器中的一个), 并获取返回信息(response),然后将response发送回客户端。

更重要的一点是,接受到的请求不仅仅可以发送给HTTP服务器,还可以发送给一些非HTTP服务器,如FastCGI等。具体支持哪些协议请查询官方文档。此处仅介绍HTTP服务器。

    location /some/path {
        proxy_pass backend_server;
    }

最基本的命令便是proxy_pass,仅用于location块内。将访问该location对应URI的请求发送给指定的backend_server,其可以是一个URL,如http://www.example.com/some/path/,也可以由IP指定,同样也可以加上端口,如http://127.0.0.1:8000/some/path/该指令用于设置被代理的服务器的地址。

当然,如果被代理的服务器是一组服务器的话,可以通过upstream指令定义一组后端服务器,并为其命名。此处backend_server必须带上http://前缀。

    upstrem backend_server_group_name {
        server backend_server;
        server backend_server;
        ....
    }
    server{
        location / {
            proxy_pass backend_server_group_name;
        }
    }

值得注意的是,在代理过程中出现的路径更换。当proxy_pass指定的后端服务器带上了资源地址,而不是单纯的IP:port或者域名,Nginx在发送报文时会替换掉location中匹配的地址。

举个例子,如Nginx配置如下:

1
2
3
location /some/path/ {
proxy_pass http://www.someserver.com/link/;
}

当请求为http://www.server.com/some/path/index.html,匹配了/some/path/,而指定的后端服务器带有资源地址/link/,因此处理后的请求为http://www.someserver/com/link/index.html.原请求中的/some/path/被替换为了/link/

当后端服务器未指定资源路径,或者无法确认哪一个部分能够被替换时(常见于正则匹配),Nginx不会做路径更改。

注意,/也算资源路径

    proxy_set_header field value;

此语句可用于http, server, location块。一般与pass_proxy连用,用于修改HTTP报文头部的部分字段。其中field为头部对应名,value为对应值。

默认地,Nginx会修改头部的Host字段和Connection字段.

它会将Host设置为$proxy_host,即被代理的服务器的名字和端口,将Connection设置为close.

一般而言,使用反向代理的一个作用就是掩盖真实的后端服务器IP,因此需要手动调整Host的值.类似如下操作。

1
2
3
4
location / {
proxy_set_header Host $host;
proxy_pass http://localhost:8000;
}

反向代理需要配置的仅有这些。

还有一些常用的HTTP头,如X-Real-IP, 设置某个报文的真实IP等。具体需要修改的报文头请根据自身需求配置。

这些值的设置基本都会用到Nginx的内置变量,附录部分给出了常用的内置变量

    proxy_buffering on | off;
    proxy_buffers number size;
    proxy_buffer_size size;

配置Proxy Buffer。首先,来阐释一下Proxy Buffer的工作原理。

Proxy Buffer是针对每连接生效的,为每个连接单独配置。

开启该功能后,Nginx会首先尽可能地从被代理的后端服务器那里接收数据,并放置在为其单独配置的Proxy Buffer之中,其大小由proxy_buffers和proxy_buffer_size决定。同时,响应报文的第一部分被放置在一个单独的Buffer之中。单次响应数据接受完或者Buffer装满后,Nginx才开始向客户端传输数据。

其中,proxy_buffers配置单次连接中,Proxy Buffer总的数量以及大小。故整个Proxy Buffer的大小为number × size.

proxy_buffer_size用于存储响应报文的第一部分,一般为报文头,用于获取改连接的相关元信息。

由此,可以推断出Proxy Buffer的大小不宜过大。一般设置number为16, size为2K / 4K.

自此,反向代理讲述完毕。

下面是一个常用应用,即对于使用现代HTTP框架写出的Web应用,将多个服务置于同一台主机,且无需在通过域名访问时输入端口号。

此处假设有两个服务,一为动态博客服务,配置域名为blog.xylonx.com,运行在端口7894,一为现代HTTP框架所写的服务,配置域名为service.xylonx.com,运行在端口8000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
user www www;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;

events {
worker_connections 1024;
}

http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

include /etc/nginx/mime.types;
default_type application/octet-stream;

include /etc/nginx/conf.d/*.conf;

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name blog.xylonx.com;

root /usr/share/nginx/html;
index index.html;

location / {
proxy_pass http://localhost:7894;
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
}

}

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name service.xylonx.com;

root /usr/share/nginx/html;
index index.html;

location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

负载均衡

同样地,首先讲讲负载均衡(load balancing)的概念。负载均衡技术的诞生源于互联网的庞大流量。对于某些大型企业,可能在任何一个瞬间,都会接受到数百万,乃至上亿的网络请求(如某宝的双十一)。将这些请求全部交给一台计算机处理明显的不可能的。因此他们的后端都是由拥有庞大数量的计算机集群构成。

负载均衡技术简单来讲,就是如何将这些请求以一种合理的方式“分给”那些后端服务器,从而保证运行速度,资源利用率,系统容错率以及很好的可扩展性。保证任何一台服务器接受的请求不会超过其处理能力,能够随时、简单的添加新的服务器并自动为其分配任务,且能够检测集群中的单点故障并不再为其分配任务。

load balancing diagram

能够起到负载均衡作用的称之为负载均衡器(load balancer)。一般分为硬件负载均衡器软件负载均衡器。二者最大的区别在于,硬件负载均衡器需要一些专有,定制的硬件设备。而软件负载均衡器几乎能简单的安装在任意一台机器上。

无论如何,负载均衡的实现要基于某些算法。分为静态算法和动态算法两种。其中静态算法不考虑不同机器之间的状态(如最普遍的Round-Robin算法),一般为中心化算法; 而动态算法(如Map-Reduce)考虑各计算单元的当前负载,但是涉及到不同计算单元之间的信息交换,可能导致效率的损失.一般为分布式算法。

值得注意的是,负载均衡技术并不为Nginx所独有,而是广泛分布在互联网的各个角落。在OSI七层网络模型中的任意一层,均可以实现负载均衡。

其中Nginx仅仅是实现了静态的、基于优先级的加权轮询算法。主要使用的命令就是proxy_pass和upstream

由此可见,Nginx的负载均衡其实是与反向代理密切相关的。可以被认为是反向代理的一个加强功能.

首先配置一组后端服务器。可以为其设置权重,通过在server_name后加weight(只能为正整数)。默认权重为1。Nginx的某些算法可以基于权重分发连接。

1
2
3
4
5
6
7
upstream backend{
server http://backend1.example.com weight=5;
server http://backend2.example.com;
server http://backend3.example.com;

server http://192.0.0.1 backup;
}

注意,若是某个服务器被指定为backup, 则该服务器仅在其他服务器全部挂掉后才启用.

而后选择负载均衡算法。Nginx支持的算法有以下几种。其中除去Round Robin算法外,其余算法需要显式配置。方法为在upstream块中的server之前加入方法名以下方法若无显式说明,则不支持weight参数。

  • Round Robin

    默认算法。请求会根据权重均匀地分发给所配置的后端服务器。如上命令块,权重分别为5, 1, 1。故请求会按照5/7, 1/7, 1/7的比例分发给所配置的三台服务器。

  • Least Connections

    请求会发送给拥有最少活跃连接的服务器。同样地,该算法考虑权重。但是优先考虑连接数,其次才是权重。

    1
    2
    3
    4
    5
    6
    upstream backend{
    least_conn;
    server http://backend1.example.com;
    server http://backend2.example.com;
    server http://backend3.example.com weight=3;
    }
  • IP Hash

    任意一个连接被分发给哪个服务器取决于对其IP进行Hash后的值。其中IPv4和IPv6均参与运算。该方法保证了同一IP的请求总是会被指向同样的后端服务器.

    如果其中某一个服务器因为某些原因暂时无法接受连接(如维护等),可以在该服务器后添加down参数。down会保持当前hash函数(因为server数变了),原来被分配给down的连接会自动分配给下一个服务器。

    1
    2
    3
    4
    5
    6
    upstream backend{
    ip_hash;
    server http://backend1.example.com;
    server http://backend2.example.com down;
    server http://backend3.example.com;
    }
  • Generic Hash

    与IP Hash类似,但是更通用,用户可以指定用于Hash的键。

    其中有可选项consistent,用于启用ketama算法——一种Hash算法,增减server数不会导致所有hash值的变化。

    1
    2
    3
    4
    5
    6
    upstream backend{
    hash $request_uri consistent;
    server http://backend1.example.com;
    server http://backend2.example.com;
    server http://backend3.example.com;
    }
  • Least Time

    仅用于Nginx Plus(Nginx的商业化版本)。对任意一个请求,Nginx Plus将其分配给拥有最短平均延迟以及最低的活跃连接数。

    其中,最短平均延迟的计算方法有一下三种,需显式配置:

    • header

      接受到来自服务器的第一个Byte的耗时

    • last_byte

      接受到来自服务器的最后一个Byte的耗时,即整个response

    • last_byte inflight

      接受到来自服务器的最后一个Byte的耗时,即整个response, 且考虑不完整的请求。

    1
    2
    3
    4
    5
    6
    upstream backend{
    least_time header;
    server http://backend1.example.com;
    server http://backend2.example.com;
    server http://backend3.example.com;
    }
  • Random

    随机选取。其中若是指定参数two,则Nginx随机选取两个后端服务器,而后根据指定的方法在二者中选取一个。

    方法指定有一下三种:

    • least_conn
    • least_time=header
    • least_time=last_byte

    与Least Connections和Least Time相同。同样地,least_time仅限nginx_plus使用

    1
    2
    3
    4
    5
    6
    upstream backend{
    random two least_time=last_byte;
    server http://backend1.example.com;
    server http://backend2.example.com;
    server http://backend3.example.com;
    }

负载均衡还有写其他可选项,但是仅限于Nginx Plus使用。此处不做过多展开。

以下为负载均衡的简单使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
http {
upstream backend{
# choosing Round Robin algo
server backend1.example.com;
server backend2.example.com;
server backend3.example.com weight=5;
server backup.example.com backup;
}

server {
listen 80;

server_name exmple.com;

location / {
proxy_pass http://backend;
proxy_set_header Host $proxy_host;
}
}
}

HTTPS

众所周知,HTTP协议是明文传送,一旦处于不安全的网络环境中(如公共网络),就会存在安全隐患。

而HTTPS就是用于解决这一问题。HTTPS采用一种加密协议对HTTP报文进行加密,该协议一般被称作TLS(Transport Layer Security)或者SSL(Secure Sockets Layer)。该协议采用非对称加密算法,对HTTP报文进行加密。客户端使用公钥对报文加密,服务器使用私钥对报文解密。从而保证了安全性。

HTTPS的默认端口与HTTP不同。HTTP为80, 而HTTPS在443

回到Nginx,Nginx通过在listen后添加ssl参数启用SSL服务。

而后,指定网站证书和私钥。其中网站证书简单来说就是起着分发公钥的作用。其将公钥分发给任意一个向HTTPS服务器发请求的客户端。

1
2
3
4
5
6
7
8
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate www.example.com.crt;
ssl_certificate_key www.example.com.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
}

值得注意的是,私钥文件,Nginx的主进程必须拥有读取权限。

其中ssl_protocols为安全链接可选的加密协议。一般保持为TLSv1 TLSv1.1 TLSv1.2即可。

ssl_ciphers为支持的加密协议。具体协议可以通过openssl ciphers命令查看。

Nginx也支持OCSP(Online Certificate Status Protocol)协议——一种用于验证证书有效性的协议。配置指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 443 ssl;

ssl_certificate /etc/ssl/example.com.crt;
ssl_certificate /etc/ssl/example.com.key;

ssl_verify_client on;
ssl_ocsp on; # enable OCSP validation
ssl_trusted_certificate /etc/ssl/cachain.pem;
ssl_ocsp_responder http://ocsp.example.com/;
ssl_ocsp_cache shared:one:10m;
}

通过ssl_verity_clientssl_ocsp开启OCSP服务。使用ssl_trusted_certificate配置信任的证书。ssl_ocsp_responder用于指定OCSP服务器。ssl_ocsp_cache用于指定缓存OCSP应答的内存区域,且该区域能够被Nginx的所有worker processes所读取。格式为 shared:name:size

SSL协议作为加密协议,必然会消耗额外的CPU资源。而其中最大的开销便是SSL握手(SSL handshake)。有两种优化方式用于减少这部分开销。

  • 开启keepalive connections,从而使多个请求能够通过同一个连接发送。
  • 启用SSL会话(SSL session)缓存,从而避免并行连接(HTTP/2)和后续连接的SSL握手。

keepalive的配置前文有讲过,此处不作赘述。下面是SSL session缓存的相关配置(开启http2以启用并行连接).

1
2
3
4
5
6
7
8
9
10
http {
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

server {
listen 443 ssl http2;

# config for ssl and others
}
}

ssl_session_cache用于设置ssl session的缓存名称,大小。格式为shared:name:size。1M的缓存能够容纳大约4000个session。

ssl_session_timeout用于配置一个SSL session的超时时间。默认值是5分钟。

最后,如果不想为每个虚拟主机单独都配置证书,则可以将ssl相关配置放入http块中,从而使多个虚拟主机共用同一份证书和私钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http {
ssl_certificate common.crt;
ssl_certificate_key common.key;

server {
listen 443 ssl http2;
server_name server1.example.com;
# ...
}

server {
listen 443 ssl http2;
server_name server2.example.com;
# ...
}
}

防盗链

盗链是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取最终用户的浏览和点击率。受益者不提供资源或提供很少的资源,而真正的服务提供商却得不到任何的收益。

最常见的盗链便是盗取网站图片。说的就是那些盗我博客封面图的人

值得一提的是,盗链想完全禁止几乎是一件不可能的事。比如图片,只要能加载,就有办法获取。防盗链技术只能让到盗链变难。

此处提一个HTTP头,名为Referer。当浏览器向Web服务器发送请求时,会带上Referer头,告诉服务器从哪个链接跳转过来的。因此可以用其在一定程度上防盗链。但是HTTP Referer本身是可以通过程序手动设置的,所以并非100%可靠。

1
2
3
4
5
6
7
location ~.*\.(gif|jpg|png|bmp|jpeg|swf)$ {
valid_referers example.com *.example.com *.google.com;

if ($invalid_referer){
return 403;
}
}

valid_referers用于设置允许通过的referer。多个referer之间通过空格隔开.

其中有几个特殊的值:

  • none

    允许不带referer请求资源

  • blocked

    允许不带http://开头的,不带协议的访问资源。

若是用作防盗链,以上二者无需配置。

Nginx中准许使用if语句,用法与C基本类似。其中$invalid_referer为内置变量,用于判断referer是否有效。

这样,基于HTTP Referer Header的简单防盗链便配置成功了。

限制请求(限流)

且不说DDos,有些时候服务器会遭到恶意请求——短时间内高频访问。如某些盗链操作者会频繁获取资源。

此时就可以通过配置Nginx的limit相关指令进行限制。

    limit_conn_zone    key zone=name:size;
    limit_conn name number;

其中limit_conn_zone用于定义一个限制,limit_conn用于使用一个已经定义过的限制

limit_conn_zone中,使用zone=name:size定义该限制的名称的记录相关信息的缓存大小。类似于C中定义变量。其中,key为被限制的对象。如$binary_remote_addr——客户端IP

$binary_remote_addr$remote_addr类似,均包含客户端IP,但是$binary_remote_addr更短,IPv4地址仅有4字节。在64位计算机上,存储相关状态占128字节。因此,1M的缓存大概能缓存16000个IP

limit_conn中,number用于限制每个key允许的连接数。

举个例子,下面配置表明,对每个IP,仅允许同时有20个连接。

1
2
3
4
5
6
7
8
9
http {
limit_conn_zone $binary_remote_addr zone=ip_conn_limit:10m;

server {
listen 80;

limit_coon ip_conn_limit 20;
}
}

Nginx同样可以限制访问速度。使用方法与limit_conn类似,需要先定义相关zone。

    limit_req_zone key zone=name:size rate=rate;
    limit_req name [brust=number] [nodelay] [delay=number];

其中rate有两种写法:

  • 每秒访问数(request per second): r/s
  • 每分钟访问数(request per minute): r/m

举个例子, 下面配置表明,对每个IP限制,仅允许每秒访问一次

1
2
3
4
5
6
7
8
9
http {
limit_req_zone $binary_remote_addr zone=ip_frequence_limit:10m rate=1r/s;

server {
listen 80;

limit_req ip_frequence_limit;
}
}

当请求速率超过指定rate时,多余的请求会被Nginx放弃并返回error。此时可以设置brust参数,将过剩的请求缓存起来,并按照指定速率处理。brust指定的值就是缓存的请求数。只有超出缓存的请求才会被拒绝。

但是此时会出现延迟问题。请求实际上是被放入了一个大小为brust指定的队列。因此必然会导致排在后面的请求延迟非常高。因此此时可以指定参数nodelay。此时,在指定burst内的请求不会被加入队列,而是直接处理并响应,不会受到指定rate的限制。但是此时只是处理的速度加快了,实际上仍然起到了限流的作用。更多是对多个请求进行并发处理。

还可以更加精细化地操作,指定并发数量。delay=number。上述nodelay可以视作是delay的一个特殊情况,即delay数等于brust数。如brust=5 delay=3表示缓存5个溢出请求,且每次并发处理三个请求。

limit_req的设计是基于leaky bucket算法。Xin Zhao的图解Nginx限流配置对limit_req有非常生动的讲解,配图十分清晰明了,不妨移步一观。

除此之外,Nginx还可以限制下载速度。

    limit_rate rate;
    limit_rate_after rate;

limit_rate用于限制单个连接的所能允许的最大下载速度。但是值得注意的是,客户端可以通过开启多个连接以绕开该限制。故若要严格限制,应与limit_conn联合使用,限制连接数与单连接最大速度。

limit_rate_after用于放开对起始数据的限制.这些数据的速度不受limit_rate的限制。如limit_rate_after 10m表示前10M数据会以服务器最大速度发出,而后面的数据才会受到limit_rate的限制。

以下为该命令的一个例子。允许单个IP同时开启5个连接,对/download/下的资源进行限制,仅允许开启一个连接,且限速为50k。其中起始1M数据不受限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http {
limit_conn_zone $binary_remote_address zone=addr:10m;

server {
listen 80;
root /www/data;
limit_conn addr 5;

location / {
}

location /download/ {
limit_conn addr 1;
limit_rate_after 1m;
limit_rate 50k;
}
}
}

除了设置固定的常数以外,limit_rate以及limit_rate_after均支持变量设置。

如下配置所示。其中map为定义变量的一种方式——基于某个值定义变量。语法为

    map string $variable {
        ...
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
map $ssl_protocol $response_rate {
"TLSv1.1" 10k;
"TLSv1.2" 100k;
"TLSv1.3" 1000k;
}

server {
listen 443 ssl;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_certificate www.example.com.crt;
ssl_certificate_key www.example.com.key;

location / {
limit_rate $response_rate; # limit bandwith based on TLS version
limit_rate_after 512;
}
}

总结

自此,本博文的Nginx讲解结束。但是Nginx远不止这些内容,其他诸如对TCP/UDP流的处理,对请求URI的改写,缓存机制等等。

Nginx终究只是一个工具罢了,只要掌握了其基本使用方法及其处理思想,其他的功能可待到有需求时再查阅相关手册(尤其是官方文档)。

附录

事件驱动模型

Web服务本身是一个一对多的模型。因此对Web服务器而言,同时处理多个请求的并行能力就非常重要。一般有三种解决方案:

  • 多进程模式

    每接受到一个请求,便生成一个子进程处理。连接断开,子进程结束。

  • 多线程模式

    每接受到一个请求,便生成一个子线程处理。连接断开,子线程结束。

  • 异步模式

    请求形成一个队列,依次处理并返回。

Nginx使用的是多进程模式结合异步机制。其中异步采用的为异步非阻塞方式。

同步,异步一般针对通信而言。如果客户端发起请求后一直等待返回称同步,若客户端发起请求后不待返回便继续执行后续程序,称异步。

阻塞,非阻塞一般用于描述进程处理调用的方式。Linux中一切皆文件,因此网络Socket实质上就是IO操作。阻塞指IO调用结果返回前,当前进程被挂起,等到结果返回后再继续进行,而非阻塞方式则是发起IO调用后,该进程并不会被挂起,而是继续执行。

Nginx在启动时会预开启多个worker process,等待处理所有的客户端的请求。而其中每个worker process采用异步非阻塞模式,处理多个客户端请求。

既然是非阻塞方式,则需要一个机制用于检测IO操作是否完成。Nginx采用的方式为在IO调用完成后,主动通知进程。而这一解决方案,一般称之为事件驱动模型。其提供一种机制,用于管理IO调用,让进程可以并行处理多个请求,而不需要关心IO操作的具体状态。对应的实现称之为事件驱动处理库。最常见的有三种:select,poll,epoll。Nginx还支持一些其他的事件驱动处理模型(此处不作过多介绍).

  • select

    各版本Linux和Windows都支持的基本事件驱动模型库。

  • epoll

    Linux平台的基本事件驱动模型。Windows不支持。可以视作select的一个优化版本

  • epoll

    Linux下的非常优秀的事件驱动模型。属于poll的一个变种,但是拥有非常高的性能。

MIME

    include mime.types;
    default_type mime-type;

以上为nginx默认配置文件中的语句。

MIME type是用于标识数据类型的标签。和Windows中.html .xml .txt .exe等后缀类似。

以下描述摘自stack overflow: What is a MIME type

A MIME type is a label used to identify a type of data. It is used so software can know how to handle the data. It serves the same purpose on the Internet that file extensions do on Microsoft Windows.

So if a server says "This is text/html" the client can go "Ah, this is an HTML document, I can render that internally", while if the server says "This is application/pdf" the client can go "Ah, I need to launch the FoxIt PDF Reader plugin that the user has installed and that has registered itself as the application/pdf handler."

You'll most commonly find them in the headers of HTTP messages (to describe the content that an HTTP server is responding with or the formatting of the data that is being POSTed in a request) and in email headers (to describe the message format and attachments).

内置变量

  • $host

    请求报文头中Host的值。如果为空,则设置为server_name。

  • $proxy_host

    被代理的服务器的名字和端口

  • $remote_addr

    客户端地址 client address,字符表示

  • $binary_remote_addr

    客户端地址,二进制表示

  • $remote_port

    客户端端口 client port

  • $host

    请求头中的Host的值。若请求头中无Host行,则为服务器名。

  • $http_referer

    HTTP应用,Referer头中的值

  • $request

    客户端请求

  • $request_filename

    当前请求的文件路径名。由root/alias和URI请求生成

  • $request_method

    请求方法

  • $request_uri

    请求的URL,带参数,不带主机名

Gzip

对response进行压缩,能够显著降低所传输数据的大小。一般现代浏览器可以对压缩数据进行解压,所以不必担心无法解析。

同时,压缩仅在reponse被发送给客户端时进行,因此不必担心反向代理时会“双重压缩”。

相关指令如下:

    gzip on | off;
    gzip_buffers number size;
    gzip_comp_level level;
    gzip_types mime_type;
    gzip_min_length number;
    gzip_proxied off | expired | no-cache | no-store | private | no_last_modified | no_etag | auth | any ...;

gzip_buffers用于设置压缩文件时使用的缓存空间大小。其中size一般取系统内存页一页的大小,4K或者8K。number为向系统申请的size数量。因此总大小为number × size。默认值为128.

gzip_comp_level用于设定Gzip的压缩程度,从1-9压缩程度依次上升,但是压缩效率依次下降。默认为1

gzip_types用于指定对何种内容进行压缩。默认为text/html,即仅当response为HTML时才压缩。其值必须为MIME type中的一种,多种格式之间用空格隔开。

gzip_min_length为指定文件达到多大才进行压缩。单位是byte。默认值是20字节,Nginx官方文档推荐值为1000

gzip_proxied为反向代理时根据条件决定是否进行压缩。但是前提是response的header中有Via头域。

几个可选项表示的意义分别为:

  • off

    不压缩

  • expired

    当response header中有Expires头域信息,则压缩

  • no-cache

    当response header中有Cache-Control:no-cache信息,则压缩

  • no-store

    当response header中有Cache-Control:no-store信息,则压缩

  • private

    当response header中有Cache-Control:private信息,则压缩

  • no_last_modified

    当response header中没有Last-Modified信息,则压缩

  • no_etag

    当response header中没有Etag信息,则压缩

  • auth

    当response header中有Authorization信息,则压缩

  • any

    压缩

支持多个选项,之间通过空格隔开。

以下为一个例子:

1
2
3
4
5
6
7
server {
gzip on;
gzip_types text/plian application/xml;
gzip_proxied no-cache no-store expired auth;
gzip_min_length 1000;
...
}

参考资料

Nginx官网

《Nginx高性能Web服务器详解》

《实战Nginx:取代Apache的高性能Web服务器》

What is A Reverse Proxy? | Proxy Servers Explained

HAProxy protocol

Hardware Load Balancer

高性能Nginx HTTPS调优