了解Nginx HTTP代理、负载均衡、缓冲和缓存

前言

本文将讨论Nginx的HTTP代理功能,该功能可以将前端请求转发到后端服务器。规模较大的应用经常会使用Nginx作为反向代理,以处理后端服务器处理不了的请求量。

我们还将讨论Nginx的负载均衡(load balancing)功能,以及如何使用缓冲(buffer)和缓存(cache)提高代理的性能。

有关代理的基本概念

如果你之前只配置过最简单的Web服务,那么最好先了解一下有关代理请求的基本概念。

使用Nginx做代理的目的之一是扩展基础架构的规模。Nginx可以处理大量并发连接,因此很适合作为接待客户端的前台。请求到来后,Nginx可将其转发给任意数量的后台服务器进行处理,这等于将负载分散到整个集群。与此同时,维护的工作也更加灵活,因为你可以随时将一台需要维护的后台服务器抽离集群而不用担心上面还在处理着客户的工作。

使用HTTP代理的另一目的是配合那些无法在生产环境直接处理客户请求的应用服务器。很多框架自带测试用的Web服务器,不过它们只是被设计用于测试,性能没有Nginx那么好。把Nginx放在其前端可以提供更好的用户体验和安全性。

Nginx的代理过程,就是将请求发给Nginx,然后将请求转发给后端服务器,后端服务器处理完毕的结果再发给Nginx,Nginx再把该结果传递给客户端。后端服务器可在远程也可在本地,也可以是Nginx服务器内部定义的其他虚拟机。这些接受Nginx转发的服务器被称为上游(upstream)。

Nginx转发的请求可以是http(s)、FastCGI、SCGI、uwsgi或memcached协议,不同类型的协议通过不同组条目(directive)发送。本文将着重http协议。Nginx实例需要确保其转发的请求和信息使用的格式是上游能够看懂的。

一次基本的HTTP转发

最简单的代理模式,就是Nginx与可以看懂http协议的上游进行配合。此类代理模式统称为“proxy pass”,使用名为proxy_pass的条目进行处理。

proxy_pass条目主要出现在“location”内容块下,也可以在“location”下的if块、以及limit_except内容块内使用。Nginx会检查一个请求是否与“location”下的proxy_pass条目匹配,如果匹配则转发该请求给其中定义的URL。

范例:

# server context

location /match/here {
    proxy_pass http://example.com;
}

. . .

在上述配置文件中,proxy_pass定义的末尾没有指定URI,意味着客户端发送的URI请求会被原样发送给上游。

比如,如果客户端发来的请求是/match/here/please,则发送给example.com服务器的请求会是http://example.com/match/here/please

另一个范例:

# server context

location /match/here {
    proxy_pass http://example.com/new/prefix;
}

. . .

这个范例在proxy_pass末尾定义了URI的部分(/new/prefix),意味着匹配该location定义的请求将在转发时被替换为此处定义的URI。

比如,当一个/match/here/please请求到来时,Nginx转发给上游的请求则是http://example.com/new/prefix/please/match/here被替换成了/new/prefix,这点需要注意。

有时候,此类替换无法完成,则proxy_pass处定义的URI会被忽略,Nginx会把原始请求的URI或其他地方定义的URI发送给上游。

比如,对于一个使用正则表达式定义的location,Nginx无法决定哪部分URI匹配该表达式,所以会把原始请求的URI发送给上游。而如果在同一location下还定义了一个rewrite条目改写了该请求的URI,则改写的URI会被发送给上游。

Nginx处理头部(header)的原理

需要澄清的一点是,如果你期待上游对你的请求进行正确处理,则需要给它传递URI以外的其他信息。来自Nginx的请求和直接来自客户端的请求看起来是不同的,其中一个很大的不同就是header。

Nginx在处理请求时会自动对信息进行如下调整:

  • 清理空header。将空值传递给上游是没有意义的,只会增加上游的负担。
  • Nginx默认将带有下划线(_)的header视为无效,并将其从转发的请求中移除。如果你不希望如此,可以在Nginx设置中将underscores_in_headers条目改为“on”。
  • 把“Host” header根据$proxy_host变量的值进行重写,新的值是proxy_pass中定义的上游IP地址或域名。
  • “Connection” header变更为“close”。这个header用于表示双方连接的信息。在我们的场景下,Nginx与上游的连接在给客户返回结果之后就会关闭,因此将其设置为close,即不用维持该连接。

如上所述,我们可以把不想转发的header都设置为空,这样可以被Nginx过滤掉。

此外,如果后端应用需要处理非标准header,则我们要么需要确保这些非标准header中不包含下划线,要么在Nginx设置中把underscores_in_headers条目开启(该条目写在http部分下或者默认服务器声明IP端口的部分下均可)。否则,这些header会被Nginx丢弃。

