Nginx与Gzip请求,NginxGzip请求


https://huoding.com/2013/09/02/283

Nginx与Gzip请求

发表于2013-09-02

前些天,移动端的同事跑来问:某些API需要传输大数据,Nginx服务器能否支持Gzip请求?一方面可以节省移动端流量;另一方面还可以加快传输速度,提升用户体验。对于Apache来说,利用SetInputFilter,可以很轻松的实现这个功能,那么Nginx如何做呢?

既然移动端发送的是Gzip请求,自然需要想想如何在服务端解压缩。搜索一下现成的Nginx的模块,发现和Gzip相关的模块有如下几个:

  • Gzip: Gzip responses.
  • Gzip Precompression: Serves precompressed versions of static files.
  • Gunzip: On-the-fly decompressing of gzipped responses.

可惜它们都是和Response相关的Gzip,而我们需要的是和Request相关的Gzip。

在我们的实际情况里,很多接口都是用PHP做的,于是自然想到用PHP的gzdecode方法来解压缩Gzip请求,不过最终出于效率的担心放弃了。

每当我遇到难题的时候就会想起lua-nginx-module,它总是能屡建奇功,这次自然也不例外,仔细搜索了一下OpenResty社区,发现有人遇到了同样的问题,春哥在讨论中给出了建议,不过并没有涉及具体的实现逻辑,于是我查了资料总结了一下。

方案

第一个选择是使用lua-zlib:

local zlib = require "zlib"

local encoding = ngx.req.get_headers()["Content-Encoding"]

if encoding == "gzip" then
    local body = ngx.req.get_body_data()

    if body then
        local stream = zlib.inflate()
        ngx.req.set_body_data(stream(body))
    end
end

第二个选择是通过LuaJIT的FFI库来包装ZLIB模块,官方教程里有一些现成的可供参考的的例子,不过例子里介绍的是Deflate,而不是Gzip,自己用FFI封装Gzip的话又有点小复杂,好在别人已经做了相关的工作,那就是lua-files:

local ffi  = require "ffi"
local zlib = require "zlib"

local function reader(s)
    local done
    return function()
        if done then return end
        done = true
        return s
    end
end

