做个技术宅

记一次lua网关层对服务器性能的优化

随着公司业务的增长,公司三台四核8g服务器的负载和CPU日益见长,终于到了100%老大想起要优化一下了,但是之前该优化的都优化了,sql和PHP代码层。实在找不出其他优化的方案,并且所做的优化根本无济于事。
通过观察nginx请求日志,发现有2个接口被刷了,刷的特别厉害,并发上3000了,于是购买了cc防护,但是效果甚微,因为都是合法请求。我们也查明白了原因,实际上并不是被恶意刷接口了,而是用户使用“外挂”来听单和查看是否有新的单子,“听单”实质就是用户点击按钮告诉服务器他在线,他可以接单,但是我们做了限制的,每人每天最多可听12次,每次听单有效时长为1小时,到了时间自动停止,代码实现也很简单,点击听单,uid存redis,expiry time为1小时。还有一个待接单列表的接口,每次调用都会去查mysql查出用户的单子。就是这两个接口被刷了,统计过一台服务器的日志,5分钟这两个接口被请求42000次!
统计.png
我粗略统计过,大概有七八千人使用外挂,并且这个数据每天都在增长,后来我们找到了那个“外挂”APP,逻辑也很简单,就是向我们服务器发请求,用户可以设置请求时间,最低是2秒向服务器发起请求,七八千人一起使用的话,这个请求量对我们来说是巨大的。向老板反映,但是老板不让禁用户,因为这个对我们的业务没有坏处,反而有一定益处,但是目前也不让加服务器,因为上次加的一台刚过半个月负载又满了,所以老板还是让我们技术来做优化,我们老大又把这棘手的茬抛给了我。
老大给我推荐了lua,说这个应该可以有用,我得知后就去看了一下,它是介于nginx和服务层之间的一个网关,它的脚本可以直接嵌在nginx配置文件里面。说简单点就是当一个PHP请求进来后,可以让请求先到网关层做一次筛选,再决定要不要把这个请求抛给PHP去处理。很显然,这正是我们想要的,不能一概的使用禁止用户和限流,只能根据业务需求来智能限流。
说干就干,当天晚上看了一下语法,还是比较简单,然后开始装环境,环境用的是openresty,openresty里面集成了nginx和lua以及一些主要的lua模块。这里就不阐述安装教程了,官网有,安装很简单,http://openresty.org/cn/
安装好了后,做一些准备工作就开始写脚本,准备工作就是如何获取传递过来的头信息和参数,以及解密token获取uid,再通过uid去查redis,还好这些在默认安装的模块里都可以实现,然后就开始写业务代码。这里贴一下我的lua代码:

--获取传递过来的token
function getToken()
--获取头信息token
    headers = ngx.req.get_headers()
    token = headers['token']
    if not token then
        ngx.say("认证失败1")
        ngx.eof()
    else
        return token
    end
end
--字符串分割数组方法
function string.split(input, delimiter)
    input = tostring(input)
    delimiter = tostring(delimiter)
    if (delimiter=='') then ngx.say("认证失败2") ngx.eof() end
    local pos,arr = 0, {}
    -- for each divider found
    for st,sp in function() return string.find(input, delimiter, pos, true) end do
        table.insert(arr, string.sub(input, pos, st - 1))、
        pos = sp + 1
    end
    table.insert(arr, string.sub(input, pos))
    if #arr ~= 2 then
        ngx.say("请求不合法")
        ngx.eof()
    end
    return arr
end
--解析uid
function getUid(token)
    json = require("cjson")
    arr = string.split(token, '.')
    base64 = arr[1]
    str = ngx.decode_base64(base64)
    data = json.decode(str)
    uid = data['user_id']
    if not uid then
        ngx.say("认证失败3")
        ngx.eof()
    else
        return uid
    end
end
token = getToken()
uid = getUid(token)
--连接redis
local redis = require "resty.redis"
local rediscache = redis.new()
rediscache:set_timeout(1000)
rediscache.connect(rediscache, 'xxip', 'xxport')
rediscache:auth('xxpwd')
rediscache:select(15)
local exists = rediscache:exists('wait_order_time_'..uid)
rediscache:close()
if exists == 1 then
    ngx.say('{"code":0,"msg":"听单成功!","data":{}}')
    ngx.eof()
end

然后还得配置一下nginx的配置文件:WechatIMG68.png
/task/order/wait是接口路由
access_by_lua_file 是指在请求访问阶段处理,用于访问控制(这里lua有七个阶段,具体用哪个看业务需求,一定不能弄错,我这里使用的是访问控制,后面是可以加try-files的)
try-files 请求经过lua处理后继续执行你的PHP文件
(具体的阶段的区别可以看看这篇文章https://www.jianshu.com/p/1bb7814b4b88
重启nginx服务
再次请求接口,请求就会通过lua文件了
下面是优化前后的服务器状态对比
高.png
最低.png
走势.png

回复

This is just a placeholder img.