干货!OpenResty实战应用

564 阅读24分钟
原文链接: mp.weixin.qq.com

☝点击上方蓝字,关注我们!

本文字数:4680

预计阅读时间:30分钟

导读

Nginx[1]是一个HTTP和反向代理服务器,因其占用资源少、高并发、稳定性等优点被互联网公司广泛使用。根据Netcraft发布了Web服务器调查报告,可以看出 Nginx使用非常广泛。不过Nginx是使用C语言开发的,对Nginx进行二次开发开发门槛高、效率低。OpenResty将Lua嵌入到Nginx中,使用OpenResty提供Lua Ngx API可以提高开发效率,并保持高性能。本文主要介绍我们在生产环境中,基于OpenResty在黑名单、限流、ABTest、服务质量监控等方面的应用。

什么是OpenResty?

OpenResty官网描述如下:

OpenResty®是一个基于Nginx与Lua的高性能Web平台,其内部集成了大量精良的Lua库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态Web应用、Web服务和动态网关。

OpenResty®通过汇聚各种设计精良的Nginx模块(主要由OpenResty团队自主开发),从而将Nginx有效地变成一个强大的通用Web应用平台。这样,Web开发人员和系统工程师可以使用Lua脚本语言调动Nginx支持的各种C以及Lua模块,快速构造出足以胜任10K乃至1000K以上单机并发连接的高性能Web应用系统。

OpenResty®的目标是让你的Web服务直接跑在Nginx服务内部,充分利用Nginx的非阻塞I/O模型,不仅仅对HTTP客户端请求,甚至于对远程后端诸如MySQL、PostgreSQL、Memcached以及Redis等都进行一致的高性能响应。

OpenResty已经被很多公司使用,使用提供的Lua Ngx API、以及大量的Nginx模块进行开发有以下优点:提高Nginx开发效率(Nginx使用C语言开发,需要使用C语言进行二次开发)、减少代码量、方便维护等。

本文主要介绍我们在生产环境中,基于OpenResty在以下几方面的应用:黑名单、限流、ABTest、服务质量监控。本文不着重解释一些原理,重点放在实践上。

黑名单

为了防止恶意用户或者爬虫请求服务器,从而造成对正常请求的影响,一般会为这些用户创建一个黑名单,阻止访问。在OpenResty access_by_lua*指令处于请求访问阶段,用于访问控制。我们将代码黑名单代码使用access_by_lua*执行。本文提供了以下三种添加黑名单的方法:

静态黑名单

该方法将黑名单配置在lua文件中。

  • Nginx配置示例:

1location /lua {2    default_type 'text/html';3    access_by_lua_file /path/to/access.lua;4    content_by_lua 'ngx.say("hello world")';5}
  • lua代码示例:

 1-- 加入要限制的黑名单,例如IP黑名单 2local blacklist = { 3    ["10.10.76.111"] = true, 4    ["10.10.76.112"] = true, 5    ["10.10.76.113"] = true 6} 7 8local ip = ngx.var.remote_addr 9if blacklist[ ip ] then10    return ngx.exit( ngx.HTTP_FORBIDDEN )11end

上面的方式直接将黑名单放在lua table中,每个请求都查询lua table中是否包含,若是则阻止访问后端服务器,返回HTTP_FORBIDDEN。该方案每次添加删除会修改配置文件和Reload Nginx,不太适合频繁操作。下面提供一种动态黑名单方法。

动态黑名单(一)

将黑名单放在Redis服务器中,对于每一个请求,获取参数,查询Redis是否存在,若存在和上面方案类似返回HTTP_FORBIDDEN。

  • lua代码示例:

 1local redis = require "resty.redis" 2local redis_host = "127.0.0.1" 3local redis_port = 6379 4 5local red = redis:new() 6red:set_timeout(100) 7local ok, err = red:connect(redis_host, redis_port) 8if not ok then 9    return10end1112local cid = ngx.var.arg_cid13local res, _ = red:get( cid )14if res and res ~= ngx.null then15    return ngx.exit( ngx.HTTP_FORBIDDEN )16end

