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。

nginx-config

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时,按书写顺序。

default server

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

server_name name;

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

severName

通过设置不同的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应用的大部分需求。但是也有少数例外,如本站搭建所用的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

forward-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://前缀。

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-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_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以启用并行连接).

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官网

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

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

What is A Reverse Proxy? | Proxy Servers Explained

HAProxy protocol

Hardware Load Balancer

高性能Nginx HTTPS调优