跳转至

Nginx服务之代理和负载

缺点分析

Nginx 社区版的缺点**(反向代理和负载均衡功能上)

Session 会话持久保持

Nginx官方给出的解决方案是使用ip_hash调度算法,根据访问客户端的源IP地址进行请求分发调度到后端服务器的。表面上看起来没有什么问题,但是实际应用起来就会发现几乎不可行。 比如为学校做一个选课系统,服务器放在服务网段,学生在普通网段,两个网段互通全靠NAT,Nginx收到的IP全都是同一个源IP,那么这个负载均衡没有就没有生效。

后端服务器的健康监测

Nginx官方给出的解决方案是被动监测的方式。 如下例所示,意思就是如果 9191 这个端口某次连接超时或者失败次数超过了 max_fails 次,就会被判定为服务不可用,然后等待 fail_timeout 这么长时间之后自动认为其可用,不管到底真的修复了没有。然后再次重试 max_fails 次。 这还不算最坑爹的,更麻烦的是在 Nginx 上你无法查看各个后端节点的状态,不知道哪台宕机了或者卡了不好用了。如果你后端有一百台被负载均衡的服务器,光检查可用性这一点 Nginx 社区版就完败。

替代方案

如何选择负载均衡器呢(一般来说)

1、基于Cookie的会话保持
2、监控后台服务器的负载状态和健康状况,主动进行健康检查
3、支持以最小连接数为基准的负载均衡分发策略

对于 HAProxy 来说

上述功能全部都可以实现,而且全部是官方支持;
不需打补丁,直接在yum源上安装就可以全部实现;
据网上的资料,HAProxy的并发数还要高于NGINX;
HAProxy还可以支持TCP连接的负载均衡,这一点是NGINX社区版和plus在1.9版本之后才实现的功能,而Tengine由于基础版本才1.8.1所以并不支持;

对于 Tengine 来说

1、2、3 也可以支持,但是 1、3 不能同时支持 后台监控页面比较简陋,看不到具体每台节点的负载情况 也就是说如果使用Cookie作为会话保持策略,那就不能再以最少连接数作为分发策略了,只能使用轮训(round_robin) ,这样的话很容易出现某些服务器占用超级高,然后其他服务器几乎没有负载的糟糕情况

1. 反向代理

ngx_http_proxy_module模块提供

1.1 使用格式

# 【基本使用方式】
# /uri --> /newuri
# 使用newuri替换原有的uri请求地址
location /uri {
    proxy_pass http://back_server[:port]/newuri;
}

# 【模式匹配特例】
location ~* /api/.* {
  proxy_pass http://api.example.com;
}
~* 运算符用于匹配正则表达式模式。
.* 匹配任何字符的任意数量。
请求 http://example.com/api/users 将被代理到 http://api.example.com/users

# 【URL重定向特例】
location /api {
  rewrite /api/(.*) /\$1;
  proxy_pass http://api.example.com/\$1;
}
请求 http://example.com/api/users 将被重写为 http://example.com/users。
然后,请求将被代理到 http://api.example.com/users。
1
2
3
4
5
6
7
8
# 通过proxy_set_header设置自定义变量给后端服务器使用
# $host表示客户端请求的主机名,用于虚拟主机中使用
# $remote_addr表示客户端IP地址,X-Real-IP用于后端服务器记录日志
location / {
    proxy_set_header  Host      $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_pass        http://localhost:8000;
}
1
2
3
4
5
6
7
8
9
# 设置真实scheme协议
map $http_x_scheme  $real_http_x_scheme {
    default $http_x_scheme;
    "" $scheme;
}
如果请求包含 X-Scheme 头部,则 $real_http_x_scheme 变量将包含该头部的值。
如果请求不包含 X-Scheme 头部,则 $real_http_x_scheme 变量将包含 $scheme 变量的值,该变量通常设置为请求的实际协议(例如,http 或 https)。
default 关键字指定默认映射规则。
"" 匹配空字符串。

1.2 常用指令

代理服务器本身存在缓存,用户请求之后立即返回,所有让第二阶段(代理服务器和后端服务器之间的连接方式)一直保持长连接其实没有必要
可以根据业务情况让其使用短连接方式通信,提高效率。

# 定义传递给后端服务器的Host变量
proxy_set_header Host       $host;

