京东活动系统--openresty实践之路,--openresty实践之路
京东活动系统--openresty实践之路,--openresty实践之路
背景
先来说下今天的主角openresty,它是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。可以使用Lua编写脚本,然后部署到Nginx Web容器中运行。利用nginx的高并发处理能力,轻松构建自己的高性能的Web服务。
但个人觉得,使用lua编写一些复杂业务逻辑不是它的专长,编写成本也比较高。现有业务的web系统,大多还是以java、c#等为主。如要完全改为lua实现,几乎不可能完成。
上一篇分享《京东活动系统亿级流量架构应对之术》的核心,其实是活动页面的浏览与页面的渲染的异步化。即:活动页面在被浏览时,view工程直接从redis或者硬盘获取已经在engine工程被渲染好的页面返回。简而言之,就是通过engine工程提前渲染好页面,实现页面全静态化。保证view工程每次请求都直接获取静态页面返回,无需等待页面渲染。
这样复杂的业务逻辑已经被完全剥离到engine工程。view工程可以采用nginx+lua+redis+硬盘实现自己的高性能web服务。最终架构如下:
其中,硬盘和redis互为主备,做一个开关相互切换。
lua+redis 字符串压缩
engine工程采用java把静态html字符串放入redis,view工程再采用lua从redis中获取。这里本身没有问题,但是由于活动页面的内容较多,一个静态页面html动辄几百kb,大的甚至几兆。存取速度势必会很慢,很自然的会想到压缩。
在view工程没有引入openresty之前,我们所有的活动页面都是采用的java的gzip(GZIPInputStream和GZIPOutputStream)来进行压缩和解压,也收到很好的收益,并且待压缩内容越长压缩效果越是明显。存入redis前先用GZIPInputStream压缩,把压缩后的字节数组存入redis(在engine中完成)。view工程从redis中取出,再用GZIPOutputStream解压,还原成正常的活动页面静态html字符串返回。如下:
现在要把view工程改为用openresty实现,难点就在于用lua从redis中取出字符串的内容是用gzip压缩后的,直接返回是乱码。通过查找资料,lua可以调用zlib来实现对字符串的压缩和解压(参考
https://github.com/brimworks/lua-zlib)。
在engine工程压缩端把gzip压缩方式改成zlib压缩、并存入redis(java zlib压缩可以参考http://snowolf.iteye.com/blog/465433)。view工程就可以直接从redis中获取,再通过zlib解压即可。但由于redis已存在大量gzip压缩的活动页,这种方式没办法实现平滑过渡。
通过分析gzip和zlib,可以发现他们本质上都是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+硬盘
先说下,静态活动页面存硬盘:
1、engine:渲染页面完成;
2、engine:页面内容存入redis;
3、engine:根据活动url hash规则,向指定的view服务器发送数据推送请求;
4、view:view服务器收到请求,从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。
好了就写这么多了吧,回顾下这次讲解的主要内容:lua读redis、lua-zlib读gzip压缩字符串、lua读硬盘等。
评论暂时关闭