“Host” header对于大部分代理场景而言是最重要的一部分。$proxy_host的值来自proxy_pass中定义的IP地址和端口,这是确保能接收到上游服务器返回结果的地址,相比请求中原本的地址而言要更加靠谱。

“Host” header的常用值有下面这些:

  • $proxy_host:即proxy_pass中定义的域名/IP地址和端口。这是Nginx的默认设置,被视为安全选项,不过往往不是我们所需要的。
  • $http_host:即客户端请求中的原始“Host” header。客户端发送的header在Nginx中都是可以获取的变量,变量名称以$http_前缀开始,然后是小写的header名称,其中所有的横杠都被替换为下划线。这个值一般是没有问题的,但如果客户端请求中没有有效的“Host” header,则该转发会失败。
  • $host:以下三个值按顺序选取:A)客户端请求内容中的host name,B)客户端请求中的“Host” header,C)匹配该请求的服务器名。

一般我们都把“Host” header设置为$host,这样灵活性最好,也相对准确。

Header的设置与重设

Header的设置可以通过proxy_set_header条目来操作。比如,如果我们想把“Host” header按上面说的内容变更,再添加一些代理常用的header,则可以写入如下内容:

# server context

location /match/here {
    proxy_set_header HOST $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_pass http://example.com/new/prefix;
}

. . .

如上,我们将“Host” header设置为$host变量,其中包含了原始的请求主机信息。X-Forwarded-Proto header为代理服务器提供了原始请求的schema信息(http或https请求)。

X-Real-IP设置为客户端的IP地址,这样有助于代理服务器进行正确的判断和记录。X-Forwarded-For header是一个包含了该客户已被代理过的所有代理服务器的IP地址,我们设置为$proxy_add_x_forwarded_for变量,该值等于原始请求中的X-Forwarded-For header值再在末尾加上本Nginx服务器的IP地址。

我们也可以将proxy_set_header条目移至server内容块或http内容块,这样可以对多个location通用:

# server context

proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_Header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

location /match/here {
    proxy_pass http://example.com/new/prefix;
}

location /different/match {
    proxy_pass http://example.com;
}

为负载均衡后的连接定义上游

至此,我们已经演示了如何将简单的http请求代理到一个单独的后端服务器。接下来我们要将该配置扩展至多台服务器,这在Nginx下很容易实现。

服务器资源池的定义可以通过upstream条目完成。该配置假设此处列出的服务器中的任意一台都可以处理客户端请求。这个操作完成起来相当简单,只是要注意upstream条目必须在http内容块中定义。

一个简单的示范:

# http context

upstream backend_hosts {
    server host1.example.com;
    server host2.example.com;
    server host3.example.com;
}

server {
    listen 80;
    server_name example.com;

    location /proxy-me {
        proxy_pass http://backend_hosts;
    }
}

如上,我们定义了一个叫做backend_hosts的上游列表,该命名在代理服务器中可以当作一个标准域名来使用。在server内容块中,我们将所有指向example.com/proxy-me/...的请求都转发给了上面那个资源池,至于具体选择池子内的哪一台主机,则取决于一个配置算法。默认的算法是一个简单的round-robin选择过程(每一个新的请求都被路由至一台不同的主机)。

更改负载均衡算法

我们可以在upstream内容块中使用如下条目或标识(flag)以更改负载均衡算法:

  • (round robin):默认生效的算法,队列中的服务器按次序接收请求。
  • least_conn:新请求被发送给当前活动连接数最少的服务器。对于长连接较多的场景,这个算法比较合适。
  • ip_hash:根据客户端的IP地址分发请求。IP地址中的前三位作为决定分发目标的键值。其结果是来自同一个客户端的请求更容易被同一台服务器处理,这对于需要会话一致性的场景比较合适。
  • hash:主要用于memcached代理。服务器们按照一个用户指定的哈希键值被分组,该键值可以是纯文本、变量或两者的组合。这是唯一一个需要用户提供数据的负载均衡算法。

指定负载均衡算法的写法如下:

# http context

upstream backend_hosts {

    least_conn;

    server host1.example.com;
    server host2.example.com;
    server host3.example.com;
}

. . .

以上示范启用了least_conn算法。同样的格式也可用于ip_hash算法的设置。

对于hash算法,我们需要额外提供一个键值(内容随意):

# http context

upstream backend_hosts {

    hash $remote_addr$remote_port consistent;

    server host1.example.com;
    server host2.example.com;
    server host3.example.com;
}

. . .

上述范例将依据客户端IP和端口进行请求分发。我们还添加了一个可选参数consistent,这代表ketama持久哈希算法。这个算法的好处是,上游服务器的变动不会对缓存造成太大影响。

