Nginx实战(高级篇)

26

一、扩展

1.1 垂直扩展

对服务进行垂直扩展的一般方法是增加服务器的硬件配置,如升级网卡、增加内存条、选择更高主频的CPU等。该方式虽然提高了单机的吞吐量,但是提升幅度有限。

1.2 水平扩展

对服务进行水平扩展的一般方法是将服务部署在多台主机上,它们提供相同的服务,这些提供相同服务的主机也被称为集群。

1.2.1 集群

集群由提供相同服务的多台主机组成。对于一个已开发完毕的单体架构的系统,现在因为系统并发量不足,需要对其进行扩展,正常的思路是将该单体系统复制多份,部署在不同的主机上,构成集群。但由于该系统未考虑分布式开发,如用户会话保持等都实现在单体系统内,因此在构建成集群后,为了减少代码重构,可以采用Nginx做反向代理,然后使用sticky做会话保持,让同一用户的请求转发到相同的上游服务器。

1.2.2 会话保持

使用第三方模块 sticky 实现静态服务器的会话保持。因为一些原因(上游服务器希望保持用户会话),上游服务器希望用户的多次请求都打到同一上游服务器,在Nginx中,可以通过引入第三方模块 sticky 来实现。该模块是基于cookie来实现它的功能的,因此需要保证客户端支持cookie。

sticky module
模块下载地址:ngx-sticky-module
安装模块步骤:

  1. 解压缩 tar xzvf ngx-sticky-module
  2. cd到nginx源文件,进行配置./configure --prefix=/usr/local/nginx --add-module=/root/ngx-sticky-module
  3. 编译nginx,执行make
  4. 使用平滑升级的方式进行安装

开启sticky的配置如下:

upstream {
  sticky; #开启sticky
  server 127.0.0.1:9000;
  server 127.0.0.1:9001;
  server 127.0.0.1:9002;
}

sticky还有一些可选择的配置:

sticky [name=route] [domain=.foo.bar] [path=/] [expires=1h] 
       [hash=index|md5|sha1] [no_fallback] [secure] [httponly];

下面介绍几个常用的参数:

  • name:sticky持有的cookie的字段名称,应避免保持冲突
  • expires:cookie过期时间
  • hash:使用的哈希算法

Nginx平滑升级
Nginx平滑升级其实就是在执行完make命令后,不进行make install,而是手动替换可执行文件,具体步骤如下:

  1. 停止nginx,执行命令systemctl stop nginx
  2. 备份原nginx,执行命令mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
  3. 复制编译后的nginx,执行命令cp objs/nginx /usr/local/nginx/sbin/
1.2.3 keepalive 保持连接

Nginx作为反向代理服务器时,客户端和Nginx代理服务器相连,Nginx按需和上游服务器相连,因此在Nginx的配置中可以对客户端和上游服务器分别进行配置。

客户端保持连接
客户端保持连接的相关配置文档:Module ngx_http_core_module (nginx.org)
客户端保持连接即客户端和Nginx网关服务器之间的保持连接,通过一些参数的配置,优化请求效率。

keepalive_requests: 一次连接中请求的最大数量,当达到该值时,断开连接。

Syntax:**keepalive_requests** `_number_`;
Default:keepalive_requests 1000;
Context:httpserverlocation

keepalive_time: 连接的最大空闲时间,如果连接的空闲期超过该时间,则断开连接。

Syntax:**keepalive_time** `_time_`;
Default:keepalive_time 1h;
Context:httpserverlocation

keepalive_timeout:连接的超时时间,如果该时间内还没有重连成功,则释放连接。

Syntax:**keepalive_timeout** `_timeout_` [`_header_timeout_`];
Default:keepalive_timeout 75s;
Context:httpserverlocation

上游服务器保持连接
Nginx对上游服务器的官方配置文档:Module ngx_http_upstream_module (nginx.org)

keepalive: 设置处于空闲状态保持连接的连接数,该数值不限制Nginx的连接总数。

Syntax:**keepalive** `_connections_`;
Default:
Context:upstream

一般情况下,Nginx向上游服务器发出的http请求的headers中的Conncetion为false,当我们希望与上游服务器保持连接时,需要将该字段置空,具体配置示例如下:

upstream http_backend {
    server 127.0.0.1:8080;

    keepalive 16;
}

server {
    ...

    location /http/ {
        proxy_pass http://http_backend;
        # 设置代理后的http版本为1.1
        proxy_http_version 1.1;
        # 设置代理后的header字段
        proxy_set_header Connection "";
        ...
    }
}

剩余的几个参数有 keepalive_requestskeepalive_timekeepalive_timeout,含义同客户端保持连接一致。

AB(apache benchmark)压测工具
apache benchmark 工具的官方文档:ab - Apache HTTP server benchmarking tool - Apache HTTP Server Version 2.4

在centos中可以通过yum install httpd-tools来安装ab测试工具。

命令示例:ab -n 100000 -c 30 http://192.168.1.111

  • -n :后面跟请求个数,即请求总个数
  • -c :后面跟并发个数,即开启多少个线程去并发请求
1.2.4 上游服务器缓冲配置

配置Nginx服务器和上游服务器之间的缓冲。官方配置文档为:Module ngx_http_proxy_module (nginx.org)
proxy_buffer:从代理服务器返回响应的第一部分(响应头)的缓冲区大小

Syntax:**proxy_buffer_size** `_size_`;
Default:proxy_buffer_size 4k|8k;
Context:httpserverlocation

proxy_buffering:如果为on,则从代理服务器接收到的响应会先被放入缓冲中,然后在发送给客户端,如果为off,响应会被同步写入到客户端,不会进行缓冲。

Syntax:**proxy_buffering** `on` | `off`;
Default:proxy_buffering on;
Context:httpserverlocation

proxy_buffers:设置接收代理服务器响应的缓冲区个数和大小。

Syntax:**proxy_buffers** `_number_` `_size_`;
Default:proxy_buffers 8 4k|8k;
Context:httpserverlocation
1.2.5 客户端缓冲配置

Nginx与客户端之间的缓冲配置,官方文档地址为:Module ngx_http_core_module (nginx.org)
client_body_buffer_size:客户端响应内容的缓冲区大小,默认32位电脑上为8k,64位电脑上为16k。