local function writer()
    local t = {}
    return function(data, sz)
        if not data then return table.concat(t) end
        t[#t + 1] = ffi.string(data, sz)
    end
end

local encoding = ngx.req.get_headers()["Content-Encoding"]

if encoding == "gzip" then
    local body = ngx.req.get_body_data()

    if body then
        local write = writer()
        zlib.inflate(reader(body), write, nil, "gzip")
        ngx.req.set_body_data(write())
    end
end

如上例子代码源自zlib_test.lua,乍看上去,代码里的reader和writer可能会令人费解,其实你可以把它们理解成输入输出接口,可以修改成文件,数据库等等形式。

别高兴太早,当你运行时,很可能会遇到如下错误:

libzlib.so: cannot open shared object file.

实际上这是因为如下zlib.lua代码的缘故:

local C = ffi.load 'zlib'

运行时,ffi.load会自动补全文件名,如果是Windows,则加载zlib.dll文件,如果是Linux,则加载libzlib.so,但实际上在Linux下,ZLIB扩展的名字是libz.so,而非libzlib.so。

知道的问题的原委,我们自然就知道如何修改代码了:

local C

if ffi.os == "Windows" then
    C = ffi.load "zlib"
else
    C = ffi.load "z"
end

有时候我们不推荐直接修改第三方库的代码,因为这样的话,每次第三库更新代码,我们都要做对应的修改,一旦忘记就会出错,这时候可以考虑做一个软连接别名。

测试

开篇说过,接口都是用PHP做的,不过请求里的Gzip数据是用LUA处理的,如何让PHP使用LUA处理后的数据呢?不同的语言似乎是个难题,好在Nginx有Phases一说,PHP作为FastCGI模块工作在content阶段,LUA可以工作在access阶段,这样它们就和谐了:

location ~ \.php$ {
    access_by_lua_file /path/to/lua/file;

    include fastcgi.conf;
    fastcgi_pass 127.0.0.1:9000;
}

那么lua-zlib和lua-files两种方案效率如何?下面是我用PHP写的测试脚本:

<?php

$url = 'http://url';

$header = implode("\r\n", array(
    'Content-Type: application/x-www-form-urlencoded',
    'Content-Encoding: gzip',
    'Connection: close',
));

$content = gzencode(http_build_query(array(
    'foo' => str_repeat('x', 100),
    'bar' => str_repeat('y', 100),
)));

$options = array(
    'http' => array(
        'protocol_version' => '1.1',
        'method' => 'POST',
        'header' => $header,
        'content' => $content,
    ),
);

$context = stream_context_create($options);

for ($i = 0; $i < 1000; $i++) {
    file_get_contents($url, false, $context);
}

?>

很多人写测试脚本的时候,喜欢在开始结束部分加上时间,这样相减就得到了代码实际运行的时间,其实这是不必要的,利用Linux自带的time就可以获取运行时间:

shell> time php /path/to/php/file

按春哥说的,理论上FFI应该更高效,不过从我的测试结果看,lua-zlib比lua-files更快一些,这是因为目前的FFI还不能完整编译LUA代码,新版本会好些。

此条目由老王发表在Technical分类目录,并贴了Lua、Nginx标签。将固定链接加入收藏夹。

NGINX与GZIP请求》上有13条评论

  1. agentzh在2013-09-0309:27:36说道:

    当使用 FFI 的时候,只有当你的 Lua 代码确实被 JIT 编译才有可能比使用 CFunction 的 Lua 绑定更快,否则在 LuaJIT 的解释器上运行时肯定更慢。你可以使用 LuaJIT 自带的 jit.v 或者 jit.dump 模块检查你的 Lua 代码有哪些能被 JIT 编译,哪些不能。

    当然,由于 LuaJIT 2.0 的 JIT 编译器缺少很多功能,所以使用 LuaJIT 2.0 的时候就不指望了。可以尝试使用最新的 LuaJIT 2.1,并配合使用 ngx_lua 基于 FFI 实现的新接口 lua-resty-core: https://github.com/agentzh/lua-resty-core

    另外,我们总是可以使用 on-CPU 火焰图分析 CPU 时间在各条代码路径上是如何分配的。

    回复 
    • 老王在2013-09-0309:31:34说道:

      谨记春哥教诲!

  2. blogphp230com在2013-09-0311:17:07说道:

    这个讲得是移动端发送gzip请求,服务器端解析。如果是反反过来,服务器段发送的数据是gzip压缩的,移动端需要进行相应解压缩数据吗?现在我们的APP接口数据是json格式的

    回复 
  3. shenzhe在2013-09-1414:32:12说道:

    我比较好奇,直接用php去gzdecode真会影响效率么?理论上request过的的参数一般都比较小。

    回复 
  4. webwlsong在2013-12-0911:52:21说道:

    王哥 我测试的显示用了lua-zlib CPU耗时更长,有时候还会出现长时间的延迟。

    回复 
    • 法拉利在2016-10-1010:26:14说道:

      长时间的延迟?然后CPU一直在等待?

  5. xinster在2014-06-1214:46:34说道:

    你好, 我想请教一下 ,怎么安装lua_zlib , 为什么我在make linux的时候会出现以下问题呢 ?
    make[1]: Entering directory `/opt/script/lua-zlib-0.1′
    gcc -O -shared -fPIC -L/usr/lib -L/usr/lib lua_zlib.o -lz -llua -lm -o zlib.so
    /usr/bin/ld: skipping incompatible /usr/lib/libz.so when searching for -lz
    /usr/bin/ld: skipping incompatible /usr/lib/libz.a when searching for -lz
    /usr/bin/ld: skipping incompatible /usr/lib/libz.so when searching for -lz
    /usr/bin/ld: skipping incompatible /usr/lib/libz.a when searching for -lz
    /usr/bin/ld: /usr/local/lib/liblua.a(lapi.o): relocation R_X86_64_32 against `luaO_nilobject_’ can not be used when making a shared object; recompile with -fPIC
    /usr/local/lib/liblua.a: could not read symbols: Bad value
    collect2: ld returned 1 exit status
    make[1]: *** [zlib.so] Error 1
    make[1]: Leaving directory `/opt/script/lua-zlib-0.1′
    make: *** [linux] Error 2

    回复 
    • hhy在2015-08-1912:20:40说道:

      同问,现在您解决了吗?

    • 在2015-10-1911:01:03说道:

      这个是因为你nginx版本太高的缘故,你nginx版本是多少?我记得只支持到1.7以下

  6. narsi在2015-08-0411:24:04说道:

    Hello,麻烦请问 Nginx 的 ngx_http_gunzip_module 模块是不是就能处理相同的事情了?

    回复 
  7. ZERO在2015-09-2117:01:20说道:

    写的很好,我是一个wordpress爱好者,想问一下博主,如果我使用php(一个缓存插件)生成了静态的压缩文件(xxx.tar.gz),那么nginx的gzip功能是不是就可以关闭了,而且在访问的时候会更加节省CPU(省去了每次gzip),期待回复。。

    回复 
  8. passenger在2016-10-1219:51:21说道:

    Hello, replace_filter無法處理gzip過的php output, 是否有辦法在nginx中即時gunzip php的output呢?

    回复 
  9. 天行者在2016-12-1119:57:46说道:

    想请教下作者一个问题,下面这段代码中,
    local zlib = require(“zlib”)

    — web服务器支持的返回内容压缩编码类型
    — gzip = gzip头(10字节) + deflate编码的实际内容 + gzip尾(8字节)
    local encoding = ngx.req.get_headers()[“Content-Encoding”]

    if encoding == “gzip” then

    local body = ngx.req.get_body_data()
    if body then
    local stream = zlib.inflate()
    local r = stream(body);
    ngx.req.set_body_data(r);
    local jsonObject = json_decode(r)

    else
    local answer = {}
    answer.result = “100000”
    answer.desc = “请求参数不存在”
    ngx.print(json.encode(answer))
    return
    end
    else
    ngx.print(“body未经过gzip压缩”)
    ngx.exit(ngx.HTTP_OK)
    return
    end
    ================
    那个jsonObject返回的是个nil值,还望作者指点一番。卡在这地方要久了。谢谢


相关内容

    暂无相关文章