Nginx与Gzip请求

前些天,移动端的同事跑来问:某些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代码,新版本会好些。

Nginx与Gzip请求》上有13个想法

  1. 当使用 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 时间在各条代码路径上是如何分配的。

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

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

  4. 你好, 我想请教一下 ,怎么安装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

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

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

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

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

  8. 想请教下作者一个问题,下面这段代码中,
    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值,还望作者指点一番。卡在这地方要久了。谢谢

发表评论

电子邮件地址不会被公开。 必填项已用*标注