Syntax:**client_body_buffer_size** `_size_`;
Default:client_body_buffer_size 8k|16k;
Context:httpserverlocation

client_body_in_file_only:将客户端响应内容保存到文件,这在测试环境很有用。

Syntax:**client_body_in_file_only** `on` | `clean` | `off`;
Default:client_body_in_file_only off;
Context:httpserverlocation

client_header_buffer_size:设置客户端请求头的缓冲区大小,默认1k,但在一些特殊环境下,如果请求头很大,可以配置该值。

Syntax:**client_header_buffer_size** `_size_`;
Default:client_header_buffer_size 1k;
Context:httpserver

client_max_body_size:设置最大的请求体大小,如果超过该值,则会向客户端返回413错误码,如果设置该值为0,则表示对请求体大小不做检查。

Syntax:**client_max_body_size** `_size_`;
Default:client_max_body_size 1m;
Context:httpserverlocation
1.2.6 Nginx代理后,上游服务器如何获取用户真实IP?

客户端请求在经过Nginx代理后与上游服务器进行交互,客户端是不与上游服务器相连的,因此上游服务器不能直接获得客户端的真实IP。

通过上面的分析可知Nginx中可以获取得到真实用户的IP,我们可以对Nginx进行配置,将用户真实IP写入到Nginx向上游服务器发出的请求的请求头中,示例如下:

server {
        ...
        proxy_set_header X-For-RemoteIP $remote_addr;
} 

上游服务器可以通过请求头中的X-For-RemoteIP字段来获取用户的真实IP。

1.2.7 gzip

gzip是一种浏览器中常用的压缩算法,由nginx内置的gzip模块提供支持,具体配置选项可在官网查看:Module ngx_http_gzip_module (nginx.org)

动态压缩
gzip中的动态压缩指的是将请求体通过gzip算法压缩后,再发送给客户端,这个过程是实时进行的,是一种通过算力换带宽的方法。可以配置最低的压缩大小、压缩等级等,压缩等级越小,需要的计算量也就越大,节省的带宽更多。可以配置的参数如下:

gzip:是否开启gzip压缩

Syntax:**gzip** `on` | `off`;
Default:gzip off;
Context:httpserverlocationif in location

gzip_buffers:用来压缩请求的缓冲区大小

Syntax:**gzip_buffers** `_number_` `_size_`;
Default:gzip_buffers 32 4k|16 8k;
Context:httpserverlocation

gzip_comp_level:压缩等级,压缩等级越低,需要的算力越大,压缩后的体积越小,最大为10

Syntax:**gzip_comp_level** `_level_`;
Default:gzip_comp_level 1;
Context:httpserverlocation

gzip_disable:后面的正则表达式将和请求头中的User-Agent字段进行匹配,如果匹配上,就不使用gzip

Syntax:**gzip_disable** `_regex_` ...;
Default:
Context:httpserverlocation

gzip_min_length:启用压缩的最小阈值,当请求体大于该值,则进行压缩

Syntax:**gzip_min_length** `_length_`;
Default:gzip_min_length 20;
Context:httpserverlocation

gzip_proxied:对代理请求是否启用gzip进行配置,如果需要对所有的代理请求进行压缩,则需要配置为any

Syntax:**gzip_proxied** `off` | `expired` | `no-cache` | `no-store` | `private` | `no_last_modified` | `no_etag` | `auth` | `any` ...;
Default:gzip_proxied off;
Context:httpserverlocation

gzip_types:对文件格式进行匹配,如果格式匹配正确,则启用压缩

Syntax:**gzip_types** `_mime-type_` ...;
Default:gzip_types text/html;
Context:httpserverlocation

静态压缩
静态压缩在开启时,会先将所有的静态资源全部保存为.gz格式,然后当客户端请求静态资源时,直接读取.gz文件,发送给客户端。如果启用了sendFile,则该方式会触发sendFile,实现静态资源的零拷贝。官方文档地址:Module ngx_http_gzip_static_module (nginx.org)

gzip静态压缩需要在编译Nginx前将其通过参数--with-http_gzip_static_module配置进去,该模块的可配置参数如下:

gzip_static:启用静态压缩的方式,如果配置为always,Nginx服务器将总是发送给客户端gzip文件,不检查是否客户端支持gzip。

Syntax:**gzip_static** `on` | `off` | `always`;
Default:gzip_static off;
Context:httpserverlocation

gunzip模块
当我们启用静态压缩时,并将gzip_static配置为always时,Nginx将总是向客户端发送gzip压缩后的文件,但是如果客户端不支持gzip压缩算法,此时就会出问题,为了解决这个问题,Nginx提供了gunzip模块,该模块会在客户端不支持gzip压缩算法的时候,将压缩后的问题转换为客户端支持的格式,也就是又将文件解压回来。

gunzip需要在编译前通过配置--with-http_gunzip_module将模块加入到编译中,可通过以下命令进行配置。

gunzip:是否启用gunzip

Syntax:**gunzip** `on` | `off`;
Default:gunzip off;
Context:httpserverlocation

gunzip_buffers:gunzip可用的缓冲区大小

Syntax:**gunzip_buffers** `_number_` `_size_`;
Default:gunzip_buffers 32 4k|16 8k;
Context:httpserverlocation
1.2.8 Brotli

Brotli是google开发的一款新型的压缩算法,它是基于一张非常大的字典来实现的,因为字典中只包含了常见的文本,因此Brotli压缩算法仅针对文本有效。它目前仅支持https协议。

该模块包含两部分,一个是nginx_brotli模块,下载地址为: https://github.com/google/ngx_brotli ,另一个是单独的brotli算法包,下载地址为: https://codeload.github.com/google/brotli/tar.gz/refs/tags/v1.0.9 ,下载好算法包后,需要将算法包的所有内容放入nginx_brotli模块下的deps/brotli 文件夹中。

由于Brotli是第三方模块,我们可用将其添加为动态模块,可采用下面的命令对其进行编译:

$ cd nginx-1.x.x
$ ./configure --with-compat --add-dynamic-module=/path/to/ngx_brotli
$ make

如果采用动态模块的方式将Brotli添加到Nginx,那么我们在nginx.conf中还需要引入该模块,将下面的配置添加到配置文件的最前面:

load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

brotli_static:通gzip_static,如果选择always,将总是将静态资源通过brotli算法进行压缩

  • syntaxbrotli_static on|off|always
  • defaultoff
  • contexthttpserverlocation

brotli:是否启用brotli压缩算法

  • syntaxbrotli on|off
  • defaultoff
  • contexthttpserverlocationif

brotli_types:需要用btrotli进行压缩的文件类型,使用mime_type的格式进行指定

  • syntaxbrotli_types <mime_type> [..]
  • defaulttext/html
  • contexthttpserverlocation

brotli_comp_level:压缩等级

  • syntaxbrotli_comp_level <level>
  • default6
  • contexthttpserverlocation

brotli_window:brotli算法的窗口大小

  • syntaxbrotli_window <size>
  • default512k
  • contexthttpserverlocation

brotli_min_length:brotli开启压缩的阈值

  • syntaxbrotli_min_length <length>
  • default20
  • contexthttpserverlocation
1.2.9 压缩请求——请求合并

当页面引用了很多静态资源时,浏览器会发出多次请求来获取这些资源文件,这样导致的一个问题就是请求数过多,增加了系统的负担。那么有没有一种方式可以减少静态资源文件的请求数量呢?淘宝提供了一个nginx-http-concat模块,它可以将多个文件以流的方式拼接到一起发送给客户端。请求合并的目的是减少客户端的并发量,从而提高系统整体的并发量。

淘宝concat模块
该模块的官方地址:alibaba/nginx-http-concat: A Nginx module for concatenating files in a given context: CSS and JS files usually (github.com)

使用它需要特殊的请求格式,当我们请求多个css文件时,可以在html文件中按如下方式引用:

<link rel="stylesheet" href="http://example.com/??style1.css,style2.css,foo/style3.css">

使用两个"?"表示将后面用逗号分割的css文件拼接起来返回给客户端。

因为该模块为第三方模块,因此需要先给nginx添加该模块,添加方式这里不再赘述,下面对concat进行配置。

concat:在给定的上下文中开启concat
concat on | off
default: concat off
context: http, server, location

concat_types:在给定上下文中设置可使用concat的资源类型
concat_types MIME types
default: concat_types: text/css application/x-javascript
context: http, server, location

concat_unique:如果为on,则表示只能连接相同类型的文件,如果为off,则可以连接不同类型的文件,例如js和css文件
concat_unique on | off
default: concat_unique on
context: http, server, location

concat_delimiter:连接时文件之间的分隔符,默认为空
concat_delimiter: string
default: NONE
contexthttp, server, location

concat_ignore_file_error:是否忽略403或404错误
concat_ignore_file_erroron | off
default: off
contexthttp, server, location

js/css文件
使用concat可以合并对js和css文件的请求。

icon文件
对于icon文件,可以将多个icon放到同一张png图片中,然后在css中设置position来显示指定的icon。

1.2.10 资源静态化

资源静态化是将客户端请求的资源尽可能的做静态化处理,以加速资源传输效率。客户端请求资源时,Nginx中若存在该资源,则直接将资源发送给客户端,如果不存在,Nginx再请求上游服务器,然后利用一些方式将资源做静态化处理,将资源保存在Nginx网关中,当下次客户端访问时,Nginx直接使用 sendFile 将文件传输给客户端,无需再次请求上游服务器,节省了上游服务器的计算资源和降低了请求延迟。

以淘宝为例,淘宝上有很多商品,每个商品都需要展示详情页,关于商品的描述信息部分一般变动不大,这部分可以进行静态化处理,侧边的一些推荐系统展示的信息,可以通过引入的方式,最终用js以异步的方式来更新信息。

一致性问题
将资源静态化后,面临资源的一致性问题,比如通过模板引擎产生的静态页面和后台数据库中的数据存在冲突。为了解决一致性问题,当后台数据库的数据发生变化时,我们需要调用模板引擎或者其他静态化资源的工具去重新生成静态资源以保持资源的一致性。

在进行页面设计时,应该将易变的内容通过异步的方式来加载,在需要静态化的页面上,尽可能少包含变化的信息。

合并文件输出
Nginx的ssi(Server side includes)模块提供了一种在Nginx网关服务器上合并静态文件的方式。对于一个网站来说,有一些页面组件是多个页面共有的,这些组件可以保存到单个文件中,然后通过指定ssi的指令将公有组件引入到当前的页面中。

ssi的官方文档地址:Module ngx_http_ssi_module (nginx.org)

示例如下:

<!--# include file='header.html'-->

...
<div>
...
</div>

<!--# include file='footer.html'-->

上面的例子中,将header.html和footer.html的文件内容合并到了当前的html文件中的指定位置。

ssi的可配置参数如下:
ssi:是否开启ssi

Syntax:**ssi** `on` | `off`;
Default:ssi off;
Context:httpserverlocationif in location

ssi_last_modified:如果开启,则合并后的文件的最后修改时间(取最后的)将被赋值给响应头中的'Last-Modify',客户端可以根据该字段来判断是否更新客户端缓存

Syntax:**ssi_last_modified** `on` | `off`;
Default:ssi_last_modified off;
Context:httpserverlocation

ssi_min_file_chunk:合并后的文件如果大于该值,则会将文件保存下来,后续请求将使用sendFile

Syntax:**ssi_min_file_chunk** `size`;
Default:ssi_min_file_chunk 1k;
Context:httpserverlocation

ssi_silent_errors:解析ssi命令时,发生指令或者参数错误时,是否保持沉默,如果为off,则会在合并后的文件中显示错误信息

Syntax:**ssi_silent_errors** `on` | `off`;
Default:ssi_silent_errors off;
Context:httpserverlocation

ssi_types:ssi支持的文件格式

Syntax:**ssi_types** `_mime-type_` ...;
Default:ssi_types text/html;
Context:httpserverlocation

ssi_value_length:ssi传递的参数的值的最大长度

Syntax:**ssi_value_length** `_length_`;
Default:ssi_value_length 256;
Context:httpserverlocation

