京东活动系统--openresty实践之路,--openresty实践之路


背景

     先来说下今天的主角openresty,它是一个基于 Nginx Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。可以使用Lua编写脚本,然后部署到Nginx Web容器中运行。利用nginx的高并发处理能力,轻松构建自己的高性能的Web服务。

    但个人觉得,使用lua编写一些复杂业务逻辑不是它的专长,编写成本也比较高。现有业务的web系统,大多还是以javac#等为主。如要完全改为lua实现,几乎不可能完成。

    上一篇分享《京东活动系统亿级流量架构应对之术》的核心,其实是活动页面的浏览与页面的渲染的异步化。即:活动页面在被浏览时,view工程直接从redis或者硬盘获取已经在engine工程被渲染好的页面返回。简而言之,就是通过engine工程提前渲染好页面,实现页面全静态化。保证view工程每次请求都直接获取静态页面返回,无需等待页面渲染。

 

    这样复杂的业务逻辑已经被完全剥离到engine工程。view工程可以采用nginx+lua+redis+硬盘实现自己的高性能web服务。最终架构如下:



 

 

其中,硬盘和redis互为主备,做一个开关相互切换。

 

lua+redis 字符串压缩

engine工程采用java把静态html字符串放入redisview工程再采用luaredis中获取。这里本身没有问题,但是由于活动页面的内容较多,一个静态页面html动辄几百kb,大的甚至几兆。存取速度势必会很慢,很自然的会想到压缩。

 

 

view工程没有引入openresty之前,我们所有的活动页面都是采用的javagzipGZIPInputStreamGZIPOutputStream来进行压缩和解压,也收到很好的收益,并且待压缩内容越长压缩效果越是明显。存入redis前先用GZIPInputStream压缩,把压缩后的字节数组存入redis(在engine中完成)。view工程从redis中取出,再用GZIPOutputStream解压,还原成正常的活动页面静态html字符串返回。如下:



 

