openresty是以ngx_lua模塊為核心的nginx套件,詳見春哥項目 http://.org/ 。
nginx充分利用epoll,擅長處理高并發(fā);而lua作為天生的膠水語言,開發(fā)簡單。兩者結(jié)合起來,可以很容易實現(xiàn)以前PHP幾乎不能完成的應(yīng)用。

最近在某游戲激活碼搶號專題中,有個場景并發(fā)較高,可慮采用lua做PHP應(yīng)用層的防火墻。

搶號專題,前期評估預(yù)計2萬人參加,并發(fā)峰值按20k來扛,網(wǎng)頁前端采用隨機延時發(fā)請求來減輕負載,實際產(chǎn)生的并發(fā)連接大概在0.5k。

架構(gòu)方面,兩臺webserver + 一臺mysql,32核,36G內(nèi)存。其中一臺webserver為主,起有memcache作為分布式session以及data cache,redis做隊列,使用nginx反代做簡單的負載均衡。內(nèi)核參數(shù)皆已調(diào)優(yōu)。

根據(jù)各種參數(shù)組合的基準壓測,發(fā)現(xiàn)fpm響應(yīng)在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,初始化使用到的共享內(nèi)存
    <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 楚吟風(fēng) <chuyinfeng@gmail.com>
-- @version 1.0
-- @update 2013-04-25
-- @package com
]]
 
-- 靜態(tài)白名單,從文件加載
local whitelist = ngx.shared.access_whitelist
-- 靜態(tài)黑名單,從共享內(nèi)存載入
local blacklist = ngx.shared.access_blacklist
-- IP計數(shù)器,從共享內(nèi)存載入
local iplist = ngx.shared.access_iplist
-- 全局計數(shù)器,從共享內(nèi)存再入
local total = ngx.shared.access_total
-- 獲取客戶端IP
local ip = ngx.var.remote_addr
-- 單個IP RPS限制
local ip_rps = 50
-- 單個IP RPS 系統(tǒng)防火墻攔截標準
local iptable_rps = 100
-- 總RPS限制
local total_rps = 3000
-- 重新載入名單
local ctrl_path = '/access_ctrl/reload/'
-- 防火墻靜態(tài)黑白名單存放路徑
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
 
-- 載入靜態(tài)名單
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, 檢測當(dāng)前IP是否被屏蔽
]]
function is_block()
    -- 如果在靜態(tài)白名單,則直接放行
    if whitelist:get(ip) then
        return false
    end
 
    -- 如果在靜態(tài)黑名單,則攔截
    if blacklist:get(ip) then
        return {
            ['status'] = 1,
            ['tips'] = (ip .. ' is in blacklist, please contact us'),
        }
    end
    -- 當(dāng)前IP請求次數(shù)加1
    local ip_times = iplist:incr(ip, 1)
    -- 如果訪問記錄為空,則設(shè)置訪問次數(shù)為1
    if ip_times == nil then
        ip_times = 1
        iplist:set(ip, 1, 1)
    end
 
    -- 如果請求頻率超過單個IP系統(tǒng)防火墻限制,則寫入防火墻名單
    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
 
-- 全局請求次數(shù)加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
 
-- 主函數(shù)
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()

壓測結(jié)果顯示,采用ngx_lua,與原生nginx性能幾無差別,應(yīng)用層防護效果非常顯著。