Nginx 反向代理、负载均衡、页面缓存、URL重写及读写分离详解
目录
实验环境:CentOS 6.4 x86_64,Nginx 1.4.2。三台机器:nginx(192.168.18.208,代理)、web1(192.168.18.201)、web2(192.168.18.202)。
一、环境准备
安装 epel yum 源、时间同步、关闭防火墙与 SELinux(三个节点均执行):
# rpm -ivh http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
# ntpdate 202.120.2.101
# service iptables stop
# chkconfig iptables off
# getenforce
Disabled
二、安装 Nginx
[root@nginx src]# tar xf nginx-1.4.2.tar.gz
[root@nginx src]# groupadd -g 108 -r nginx
[root@nginx src]# useradd -u 108 -r -g 108 nginx
[root@nginx src]# yum install -y pcre-devel openssl-devel
[root@nginx nginx-1.4.2]# ./configure --prefix=/usr --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx/nginx.pid --lock-path=/var/lock/nginx.lock --user=nginx --group=nginx --with-http_ssl_module --with-http_flv_module --with-http_stub_status_module --with-http_gzip_static_module --http-client-body-temp-path=/var/tmp/nginx/client/ --http-proxy-temp-path=/var/tmp/nginx/proxy/ --http-fastcgi-temp-path=/var/tmp/nginx/fcgi/ --http-uwsgi-temp-path=/var/tmp/nginx/uwsgi --http-scgi-temp-path=/var/tmp/nginx/scgi --with-pcre
[root@nginx nginx-1.4.2]# make && make install
为 nginx 提供 SysV init 脚本 /etc/init.d/nginx:
#!/bin/sh
#
# nginx - this script starts and stops the nginx daemon
#
# chkconfig: - 85 15
# description: Nginx is an HTTP(S) server, HTTP(S) reverse \
# proxy and IMAP/POP3 proxy server
# processname: nginx
# config: /etc/nginx/nginx.conf
# config: /etc/sysconfig/nginx
# pidfile: /var/run/nginx.pid
# Source function library.
. /etc/rc.d/init.d/functions
# Source networking configuration.
. /etc/sysconfig/network
# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0
nginx="/usr/sbin/nginx"
prog=$(basename $nginx)
NGINX_CONF_FILE="/etc/nginx/nginx.conf"
[ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx
lockfile=/var/lock/subsys/nginx
make_dirs() {
# make required directories
user=`nginx -V 2>&1 | grep "configure arguments:" | sed 's/[^*]*--user=\([^ ]*\).*/\1/g' -`
options=`$nginx -V 2>&1 | grep 'configure arguments:'`
for opt in $options; do
if [ `echo $opt | grep '.*-temp-path'` ]; then
value=`echo $opt | cut -d "=" -f 2`
if [ ! -d "$value" ]; then
# echo "creating" $value
mkdir -p $value && chown -R $user $value
fi
fi
done
}
start() {
[ -x $nginx ] || exit 5
[ -f $NGINX_CONF_FILE ] || exit 6
make_dirs
echo -n $"Starting $prog: "
daemon $nginx -c $NGINX_CONF_FILE
retval=$?
echo
[ $retval -eq 0 ] && touch $lockfile
return $retval
}
stop() {
echo -n $"Stopping $prog: "
killproc $prog -QUIT
retval=$?
echo
[ $retval -eq 0 ] && rm -f $lockfile
return $retval
}
restart() {
configtest || return $?
stop
sleep 1
start
}
reload() {
configtest || return $?
echo -n $"Reloading $prog: "
killproc $nginx -HUP
RETVAL=$?
echo
}
force_reload() {
restart
}
configtest() {
$nginx -t -c $NGINX_CONF_FILE
}
rh_status() {
status $prog
}
rh_status_q() {
rh_status >/dev/null 2>&1
}
case "$1" in
start)
rh_status_q && exit 0
$1
;;
stop)
rh_status_q || exit 0
$1
;;
restart|configtest)
$1
;;
reload)
rh_status_q || exit 7
$1
;;
force-reload)
force_reload
;;
status)
rh_status
;;
condrestart|try-restart)
rh_status_q || exit 0
;;
*)
echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}"
exit 2
esac
赋权、加入服务管理并启动:
[root@nginx ~]# chmod +x /etc/init.d/nginx
[root@nginx ~]# chkconfig --add nginx
[root@nginx ~]# chkconfig nginx on
[root@nginx ~]# service nginx start
[root@nginx ~]# netstat -ntlp | grep :80
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 3889/nginx
三、反向代理
后端准备:web1、web2 安装 httpd 并提供测试页。
[root@web1 ~]# yum install -y httpd
[root@web1 ~]# echo "<h1>web1.test.com</h1>" > /var/www/html/index.html
[root@web1 ~]# service httpd start
[root@web2 ~]# yum install -y httpd
[root@web2 ~]# echo "<h1>web2.test.com</h1>" > /var/www/html/index.html
[root@web2 ~]# service httpd start
概念要点:正向代理位于客户端和原始服务器之间,客户端需特别设置,典型用途是为防火墙内的局域网客户端提供访问 Internet 的途径;反向代理对客户端透明、就像原始服务器本身,典型用途是将防火墙后面的服务器提供给 Internet 用户访问,并可为后端多台服务器提供负载均衡和缓冲服务。
proxy_pass(使用字段:location 及 location 中的 if)设置被代理服务器的地址和被映射的 URI。配置 /etc/nginx/nginx.conf:
location / {
proxy_pass http://192.168.18.201;
proxy_set_header X-Real-IP $remote_addr;
}
proxy_set_header(使用字段:http, server, location)将发送到被代理服务器的请求头重新定义或增加字段,未定义时从上级字段继承。修改后 service nginx reload 生效。
默认后端 httpd 日志记录的客户端 IP 全是 nginx 代理的 IP。要记录真实客户端 IP,除了 nginx 侧的 proxy_set_header X-Real-IP $remote_addr;,还需将 httpd 的 LogFormat 中 %h 改为 %{X-Real-IP}i:
LogFormat "%{X-Real-IP}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
重启 httpd 后日志即记录真实客户端地址。
四、负载均衡
upstream 模块通过调度算法实现客户端到后端服务器的负载均衡。示例语法:
upstream test.net {
ip_hash;
server 192.168.10.13:80;
server 192.168.10.14:80 down;
server 192.168.10.15:8009 max_fails=3 fail_timeout=20s;
server 192.168.10.16:8080;
}
server {
location / {
proxy_pass http://test.net;
}
}
支持的 4 种调度算法:
- 轮询(默认):按时间顺序逐一分配,宕机的后端自动剔除;weight 指定权值,值越大分配机率越高,用于后端性能不均的情况。
- ip_hash:按访问 IP 的 hash 结果分配,同一 IP 固定访问一个后端,解决动态网页的 session 共享问题。
- fair(第三方):根据后端响应时间分配,需 upstream_fair 模块。
- url_hash(第三方):按 url 的 hash 结果分配,使每个 url 定向到同一后端,提高后端缓存服务器效率,需 hash 软件包。
server 指令的状态参数:down(暂不参与负载均衡)、backup(备份机,其他所有非 backup 机器故障或忙时才请求)、max_fails(允许请求失败次数,默认 1,超过则返回 proxy_next_upstream 模块定义的错误)、fail_timeout(经历 max_fails 次失败后暂停服务的时间)。注意:调度算法为 ip_hash 时,后端状态不能是 weight 和 backup。
实际配置 /etc/nginx/nginx.conf:
upstream webservers {
server 192.168.18.201 weight=1;
server 192.168.18.202 weight=1;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://webservers;
proxy_set_header X-Real-IP $remote_addr;
}
}
注意 upstream 定义在 server{} 之外。reload 后不断刷新页面,web1 与 web2 交替出现,两台后端日志均记录到来自真实客户端的请求。
健康状态检查(利用 max_fails + fail_timeout):
upstream webservers {
server 192.168.18.201 weight=1 max_fails=2 fail_timeout=2;
server 192.168.18.202 weight=1 max_fails=2 fail_timeout=2;
}
停掉 web1(service httpd stop)后只能访问到 web2,恢复后又重新加入,说明健康检查生效。
backup 服务器(类似 LVS 的 sorry_server,所有后端都故障时兜底):
server {
listen 8080;
server_name localhost;
root /data/www/errorpage;
index index.html;
}
upstream webservers {
server 192.168.18.201 weight=1 max_fails=2 fail_timeout=2;
server 192.168.18.202 weight=1 max_fails=2 fail_timeout=2;
server 127.0.0.1:8080 backup;
}
[root@nginx ~]# mkdir -pv /data/www/errorpage
[root@nginx errorpage]# cat index.html
<h1>Sorry......</h1>
web1、web2 都停止后,访问返回备份页面。
ip_hash 负载均衡(电子商务网站常用;不能与 backup 并存——被分到 backup 就无法访问了):
upstream webservers {
ip_hash;
server 192.168.18.201 weight=1 max_fails=2 fail_timeout=2;
server 192.168.18.202 weight=1 max_fails=2 fail_timeout=2;
#server 127.0.0.1:8080 backup;
}
reload 后同一客户端固定落在一台后端,可用 netstat -an | grep :80 | wc -l 在后端统计连接数验证。
五、页面缓存
关键指令:
-
proxy_cache_path path [levels=number] keys_zone=zone_name:zone_size [inactive=time] [max_size=size];(使用字段:http)指定缓存路径,缓存数据存储在文件中,用代理 url 的哈希值作为关键字与文件名。levels 指定缓存子目录数,最多三级,如levels=1:2时文件名类似/data/nginx/cache/c/29/b7f54b2df7773722d382f4809d65029c。活动的 key 和元数据存储在 keys_zone 指定的共享内存池中。inactive 时间内未被请求的缓存数据被删除(默认 10 分钟);cache manager 进程按 max_size 控制磁盘缓存大小,超出后按 LRU 删除。每个内存池必须是不重复的路径:proxy_cache_path /data/nginx/cache/one levels=1 keys_zone=one:10m; proxy_cache_path /data/nginx/cache/two levels=2:2 keys_zone=two:100m; proxy_cache_path /data/nginx/cache/three levels=1:1:2 keys_zone=three:1000m; -
proxy_cache zone_name;(http, server, location)设置缓存区域名称。0.7.48 后缓存遵循后端的 Expires、Cache-Control: no-cache、Cache-Control: max-age=XXX 头;0.7.66 后 private 和 no-store 同样被遵循。nginx 缓存过程中不处理 Vary 头,为确保私有数据不被所有用户看到,后端须设置 no-cache/max-age=0,或让 proxy_cache_key 包含 $cookie_xxx 等用户数据。缓存依赖代理缓冲区,proxy_buffers 为 off 时缓存不生效。 -
proxy_cache_valid reply_code [reply_code ...] time;(http, server, location)为不同应答设置缓存时间:proxy_cache_valid 200 302 10m; proxy_cache_valid 301 1h; proxy_cache_valid any 1m;只定义时间(如
proxy_cache_valid 5m;)则只缓存 200、301、302 应答。
定义一个简单的缓存服务器,并通过响应头直观观察缓存命中情况($server_addr 为服务器地址;$upstream_cache_status 取值:MISS 未命中、EXPIRED 过期、UPDATING 正在更新将使用旧应答、STALE 后端得到过期应答、HIT 命中):
proxy_cache_path /data/nginx/cache/webserver levels=1:2 keys_zone=webserver:20m max_size=1g;
server {
listen 80;
server_name localhost;
# 增加两头部
add_header X-Via $server_addr;
add_header X-Cache $upstream_cache_status;
location / {
proxy_pass http://webservers;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache webserver;
proxy_cache_valid 200 10m;
}
}
[root@nginx ~]# mkdir -pv /data/nginx/cache/webserver
[root@nginx ~]# service nginx reload
浏览器 F12 的 Network 中可看到 X-Via、X-Cache 响应头显示命中情况;缓存目录中确实生成了缓存文件:
[root@nginx ~]# cd /data/nginx/cache/webserver/f/63/
[root@nginx 63]# ls
681ad4c77694b65d61c9985553a2763f
六、URL 重写
Rewrite 模块允许使用正则表达式重写 URI(需 PCRE 库)。在 server 字段中指定时在 location 确定之前执行;location 中执行产生新 URI 时 location 会再次确定,循环最多 10 次,超过返回 500 错误。主要指令:
-
break(server, location, if):完成当前规则,停止执行其他重写指令。 -
if (condition) { ... }(server, location):条件判断。可用:变量名(空串或以 0 开头为假);=/!=比较;~(区分大小写)/~*(不区分)正则匹配及!~、!~*;-f/!-f文件存在、-d/!-d目录存在、-e/!-e文件/目录/软链接存在、-x/!-x可执行。正则中的圆括号可用 $1-$9 引用。注意官方建议尽量用 try_files 代替 if。示例:if ($http_user_agent ~ MSIE) { rewrite ^(.*)$ /msie/$1 break; } if ($http_cookie ~* "id=([^;] +)(?:;|$)" ) { set $id $1; } if ($request_method = POST ) { return 405; } if (!-f $request_filename) { break; proxy_pass http://127.0.0.1; } if ($slow) { limit_rate 10k; } if ($invalid_referer) { return 403; } if ($args ~ post=140){ rewrite ^ http://example.com/ permanent; } -
return code(server, location, if):结束执行并返回状态码,可用 204、400、402-406、408、410、411、413、416 与 500-504;非标准代码 444 关闭连接且不发送任何头部。 -
rewrite regex replacement flag(server, location, if):按正则修改 URI,按配置文件中出现的顺序执行。替换字符串以 http:// 开头则直接重定向且不再执行后续 rewrite。flag 取值:last(完成重写后重新搜索 URI/location)、break(完成重写)、redirect(302 临时重定向)、permanent(301 永久重定向)。示例:rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 last; rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra last; return 403;若放入
location /download/中则需将 last 改为 break,否则 nginx 循环 10 次后返回 500:location /download/ { rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 break; rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra break; return 403; }替换字段中包含参数时其余请求参数会附加到后面,防止附加可在最后跟一个问号:
rewrite ^/users/(.*)$ /show?user=$1? last;。正则中使用大括号需用引号包起来,如把/photos/123456重写为/path/to/photos/12/1234/123456.png:rewrite "/photos/([0-9] {2})([0-9] {2})([0-9] {2})" /path/to/photos/$1/$1$2/$1$2$3.png;用 $request_uri 把 www.example.com 重写到 example.com:
server { server_name www.example.com; rewrite ^ http://example.com$request_uri? permanent; }重写只对路径操作,重写带参数的 URL 可用:
if ($args ^~ post=100){ rewrite ^ http://example.com/new-address.html? permanent; } -
rewrite_log on | off(默认 off):启用后在 error log 中以 notice 级别记录重写日志。 -
set variable value:定义变量并赋值,值可以是文本、变量及其组合;不能设置 $http_xxx 头部变量。 -
uninitialized_variable_warn on|off(默认 on):控制未初始化变量的警告日志。
rewrite 指令在配置加载时已编译到内部代码,由一个简单的堆栈虚拟机在请求时解释执行;将 rewrite ^/(download/.*)/media/... 的第一个斜杠括入圆括号(^(/download/.*)/media/...)可减少执行步骤。
实验: 跨服务器重定向(302 临时重定向):
location / {
root html;
index index.html index.htm;
rewrite ^/bbs/(.*)$ http://192.168.18.201/forum/$1;
}
[root@web1 html]# mkdir forum
[root@web1 forum]# vim index.html
<h1>forum page!</h1>
访问 /bbs/ 返回 status code 302。本机跳转则是永久重定向(隐式重定向):
location / {
root html;
index index.html index.htm;
rewrite ^/bbs/(.*)$ /forum/$1;
}
[root@nginx html]# mkdir forum
[root@nginx forum]# vim index.html
<h1>192.168.18.208 forum page</h1>
一般服务器与服务器之间是临时重定向,服务器内部是永久重定向。
七、读写分离
需求:前端 nginx 负载均衡反向代理,后端两台 httpd 提供 BBS 服务。上传附件只能上传到 web1,再用 rsync+inotify 向 web2 单向同步——所以 web1 可写、web2 只读。
WebDAV(Web-based Distributed Authoring and Versioning)是基于 HTTP 1.1 的通信协议,在 GET、POST、HEAD 等标准方法外添加了新方法,使应用程序可直接对 Web Server 读写,支持写文件锁定/解锁与版本控制。
nginx 配置——读发给 web2,PUT 写发给 web1:
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://192.168.18.202;
if ($request_method = "PUT"){
proxy_pass http://192.168.18.201;
}
}
}
web1 上在 httpd.conf 的 <Directory "/var/www/html"> 中启用 WebDAV(Dav On)并重启 httpd。测试:
[root@nginx ~]# curl http://192.168.18.201
<h1>web1.test.com</h1>
[root@nginx ~]# curl http://192.168.18.202
<h1>web2.test.com</h1>
[root@nginx ~]# curl -T /etc/issue http://192.168.18.202
向 web2 上传返回 405 Method Not Allowed(未开启 WebDAV,只读)。向 web1 上传初始返回 403 Forbidden(root 目录不允许 apache 用户上传),授权后成功:
[root@web1 ~]# setfacl -m u:apache:rwx /var/www/html/
[root@nginx ~]# curl -T /etc/issue http://192.168.18.201
返回 201 Created,文件已上传:
[root@web1 html]# ll
-rw-r--r-- 1 apache apache 47 9月 4 14:06 issue
至此 nginx 的反向代理、负载均衡、页面缓存、URL 重写及读写分离全部完成。