针对接口进行总流量限制(熔断)

有些朋友可能会联想到用Nginx自带的limit_req功能来限制访问频率, 这种方案通常是基于IP限制, 是为了防止用户恶意的刷请求量访问. 这里我要介绍的是另一种情况, 接口的qps已经很高, 后端无法接受更多的请求. 这个是时候需要的就是针对整个接口进行限速.

我贴一种比较简单的实现:

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
local limit = {}

local function safe_incr(dict, key, timeout)
local ok
local newval, err = dict:incr(key, 1)
if not newval and err == "not found" then
newval = 1
ok, err = dict:safe_add(key, newval, timeout)
if not ok then
if err == "exists" then
newval, err = dict:incr(key, 1)
elseif err == "no memory" then
dict:add(key .. "|no memory", 0, timeout)
end
end
end
return newval
end

function limit.limit(dict, key, freq, err_code, err_msg, content_type)
dict:set(key .. '|freq', freq)

local time = ngx.time()
local k = key .. '|' .. tostring(time)
local newval = safe_incr(dict, k, 70)

if newval > freq then
-- local limit_info = ngx.shared.limit_info
-- safe_incr(limit_info, k, 70)

ngx.sleep(1)
ngx.status = err_code
ngx.header.content_type = content_type or 'application/json'
ngx.print(err_msg)
ngx.exit(ngx.status)
end
end

return limit

然后在Nginx的配置中这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lua_shared_dict xxx_0 10M;
server {
listen 80;
server_name aaa.bbb.com;

location = /redis/incr {
access_by_lua_block {
local limit = require("limit")
limit.limit(ngx.shared.xxx_0, 'PSAvcmVkaXMvaW5jcg==', 5, ngx.HTTP_FORBIDDEN, '{"reason": "请求排队中,请稍后重试……", "code": 500}', 'application/json')
}
proxy_pass http://xxxxx;
}
}

强制重定向https功能的实现

对于一般的网站, 重定向https一般是通过rewrite来实现的, 比如下面这样的配置:

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
...
return 301 https://$server_name$request_uri;
}

server {
listen 443;
...
}

这样的配置只适合放在最外层的Nginx, 假如没有放在用户访问的的入口Nginx, 那么会出现类似下图所示的循环重定向问题.

所以, 出现多层Nginx的情况, 最简单的方案就是在首层Nginx进行https重定向, 内部全部使用http流量.

但是考虑下面一种情况:

而且对于一个PaaS平台来说, 并不是所有的应用都想要强制https, 首层的Nginx也不由我们来控制, 不过它会给我们传递一个这样的header, 表明请求为https的形式:X-Forwarded-Proto: https,

这种时候比较合适的选择是在平台的这一层Nginx中增加对于请求方法的判断, 根据header中的forwarded-proto, 有两种方案在中间层解决:

  1. 一种使用Nginx的if语句
1
2
3
4
5
6
7
8
9
server{
listen *:80;
server_name mydomain.com www.mydomain.com;

if ($http_x_forwarded_proto = "http") {
return 301 https://$server_name$request_uri;
}
// location xxx
}
  1. 一种更加优雅的方案(使用Lua):
1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80;
add_header Strict-Transport-Security "max-age=31536000" always;
# 使用lua能使得逻辑更加清晰
rewrite_by_lua_block {
local _request_uri = ngx.var.request_uri
local _host = ngx.var.host
if ngx.var.http_x_forwarded_proto == nil or ngx.var.http_x_forwarded_proto == 'http' then
return ngx.redirect('https://'.._host.._request_uri, ngx.HTTP_MOVED_PERMANENTLY)
end
}
// location xxx
}

由于我们的项目本身已经用了Nginx+Lua, 所以直接采用了第二种方案, 如果你使用的是普通的Nginx, 就用if语句吧.