昔我往矣

使用openresty增强Nginx的Proxy_Cache缓存

2016年03月31日

这个静态首页缓存的需求是分步骤的,当你越往下走,才发现水越深。一开始都把事情想当然了。本篇文章详细描述了如何一步步深入需求,解决缓存中遇到的问题,以及踩过的水坑。

fierce


环境版本


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 语言级别完成。

所以应该不用考虑性能问题,反正我也没测试过。

当前暂无评论 »

添加新评论 »