该方案引入了lua-resty-redis模块,用于判断参数cid是否在存放黑名单的Redis服务器中。该方案避免黑名单更新而修改配置和Reload Nginx,但每次请求都要访问Redis服务器,下面的方案对此进行优化。

动态黑名单(二)

引入OpenResty提供的共享内存ngx.shared.DICT,用于存放所有黑名单,并定期从Redis更新避免每次请求均访问Redis,提高性能。

  • Nginx配置示例:

 1http { 2 3    lua_shared_dict blacklist 1m;  4    init_worker_by_lua_file "/path/to/init_redis_blacklist.lua"; 5    server { 6        listen       2019; 7 8        location /redis/blacklist/dynamic { 9             default_type 'text/html';10            access_by_lua_file "/path/to/redis_blacklist_dynamic.lua";11            content_by_lua_block{12                ngx.say('hello 2019')13            }14        }15    }16}

首先,使用lua_shared_dict指令定义变量blacklist,并分配1M大小的共享内存空间,用于存放黑名单(共享内存的大小根据具体情况评估,适当设置)。

其次,使用init_worker_by_lua_file指令执行init_redis_blacklist.lua,该代码执行定时器,定期从Redis拉取黑名单数据,注意lua_shared_dict和init_worker_by_lua_file指令的环境均在http块。

  • init_redis_blacklist.lua代码如下:

 1local redis = require "resty.redis" 2local redis_host = "127.0.0.1" 3local redis_port = 6379 4local delay = 5 5 6local redis_key = "blacklist" 7local blacklist = ngx.shared.blacklist 8 9local function timer_work( delay, worker )10    local timer_work1112    timer_work = function (premature)13        if not premature then14            local rst, err_msg = pcall( worker )15            if not rst then16                ngx.log(ngx.ERR, "timer work:", err_msg)17            end18            ngx.timer.at( delay, timer_work )19        end20    end2122    ngx.timer.at( delay, timer_work )23end2425local function update_blacklist()26    local red = redis:new()27    local ok, err = red:connect(redis_host, redis_port)28    if not ok then29        ngx.log(ngx.ERR, "redis connection error: ", err)30        return31    end3233     local new_blacklist, err = red:smembers(redis_key)34     if err then35        ngx.log(ngx.ERR, "Redis read error: ", err)36        return37    end3839    blacklist:flush_all()4041    for _, k in pairs(new_blacklist) do42        blacklist:set(k, true);43    end4445end4647if 0 == ngx.worker.id() then48    timer_work( delay, update_blacklist )49end

timer_work函数调用OpenResty提供的API ngx.timer.at,创建反复循环使用的定时器。(我们将定时器仅绑定在worker.id为0的进程上)定时器每隔delay秒执行一次update_blacklist函数,update_blacklist会连接Redis服务器,调用smember获取集合redis_key中的所有成员,先使用ngx.shared.DICT.flush_all清空共享内存blacklist中数据后,遍历new_blacklist将数据添加到blacklist中。

最后,在对应的location中,添加access_by_lua_file指令,执行redis_blacklist_dynamic.lua,代码如下,和前面静态黑名单lua代码类似:

1local blacklist = ngx.shared.blacklist23local function run()4    local cid = ngx.var.arg_cid5    if blacklist:get( cid ) then6        return ngx.exit( ngx.HTTP_FORBIDDEN )7    end8end

小结

以上列举了三种添加黑名单的方法,可以根据具体情况选择:

  • 第一种方法静态黑名单,配置简单,不依赖Redis,但不适合频繁添加和修改;

  • 第二种动态黑名单方法,对每一个请求访问Redis,相对于第三种方案,黑名单实时性较强,但是每次都通过网络访问Redis;

  • 第三种方案将黑名单存放在共享内存中,定期更新,避免每次请求都访问Redis,提高性能。

限流

限流的目的又很多,在本文提及的限流主要用于:防止非用户攻击、正常突发流量保护。

Nginx提供了模块ngx_http_limit_req_module[2]和ngx_http_limit_conn_module[3]分别用于控制速率和控制并发连接数。详细可以参考官网。下面主要介绍lua-resty-limit-traffic和lua-resty-redis-ratelimit模块的在线上的应用。

lua-resty-limit-traffic