# 此外,服务器名可以和后端服务器的端口一起传送
proxy_set_header Host       $host:$proxy_port;

# 如果某个请求头的值为空,那么这个请求头将不会传送给后端服务器
proxy_set_header Accept-Encoding "";

proxy_http_version 1.1;

# HTTP的1.1版本推荐在使用keepalive连接时一起使用


proxy_limit_rate 100k;

速率限制   基于每个客户端 IP 地址的 0不限制
k:千字节/秒
m:兆字节/秒
g:千兆字节/秒

proxy_method get;

只支持转发的HTTP方法是 GET。
POST,PUT,DELETE,HEAD,OPTIONS,CONNECT,TRACE,PATCH
需要将 proxy_method 设置为 POST 或 all

proxy_hide_header Date;
proxy_hide_header Server;

隐藏功能
Date 头字段是指定响应产生的日期和时间的标准 HTTP 标头,它在标识资源更新时间、缓存控制等方面起着重要的作用。
Server 头字段包含了服务器软件的信息,例如 Nginx/1.21.1,这可能会暴露服务器端使用的具体技术和版本信息,对安全性有一定的影响。

proxy_connect_timeout 5s;
对于上传大文件之类的有帮助

proxy_buffers 8 4k

1.3 缓存功能

img

参数 说明
语法格式 proxy_cache_path path [levels=levels] keys_zone=name:size [inactive=time] [max_size=size] [loader_files=number] [loader_sleep=time] [loader_threshold=time];
含义解释 用于设置缓存的路径和配置,需要先设置才能使用,参数十分多,很多的参数都可以使用默认值就好了,该指令只能使用在http中,可以设置多个不同名的缓存样式
作用范围 http
缓存格式 缓存数据是保存在文件中的,缓存的键和文件名都是在代理URL上执行MD5的结果。
工作方式 被缓存的响应首先写入一个临时文件,然后进行重命名。从0.8.9版本开始,临时文件和缓存可以放在不同的文件系统。但请注意,这将导致文件在这两个文件系统中进行拷贝,而不是廉价的重命名操作。因此,针对任何路径,都建议将缓存和proxy_temp_path指令设置的临时文件目录放在同一文件系统。
参数理解 所有有效的键和缓存数据相关的信息都被存放在共享内存中。共享内存通过keys_zone参数的name和size来定义。被缓存的数据如果在inactive参数指定的时间内未被访问,就会被从缓存中移除,不论它是否是刚产生的。inactive的默认值是10分钟。特殊进程cache manager监控缓存的条目数量,如果超过max_size参数设置的最大值,使用LRU算法移除缓存数据。nginx启动之后,特殊进程cache loader就被启动。该进程将文件系统上保存的过去缓存的数据的相关信息重新加载到共享内存。加载过程分多次迭代完成,每次迭代,进程只加载不多于loader_files参数指定的文件数量(默认值为100)。此外,每次迭代过程的持续时间不能超过loader_threshold参数的值(默认200毫秒)。每次迭代之间,nginx的暂停时间由loader_sleep参数指定(默认50毫秒)。

proxy_cache_path

1
2
3
4
5
6
7
8
# path设置缓存存储的物理路径位置
# levels设置缓存的路径级别和命名大小
# keys_zone设置缓存的名称和大小
# levels=1:2表示有两层,第一层目录使用一位数,第二层目录使用两位数
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=one:10m;

# 缓存中文件名看起来是这样的
/data/nginx/cache/c/29/b7f54b2df7773722d382f4809d65029c
http {
    include       /etc/nginx/mine.types;
    default_type  application/octet-stream;
    sendfile      on;
    proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=mycache:32m;
    ...

    server {
        listen       80;
        server_name  localhost;
        ...

        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }

        location /bbs/ {
            proxy_cache            mycache;
            proxy_cache_valid      200 1d;
            proxy_cache_valid      301 302 10m;
            proxy_cache_valid      any 1m;
            proxy_cache_use_stale  error timeout invalid_header http_500;
            proxy_set_header       Host      $host;
            proxy_set_header       X-Real-IP $remote_addr;
            proxy_pass             http://192.168.100.2/;
        }

        location ~* \.(jpg|png|gif)$ {
            proxy_cache       mycache;
            proxy_set_header  X-Real-IP $remote_addr;
            proxy_pass        http://192.168.100.2/;
        }
    }
}
proxy_cache_path /data/nginx/cache keys_zone=cache_zone:10m;