设置负载均衡的配重

以上声明的后端服务器默认是均等配重,即假设每台服务器有相同的处理能力(配合负载均衡算法后)。不过我们也可以在声明时手动定义配重,如:

# http context

upstream backend_hosts {
    server host1.example.com weight=3;
    server host2.example.com;
    server host3.example.com;
}

. . .

以上设置将host1.example.com的负载设置为另外两台服务器的3倍。默认的配重值是1。

用缓冲解放后端

很多用户担心添加服务器会对代理性能造成影响。大部分情况下,Nginx的缓冲(buffer)和缓存(cache)机制能够很好的规避此类性能问题。

代理过程中,有两个连接的速度会影响客户端体验:

  • 从客户端到Nginx的连接
  • 从Nginx到后端的连接

Nginx可以优化连接,不同的优化方案有不同的结果。

没有缓冲的情况下,数据直接从后端服务器发送给客户端。如果客户端的连接速度快,则可以关闭缓冲以提高数据发送速度。缓冲的作用是在Nginx上临时存储来自后端服务器的处理结果,从而可以提早关闭Nginx到后端的连接,这比较适合客户端连接较慢的情况。

Nginx默认启用缓冲,因为客户端的连接速度一般来说是差别很大的。缓冲的具体配置可以通过如下条目修改,这些条目可以放在http 
server或location内容块下。需要注意的是,涉及缓冲大小的条目是针对请求配置的,如果设置的比较高,则请求数很多的时候容易造成性能问题:

  • proxy_buffering:控制本内容块下(包括子内容块)是否启用缓冲,默认为“on”。
  • proxy_buffers:有两个参数,第一个控制缓冲区请求数量,第二个控制缓冲区大小。默认值为8个、一页(一般是4k8k)。这个值越大,缓冲的内容越多。
  • proxy_buffer_size:后端回复结果的首段(包含header的部分)是单独缓冲的,本条目定义这部分的缓冲区大小。这个值默认与proxy_buffer的值相同,我们可以把它设置的小一些,因为header内容一般比较少。
  • proxy_busy_buffers_size:设置被标记为“client-ready”(客户端就绪)的缓冲区大小。客户端一次只能从一个缓冲读取数据,而缓冲是按照队列次序被分批发送给客户端的。本条目设置的值就是这个队列的大小。
  • proxy_max_temp_file_size:每个请求可以存储临时文件的最大大小。如果上游发来的结果太大以至于无法放入一个缓冲,则Nginx会为其创建临时文件。
  • proxy_temp_path:定义Nginx存储临时文件的路径。

如上所述,Nginx提供了不少关于缓冲的配置项。大部分配置项我们都不必太在意,可能就是proxy_buffersproxy_buffer_size值得调整一下。

下面这个示范增加了每个上游请求可用的缓冲,同时缩减了header部分缓冲的大小:

# server context

proxy_buffering on;
proxy_buffer_size 1k;
proxy_buffers 24 4k;
proxy_busy_buffers_size 8k;
proxy_max_temp_file_size 2048m;
proxy_temp_file_write_size 32k;

location / {
    proxy_pass http://example.com;
}

如果你的客户端连接都很快,也可以像下面这样完全关闭缓冲。实际上Nginx还是会在上游比客户端快的时候使用缓冲,只是缓冲的内容会直接刷给客户端而不是放到缓冲池里等待。如果客户端的速度慢,则这样会导致上游连接一直保持。缓冲功能禁用时,只有proxy_buffer_size配置项可用:

# server context

proxy_buffering off;
proxy_buffer_size 4k;

location / {
    proxy_pass http://example.com;
}

高可用(可选项)

我们可以增加多个Nginx代理,以进一步提高整个集群的可用性。

一个高可用集群(HA)是没有单点故障的,即负载均衡也不存在单点。配置一个以上的负载均衡服务器提高了集群可用性,因为服务不会因为那一个负载均衡服务故障而不可用。

下图展示了一个简单的高可用设计:

HA Setup

这里,我们在一个静态IP地址后配置了多个负载均衡(一主一从或一主多从),该IP地址可以被映射到不同服务器上。客户端请求被主负载均衡器上转发给后端。有关HA的更详细说明,可以参考这篇介绍浮动IP的文章

配置代理缓存以缩短响应时间

缓冲的作用是快速解放后端,好让它们能够处理更多请求。缓存的作用则是把能直接给客户端的直接给客户端,尽可能的不去打扰后端。

代理缓存的配置

缓存的设置通过proxy_cache_path完成,这将创建一个用于保存结果数据的存储区。proxy_cache_path必须在http内容块下创建。

以下是示范:

# http context