我们使用lua-resty-limit-traffic[4]来限制location的请求速率,使upstream集群的请求速率在预估负载范围内,避免突发流量导致upstream集群被压垮(lua-resty-limit-traffic包含了resty.limit.req、resty.limit.count、resty.limit.conn以及resty.limit.traffic四个模块,以下例子仅使用了其中的resty.limit.req模块)。

  • nginx配置示例:

 1http { 2 3    lua_shared_dict location_limit_req_store 1m;  4 5    server { 6        listen       2019; 7 8        location /limit/traffic { 9            access_by_lua_file "/path/to/limit_traffic.lua";10            default_type 'text/html';11            content_by_lua_block{12                ngx.say('hello 2019')13            }14        }15    }16}

首先,使用lua_shared_dict指令定义变量location_limit_req_store,并分配1M大小的共享内存空间,用于限流。(共享内存的大小根据具体情况评估,适当设置)。其次,将limit_traffic.lua代码配置在要限流的location中,用访问控制指令access_by_lua_file执行。

  • limit_traffic.lua代码示例如下:

 1local limit_req = require "resty.limit.req" 2local json = require "cjson" 3 4local rate = 1 5local burst = 1 6 7local function do_limit() 8    local message = { 9        message = "Too Fast",10    }11    ngx.header.content_type="application/json;charset=utf8"12    ngx.say(json.encode(message))13    return ngx.exit(ngx.HTTP_OK)14end1516local function location_limit_traffic()17    local reject = false1819    local lim, err = limit_req.new("location_limit_req_store", rate, burst)20    if not lim then21        ngx.log(ngx.ERR, "init failed! err: ", err)22        return reject23    end2425    local limit_key = "location_limit_key"26    local delay, err = lim:incoming(limit_key, true)27    if not delay then28        if err == "rejected" then29            reject = true30        end3132        return reject33    end3435    if delay > 0 then36        ngx.sleep(delay)37    end3839    return reject40end41local reject = location_limit_traffic()42if reject then43    do_limit()44end

该示例为了方便演示,将平均速率rate和桶容量burst均设置为1,线上环境根据具体情况设置。变量limit_key 设置成一个字符串,对于每个请求都相同,该场景是为了保护Nginx代理到后端的请求控制在一定的范围内。也可以根据其他维度进行限流,例如IP,那么limit_key可以赋值为ngx.var.binary_remote_addr。

我们在使用resty.limit.req时,当请求被限流时,并没有马上ngx.HTTP_FORBIDDEN, 而是结合我们业务需求,返回HTTP_OK即200,并附带Json格式信息作为提示。

TIPS:Nginx作为代理, 一般将多个部署在不同机器上的Nginx作为集群。每个Nginx是相互独立的,所以在Nginx集群上对某一个location做使用resty.limit.req做限流时,每个Nginx上限制的状态不能共享,rate应该设置约为N/M,其中N location对应upstream集群能够承受负载的上限,M为Nginx的个数。

lua-resty-redis-ratelimit

在请求的参数中有一个参数,例如unique_id,来标识用户,所有请求会均匀的转发Nginx集群中,每个用户的请求会被转发到不同的Nginx进行代理,需要做跨机器速率限制,resty.limit.req做不到。我们使用lua-resty-redis-ratelimit模块来对用户进行限流,该模块将信息保存在Redis中,可以实现Nginx实例共享限流状态,跨机器速率限制。

  • nginx配置示例:

 1http { 2 3    server { 4        listen       2019; 5 6        location /redis/ratelimit { 7            access_by_lua_file "/path/to/redis_ratelimit.lua"; 8            default_type 'text/html'; 9            content_by_lua_block{10                ngx.say('hello 2019')11            }12        }13    }14}

