openresty是以ngx_lua模块为核心的nginx套件,详见春哥项目 http://.org/ 。
nginx充分利用epoll,擅长处理高并发;而lua作为天生的胶水语言,开发简单。两者结合起来,可以很容易实现以前PHP几乎不能完成的应用。

最近在某游戏激活码抢号专题中,有个场景并发较高,可虑采用lua做PHP应用层的防火墙。

抢号专题,前期评估预计2万人参加,并发峰值按20k来扛,网页前端采用随机延时发请求来减轻负载,实际产生的并发连接大概在0.5k。

架构方面,两台webserver + 一台mysql,32核,36G内存。其中一台webserver为主,起有memcache作为分布式session以及data cache,redis做队列,使用nginx反代做简单的负载均衡。内核参数皆已调优。

根据各种参数组合的基准压测,发现fpm响应在1ms,单核RPS为1K左右,32核可以跑到30K+,这是我实测见过的最高PHP 单机基准RPS了。但是nginx最高却只跑到25k,多方求解,至今无果。

总得来说,我还是相信nginx比fpm更加健壮以及更少的资源占用。 下面是我们在nginx层面的lua实践。


1
2
3
4
5
6
7
8
9
10
11
12
vim /usr/local/openresty/<a href="http://chuyinfeng.com/tag/nginx" class="st_tag internal_tag" rel="tag" title="标签 Nginx 下的日志">nginx</a>/conf/<a href="http://chuyinfeng.com/tag/nginx" class="st_tag internal_tag" rel="tag" title="标签 Nginx 下的日志">nginx</a>.conf
 
http {
 
    # access dict,初始化使用到的共享内存
    <a href="http://chuyinfeng.com/tag/lua" class="st_tag internal_tag" rel="tag" title="标签 Lua 下的日志">lua</a>_shared_dict access_whitelist 1m;
    <a href="http://chuyinfeng.com/tag/lua" class="st_tag internal_tag" rel="tag" title="标签 Lua 下的日志">lua</a>_shared_dict access_blacklist 1m;
    <a href="http://chuyinfeng.com/tag/lua" class="st_tag internal_tag" rel="tag" title="标签 Lua 下的日志">lua</a>_shared_dict access_iplist 40m;
    <a href="http://chuyinfeng.com/tag/lua" class="st_tag internal_tag" rel="tag" title="标签 Lua 下的日志">lua</a>_shared_dict access_total 1m;
 
    # access 访问控制lua脚本
    access_by_<a href="http://chuyinfeng.com/tag/lua" class="st_tag internal_tag" rel="tag" title="标签 Lua 下的日志">lua</a>_file /usr/local/openresty/lualib/com/access.<a href="http://chuyinfeng.com/tag/lua" class="st_tag internal_tag" rel="tag" title="标签 Lua 下的日志">lua</a>;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
vim /usr/local/openresty/lualib/com/access.lua
 
--[[
-- access.lua, 访问控制
-- @author 楚吟风 <chuyinfeng@gmail.com>
-- @version 1.0
-- @update 2013-04-25
-- @package com
]]
 
-- 静态白名单,从文件加载
local whitelist = ngx.shared.access_whitelist
-- 静态黑名单,从共享内存载入
local blacklist = ngx.shared.access_blacklist
-- IP计数器,从共享内存载入
local iplist = ngx.shared.access_iplist
-- 全局计数器,从共享内存再入
local total = ngx.shared.access_total
-- 获取客户端IP
local ip = ngx.var.remote_addr
-- 单个IP RPS限制
local ip_rps = 50
-- 单个IP RPS 系统防火墙拦截标准
local iptable_rps = 100
-- 总RPS限制
local total_rps = 3000
-- 重新载入名单
local ctrl_path = '/access_ctrl/reload/'
-- 防火墙静态黑白名单存放路径
local file_path = '/usr/local/openresty/lualib/com/access/'
-- ip认证
local auth = {
    ['/admin/'] = {
        '192.168.20.15'
    }
}
-- ip 认证
local is_banned = false
local uri = string.lower(ngx.var.request_uri)
for path, iplist in pairs(auth) do
    local i, _ = string.find(uri, path)
    if i == 1 then
        is_banned = true
        for _, cip in pairs(iplist) do
            if string.find(ip, cip) then
                is_banned = false
            end
        end
    end
end
if is_banned then
    --ngx.header['Content-Type'] = 'text/plain'
    --ngx.say(ip .. ' is banned')
    --ngx.exit(ngx.HTTP_OK);
end
 
-- 从文件载入字典
function load_file_to_dict(file, dict)
    dict:flush_all()
    local fh = io.open(file, 'r')
    for line in fh:lines() do
        dict:set(line, '')
    end
    fh:close()
end
 
-- 载入静态名单
if (whitelist:get('0.0.0.0') == nil) or (ngx.var.request_uri == ctrl_path) then
    load_file_to_dict(file_path .. 'whitelist.txt', whitelist)
    load_file_to_dict(file_path .. 'blacklist.txt', blacklist)
end
 
--[[
-- is_block, 检测当前IP是否被屏蔽
]]
function is_block()
    -- 如果在静态白名单,则直接放行
    if whitelist:get(ip) then
        return false
    end
 
    -- 如果在静态黑名单,则拦截
    if blacklist:get(ip) then
        return {
            ['status'] = 1,
            ['tips'] = (ip .. ' is in blacklist, please contact us'),
        }
    end
    -- 当前IP请求次数加1
    local ip_times = iplist:incr(ip, 1)
    -- 如果访问记录为空,则设置访问次数为1
    if ip_times == nil then
        ip_times = 1
        iplist:set(ip, 1, 1)
    end
 
    -- 如果请求频率超过单个IP系统防火墙限制,则写入防火墙名单
    if ip_times == iptable_rps then
        local file = io.open(file_path .. 'blocklist.txt', 'a')
        file:write("\r\n" .. ip)
        file:close()
        ngx.say('will in iptables');ngx.exit(ngx.HTTP_OK)
    end
 
    -- 如果请求频率超过单个IP限制则封禁,超过多少个封禁多少秒
    if ip_times > ip_rps then
        local sec = (ip_times - ip_rps)
        iplist:set(ip, ip_times, sec)
        return {
            ['status'] = 2,
            ['tips'] = ip .. ' is  blocked for ' .. sec .. ' seconds.',
            ['sec'] = sec,
        }
    end
 
-- 全局请求次数加1
    local total_times = total:incr('total', 1)
    if total_times == nil then
        total_times = 1
        total:set('total', 1, 1)
    end
 
    -- 全局请求拦截
    if total_times > total_rps then
        return {
            ['status'] = 3,
            ['tips'] = total_times .. ' is request, please wait for a moment',
            ['total'] = total_times,
        }
    end
end
 
-- 主函数
function main()
    local block = is_block()
    if block then
        ngx.req.set_header("Content-Type", "text/plain")
        --ngx.say(block['status'])
        ngx.say(block['tips'])
        ngx.exit(ngx.HTTP_OK)
    end
end
main()

压测结果显示,采用ngx_lua,与原生nginx性能几无差别,应用层防护效果非常显著。