ssi的常用的指令:
include:将请求结果(文件或子请求)包含进当前文件
参数:

  • file:用来包含文件,<!--# include file="footer.html" -->
  • virtual:将子请求的结果写入当前文件,由于Nignx请求是异步的,因此一般与wait参数配合使用,<!--# include virtual="/remote/body.php?argument=value" -->
  • wait:等待请求结果,一般与virtual配合使用,<!--# include virtual="/remote/body.php?argument=value" wait="yes" -->

集群文件同步
在这里介绍一种方案——rsync + inotify,rsync用来同步文件,inotify来检测文件夹变化。具体思路是:接收端打开rsync进行监听,推送端通过inotify检测文件夹变化,如果检测到改变,则调用rsync向接收端推送文件。

rsync可以通过yum -y install rsync来安装,配置文件是/etc/rsyncd.conf

安装inotify步骤:

  1. inotify可以从inotify-tools处下载,编译之前先安装依赖包automakeautoconflibtool,同时也需要g++环境(可通过yum install gcc-c++安装)
  2. 先执行inotify-tools文件夹下的./autogen.sh脚本,该脚本会生成必要的配置文件,如configure文件
  3. 执行./configure --prefix=/usr/local/inotify进行编译前的配置
  4. 最后执行make & make install编译安装。安装后的notify目录为/usr/local/inotify/bin/inotify

主节点(推送端)的示例配置如下:

# 待同步的文件夹配置
[ftp]
    path = /usr/local/nginx/html

从节点(接收端)的示例配置如下:

uid = root
gid = root
use chroot = yes
transfer logging = yes
# 接收端必须配置为no,表明可接收推送
read only = no
# 授权的用户名
auth users = zolmk
# 用户密码文件路径
secrets file = /etc/rsyncd.pwd
# 带同步的文件夹配置
[ftp]
        path = /usr/local/nginx/html 

/etc/rsyncd.pwd文件:

zolmk:123456

rsync + inotify自动化同步脚本 auto-sync.sh

#!/bin/bash

/usr/local/inotify/bin/inotifywait -mrq --timefmt '%d/%m/%y %H:%M' --format '%T %w%f %e' -e close_write,modify,delete,create,attrib,move /usr/local/nginx/html/ | while read file
do
	rsync -az --delete --password-file=/etc/rsyncd.pwd.client /usr/local/nginx/html/ zolmk@192.168.1.113::ftp/
done 

/etc/rsyncd.pwd.client文件:

123456

然后通过命令./auto-sync.sh启动脚本,即可开始实时同步。需要注意的是上面的脚本不可用于生产环境,可以让运维同学去做写更专业的同步脚本。

1.3 多级缓存

1.3.1 浏览器缓存

强制缓存
强制缓存的配置与两个关键字有关,分别是:

  • expires:设置过期时间
  • cache-control:缓存控制

在使用强制缓存时,当首次打开浏览器标签页时,将直接从磁盘读取缓存文件,不再向服务器发送请求。

expires的配置规则:

Syntax:**expires** [`modified`] `_time_`;
**expires** `epoch` | `max` | `off`;
Default:expires off;
Context:httpserverlocationif in location

配置示例:

expires    24h;
expires    modified +24h;
expires    @24h;
expires    0;
expires    -1;
expires    epoch;
expires    $expires;

cache-control的配置规则:
cache-control可以通过添加响应头来配置,配置上下文为:http, server, location, if in location可配置的值如下:

ache-directive说明
public所有内容都将被缓存(客户端和代理服务器都可缓存)
private内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
no-cache必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。
no-store所有内容都不会被缓存到缓存或 Internet 临时文件中
must-revalidation/proxy-revalidation如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证
max-age=xxx (xxx is numeric)缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高

配置文件中:

add_header Cache-Control no-cache;

协商缓存
协商缓存的配置也是与两个关键字有关,分别是:
etag:其值与资源名、资源最后修改时间、资源大小等有关,是将这些值哈希得到的
last-modified:资源最后修改时间

etaglast-modified对应http请求头中的If-None-MatchIf-Modified-Since,如果客户端首次请求服务器,服务器会在响应头中写入EtagLast-Modified,当客户端再次请求服务器时,可携带服务器下发的EtagLast-Modified(对应http请求头中的If-None-MatchIf-Modified-Since),如果服务器判断资源未修改,会向客户端返回403响应码(无响应体)。

304状态码:表示浏览器中缓存的资源未改变,不需要服务器再发送。

etag的配置选项:

Syntax:**etag** `on` | `off`;
Default:etag on;
Context:httpserverlocation

需要注意的是,即使关闭了etag,浏览器默认还是会使用协商机制来对缓存进行控制,这时候服务器用来判断资源有无修改的条件就只有Last-Modified

last-modified的配置选项:
一般不需要对其进行配置,该值由Nginx自动写入。

浏览器缓存的注意事项:

  • 多级集群负载时last-modified必须保持一致
  • 一些场景下我们希望禁用浏览器缓存。比如轮训api上报数据
  • 浏览器缓存很难彻底禁用,普遍的做法是加版本号、随机数等方法
  • 只缓存200响应头的数据,像3xx这类的跳转的页面不需要缓存
  • 对于js,css这类可以缓存很久的数据,可以通过加版本号的方式更新内容
  • 不需要强一致性的数据,可以缓存几秒
  • 异步加载的接口数据,可以使用ETa来校验
  • 在服务器添加Server头,有利于排查错误
  • 在没有联网的状态下可以展示数据
  • 避免流量消耗过多
  • 提前下发,避免秒杀时同时下发数据造成流量短时间暴增
  • 兜底数据,在服务器崩溃和网络不可用时展示
  • 临时缓存,退出即清理
  • 固定缓存,展示框架这种,可能很长时间不会更新,可以随客户端一起下发
  • 首页有时候可以看作是框架,应该禁用缓存,以保证加载的资源都是最新的
  • 父子链接,页面跳转时有一部分内容不需要重新加载,可以从父页面带过来
  • 预加载,某些逻辑可以用来判断用户接下来的操作,那么可以异步加载那些资源
  • 优雅的加载过程,异步加载,先展示框架,然后异步加载内容,避免主线程阻塞
1.3.2 CDN缓存