map $request_method $purge_method {
    PURGE   1;
    default 0;
}

server {
    ...
    location / {
         # 如果客户端请求方法是列在这个指令的响应才被缓存
         proxy_cache_methods GET HEAD;

         # 请求对于的响应的数量达到10次之后,对于的响应内容才会被缓存起来
         proxy_cache_min_uses 10;
         proxy_pass http://backend;
         proxy_cache cache_zone;
         proxy_cache_key $uri;
         proxy_cache_purge $purge_method;
         缓存修剪:将特定请求视为一个缓存清除请求

         如果缓存没有过期但后端已经变动了,用户请求将返回缓存中的响应信息,所有这个时候就需要缓存修剪操作。
         只要用户请求就删除对于请求的响应缓存数据,就会返回后端变动的响应信息

    }
}
# 需要ngx_cache_purge模块来在过期时间未到之前,手动清理缓存
# 清除缓存的防范:(1)直接删除缓存目录;(2)GET方式请求URL
http {
    ... // $upstream_cache_status记录缓存命中率
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"'
                      '"$upstream_cache_status"';
    proxy_temp_path   /usr/local/nginx-1.6/proxy_temp;
    proxy_cache_path /usr/local/nginx-1.6/proxy_cache levels=1:2 keys_zone=cache_one:100m inactive=2d max_size=2g;

    server {
        listen       80;
        server_name  ittest.example.com;
        root   html;
        index  index.html index.htm index.jsp;

        location ~ .*\.(gif|jpg|png|html|css|js|ico|swf|pdf)(.*) {
            proxy_pass  http://backend;
            proxy_set_header   Host $host;
            proxy_set_header   X-Real-IP   $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_cache cache_one;
            # 定义cache_key格式
            proxy_cache_valid  200 304 301 302 8h;
            proxy_cache_valid  404 1m;
            proxy_cache_valid  any 2d;
            proxy_cache_revalidate on;
            proxy_cache_use_stale http_404;
            proxy_cache_valid 200 302 10m;
            proxy_cache_valid 301      1h;
            proxy_cache_valid any      1m;
            proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment;
            proxy_cache_bypass $http_pragma    $http_authorization;
            proxy_cache_key    $host$uri$is_args$args;
            add_header Nginx-Cache $upstream_cache_status;
            expires 30d;
        }

        location ~ /purge(/.*) {
            # 设置只允许指定的IP或IP段才可以清除URL缓存
            allow   127.0.0.1;
            allow   172.29.73.0/24;
            deny    all;
            proxy_cache_purge cache_one $host$1$is_args$args;
            error_page 405 =200 /purge$1;
        }
    }
}

1.4 加密通信

ngx_http_ssl_module模块提供HTTPS的支持,该模块默认没有配置,需要在编译的使用--with-http_ssl_module参数开启(需要OpenSSL库的支持)。

# 减少处理器负载建议
# 1. 设置工作进程的数量等于处理器的数量
# 2. 开启keep-alive连接支持
# 3. 启用共享会话缓存,即所有工作进程之间共享一个缓存
# 4. 禁用内置的会话缓存,即禁止缓存只使用一个工作进程
# 5. 提高session会话的重用时长(ssl_session_timeout参数)

worker_processes auto;
http {
    ...
    server {
        listen              443 ssl;
        keepalive_timeout   70;
        # 客户端与服务器在单个 TCP 连接上发送和接收多个 HTTP 请求和响应.
        # 不必为每个请求都建立一个新的连接。这可以减少连接建立和关闭的开销,从而提高性能和减少延迟
        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers         AES128-SHA:AES256-SHA:RC4-SHA:DES-CBC3-SHA:RC4-MD5;
        ssl_certificate     /usr/local/nginx/conf/cert.pem;
        ssl_certificate_key /usr/local/nginx/conf/cert.key;
        ssl_session_cache   shared:SSL:10m;
        ssl_session_timeout 10m;
        ...
    }
}

2. 负载均衡