现在要把view工程改为用openresty实现,难点就在于用luaredis中取出字符串的内容是用gzip压缩后的,直接返回是乱码。通过查找资料,lua可以调用zlib来实现对字符串的压缩和解压(参考

https://github.com/brimworks/lua-zlib)。

 

engine工程压缩端把gzip压缩方式改成zlib压缩、并存入redisjava zlib压缩可以参考http://snowolf.iteye.com/blog/465433)。view工程就可以直接从redis中获取,再通过zlib解压即可。但由于redis已存在大量gzip压缩的活动页,这种方式没办法实现平滑过渡。

 

通过分析gzipzlib,可以发现他们本质上都是deflate算法进行压缩。只是gzip相对于zlib会多一个描述头部,如果直接用lua-zlib解压gzip压缩的内容,头部会多出一段乱码,其余内容完全一样。我们直接去掉这部分乱码头部即可:



 

 

lua实现如下:

nginx.conf配置如下:

   

location  ~ ^/act/(\w+)\.html{
      default_type "text/html";
      charset utf-8,gbk;
      set $shortUrl $1;
      content_by_lua_file /export/app/view/page_view_redis.lua;
    }
 

 

/export/app/view/page_view_redis.lua代码如下:

 

local redis_pool = require "redis_pool" -- lua redis缓存池封装
local zlib = require "zlib"
local stream = zlib.inflate()
local key = "pc-page-cache-key-"..ngx.var.shortUrl
local red = redis_pool:new()
local res, err = red:get(key)
if res ~= nil then
    -- 解压, page内容为:"xxxxxx<html><header></header><body>test page</body></html>"其中xxxxx即为gzip压缩的乱码头
    local page=stream(res);
    -- 去掉乱码头
    local h1,h2=string.find(r, "<html>")
 
page= string.sub(page,h1)
    -- 返回页面内容
    ngx.say(page)
end
ngx.say("页面不存在")
ngx. ngx.exit(200)
 

 

 

lua+硬盘

先说下,静态活动页面存硬盘:

1engine:渲染页面完成;

2engine:页面内容存入redis

3engine:根据活动url hash规则,向指定的view服务器发送数据推送请求;

4viewview服务器收到请求,从redis中获取页面内容存到指定目录下。

 

步骤4中,从redis中取出,还是要先进行解压,再存储到指定目录,lua代码如下:

nginx.conf配置:

 

# engine 发起的保存页面到本地硬盘请求
    location  ~ ^/save/(\w+)\.html{
      default_type "text/html";
      charset utf-8,gbk;
      set $key $1;
      content_by_lua_file /export/app/view/save_disk.lua;
    }
 

 

/export/app/view/save_disk.lua代码内容如下:

 

local redis_pool = require "redis_pool" -- lua redis缓存池封装
local util_tools = require "util_tools"
local zlib = require "zlib"
local stream = zlib.inflate()
 
local key = "pc-page-cache-key-"..ngx.var.shortUrl
local red = redis_pool:new();
local res, err = red:get(key)
if res ~= nil then
    -- 解压, page内容为:"xxxxxx<html><header></header><body>test page</body></html>"其中xxxxx即为gzip压缩的乱码头
    local page=stream(res);
    -- 去掉乱码头
    local h1,h2=string.find(page, "<html>")
    page= string.sub(page,h1)
 
    -- 通过md5(uri),构造存储目录和文件名
    local pageUri="/act/"..ngx.var.shortUrl..".html"
    local filePath,pageName = util_tools:get_abpath(pageUri)
    local file,err=io.open(filePath)
    if not file then  --如果文件目录不存在,就先创建
        os.execute("mkdir -p "..filePath)
    end
    local f = assert(io.open(filePath..pageName,'w')) --写入硬盘文件中
    f:write(page) -- 存储页面
    f:close()
end
ngx.exit(200)
 

 

 

util_tools.lua代码内容:

 

local rootPath="/export/static/page/"
local uri_pre = "page"
 
local util_tools = {}
 
-- get static html page
-- 如md5(url)=xxxxxxabc,最终的页面内容存放到:"/export/static/page/c/ab/xxxxxxabc",拼装的新uri为:/page/c/ab/xxxxxxabc
function util_tools:get_path(url)
    local md5path = ngx.md5(url)
    local pathlen = string.len(md5path)
    local path1 = string.sub(md5path,-1)
    local path2 = string.sub(md5path,pathlen-2,-2)
    local abPathFile = uri_pre.."/"..path1.."/"..path2.."/"..md5path
    return abPathFile
end
 
-- get static html page path and name
--类似nginx proxy cache对文件的存储结构,把链接先md5,取后三位字符串,拼成文件存放目录
--存取文件:如md5(url)=xxxxxxabc,最终的页面内容存放到:"/export/static/page/c/ab/xxxxxxabc"
function util_tools:get_abpath(url)
    local md5path = ngx.md5(url)
    local pathlen = string.len(md5path)
    local path1 = string.sub(md5path,-1)
    local path2 = string.sub(md5path,pathlen-2,-2)
    local abPathFile = rootPath.."/"..path1.."/"..path2.."/"
    return abPathFile,md5path
end
return util_tools
 

 

步骤3中的url hash规则讲解:由于京东活动页面较多,所有在线的活动静态页面大小已经超过10G。如果把如此大量的数据存放到每台sale服务上,是不可取的。

首先不方便扩展,随着京东业务快速增长,活动数据也会快速增加,而硬盘大小始终有限。

其次,在用lua读取静态页面时,从如此大量的静态文件中查找也会增加耗时。通过活动url规则进行hash,把这些活动页面内容平均分配到不同sale服务器分组,是我们目前采取的方式。其中还涉及到sale服务器主备分组,这里就不再详细讲解,后面单独再做一次分享。

 

再说下从硬盘获取,根据url拼装出文件存放路径,直接从本地硬盘获取即可。代码如下:

 

location  ~ ^/act/(\w+)\.html{
      default_type "text/html";
      charset utf-8,gbk;
      set $shortUrl $1;
      content_by_lua_file /export/app/view/page_view_redis.lua;
    }
/export/app/view/page_view_redis.lua代码内容:
local util_tools = require "util_tools"
-- 如果链接为http://sale.jd.com/act/Ok70VSmKFZo1Weca.html,pageUri为:
-- /act/Ok70VSmKFZo1Weca.html
local pageUri="/act/"..ngx.var.key..".html"
local abpath = util_tools:get_abpath(pageUri)
--abpath相对路径 规则为:/page/c/ab/xxxxxxabc
local resSta = ngx.location.capture(abpath)
if resSta.status == ngx.HTTP_OK then  --读取到硬盘
    if resSta.body ~= nil and resSta.body ~= "nil" and resSta.body ~= "" then
        ngx.say(resSta.body) --返回页面内容
        return ngx.exit(200)
    end
end
 

 

view 工程代码结构(lua

 

再来看下代码结构:

|--export

|-----|app

|---------|view

|--------------| page_view_redis.lua

|--------------| page_view_disk.lua

|--------------| save_disk.lua

|--------------| util_tools.lua

|-----|pro

|--------|nginx

|------------|conf

|----------------|nginx.conf

 

比起java来是不是简洁多了。页面浏览可以读redis和硬盘,可以在lua_shared_dict中存放一个切换开关,根据开关值选择执行page_view_redis.lua或者page_view_disk.lua

 

    好了就写这么多了吧,回顾下这次讲解的主要内容:luaredislua-zlibgzip压缩字符串、lua读硬盘等。

 

相关内容

    暂无相关文章