☝点击上方蓝字,关注我们!
本文字数: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
2019-04-18
2018-08-16
加入搜狐技术作者天团
千元稿费等你来!
戳这里!☛