<audio id="audio" title="43 | 灵活实现动态限流限速，其实没有那么难" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1e/00/1e9f66a8020ff7deae491ed0eee45a00.mp3"></audio>

你好，我是温铭。

前面的课程中，我为你介绍了漏桶和令牌桶算法，它们都是应对突发流量的常用手段。同时，我们也学习了如何通过 Nginx 配置文件的方式，来实现对请求的限流限速。不过很显然，使用 Nginx 配置文件的方式，仅仅停留在可用的层面，距离好用还是有不小的距离的。

第一个问题便是，限速的 key 被限制在 Nginx 的变量范围内，不能灵活地设置。比如，根据不同的省份和不同的客户端渠道，来设置不同的限速阈值，这种常见的需求用 Nginx 就没有办法实现。

另外一个更大的问题是，不能动态地调整速率，每次修改都需要重载 Nginx 服务，这一点我们在上节课的最后也提到过。这样一来，根据不同的时间段限速这种需求，就只能通过外置的脚本来蹩脚地实现了。

要知道，技术是为业务服务的，同时，业务也在驱动着技术的进步。在 Nginx 诞生的时代，并没有什么动态调整配置的需求，更多的是反向代理、负载均衡、低内存占用等类似的需求，在驱动着 Nginx 的成长。在技术的架构和实现上，并没有人能够预料到，在移动互联网、IoT、微服务等场景下，对于动态和精细控制的需求会大量爆发。

而 OpenResty 使用 Lua 脚本的方式，恰好能够弥补 Nginx 在这方面的缺失，形成了有效的互补。这也是 OpenResty 被广泛地用于替换 Nginx 的根源所在。在后面几节课中，我会为你继续介绍更多 OpenResty 中动态的场景和示例。今天，就让我们先来看下，如何使用 OpenResty 来实现动态限流和限速。

