使用openresty增强Nginx的Proxy_Cache缓存
这个静态首页缓存的需求是分步骤的,当你越往下走,才发现水越深。一开始都把事情想当然了。本篇文章详细描述了如何一步步深入需求,解决缓存中遇到的问题,以及踩过的水坑。
环境版本
openresty --> openresty/1.9.7.1 # Nginx的增强版
Flask --> Flask-0.10.1
upstream server --> 192.168.124.97:5000 # 用Flask简单实现
openresty虚拟主机域名 --> test.xnow.me
基础服务搭建
为此,我特别实现了一个简化版的服务demo.py,使用flask框架,代码如下:
demo.py内容
#!/usr/bin/python
from flask import Flask
from flask import request
app = Flask(__name__)
@app.route("/")
def index():
xid = request.cookies.get('xid')
if not xid:
xid = "guest"
return "<h1>Hello, " + xid + "</h1>"
if __name__ == '__main__':
app.run(host='0.0.0.0')
启动服务
python demo.py
启动之后,服务监听在5000端口。
然后配置nginx,配置文件nginx.conf中test.xnow.me的虚拟主机如下:
...
server {
listen 80;
server_name test.xnow.me;
location = / {
proxy_pass http://192.168.124.97:5000;
}
}
...
测试下服务配置,不带cookie的情况
$ curl test.xnow.me
<h1>Hello, guest</h1>
携带xid的情况
$ curl -H "cookie: xid=World" test.xnow.me
<h1>Hello, World</h1>
通过上面curl的测试结果,看到服务正常。
基本版需求
实现首页静态化,思路是通过nginx的proxy_cache模块,把 http://test.xnow.me/ 页面缓存起来,Nginx的配置如下
...
proxy_cache_path /tmp/proxy_cache/ levels=1:2 keys_zone=cache_www:100m inactive=60m max_size=10g;
server {
listen 80;
server_name test.xnow.me;
add_header X-Cache-Status $upstream_cache_status;
location = / {
proxy_cache cache_www;
proxy_cache_key $host$uri;
proxy_cache_valid 200 304 60m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_pass http://192.168.124.97:5000;
}
}
...
配置解释
- proxy_cache_path : 设置缓存路径/tmp/proxy_cache。levels=1:2,表示第一级目录1个字符,第2级目录两个字符。 keys_zone=cache_www:100m表示这个zone的名字叫cache_www,分配内存的大小为100MB。inactive表示如果这个资源在inactive规定的时间内没有被访问到就会被删除。max_size表示这个zone可以使用的硬盘空间。
- add_header : 在给客户端的返回中,增加名为X-Cache-Status的header,其值是缓存命中情况,比如MISS,HIT等等。
- proxy_cache : 设置缓存资源的zone
- proxy_cache_key : 设置缓存文件中的key,硬盘中缓存文件的名字key值的MD5。譬如key是test.xnow.me/,则在硬盘上的md5值是c9d71dc81143d6d9a60165bdcb1b9c9f,计算方法:echo -n "test.xnow.me/" | md5sum
- proxy_cache_valid : 设置缓存的状态码,把返回状态是200和304的请求缓存起来。缓存时间是60分钟,过了缓存时间之后,设置缓存状态为EXPIRED,这是绝对时间,和上次更新时间相比。
- proxy_cache_use_stale 返回码出错的时候,使用缓存数据。譬如出现超时,502和503等等情况。
配置测试如下
$ curl -H "cookie: xid=World" test.xnow.me
<h1>Hello, World</h1>
$ curl -H "cookie: xid=xnow" test.xnow.me
<h1>Hello, World</h1>
$ curl test.xnow.me
<h1>Hello, World</h1>
通过以上的设置看到,第一次请求的页面被缓存了,再次发起请求,不论有没有cookie或者cookie值是多少,都返会缓存页面。我们要根据cookie来识别用户的,这个配置还需要改进。所以有了第二版配置。
进阶配置
在上一版的配置中,不能通过cookie识别用户,所以我们在后面添加2行的配置:
...
location = / {
proxy_cache cache_www;
proxy_cache_key $host$uri;
proxy_cache_valid 200 304 60m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_no_cache $cookie_xid;
proxy_cache_bypass $cookie_xid;
proxy_pass http://192.168.124.97:5000;
}
...
配置解释
- proxy_no_cache $cookie_xid : $cookie_xid 是 nginx变量,这一行的意思是不缓存cookie中有xid字段的请求。
- proxy_cache_bypass $cookie_xid : 意思是请求中的cookie有xid字段的,就直接发给后端服务器。
正常测试
以上配置合情合理,在正常情况下测试结果如下。
$ curl -H "cookie: xid=World" test.xnow.me # 携带xid的cookie
<h1>Hello, World</h1>
$ curl -H "cookie: xis=123" test.xnow.me # 携带xis的cookie
<h1>Hello, guest</h1>
$ curl test.xnow.me #不携带cookie
<h1>Hello, guest</h1>
以上测试和预计的效果一样,当cookie中有xid的时候就发给后端服务器,如果没有cookie,或者cookie不带xid,就返回缓存。
邪门测试
再测试下,在极端情况下,后端服务器挂掉了,再看看表现。
$ curl -H "cookie:xid=123" test.xnow.me # 携带xid的cookie
<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>openresty/1.9.7.1</center>
</body>
</html>
$ curl -H "cookie:xis=123" test.xnow.me # 携带xis的cookie
<h1>Hello, guest</h1>
$ curl test.xnow.me #不带cookie
<h1>Hello, guest</h1>
在后端服务挂掉之后,由于所有带有cookie为xid的请求会往后端转发,此时openresty返回了502。而不携带xid的请求,都能匹配proxy_cache_use_stale而命中缓存。为了给用户一个稳定的好印象,我们必须在后端服务挂了以后,继续给用户提供首页面,即使是Guest的默认首页面。所以,请看高级版本缓存方案。
高阶缓存
为了弥补第二版本在后端服务器挂掉之后,携带cookie用户不能看到首页的问题,我们提出了新的需求:在后端服务器挂掉之后,还是要给用户返回首页面,即使是不能登录的Guest用户页面。在这一版中,首先引入了openresty的高级特性,使用Lua嵌入到Nginx的配置中,修改请求的处理过程。在上面的配置基础上,增加了Lua代码,如下:
...
location = / {
proxy_cache cache_www;
proxy_cache_key $host$uri;
proxy_cache_valid 200 304 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_no_cache $cookie_xid;
proxy_cache_bypass $cookie_xid;
access_by_lua_block {
local res = ngx.location.capture("/")
if res.status == 502 then
ngx.header["content_type"] = "text/html"
ngx.header["Cache-Control"] = "no-cache"
ngx.req.clear_header("Cookie")
local res2 = ngx.location.capture("/")
ngx.say(res2.body)
end
}
proxy_pass http://192.168.124.97:5000;
}
...
其余配置不变,只说说Lua代码的作用。
因为openresty不能直接感知后端服务器的状态,所以首先发起ngx.location.capture子查询,如果返回码为502,说明后端服务器都已经死掉了。然后,开始准备发起第二次请求,第二次请求把header中的cookie清空,然后添加上两个header头部:content_type="text/html"和Cache-Control="no-cache"。第一个header是为了方便浏览器展示,第二个header是为了避免浏览器缓存页面,如果浏览器缓存了502返回的Guest页面,可能导致用户携带cookie登录的时候,直接获得浏览器中的缓存。
最后的测试
$ curl -H "cookie:xid=123" test.xnow.me -I # 携带xid的cookie,查看返回的头部
HTTP/1.1 200 OK
Server: openresty/1.9.7.1
Date: Thu, 31 Mar 2016 15:17:00 GMT
Content-Type: text/html
Connection: keep-alive
Cache-Control: no-cache
$ curl -H "cookie:xid=123" test.xnow.me # # 携带xid的cookie,查看返回的内容
<h1>Hello, guest</h1>
技能看到Guest缓存页,也在Header中看到了我们添加的两个HEADER。一切正常,世界又美好了。
总结
这个配置过程劳心劳力,也不断测试了,思考了很多方案。最开始考虑从返回的值里面获取状态码,但是header_filter_by_lua环境中可用的指令太少,不能使用子请求的方法,最后慢慢摸索,考虑直接在最开始就介入到处理过程。
有同学可能认为,平白多出来两次请求,会增加服务器的压力,但是openresty的官方说:
Nginx 子请求是一种非常强有力的方式,它可以发起非阻塞的内部请求访问目标 location。目标 location 可以是配置文件中其他文件目录,或 任何 其他 nginx C 模块,包括 ngx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle,甚至 ngx_lua 自身等等 。
需要注意的是,子请求只是模拟 HTTP 接口的形式, 没有 额外的 HTTP/TCP 流量,也 没有 IPC (进程间通信) 调用。所有工作在内部高效地在 C 语言级别完成。
所以应该不用考虑性能问题,反正我也没测试过。
当前暂无评论 »