proxy_cache_path /var/lib/nginx/cache levels=1:2 keys_zone=backcache:8m max_size=50m;
proxy_cache_key "$scheme$request_method$host$request_uri$is_args$args";
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;

我们在proxy_cache_path里定义了存储路径/var/lib/nginx/cache。如果该路径不存在,可以执行如下命令创建并赋予其正确权限:

sudo mkdir -p /var/lib/nginx/cache
sudo chown www-data /var/lib/nginx/cache
sudo chmod 700 /var/lib/nginx/cache

levels=参数定义了缓存区的分类方法。根据上面的设置,Nginx会基于一个键值(在下面有定义)哈希生成一个缓存键(cache key),该键的最后一个字母作为一级目录名,倒数第二、三个字母作为二级目录名。这主要是方便Nginx快速索引内容,我们不需要太关注。

keys_zone=参数定义了缓冲区的命名,我们在此命名为backcache。我们还在这里定义了要存储的元数据大小,即8 MB的键值。每个MB能够存储差不多8000条内容。max_size参数设置了实际缓存数据的大小。

我们还用到了一个proxy_cache_key条目,该条目定义了一个键值,即上述用于区分目录用的键值。通过该键值,Nginx可以判断一个客户端请求需要的内容是否可以直接从缓存里找给它。对于这个键值,我们用scheme(http或https)、HTTP请求方式、host和URI的组合来创建。

proxy_cache_valid条目可以多次设置,用于配置缓存保存的时间,时间长短取决于状态码(status code)。我们在这里为success(200 成功)和redirect(302 转发)保存10分钟的缓存,并且每分钟将404的缓存清理过期。

以上,我们完成了缓存区的配置,就差告诉Nginx启用缓存了。

在location下输入如下内容启用该缓存:

# server context

location /proxy-me {
    proxy_cache backcache;
    proxy_cache_bypass $http_cache_control;
    add_header X-Proxy-Cache $upstream_cache_status;

    proxy_pass http://backend;
}

. . .

我们在proxy_cache条目下写入了backcache,即为本location启用刚才定义的那个缓冲区。Nginx会先在这个缓冲区检查是否有客户端需要的内容,如果没有才把请求发给后端。

proxy_cache_bypass条目在这里设置为$http_cache_control变量,该变量包含一个指示符,说明客户端请求是否明确要求返回一个非缓存的“新鲜”结果。如果是,则Nginx不会从缓存区找回复给客户端,而是按照客户端的要求,从后端取新回复。这里不需要做其他的设置。

我们还添加了一个新的header X-Proxy-Cache,将其设置为$upstream_cache_status。简单来说,这个变量显示了一个请求是命中了缓存、没命中缓存、还是被指定不使用缓存。这对于debug而言比较有用,对于客户端也有一些价值。

有关缓存结果的说明

缓存可以大幅提升代理性能,不过配置缓存的时候需要注意以下几点。

首先,任何用户相关的数据不应该被缓存,否则可能导致一个用户的信息被另一个用户看到。当然如果你的网站是纯静态的,那就没有问题。

如果你的网站包含一些动态元素,则这点就需要留意。你如何处理这些元素的缓存,取决于应用处理后端进程的方式。对于私人信息,可以根据情况将Cache-Control header设置为no-cache、no-store或private:

  • no-cache:请求应先到后端检查是否有变更,然后才返回给客户端。适用于动态的、重要的内容。对于每个请求,检查其ETag哈希元数据header,只有在后端返回了完全一致的哈希值的情况下才把缓存区内容返回给客户端。
  • no-store:完全不在缓存区保存本内容。对于私人数据而言这是最安全的,这意味着每次都要从后端获取。
  • private:不使用共享缓存。这意味着用户的浏览器可以缓存该数据,但代理服务器不可以。
  • public:公开数据,可以随意缓存。

有一个max-age header可以控制此类缓存的保存时间,默认以秒计算。

正确的配置这些header,可以在确保私人信息安全、动态信息实时的前提下,还能享受到缓存的好处。

如果你的后端也使用Nginx,还可以使用expires条目,这会定义Cache-Control中的max-age

location / {
    expires 60m;
}

location /check-me {
    expires -1;
}

以上,第一个location下启用了一小时的缓存,第二个location下则将Cache-Control header设置为no-cache。我们还可以使用add_header条目设置其他的值,比如这样:

location /private {
    expires -1;
    add_header Cache-Control "no-store";
}

总结

Nginx既可以用作代理/反向代理,又可以作为Web服务器来使用,这样的设计使得我们很容易用它来配合其他后端服务器一起使用。由于Nginx非常灵活,我们可以用它来配置更加复杂的代理。取决于不同的需求,它的玩法实在太多,还有待我们一一发掘。

猜你喜欢

转载自blog.csdn.net/hemin1003/article/details/77196187