和lua-resty-limit-traffic模块类似,使用指令access_by_lua_file执行限流代码redis_ratelimit.lua。

  • redis_ratelimit.lua代码如下:

 1local ratelimit = require "resty.redis.ratelimit" 2local json = require "cjson" 3 4local redis = { 5        host = "127.0.0.1", 6        port = 6379, 7        timeout = 0.02 8} 910local rate = "1r/s"11local burst = 012local duration = 11314local function do_limit()1516    local message = {17        message = "Too Fast",18    }1920    ngx.header.content_type="application/json;charset=utf8"21    ngx.say(json.encode(message))2223    return ngx.exit( ngx.HTTP_OK )2425end2627local function user_rate_limit()2829    -- 限流参数根据实际情况而定30    local limit_key = ngx.var.arg_unique_id31    if limit_key == nil then32        return33    end3435    local reject = false3637    local lim, err = ratelimit.new("user-rate", rate, burst, duration)38    if not lim then39        ngx.log(ngx.ERR, "failed to instantiate, err: ", err)40        return reject41    end4243    local rds = {44            host = redis.host,45            port = redis.port,46            timeout = redis.timeout47    }4849    local delay, err = lim:incoming(limit_key, rds)50    if not delay then51        if err == "rejected" then52            reject = true53        end54        return reject55    end5657    if delay >= 0.001 then58        ngx.sleep(delay)59    end6061    return reject6263end6465local reject = user_rate_limit()66if reject then67    do_limit()68end

该示例为了方便演示,将平均速率rate、桶容量burst、延迟duration分别设置为设置为"1r/s"、0、1,线上环境根据具体情况设置。url中的参数unique_id的值赋值给变量limit_key,将user-rate:limit_key作为唯一标识存于Redis。当一个用户访问频率超过设定的rate后就会返回一个Json信息,提示用户刷新太快。

ABTest