cdn缓存指将常用的静态资源放到第三方cdn云服务器上,加速访问。一般而言,使用第三方提供的cdn服务就可以了,比如阿里、腾讯。cdn服务的开发目前大多数都是通过对Nginx进行扩展,提高Nginx的并发量。

GeoIP2
一个可用通过ip获取地理位置信息的工具包。
依赖工具包地址:maxmind/libmaxminddb: C library for the MaxMind DB file format (github.com)
数据库下载地址:GeoLite2 Free Geolocation Data | MaxMind Developer Portal

对应的nginx模块为ngx_http_geoip2_module,可以在Github上下载。

1.3.3 正向代理缓存

使用Nginx作为正向代理,一般起到跳板机的作用。正向代理的使用场景:当前主机和目标主机在网络上是不可达的,但是当前主机所在网络的某台机器(网关)是可以和目标主机进行网络通信,当前主机可以将代理地址设置为网关,这样它的所有请求将被转发给网关,代理网关实现了类似中转站的功能将请求转发出去,将响应返回给当前主机。

Nginx作为正向代理的缓存配置和作为反向代理的缓存配置类似,见下节。

1.3.4 反向代理缓存

Nginx作为代理网关时的缓存配置文档地址:Module ngx_http_proxy_module (nginx.org)

缓存管理工具——ngx_cache_purge
使用该工具可以很方便地删除单个缓存的资源文件。
使用方法见github里的README。

在Nginx缓存中,是将代理请求返回的结果保存到磁盘文件中,可以设置过期时间,当遇到新请求时,直接将缓存的文件内容发送给客户端。

Nginx也支持对断点续传进行缓存配置。配置详见官方文档。

1.3.5 Nginx内存缓存

Nginx在接收到客户端发来的请求时,将根据请求uri来得到文件的路径,然后根据路径从操作系统获取文件句柄,然后才能得到文件元数据和读取文件内容。如果我们能将文件句柄等信息缓存到内存中,能否加速资源的请求速度呢?答案是肯定的。

Nginx内存缓存是将需要缓存的内容缓存到内存中,这些内容包括:资源文件的元数据信息(包括文件句柄、大小等)、热点资源文件的内容(多进程共享缓存)等。

具体做法将在OpenResty中进行详细说明。

1.3.6 外置内存缓冲

memcached和Redis

1.4.7 上游服务器应用缓存

静态缓存、ehcache、guava cache、缓存中间件

二、高效

2.1 Nginx内置缓存

2.1.1 strace——查看Linux内核执行过程神器

strace是一款能够查看Linux内核执行过程的小工具,在CentOs中可以通过yum install -y strace来安装。它的使用步骤如下:

  • 查看目标进程ps -ef | grep nginx
  • 跟踪目标调用内核的执行过程strace -p nginx_pid,nginx_pid即上面查到的nginx的worker进程的进程id
2.1.2 进一步提升sendfile效率

通过strace得到的sendfile的执行过程如下所示:

epoll_wait(8, [{EPOLLIN, {u32=1002578417, u64=140360533811697}}], 512, 60000) = 1                                 
recvfrom(3, "GET / HTTP/1.1\r\nHost: 192.168.1."..., 1024, 0, NULL, NULL) = 511                                   
stat("/usr/local/nginx/html/index.html", {st_mode=S_IFREG|0644, st_size=1554, ...}) = 0                           
open("/usr/local/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 10                                                
fstat(10, {st_mode=S_IFREG|0644, st_size=1554, ...}) = 0                                                          
writev(3, [{iov_base="HTTP/1.1 200 OK\r\nServer: nginx/1"..., iov_len=239}], 1) = 239                             
sendfile(3, 10, [0] => [1554], 1554)    = 1554                                                                    
write(4, "192.168.1.9 - - [02/Oct/2023:16:"..., 189) = 189                                                        
close(10)                               = 0

从上面可以看到在进行sendfile之前,nginx查询了文件的状态和打开文件获取了句柄,然后才进行sendfile,如果我们将文件的元数据信息在内存中缓存起来,下次就不需要再进行这两步操作了,这样就可以加速sendfile的执行效率。

示例配置如下:

open_file_cache          max=1000 inactive=20s;
open_file_cache_valid    30s;
open_file_cache_min_uses 2;
open_file_cache_errors   on;

open_file_cache:如其名,对打开文件做缓存,配置上下文httpserverlocation

  • max表示最多缓存多少个文件元数据
  • inactive 定义一个时间,如果在此时间内未访问元素,则从缓存中删除该元素;缺省值是60秒

open_file_cache_valid:表示多长时间验证一下文件是否被修改,配置上下文httpserver, `location

open_file_cache_min_uses:表示文件最少被访问多少次才对元数据进行缓存,配置上下文httpserver, `location

open_file_cache_errors:是否开启对文件不存在、无权限访问等错误进行缓存,配置上下文httpserver, `location

2.2 Nginx外置缓存

2.2.1 error_page

error_page用来配置当错误(通过错误码确定错误类型)发生时,应该怎么进行处理。
error_page的官方地址为:Module ngx_http_core_module (nginx.org)

error_page的配置格式为:error_page code ... [=[response]] uri;。配置上下文为http, server, location, if in location

error_page的几种常用配置方式:

  • error_page+匿名location
error_page 404 502 504 = @fallback;
location @fallback {
	proxy_set_header X-For-RemoteIP $remote_addr;
	proxy_pass http://backend;
}
  • error_page+命名location
error_page 404 502 504=200 /error.html;
location = /error.html {
	root html/error;
}
  • error_page+uri
root html;
error_page 404 502 504 /error/error.html;

error_page中=号的作用
起重新设置响应码的作用,用法:=response_code,=号后必须紧接响应码,不能有空格。如果=号后不接响应码,则取默认响应码200。

2.2.2 匿名location

匿名location的写法如下:

location @fallback {
	proxy_pass http://backend;
}

匿名location用@符号+单词组成,在使用时和命名location相同,如下:

error_page 404 @fallback;
2.2.3 nginx + memcached

在nginx中使用memcached时,nginx只能从memcached中读取数据,而不能写入。因此需要上游服务器和nginx配合才能完成缓存任务,即Nginx和上游服务器约定key格式,上游服务器向memcached中写入数据,Nginx从中读取。从上面的描述中可以看出,该方案将缓存的任务交给了nginx和上游服务器,耦合度较高,因此在实际场景中不太实用。

nginx+memcached的官方文档地址:Module ngx_http_memcached_module (nginx.org)

在nginx中使用memcached非常简单,nginx中内置了memcached模块,使用步骤如下:

  1. 选择一台机器安装memcached,可以使用命令安装yum install -y memcached,假设该机器ip为192.168.1.10
  2. 在nginx上配置memcached
server {
	location /api {
		set $memcached_key "$uri?$args";
		# memcached默认的端口为 11211
		memcached_pass 192.168.1.10:11211;
		# 当资源不存在时,进入@fallback
		error_page 404 502 504 = @fallback;
	}
	location @fallback {
		# 从上游服务器获取
		proxy_set_header memcached_key $memcached_key;
		proxy_pass http://backend;
	}
}
  1. 上游服务器进行响应,并将结果根据请求头中的key存入到memcached中。
2.2.4 nginx + redis

该模块是由openresty提供的,可以作为第三方模块添加到nginx中,github地址为:openresty/redis2-nginx-module: Nginx upstream module for the Redis 2.0 protocol (github.com)

nginx使用redis作为外置缓存时,与memcached不同的是,nginx中可以对redis进行读写。因此,这种方式比memcached更加常用,并且redis2-nginx-module提供了更多更强大的功能。

具体的配置详见上述的github主页。

2.3 使用Stream模块代理Mysql集群

企业项目中常用的分布式数据库中间件是Mycat,它提供了高可用、分库分表、跨库查询等众多功能。官方的Mysql本身就提供了主从同步的功能,如果仅需要来实现高可用和负载均衡,则用Nginx代理Mysql集群就可。

在上游服务器和数据库之间加一台Nginx来做负载均衡,主要使用了nginx的stream模块,该模块是nginx的内置模块,要使用该模块,需要在编译前在配置中加入参数--with-stream

示例配置如下:

stream {
	upstream mysql_cluster {
		server 192.168.1.112:3306 weight=8;
		server 192.168.1.113:3306 weight=4;
		server 192.168.1.114:3306 weight=4;
	}
	server {
		listen 3306;
		proxy_pass mysql_cluster;
	}

}

从上面的例子可以看出,nginx可以通过stream来代理所有基于tcp协议的请求。

2.4 Nginx限速

2.4.1 Nginx qps限制

当项目上线后,如果对服务器的qps不做限制,当服务器涌入大量流量时,就有可能导致上游服务器宕机。为了避免这种情况,可以在nginx代理网关上对用户流量做出限制。

在nginx上,可以对接口并发请求量、并发连接数、用户并发请求数、连接速率做出限制,下面展示对Nginx qps做限制时的可配置项。官方配置文档地址:Module ngx_http_limit_req_module (nginx.org)

limit_req_zone:为共享内存区域设置参数,该区域将保留各种键的状态。特别是,状态存储当前的过量请求数。键可以包含文本、变量及其组合。键值为空的请求不被考虑在内。

Syntax:**limit_req_zone** `_key_` `zone`=`_name_`:`_size_` `rate`=`_rate_` [`sync`];
Default:
Context:http

limit_req:设置共享内存区域和请求的最大突发大小。如果请求速率超过为某个区域配置的速率,则会延迟处理请求,以便以定义的速率处理请求。过多的请求被延迟,直到它们的数量超过最大突发大小,在这种情况下,请求以错误终止。缺省情况下,最大突发大小为0。

Syntax:**limit_req** `zone`=`_name_` [`burst`=`_number_`] [`nodelay` | `delay`=`_number_`];
Default:
Context:httpserverlocation

一般情况下,可以使用如下配置qps:

# 用二进制地址来作为限制请求速率的key,设置共享内存区域的名字为one,大小为10m;速率为每秒一个请求
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
	location /search {
		...
		# 限制请求使用的共享内存区域为one,突发请求数为5,不开启延迟,当桶满时,快速失败
		limit_req zone=one burst=5 nodelay;
	}
}

nginx限制请求数模块用的算法是《漏桶算法》,burst的值为桶的容量,当配置了nodelay后,如果桶满,则后续请求会被快速失败。当不配置nodelay并且桶满时,后续请求将被延迟。漏桶算法的速率为limit_req_zone中的rate的值。

2.4.2 传输带宽限制

当用户在下载nginx上的静态资源时,如果文件特别大,若没有对传输速率进行限制,则Nginx会全速传输。为了避免这种情况发生,我们可以在nginx上对单个连接的传输速率进行限制。

Nginx传输带宽限制使用了《令牌桶算法》,持有令牌的连接获得对应的下载速率。

传输带宽限制的可配置项如下所示,官方配置文档地址为:Module ngx_http_core_module (nginx.org)

limit_rate:限制向客户端传输响应的速率。速率默认以字节/秒指定。0值禁用速率限制。该限制是根据每个请求设置的,因此如果客户端同时打开两个连接,则总速率将是指定限制的两倍。

Syntax:**limit_rate** `_rate_`;
Default:limit_rate 0;
Context:httpserverlocationif in location

limit_rate_after:设置初始值,在此值之后,向客户端的响应的进一步传输将受到速率限制。参数值可以包含变量。

Syntax:**limit_rate_after** `_size_`;
Default:limit_rate_after 0;
Context:httpserverlocationif in location

示例配置为:

limit_rate 500k;
limit_rate_after 20m;
2.4.3 并发连接数限制

nginx中可以进一步对并发连接的数量做限制,具有相同key的连接的个数将被限制。如果key是用户级的,那么可以限制用户发起的并发连接数量。

并发连接数限制的配置项如下,官方配置文档地址为:Module ngx_http_limit_conn_module (nginx.org)

limit_conn_zone:为共享内存区域设置参数,该区域将保留各种键的状态。特别是,状态包括当前的连接数。键可以包含文本、变量及其组合。键值为空的请求不被考虑在内。

Syntax:**limit_conn_zone** `_key_` `zone`=`_name_`:`_size_`;
Default:
Context:http

limit_conn:为给定键值设置共享内存区域和允许的最大连接数。当超过此限制时,服务器将在响应请求时返回错误。

Syntax:**limit_conn** `_zone_` `_number_`;
Default:
Context:httpserverlocation

示例配置为:

limit_conn_zone $binary_remote_addr zone=addr:10m;
...
server {
	...
	location /download {
		...
		limit_conn addr 2;
	}
}
2.3.4 图像化测试工具——JMeter

JMeter是apache下开源的图像化的测试工具,在这里可以用来做qps、带宽和并发连接数的测试,JMeter也提供了众多结果展示的方式,如Summary、Tree等。

2.5 Nginx日志

企业系统中的日志在构建用户画像,了解用户行为方面非常有用,常用来做大数据分析、推荐系统等。

2.5.1 access日志

access日志即用户成功访问的日志记录,配置access日志的官方文档地址为:

配置的可选项有:

access_log:配置access日志的保存地址、格式、缓冲区大小、是否压缩、刷新时间等。

Syntax:**access_log** `_path_` [`_format_` [`buffer`=`_size_`] [``gzip[=`_level_`]``] [`flush`=`_time_`] [`if`=`_condition_`]];
**access_log** `off`;
Default:access_log logs/access.log combined;
Context:httpserverlocationif in locationlimit_except
  • path:日志文件地址
  • format:日志格式名称,与log_format中的name对应
  • buffer:设置日志缓冲区大小
  • gzip:是否启用gzip压缩,默认压缩等级为1,可设置1-9
  • flush:间隔多长时间将日志刷入文件

如果开启了gzip压缩,则日志文件就不再是文本格式了,需要使用gzip -d 文件名来将文件解压成文本文件。

log_format:配置日志格式

Syntax:**log_format** `_name_` [`escape`=`default`|`json`|`none`] `_string_` ...;
Default:log_format combined "...";
Context:http
  • name:格式名称
  • escape:配置字符转译格式,详见官方文档
  • string:格式化字符串

open_log_file_cache:已打开的日志文件句柄缓存,默认情况下,nginx写日志时会打开文件,写完关闭文件,如果写入操作特别频繁,就会频繁打开关闭文件,非常耗时。有两个点可以进行优化,一个是配置日志缓冲,另一个是配置日志文件句柄缓存。

Syntax:**open_log_file_cache** `max`=`_N_` [`inactive`=`_time_`] [`min_uses`=`_N_`] [`valid`=`_time_`];
**open_log_file_cache** `off`;
Default:open_log_file_cache off;
  • max:缓存文件句柄的最大数量
  • inactive:如果在此时间内没有访问,则设置缓存描述符关闭的时间;缺省值是10秒
  • min_uses:在最少打开此次数后进行缓存
  • valid:验证文件是否存在的时间

示例配置如下:

log_format compression '$remote_addr - $remote_user [$time_local] '
                       '"$request" $status $bytes_sent '
                       '"$http_referer" "$http_user_agent" "$gzip_ratio"';

access_log /spool/logs/nginx-access.log compression buffer=32k;
2.5.2 error日志

nginx中error日志的格式不能自定义,只能对错误日志的保存路径和错误等级进行配置。
官方配置文档地址:Core functionality (nginx.org)

error_log:配置错误日志的文件地址和日志等级

Syntax:**error_log** `_file_` [`_level_`];
Default:error_log logs/error.log error;
Context:mainhttpmailstreamserverlocation
  • level:可选的值有:debuginfonoticewarnerrorcritalert, or emerg
2.5.3 日志分割

方式:

  • 脚本
  • Logrotate

2.6 上游服务器健康状态

2.6.1 重试机制——被动式健康检查

被动式健康检查通过重试机制判定上游服务器是否宕机。如果上游服务器宕机,则在一段时间内将不再向该上游服务器发送请求。并且可以设置重试次数和超时时间。
官方配置文档地址为:Module ngx_http_proxy_module (nginx.org)

proxy_next_upstreasm:配置在哪种情况下可以请求下一个上游服务器

Syntax:proxy_next_upstream `error` | `timeout` | `invalid_header` | `http_500` | `http_502` | `http_503` | `http_504` | `http_403` | `http_404` | `http_429` | `non_idempotent` | `off` ...;
Default:proxy_next_upstream error timeout;
Context:httpserverlocation

proxy_next_upstream_timeout:限制将请求传递到下一个服务器的时间。0值关闭此限制。如果超过此时间,nginx直接返回失败。

Syntax:proxy_next_upstream_timeout `_time_`;
Default:proxy_next_upstream_timeout 0;
Context:httpserverlocation

proxy_next_upstream_tries:限制将请求传递到下一个服务器的可能尝试次数。0值关闭此限制。

Syntax:**proxy_next_upstream_tries** `_number_`;
Default:proxy_next_upstream_tries 0;
Context:httpserverlocation

server:设置上游服务器地址和可选参数,在此处可以设置失败多少次后认为该上游服务器宕机。

Syntax:**server** `_address_` [`_parameters_`];
Default:
Context:upstream
  • max_fails=_number_:最大失败次数,当在fail_timeout时间内有超过该次数的请求失败,nginx即认为该上游服务器宕机,默认一次。当判定服务器宕机后,经过fail_timeout时间后,会让该服务器再次上线(可以处理服务器短时故障问题)。
  • fail_timeout=_time_:失败超时时间,默认10s。有两次含义,在max_fails参数中有说明。

示例配置为:

upstream backend {
	# 该上游服务器在10s内有超过5次请求失败,判定宕机。
	server 192.168.1.111 max_fails=5 fail_timeout=10s;
	server 192.168.1.112 max_fails=5 fail_timeout=10s;
}
server {
	...
	location / {
		...
		# 当发生错误和超时时,换下一个上游服务器
		proxy_next_upstream error timeout;
		# 一个请求最多在nginx中停留20s,如果20s后还没有从上游服务器获取到响应,则直接返回失败
		proxy_next_upstream_timeout 20s;
		# 一个请求最多重试多少次
		proxy_next_upstream_tries 5;
	}
}
2.6.1 状态检查——主动式健康检查

上面讲的是被动式的健康检查,当用户请求来临时,被动地去请求上游服务器,根据请求结果来判断上游服务器是否宕机。这种方式会导致用户请求延迟变大,可不可以在nginx上有一个单独的线程专门来主动和上游服务器进行交互,判定是否宕机,将用户请求打向在线的上游服务器,这种方式就是主动式健康检查。

nginx开源版没有包含主动健康检查模块,但是商业版是支持的。Tengine中提供了主动式健康检查,在修改nginx源码后,可以用在开源版的nginx上。

使用步骤:
下载模块
nginx_upstream_check_module模块地址:nginx_upstream_check_module

使用补丁文件修改对应版本的nginx源码
需要先下载patch工具,可以用yum install -y patch命令安装。修改源码时要注意版本,先cd到nginx根目录,然后运行patch -p1 < patch_file,如无报错,则修改完成

配置编译安装
在nignx根目录运行./configure --prefix=/usr/local/nginx --add-module=module_dir,然后make & make install

配置nginx_upstream_check_module模块

可配置的项有:
check:需要在upstream中配置,可以配置检查间隔,主要参数如下:

  • 语法:check params=value|...
  • interval:检查间隔,单位毫秒
  • rise:检查成功此次数后,该服务器将被标记为可用
  • fall:检查失败此次数后,该服务器将被标记为不可用
  • timeout:check请求的超时时间
  • port:指定check请求的端口,可以和业务端口不一致,默认使用业务端口
  • type:检查的协议类型,可选:tcp ssl_hello http mysql ajp fastcgi

check_http_send:指定检查http服务器时发送的内容,默认GET / HTTP/1.0\r\n\r\n,一般默认即可,需要在upstream中配置

  • 语法:check_http_send http_packet

check_http_expect_alive:如果检查的类型为http,使用该参数可以指定哪些响应码表示检查成功,默认是http_2xx http_3xx,需要在upstream中配置

  • 语法:`check_http_expect_alive [http_2xx | http_3xx | http_4xx | http_5xx]

check_status:展示健康检查服务状态,可以指定格式,需要在location 中进行配置,通过该配置可以可视化展示上游服务器的状态

  • 语法:check_status [html|csv|json]

示例配置如下:

upstream backend {
	...
	check interval=5000 rise=1 fall=3 timeout=4000 type=http;
	check_http_send "HEAD / HTTP/1.0\r\n\r\n";
	check_http_expect_alive http_2xx http_3xx;
}

server {
	...
	location /status {
		# 返回状态检查结果
		check_status;
		# 关闭该地址的日志记录
		access_log off;
		# 这里还可以配置允许哪些ip请求该地址,哪些ip将被拒绝访问
		# allow someip;
		# deny all;
	}
}

2.7 使用Lua对Nginx二次开发

2.7.1 OpenResty安装与测试Lua代码

最新版本的OpenResty可以通过命令来安装,官方给出的安装教程地址为:OpenResty - OpenResty® Linux Packages。当然,也可以下载源码包进行编译安装,如果需要对OpenResty添加第三方模块,推荐自己编译安装,对OpenResty编译安装和Nginx一致,不再赘述。

OpenResty自身提供的模块可以在这个地址查看:OpenResty - Components

模块食用方法:(以LusNginxMudle为例)

2.7.2 Nginx进程缓存

多进程共享缓存 shared_dict
多进程共享缓存是被多个workers进程共享的缓存,通过锁来保证数据的一致性,所以在性能上有所消耗,但是对于一些全局计数之类的功能很有用。

shared_dict所在的组件(模块)地址为:openresty/lua-nginx-module: Embed the Power of Lua into NGINX HTTP servers (github.com)

使用步骤:

  1. 先对nginx进行配置,在nginx.conf文件中添加下面的内容:
http {
	# 指定共享字典的名称和大小
	lua_shared_dict dogs 10m;
}
  1. 在Lua脚本中使用在配置文件中声明的共享字典
-- 获取在nginx中配置的共享缓存
dogs = ngx.shared.dogs

单进程缓存 lrucache
该缓存模块使用了lru算法,它是纯Lua中的缓存,不涉及nginx。
该模块的官方文档地址为:openresty/lua-resty-lrucache: Lua-land LRU Cache based on LuaJIT FFI (github.com)

使用示例如下:

-- file myapp.lua: example "myapp" module

local _M = {}

-- alternatively: local lrucache = require "resty.lrucache.pureffi"
local lrucache = require "resty.lrucache"

-- we need to initialize the cache on the lua module level so that
-- it can be shared by all the requests served by each nginx worker process:
local c, err = lrucache.new(200)  -- allow up to 200 items in the cache
if not c then
    error("failed to create the cache: " .. (err or "unknown"))
end

function _M.go()
    c:set("dog", 32)
    c:set("cat", 56)
    ngx.say("dog: ", c:get("dog"))
    ngx.say("cat: ", c:get("cat"))

    c:set("dog", { age = 10 }, 0.1)  -- expire in 0.1 sec
    c:delete("dog")

    c:flush_all()  -- flush all the cached data
end

-- 返回该table
return _M

上面的脚本文件返回了一个table,在nginx中,我们可以按下面的方式使用这个脚本文件。


location = /t {
	content_by_lua_block {
		# 请求myapp脚本文件,然后调用该脚本文件返回的go方法
		require("myapp").go()
	}
}

在其他脚本中,可以按如下方式使用该脚本:

local myapp = require "myapp"
myapp.go()

Lua代码热部署

开启后,脚本每次都会重新加载,会产生逻辑错乱。仅限开发阶段打开。配置上下文http, server, location, location if
开启的配置:

lua_code_cache on;
2.7.3 OpenResty连接Redis

这部分内容可以参考官方文档,里面有详细的说明。
连接单台Redis
官方文档所在地址:https://github.com/agentzh/lua-resty-redis
连接Redis集群
可以使用一个第三方模块:steve0511/resty-redis-cluster: Openresty lua client for redis cluster. (github.com)

2.7.4 OpenResty连接Mysql

这部分内容可以参考官方文档,里面有详细的说明。
官方文档地址:https://github.com/agentzh/lua-resty-mysql

2.7.5 模版引擎——这玩意在当下还有存在的意义吗?

OpenResty默认没有模版引擎的包,可以使用的有:bungle/lua-resty-template: Templating Engine (HTML) for Lua and OpenResty. (github.com)
使用方式不在赘述,上面的地址里面有,而且现在前后端分离比较火热,这种模版引擎使用不多。