在 OpenResty 中，我们推荐使用 [lua-resty-limit-traffic](https://github.com/openresty/lua-resty-limit-traffic) 来做流量的限制。它里面包含了 `limit-req`（限制请求速率）、 `limit-count`（限制请求数） 和 `limit-conn` （限制并发连接数）这三种不同的限制方式；并且提供了`limit.traffic` ，可以把这三种方式进行聚合使用。

## 限制请求速率

让我们先来看下 `limit-req`，它使用的是漏桶算法来限制请求的速率。

在上一节中，我们已经简要介绍了这个 resty 库中漏桶算法的关键实现代码，现在我们就来学习如何使用这个库。我们来看下面这段示例代码：

```
resty --shdict='my_limit_req_store 100m' -e 'local limit_req = require &quot;resty.limit.req&quot;
local lim, err = limit_req.new(&quot;my_limit_req_store&quot;, 200, 100)
local delay, err = lim:incoming(&quot;key&quot;, true)
if not delay then
    if err == &quot;rejected&quot; then
        return ngx.exit(503)
    end
    return ngx.exit(500)
end

 if delay &gt;= 0.001 then
    ngx.sleep(delay)
end'

```

我们知道，`lua-resty-limit-traffic` 是使用共享字典来对 key 进行保存和计数的，所以在使用 `limit-req` 前，我们需要先声明 `my_limit_req_store` 这个 100m 的空间。这一点对于 `limit-conn` 和 `limit-count` 也是类似的，它们都需要自己单独的共享字典空间，以便区分开。

```
limit_req.new(&quot;my_limit_req_store&quot;, 200, 100)

```

上面这行代码，便是其中最关键的一行代码。它的含义，是使用名为 `my_limit_req_store` 的共享字典来存放统计数据，并把每秒的速率设置为 200。这样，如果超过 200 但小于 300（这个值是 200 + 100 计算得到的） 的话，就需要排队等候；如果超过 300 的话，就会直接拒绝。

在设置完成后，我们就要对终端的请求进行处理了，`lim: incoming("key", true)` 就是来做这件事情的。`incoming`这个函数有两个参数，我们需要详细解读一下。

第一个参数，是用户指定的限速的 key。在上面的示例中它是一个字符串常量，这就意味着要对所有终端都统一限速。如果要实现根据不同省份和渠道来限速，其实也很简单，把这两个信息都作为 key 即可，下面是实现这一需求的伪代码：

```
local  province = get_ province(ngx.var.binary_remote_addr)
local channel = ngx.req.get_headers()[&quot;channel&quot;]
local key = province .. channel
lim:incoming(key, true)

```

当然，你也可以举一反三，自定义 key 的含义以及调用 `incoming` 的条件，这样你就能收到非常灵活的限流限速效果了。

我们再来看`incoming` 函数的第二个参数，它是一个布尔值，默认是 false，意味着这个请求不会被记录到共享字典中做统计，这只是一次 `演习`。如果设置为 true，就会产生实际的效果了。因此，在大多数情况下，你都需要显式地把它设置为 true。

你可能会纳闷儿，为什么会有这个参数的存在呢？我们不妨考虑一下这样的一个场景，你设置了两个不同的 `limit-req` 实例，针对不同的 key，一个 key 是主机名，另外一个 key 是客户端的 IP 地址。那么，当一个终端请求被处理的时候，会按照先后顺序调用这两个实例的 `incoming` 方法，就像下面这段伪码表示的一样：

```
local limiter_one, err = limit_req.new(&quot;my_limit_req_store&quot;, 200, 100)
local limiter_two, err = limit_req.new(&quot;my_limit_req_store&quot;, 20, 10)

limiter_one :incoming(ngx.var.host, true)
limiter_two:incoming(ngx.var.binary_remote_addr, true)

```

如果用户的请求通过了 `limiter_one` 的阈值检测，但被 `limiter_two` 的检测拒绝，那么 `limiter_one:incoming` 这次函数调用就应该被认为是一次 `演习`，不应该真的去计数。

这样一来，上述的代码逻辑就不够严谨了。我们需要事先对所有的 limiter 做一次演习，如果有 limiter 的阈值被触发，可以 rejected 终端请求，就可以直接返回：

```
for i = 1, n do
    local lim = limiters[i]
    local delay, err = lim:incoming(keys[i], i == n)
    if not delay then
        return nil, err
    end
end

```

这其实就是 `incoming` 函数第二个参数的意义所在。刚刚这段代码就是 `limit.traffic` 模块最核心的一段代码，专门用作多个限流器的组合所用。

## 限制请求数

再来看下 `limit.count` 这个限制请求数的库，它的效果和 GitHub API 的 Rate Limiting 一样，可以限制固定时间窗口内有多少次用户请求。老规矩，我们先来看一段示例代码：

```
local limit_count = require &quot;resty.limit.count&quot;

local lim, err = limit_count.new(&quot;my_limit_count_store&quot;, 5000, 3600)

local key = ngx.req.get_headers()[&quot;Authorization&quot;]
local delay, remaining = lim:incoming(key, true)

```

你可以看到，`limit.count` 和 `limit.req` 的使用方法是类似的，我们先在 Nginx.conf 中定义一个字典：

```
lua_shared_dict my_limit_count_store 100m;

```

然后 `new` 一个 limiter 对象，最后用 `incoming` 函数来判断和处理。

不过，不同的是，`limit-count` 中的`incoming` 函数的第二个返回值，代表着还剩余的调用次数，我们可以据此在响应头中增加字段，给终端更好的提示：

```
ngx.header[&quot;X-RateLimit-Limit&quot;] = &quot;5000&quot;
ngx.header[&quot;X-RateLimit-Remaining&quot;] = remaining

```

## 限制并发连接数

第三种方式，也就是`limit.conn` ，是用来限制并发连接数的库。它和前面提到的两个库有所不同，有一个特别的 `leaving` API，这里我来简单介绍下。

前面所讲的限制请求速率和限制请求数，都是可以直接在 access 这一个阶段内完成的。而限制并发连接数则不同，它不仅需要在 access 阶段判断是否超过阈值，而且需要在 log 阶段调用 `leaving` 接口：

```
log_by_lua_block {
    local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
    local key = ctx.limit_conn_key

    local conn, err = lim:leaving(key, latency)
}

```

不过，这个接口的核心代码其实也很简单，也就是下面这一行代码，实际上就是把连接数减一的操作。如果你没有在 log 阶段做这个清理的动作，那么连接数就会一直上涨，很快就会达到并发的阈值。

```
local conn, err = dict:incr(key, -1)

```

## 限速器的组合

到这里，这三种方式我们就分别介绍完了。最后，我们再来看看，怎么把 `limit.rate`、`limit.conn` 和 `limit.count` 组合起来使用。这就需要用到 `limit.traffic` 中的 `combine` 函数了：

```
local lim1, err = limit_req.new(&quot;my_req_store&quot;, 300, 200)
local lim2, err = limit_req.new(&quot;my_req_store&quot;, 200, 100)
local lim3, err = limit_conn.new(&quot;my_conn_store&quot;, 1000, 1000, 0.5)

local limiters = {lim1, lim2, lim3}
local host = ngx.var.host
local client = ngx.var.binary_remote_addr
local keys = {host, client, client}

local delay, err = limit_traffic.combine(limiters, keys, states)

```

有了刚刚的知识基础，这段代码你应该很容易看明白。`combine` 函数的核心代码，在我们上面分析 `limit.rate` 的时候已经提到了一部分，它主要是借助了演习功能和 uncommit 函数来实现。这样组合以后，你就可以为多个限流器设置不同的阈值和 key，实现更复杂的业务需求了。

## 写在最后

`limit.traffic` 不仅支持今天所讲的这三种限速器，实际上，只要某个限速器有 `incoming` 和 `uncommit` 接口，都可以被 `limit.traffic` 的 `combine` 函数管理。

最后，给你留一个作业题。你可以写一个例子，把之前我们介绍过的基于令牌桶的[限速器](https://github.com/upyun/lua-resty-limit-rate)组合起来吗？欢迎在留言区写下你的答案与我讨论，也欢迎你把这篇文章分享给你的同事朋友，一起学习和交流。