# nginx的商用软件支持健康监测功能
upstream backend {
   server backend1.example.com weight=5;:
   指定具有权重为 5 的backend1.example.com服务器,权重越高,处理请求的概率就越大。

   server backend2.example.com:8080;
   定义了端口为 8080 的 backend2.example.com 服务器作为第二个后端服务器。

   server unix:/tmp/backend3;
   使用 Unix 域套接字连接到 /tmp/backend3 的服务器,实现与本地服务器的通信。

   server backup1.example.com:8080 backup;
   指定 backup1.example.com:8080 作为备用服务器(backup),仅当其他服务器不可用时才会使用。

   server backup2.example.com:8080 backup;
   另一个备用服务器定义,同样是作为备份服务器备用。
}

server {
    location / {
        proxy_pass http://backend;
    }
}

2.1 常用指令

【1】upstream

# 使用默认带权重的RR算法,在上面的例子中,每7个请求将被如下分配
# 5个请求去backend1.example.com
# 1个请求去127.0.0.1:8080
# 1个请求去unix:/tmp/backend3
upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080       max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;
    server backup1.example.com  backup;
}

【2】server

参数格式 含义解释和说明
weight=number 设定服务器的权重,默认值为1
max_fails=number 定义最大错误次数;设定Nginx与服务器通信的尝试失败的次数;失败的尝试次数默认值为1,设为0就会停止统计尝试次数,认为服务器是一直可用的;默认配置时,http_404状态不被认为是失败的尝试,可以通过修改参数进行修改
fail_timeout=time 定义超时是时长;在这段时间中,服务器失败次数达到指定的尝试次数,服务器就被认为不可用;默认情况下,该超时时间是10
backup 标记为备用服务器;当主服务器不可用以后,请求会被传给这些服务器
down 标记服务器永久不可用;可以跟ip_hash指令一起使用
max_conns=number 商用版本才有;设置代理服务器的最大的活动连接数;默认值为0表示不受限制
1
2
3
4
5
6
upstream backend {
    server unix:/tmp/backend3;
    server backend1.example.com  weight=5;
    server 127.0.0.1:8080        max_fails=3 fail_timeout=30s;
    server backup1.example.com:8080 backup;
}

2.2 会话保持

sticky**是nginx的一个模块,它是基于cookie的一种nginx的负载均衡解决方案
通过分发和识别cookie,来使同一个客户端的请求落在同一台服务器上。

【1】ip_hash

1
2
3
4
5
6
7
8
# 如果其中一个服务器想暂时移除,应该加上down参数,这样可以保留当前客户端IP地址散列分布
upstream backend {
    ip_hash;
    server backend1.example.com;
    server backend2.example.com;
    server backend3.example.com down;
    server backend4.example.com;
}
现在大量用户都是使用SNAT进行上网的,多用户使用同一个IP
如果调度的时候根据cookie进行调度,颗粒度就比较好了。
如果客户端请求到达Nginx时,Nginx不做IP地址绑定而是做cookie绑定,即保证了会话绑定和力度更大(sticky)。

【2】sticky

语法格式  调度的时候根据cookie进行调度 在 upstream{}里面使用   
sticky cookie name [expires=time] [domain=domain] [httponly] [secure] [path=path];
sticky route $variable ...;
sticky learn create=$variable lookup=$variable zone=name:size [timeout=time] [header];



# 【可用方法一】
# 当使用cookie的方法时,由nginx传入cookie信息给后端服务器。
# 新请求进来时将被分派到配置的均衡算法选择的服务器,后续带有cookie的请求将被分派给指派的服务器。

# name: 可以为任何的字符,默认是route,这里是srv_id
# expires: 设置浏览器的cookie过期时间,默认浏览器关闭就过期
# domain: 哪些域名下可以使用这个cookie
# httponly: 添加HttpOnly属性到cookie
# secure: 添加Secure属性到cookie
# path: 对哪些路径启用sticky模式,这里的/代表整个网站
upstream backend {
    server backend1.example.com;
    server backend2.example.com;
    sticky cookie srv_id expires=1h domain=.example.com path=/;
}
# 【可用方法二】
# 当使用route的方式时,用户的第一次请求根据调度发送给后端服务器
# 用户之后的请求URI中将携带route标记的信息,用于负载均衡调度。

# 将$cookie_jsessionid标记为$route_cookie
map $cookie_jsessionid $route_cookie {
    ~.+\.(?P<route>\w+)$ $route;
}