利用OpenResty可以很容易实现ABTest,下面的例子使用和Nginx set对应指令set_by_lua*,通过该指令设置Nginx变量,可以实现赋值逻辑,根据不同url中参数cid的不同将请求分发到不同的upstream集群。

  • 首先看看nginx配置示例:

 1http { 2 3    upstream pool_1{ 4        server 0.0.0.0:2020; 5    } 6 7    upstream pool_2{ 8        server 0.0.0.0:2021; 9    }1011    server {12        listen       2019;1314        location /select/upstream/according/cid {15            set_by_lua_file $selected_upstream "/path/to/select_upstream_by_cid.lua" "pool_1" "pool_2";16            if ( $selected_upstream = "" ){17                proxy_pass http://pool_1;18            }1920            proxy_pass http://$selected_upstream;21        }2223    }2425}

set_by_lua_file指令将"pool_1"、"pool_2"作为参数传递到lua代码select_upstream_by_cid.lua中,select_upstream_by_cid.lua的返回值,初始化变量selected_upstream。返回空字符串时,在nginx conf中做特殊处理,proxy_pass默认代理到pool_1,否则代理到selected_upstream。

  • select_upst_by_cid.lua逻辑如下:

 1    local first_upstream  = ngx.arg[1] 2    local second_upstream = ngx.arg[2] 3 4    local cid = ngx.var.arg_cid 5    if cid == nil then 6        return "" 7    end 8 9    local id = tonumber(cid)1011    if id == nil then12        return ""13    end1415    if id % 2 == 0 then16        return first_upstream17    end1819    return second_upstream

获取参数cid,将cid转换成数字类型,取模运算,根据结果返回参数,从而将请求按照cid分流到不同upstream。

利用OpenResty提供很多的模块或指令进行ABTest有很多玩法:例如模块lua-upstream-nginx-module[5]和balancer[6]提供一些API可以对upstream进行动态管理,可以将upstream信息存于Redis、Consul等服务器中,利用OpenResty提供API实现动态分流,可以参考开源项目ABTestingGateway[7]

服务质量监控

使用OpenResty提供的API对Nginx进行服务质量监控,有实时、占用资源少等特点。通过ngx.var.VARIABLE获取Nginx内置变量,例如:request_time、upstream_response_time、upstream_status等,所有的Nginx内置变量在这里:Alphabetical index of variables.[8]。本文只统计几个,可以根据需求定制。该统计模块代码在目录nginx_metric中,包含三个文件:nginx_metric.lua、nginx_metric_output.lua和metric.lua。

  • 首先看看nginx配置示例:

 1http { 2 3    lua_package_path "/path/to/nginx_metric/?.lua;;"; 4    lua_shared_dict nginx_metric 1m; 5    log_by_lua_file "/path/to/nginx_metric/nginx_metric.lua"; 6 7    upstream test{ 8        server 0.0.0.0:2020; 9    }1011    server {12        listen       2019;1314        location /metric {15            proxy_pass http://test;16        }1718        location /nginx/metric/output {19            default_type 'application/json';20            content_by_lua_file "/path/to/nginx_metric/nginx_metric_output.lua";21        }2223    }2425    server {26        listen       2020;2728        location /metric {29            default_type 'text/html';30            content_by_lua_block{31                ngx.sleep(1)32                ngx.say('hello 2020')33            }34        }35    }36}

配置分成4步:

1.lua_package_path指令添加nginx_metric路径;

2.lua_shared_dict指令定义共享内存用于存放统计数据;

3.log_by_lua_file指令执行nginx_metric.lua代码,对每一个请求在log阶段进行数据统计。这段代码是统计的核心,log_by_lua*是请求的最后阶段,统计代码只做统计,不影响正常请求。

  • nginx_metric.lua代码如下:

 1local nginx_metric = require "metric" 2 3local dict = ngx.shared.nginx_metric 4local item_sep = "|" 5local exptime = 3600 * 24 --second 6 7local metric_prefix = ngx.var.proxy_host 8if metric_prefix == nil then 9    return10end1112nginx_metric = nginx_metric:new(dict, item_sep, metric_prefix, exptime)13nginx_metric:record()

Nginx常常用于反向代理,该示例中对配置了upstream的location每一个请求进行统计。ngx.var.proxy_host获取proxy_pass指令定义的变量。对只配置了upstream的location每一个请求进行统计,对没有配upstream的location,不进行统计,对参数metric_prefix可以根据需求定制。实现统计的是metric.lua代码,以下是统计代码:

  1local _M = {}  2local mt = { __index = _M }  3  4function _M.new(_, dict, item_sep, metric_prefix, exptime)  5    local self = {  6        dict = dict,  7        item_sep = item_sep,  8        metric_prefix = metric_prefix,  9        exptime = exptime, 10    } 11    return setmetatable(self, mt) 12end 13 14function _M.req_sign(self, t) 15    return self.metric_prefix .. self.item_sep .. t 16end 17 18local function dict_safe_incr(dict, metric, value, exptime) 19    if tonumber(value) == nil then 20        return 21    end 22 23    local newval, err = dict:incr(metric, value) 24    if not newval and err == "not found" then 25        local ok, err = dict:safe_add(metric, value, exptime) 26        if err == "exists" then 27            dict:incr(metric, value) 28        elseif err == "no memory" then 29            ngx.log(ngx.ERR, "no memory for nginx_metric add kv: " .. metric .. ":" .. value) 30        end 31    end 32end 33 34local function str_split(inputstr, sep) 35    if sep == nil then 36        sep = "%s" 37    end 38    local t={} ; i=1 39    for str in string.gmatch(inputstr, "([^"..sep.."]+)") do 40        t[i] = str 41        i = i + 1 42    end 43    return t 44end 45 46local function add(dict, metric, value, exptime) 47    dict_safe_incr(dict, metric, tonumber(value), exptime) 48end 49 50-- request count 51function _M.request_count(self) 52    local status_code = tonumber(ngx.var.status) 53    if status_code < 400 then 54        local metric = self:req_sign("request_count") 55        add(self.dict, metric, 1, self.exptime) 56    end 57end 58 59-- request time 60function _M.request_time(self) 61 62    local metric = self:req_sign("request_time") 63    local req_t = tonumber(ngx.var.request_time) or 0 64    add(self.dict, metric, req_t, self.exptime) 65 66end 67 68-- http error status stat 69function _M.err_count(self) 70 71    local status_code = tonumber(ngx.var.status) 72    if status_code >= 400 then 73        local metric_err_qc = self:req_sign("err_count") 74        local metric_err_detail = metric_err_qc.."|"..status_code 75        add(self.dict, metric_err_detail, 1, self.exptime) 76    end 77 78end 79 80---- upstream time and count 81function _M.upstream(self) 82 83    local upstream_response_time_s = ngx.var.upstream_response_time or "" 84    upstream_response_time_s = string.gsub(string.gsub(upstream_response_time_s, ":", ","), " ", "") 85    --Times of several responses are separated by commas and colons 86 87    if upstream_response_time_s == "" then 88        return 89    end 90 91    local resp_time_arr = str_split(upstream_response_time_s, ",") 92 93    local metric_upstream_count = self:req_sign("upstream_count") 94    add(self.dict, metric_upstream_count, #(resp_time_arr), self.exptime) 95 96    local duration = 0.0 97    for _, t in pairs(resp_time_arr) do 98        if tonumber(t) then 99            duration = duration + tonumber(t)100        end101    end102103    local metric_upstream_response_time = self:req_sign("upstream_response_time")104    add(self.dict, metric_upstream_response_time, duration, self.exptime)105106end107108function _M.record(self)109    self:request_count()110    self:err_count()111    self:request_time()112    self:upstream()113end114115return _M

主要逻辑是通过ngx.var.VARIABLE获取变量值,进行类型转换,并累加。nginx_metric模块代码参考了falcon-ngx_metric[9]

4.添加一个location,例如location /nginx/metric/output,用于查看统计数据。nginx_metric_output.lua代码获取共享字典中的数据,整理成以upstream为key,以Json格式返回数据。代码如下:

 1local json = require("cjson") 2local nginx_metric = ngx.shared.nginx_metric 3 4local function output() 5    local keys = nginx_metric:get_keys() 6    local res = {} 7 8    for _, k in pairs(keys) do 910        local value = nginx_metric:get(k)11        local s, e = string.find(k, '|')12        local upst_name = string.sub(k, 1, s -1)13        local metric = string.sub(k,  e + 1)1415        if res[ upst_name ] == nil then16            res[ upst_name ] = {}17        end1819        res[upst_name][metric] = value2021        if string.find(metric, "err_count") then22            if res[upst_name]["err_count"] == nil then23                res[upst_name]["err_count"] = 024            end25            res[upst_name]["err_count"] = res[upst_name]["err_count"] + value26        end2728    end2930    local ret = json.encode( res )31    ngx.status = ngx.HTTP_OK32    ngx.print( ret )33    ngx.exit(ngx.HTTP_OK)3435    return ret36end3738output
  • 演示

执行以下指令:

1curl -v  http://127.0.0.1:2019/nginx/metric

执行以下指令进行查询:

 1curl http://127.0.0.1:2019/nginx/metric/output 2 3{ 4    "test":{ 5        "request_count":1, 6        "upstream_count":1, 7        "upstream_response_time":1.001, 8        "request_time":1.001 9    }10}

request_count为请求数,request_time为响应时间,upstream_count为代理到upstream的请求数,upstream_response_time为upstream server的响应时间,以上数据均是累加总和。要使用该数据进行监控或者告警,可以每隔N秒获取一次,将相邻两次的数据求差、求平均即可求出平均响应时间和QPS。

总结

本文介绍了基于OpenResty在黑名单、限流、ABTest、服务质量监控等方面的应用,使用OpenResty开发具有较高的开发效率,避免了以往使用C语言对Nginx二次开发效率低的问题。OpenResty在业界还有很多方面的应用,本文只是粗略介绍了几个方面,希望对大家有所帮助。

参考: [1].Nginx(http://openresty.org/cn/nginx.html) [2].ngx_http_limit_req_module(http://nginx.org/en/docs/http/ngx_http_limit_req_module.html) [3].ngx_http_limit_conn_module(http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html) [4].lua-resty-limit-traffic(https://github.com/openresty/lua-resty-limit-traffic) [5]lua-upstream-nginx-module(https://github.com/openresty/lua-upstream-nginx-module) [6]balancer(https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/balancer.md) [7]ABTestingGateway(https://github.com/CNSRE/ABTestingGateway) [8]Alphabetical index of variables(http://nginx.org/en/docs/varindex.html) [9]falcon-ngx_metric(https://github.com/GuyCheung/falcon-ngx_metric)

也许你还想看

(▼点击文章标题或封面查看)

搜狐新闻推荐算法 | 呈现给你的,都是你所关心的

2018-08-30

新闻推荐系统的CTR预估模型

2019-04-18

互联网架构演进之路

2018-08-16

加入搜狐技术作者天团

千元稿费等你来!

戳这里!☛