Nginx指北
从零点五开始的Nginx学习经历
Nginx毕竟是个工具类应用,故笔者强烈建议读者在观看本博文的同时同步操作您的Nginx。唯有多用方能熟悉并掌握。
同时请注意,附录的存在是为了在不影响主体的情况下,增加对相关知识的认知,或者作为一种附加项,或者手册。笔者建议初学者在学习过程中,除查询手册等必要举措外,不要受到附录的影响。它并不影响你对Nginx的学习之旅。
安装部署
一般通过yum/apt-get/pacman等包管理器安装即可。
在安装完成后,通过systemctl start nginx.service
启动Nginx。亦可通过Nginx自带工具启动,参考基本操作。
不同Linux发行版系统工具可能不同,如
init.d
等,具体请单独查询。
而后向防火墙中添加HTTP服务。不然直接访问流量会被防火墙干掉。
# 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 | 效果等同 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配置。
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文件的基本结构并不复杂。
...
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块,若无特殊性能需求,一般默认即可。
global
user user [group];
用于配置运行Nginx服务器的用户和组。非指定用户/组内用户不可启动,关闭,重启Nginx服务。若希望所有用户都可以起停Nginx(相信你不会想这样做),不写该语句或者将user设置为
worker_processes number | auto;
配置worker process数。其为Nginx实现高并发的关键之所在。理论上,值越大,支持并发量越高。实际受硬件、资源限制。一般设置为auto,自动检测并配置。附上nginx进程图。可见笔者电脑自动配置了4个worker processes。
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不可超过操作系统支持的最大文件句柄数量(可通过ulimit -n ${fdnum}
调整)。
http
默认配置中的mime参考附录
access_log path [format [buffer=size]];
log_format name format;
自定义服务日志。其中access_log中的format可以通过log_format指定。此举在于精简access_log语句,同时将一种format格式用于多个日志记录语句,减少冗余(类似变量定义)。
其中值得注意的是,format采用大量的内置变量。正是这些内置变量,使得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块。
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时,按书写顺序。
ATTENTION: 如果监听非80端口一定要记得开放相关端口,否则请求会一直返回错误503
server_name name;
配置主机名。一个server块对应一个虚拟主机。不同虚拟主机之间便可用过server_name区分。一般情况下,设置为该虚拟主机提供的服务对应的域名。如本站域名为xylonx.com,则设置为server_name xylonx.com;
. 与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
- 将URI与所有的prefix string匹配
- 存储当前最长匹配的prefrx string. 如对URI
/home/xylonx/path/to/xxx.html
, prefix string/home/xylonx比/home/
更长,所以选取prefix string/home/xylonx
- 将URI与所有的regular string匹配
- 一旦发现有regular string匹配上了,停止匹配并选取该location块
- 否则选取之前存储的最长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应用的大部分需求。但是也有少数例外,如本站搭建所用的jekyll技术,便是纯静态网站。
此处,容笔者插点题外话,讲述一下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的Gin, 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由于其本身的高性能,被嵌入在各种”现代”网关中,比如k8s就有nginx相关路由插件
高级应用
本阶段涉及Nginx的一些高级功能,包括反向代理,负载均衡等。
在学习这些高级应用的过程中,应该时刻思考这些功能的本质是什么。应该注意到Nginx对请求的处理很多时候是基于对HTTP报文的处理。
PS:本文将gzip相关知识点转移至附录部分,因为其仅为在传输过程前进行压缩,从而加快速度减少流量。并无理解上的难度。
代理与反向代理
首先需要了解代理与反向代理的概念。
代理服务器是一种作为客户端与资源提供者之间的中间服务器。允许客户端与资源拥有者之间建立非直接的连接。
如下图所示,客户端(Client)A并不是直接向资源提供者(Resource Owner)C发起请求,而是首先通过与一个代理服务器(Proxy Server)B建立连接,而后该代理服务器B通过A发送来的报文向C请求资源,而后将该资源返回给A。
此时,服务端只知道代理服务器的IP,而不知道客户端的真实IP
但是网络请求是”双边”的,如果将报文的目的IP设置为代理服务器,那么代理服务器又如何能够得知该报文的真正目的地呢?
答案是通过特殊的代理协议,如HAProxy。一般来说,代理协议是作为某个协议的一种补充部分。如HTTP协议,方法为添加首部字段。
反向代理与代理类似。反向代理是接受来自客户端的请求,从其关系的一组或多组后端服务器上获取资源,并返回给客户端。
此时,客户端只知道代理服务器IP,而不知道服务器的真实IP
如下图所示,客户端(Client)D通过因特网访问的”资源拥有者(Resource Owner)”F实际为其设置的反向代理服务器(Reverse Proxy Server)E,改服务器通过请求,选取一系列后端服务器中的一个获取资源,然后返回给客户端D。
由此可以看出,代理与反向代理本质上是类似的。都是起到一个”通道”的作用,从而掩盖真实的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://前缀。
upstream 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配置如下:
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的值.类似如下操作。
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
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 balancer)。一般分为硬件负载均衡器和软件负载均衡器。二者最大的区别在于,硬件负载均衡器需要一些专有, 定制的硬件设备。而软件负载均衡器几乎能简单的安装在任意一台机器上。
无论如何,负载均衡的实现要基于某些算法。分为静态算法和动态算法两种。其中静态算法不考虑不同机器之间的状态(如最普遍的Round-Robin算法),一般为中心化算法; 而动态算法(如Map-Reduce)考虑各计算单元的当前负载,但是涉及到不同计算单元之间的信息交换,可能导致效率的损失.一般为分布式算法。
值得注意的是,负载均衡技术并不为Nginx所独有,而是广泛分布在互联网的各个角落。在OSI七层网络模型中的任意一层,均可以实现负载均衡。
其中Nginx仅仅是实现了静态的、基于优先级的加权轮询算法。主要使用的命令就是proxy_pass和upstream
由此可见,Nginx的负载均衡其实是与反向代理密切相关的。可以被认为是反向代理的一个加强功能.
首先配置一组后端服务器。可以为其设置权重,通过在server_name
后加weight
(只能为正整数)。默认权重为1。Nginx的某些算法可以基于权重分发连接。
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
请求会发送给拥有最少活跃连接的服务器。同样地,该算法考虑权重。但是优先考虑连接数,其次才是权重。
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
的连接会自动分配给下一个服务器。
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值的变化。
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, 且考虑不完整的请求。
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使用
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使用。此处不做过多展开。
以下为负载均衡的简单使用:
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服务器发请求的客户端。
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)协议——一种用于验证证书有效性的协议。配置指令如下:
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_client
与ssl_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以启用并行连接).
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块中,从而使多个虚拟主机共用同一份证书和私钥。
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%可靠。
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个连接。
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限制,仅允许每秒访问一次
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数据不受限制。
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 {
...
}
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等后缀类似。
有关MIME type的详细描述, 可以参考stack overflow上的这篇文章What is a MIME type
内置变量
$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
压缩
Gzip支持多个选项,之间通过空格隔开。
以下为一个例子:
server {
gzip on;
gzip_types text/plian application/xml;
gzip_proxied no-cache no-store expired auth;
gzip_min_length 1000;
...
}
参考
《Nginx高性能Web服务器详解》
《实战Nginx:取代Apache的高性能Web服务器》