# 将$request_uri标记为$route_uri
map $request_uri $route_uri {
    ~jsessionid=.+\.(?P<route>\w+)$ $route;
}

upstream backend {
    server backend1.example.com route=a;
    server backend2.example.com route=b;
    sticky route $route_cookie $route_uri;
}
# 【可用方法三】
# 当使用learn的方式时
# nginx分析upstream服务器的响应并学习通常在cookie中传递的server-initiated会话

# 在这个例子中,upstream服务器通过在应答中设置cookie的"EXAMPLECOOKIE"创建会话。
# 带有这个cookie的后续请求将被分派到同一个服务器。
# 如果服务器不能处理请求,新的服务器将被选择,就如同客户端没有被绑定一样。

# 参数create和lookup分别指定变量来指示如何创建新会话和搜索已经存在的会话。
# 两个参数都可以指定多个,这样第一个非空的变量将被使用。

# 会话存储在zone中,在zone属性中配置名字和大小。
# 在64位平台上一个megabyte zone可以存储大概8000个会话。
# 在timeout参数指定的期间内没有被访问的会话将被从zone上移除。默认,超时时间设置为10分钟。
upstream backend {
   server backend1.example.com:8080;
   server backend2.example.com:8081;
   sticky learn
          create=$upstream_cookie_examplecookie
          lookup=$cookie_examplecookie
          zone=client_sessions:1m;
}

2.3 调度算法

调度算法的官方文档

调度算法 解释说明
rr 默认为轮询算法; 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某台服务器宕机,故障系统被自动剔除,使用户访问不受影响;权重越大,分配到的访问机率越高,主要用于后端每个服务器性能不均的情况下
ip_hash 每个请求按访问IPhash结果分配;这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题;当然如果这个节点不可用了就会发到下个节点,而此时没有session同步的话就注销掉了
least_conn 分派请求到活动连接数量最少的服务器,兼顾服务器权重
least_time 分派请求到平均响应时间最快和活动连接数量最少的服务器, 兼顾服务器权重
hash 根据客户端/服务器映射基于散列key值;key可以包含文本、变量和他们的组合
url_hash 按访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率;nginx本身是不支持url_hash的,如果需要使用这种调度算法,必须安装nginxhash软件包 nginx_upstream_hash模块
fair 更加智能的负载均衡算法;该算法可以依据页面大小和加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配;nginx本身是不支持fair的,如果需要使用这种调度算法,必须下载nginxupstream_fair模块

2.4 持久连接

keepalive

# 使用keepalive连接后端memcached服务器配置的例子
upstream memcached_backend {
    server 127.0.0.1:11211;
    server 10.0.0.2:11211;
    keepalive 32; 
    # 激活代理服务器到upstream服务器的连接缓存,即长连接
    # 当超过这个数量时,最近使用最少的连接将被关闭
    # 通常upstream服务器为memcached等缓存服务器,而非http服务器
}

server {
    ...
    location /memcached/ {
        set $memcached_key $uri;
        # $uri 的值赋给了自定义变量 $memcached_key,以备后续在 memcached 中进行缓存查找时使用

        memcached_pass memcached_backend;
    }
}
这样的配置允许 Nginx 通过 memcached_pass 将请求动态路由到指定的 memcached 后端组中,
并使用 $memcached_key 设置的值作为查找键,以实现缓存的读取或写入操作。
# 对于HTTP请求,proxy_http_version应该设置为1.1,而Connection的header应该被清理
# 或者HTTP/1.0持久连接可以通过传递"Connection: Keep-Alive"的header到upstream服务器,但是不推荐使用这种方法。
upstream http_backend {
    server 127.0.0.1:8080;
    keepalive 16;
}

server {
    ...
    location /http/ {
        proxy_pass http://http_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        ...
    }
}
# 对于FastCGI服务器,要求设置fastcgi_keep_conn来让长连接工作
# 当使用默认的RR之外的负载均衡算法时,必须在keepalive指令之前激活他们
# 对于scgi和uwsgi协议没有keepalive连接的概念
upstream fastcgi_backend {
    server 127.0.0.1:9000;
    keepalive 8;
}

server {
    ...
    location /fastcgi/ {
        fastcgi_pass fastcgi_backend;
        fastcgi_keep_conn on;
        ...
    }
}