使用 Varnish 模块和 Varnish 配置语言优化 Varnish
这是由Denis Brækhus 和 Espen Braastad 撰写的客座文章, 他们是来自Varnish Software 的 Varnish API Engine 的开发者。Varnish长期用于认证后端,所以让我们来看看他们在做什么。
Varnish Software刚刚放出Varnish API Engine的发布版,它是一个高性能的 HTTP API 网关,用于处理认证、授权和所有基于Varnish Cache之上的调节。Varnish API Engine可以用一个统一访问控制层轻易地扩展你目前的API集。这个统一访问控制层内置了高容量读取操作缓存能力,而且它提供了实时度量。
Varnish API Engine使用了众所周知的组件如memcached、SQLite和最重要的Varnish Cache。管理 API是由Python写成的。该产品的核心部分在Varnish的基础上使用VCL (Varnish Configuration Language)编写成一个应用并使用VMODs (Varnish Modules)提供扩展能力。
我们希望以这篇文章作为一个机会来向您展示怎样在VMODs的协助下使用VCL创建一个您自己的灵活且高性能的应用。
VMODs (Varnish 模块)
VCL是用于配置Varnish Cache的语言。当varnishd加载VCL配置文件时,它将会把这个文件转换成C代码,将之编译并动态加载。因此可以通过在VCL配置文件中直接嵌入C代码来使VCL具有扩展功能,但是从Varnish Cache 3开始,已经变成使用Varnish Modules,或简称VMODs。
在Varnish Cache的一个堆栈中,典型的请求流程入下:
客户端发出的HTTP请求被Varnish Cache接收并处理。Varnish Cache将会检查请求的内容是否在缓存中,最后它可能从后端读取内容。这工作的很好,但我们能做更多。
VCL语言是为性能设计的,因此它本身并不提供循环或外部调用。 VMODs,换句话说,是为了打破这些限制的。这对灵活性来说非常棒,但将确保性能和避免延迟的责任放到了VMOD代码和行为上了。
API Engine用于说明用VCL和自定义的VMOD的组合来开发新应用是多么强大。在Varnish API Engine中,请求流程是:
每个请求匹配一个使用SQLite VMOD的规则集合和使用memcached VMOD的Memcached计数器组。如果任何一个不匹配,请求都将会被拒绝,例如:认证失败或超过了请求限制。
应用示例
下面的例子是 Varnish API Engine 中一些概念的非常简单版本。我们将创建一个用 VCL 写的小应用,它将在一个包含限流规则的数据库中搜索请求 URL,并在每一个 IP 的基础上执行。
由于开发应用程序时的测试和可维护性是至关重要的,我们将用 Varnish 的集成测试工具:varnishtest。Varnishtest 是一个测试 Varnish 缓存所用方面的强大工具。Varnishtest 的简单接口意味着开发者和操作工程师利用它来测试他们的 VCL/VMOD 配置。
Varnishtest 读取一个描述一组模拟服务器、客户端和 varnish 实例的文件。客户端执行通过varnish 到达服务器的请求。Expectations 可以被设置为内容、标题、HTTP 响应代码,或者更多。使用 varnishtest,我们可以快速测试我们的示例应用,并验证我们的请求对每一个定义的expectations是通过还是阻塞。
首先,我们需要一个带着我们限流规则的数据库。使用 sqlite3 命令,我们在 /tmp/rules.db3创建这个数据库并增加两三个规则。
$ sqlite3 /tmp/rules.db3 "CREATE TABLE t (rule text, path text);" $ sqlite3 /tmp/rules.db3 "INSERT INTO t (rule, path) VALUES ('3r5', '/search');" $ sqlite3 /tmp/rules.db3 "INSERT INTO t (rule, path) VALUES ('15r3600', '/login');"
这些规则将允许每秒3个请求到 /secarch 和每小时15个请求到 /login。这个想法是在每个 IP 基础上执行这些规则。
为了简单起见,我们将在一个文件中编写测试和 VCL 配置,throttle.vtc。然而,在测试文件中使用包含语句包含单独的 VCL 配置,分离 VCL 配置和不同的测试也是可行的。
这个文件中的第一行是用来设置这个测试的标题或名字。
varnishtest "Simple throttling with SQLite and Memcached"
我们的测试环境由一个调用 s1 的后端构成。我们将首先 expect 一个请求到一个在数据库中没有规则的 URL。
server s1 { rxreq expect req.url == "/" txresp
根据接下来的 expectations,到达后我们再 expect 4个请求到/search。注意,查询参数略有不同,来制作所有这些独特的请求。
rxreq expect req.url == "/search?id=123&type=1" expect req.http.path == "/search" expect req.http.rule == "3r5" expect req.http.requests == "3" expect req.http.period == "5" expect req.http.counter == "1" txresp rxreq expect req.url == "/search?id=123&type=2" expect req.http.path == "/search" expect req.http.rule == "3r5" expect req.http.requests == "3" expect req.http.period == "5" expect req.http.counter == "2" txresp rxreq expect req.url == "/search?id=123&type=3" expect req.http.path == "/search" expect req.http.rule == "3r5" expect req.http.requests == "3" expect req.http.period == "5" expect req.http.counter == "3" txresp rxreq expect req.url == "/search?id=123&type=4" expect req.http.path == "/search" expect req.http.rule == "3r5" expect req.http.requests == "3" expect req.http.period == "5" expect req.http.counter == "1" txresp } -start
现在是时候写一个VCL的迷你程序了。我们的测试环境由一个varnish的实例v1组成。首先,加入vaernish版本和VMOD imports。
varnish v1 -vcl+backend { vcl 4.0; import std; import sqlite3; import memcached;
VOMD通常在vcl_init中配置,sqlite3和memcacheed也是这样。对于sqlite3,我们设置数据库路径和用在多列结果中的分隔符。memcached VMOD可以有各种各样 libmemcached 支持的配置选项。
sub vcl_init { sqlite3.open("/tmp/rules.db3", "|;"); memcached.servers("--SERVER=localhost --BINARY-PROTOCOL"); }
在vcl_recv中,传入的HTTP请求被接受。我们首先提取没有查询参数和潜在危险字符的请求路径。这非常重要,因为这个路径是稍后SQL请求的一部分。接下来的正则表达式将从一行的开始直到字符(?&;”')或空格结束匹配req.url。
sub vcl_recv { set req.http.path = regsub(req.url, {"^([^?&;"' ]+).*"}, "\1");
在正则表达式中使用{”“}可以支持正则表达式规则中“字符的处理。我们刚刚提取的路径仅在数据库中查找规则时使用。响应(如果有的话)存储在req.hhtp.rule中。
set req.http.rule = sqlite3.exec("SELECT rule FROM t WHERE path='" + req.http.path + "' LIMIT 1");
如果我们得到一个响应,它将会是RnT格式的,这里的R是T秒时间段内允许的请求量。由于这是一个字符串,我们需要应用额外的正则表达式来分割。
set req.http.requests = regsub(req.http.rule, "^([0-9]+)r.*$", "\1"); set req.http.period = regsub(req.http.rule, "^[0-9]+r([0-9]+)$", "\1");
只有当我们从前面的正则表达式过滤器中获得正确的值时才限制请求。
if (req.http.requests != "" && req.http.period != "") {
给这个client.ip增加或创建一个独一无二的Memcached计数器。并将path值设置为1。我们将失效时间指定为与数据库中限制规则设置的时间相同。在这种方式中,限制规则可以灵活地设置时间。返回值就是计数器的新值,与这个client.ip在当前路径和当前时间段内的请求数量相符。
set req.http.counter = memcached.incr_set( req.http.path + "-" + client.ip, 1, 1, std.integer(req.http.period, 0));
检查计数器是否高于数据库中设置的限制。如果是,放弃当前的请求并返回一个429响应码。
if (std.integer(req.http.counter, 0) > std.integer(req.http.requests, 0)) { return (synth(429, "Too many requests")); } } }
在 vxl_deliver 中我们设置了显示限流限制的响应 headers 和有助于用户的每一个请求状态。
sub vcl_deliver { if (req.http.requests && req.http.counter && req.http.period) { set resp.http.X-RateLimit-Limit = req.http.requests; set resp.http.X-RateLimit-Counter = req.http.counter; set resp.http.X-RateLimit-Period = req.http.period; } }
在 vcl_synth 中,错误将获得一个同样的 headers 设置。
sub vcl_synth { if (req.http.requests && req.http.counter && req.http.period) { set resp.http.X-RateLimit-Limit = req.http.requests; set resp.http.X-RateLimit-Counter = req.http.counter; set resp.http.X-RateLimit-Period = req.http.period; } }
配置完成,是时候增加一些客户端来确认一下配置是否正确。首先,我们发送一个不被限流的请求,这意味着这个 URL 在数据库中没有限流规则。
client c1 { txreq -url "/" rxresp expect resp.status == 200 expect resp.http.X-RateLimit-Limit == <undef> expect resp.http.X-RateLimit-Counter == <undef> expect resp.http.X-RateLimit-Period == <undef> } -run
我们知道客户端发送的下一个请求的 URL 与限制数据库是匹配的,我们希望设置速率限制报头,对/search的限制规则是3r5,也就是说在5秒的时间段内,前三个请求应该是成功的(返回状态码200),当第四次请求时,就应该被限制了(返回状态码429)。
client c2 { txreq -url "/search?id=123&type=1" rxresp expect resp.status == 200 expect resp.http.X-RateLimit-Limit == "3" expect resp.http.X-RateLimit-Counter == "1" expect resp.http.X-RateLimit-Period == "5" txreq -url "/search?id=123&type=2" rxresp expect resp.status == 200 expect resp.http.X-RateLimit-Limit == "3" expect resp.http.X-RateLimit-Counter == "2" expect resp.http.X-RateLimit-Period == "5" txreq -url "/search?id=123&type=3" rxresp expect resp.status == 200 expect resp.http.X-RateLimit-Limit == "3" expect resp.http.X-RateLimit-Counter == "3" expect resp.http.X-RateLimit-Period == "5" txreq -url "/search?id=123&type=4" rxresp expect resp.status == 429 expect resp.http.X-RateLimit-Limit == "3" expect resp.http.X-RateLimit-Counter == "4" expect resp.http.X-RateLimit-Period == "5" } -run
在这一点,我们知道请求将会被限制,为了确定限制时间结束之后新的请求将会被允许,在我们发送下一个和最后一个请求之前,我们添加了一个延迟。这个请求应该是成功的,因为我们已经进入了一个新的限制窗口期。
delay 5; client c3 { txreq -url "/search?id=123&type=4" rxresp expect resp.status == 200 expect resp.http.X-RateLimit-Limit == "3" expect resp.http.X-RateLimit-Counter == "1" expect resp.http.X-RateLimit-Period == "5" } -run
要运行测试文件,先要确保 memcached 服务正在运行,然后执行:
$ varnishtest example.vtc # top TEST example.vtc passed (6.533)
添加 -v 选项启用详细模式,从运行测试中获得更多的信息。
对我们示例应用发送请求,将会接收到如下的响应头。第一个表示请求被接受,第二个表示请求被限制。
$ curl -iI http://localhost/search HTTP/1.1 200 OK Age: 6 Content-Length: 936 X-RateLimit-Counter: 1 X-RateLimit-Limit: 3 X-RateLimit-Period: 5 X-Varnish: 32770 3 Via: 1.1 varnish-plus-v4
$ curl -iI http://localhost/search HTTP/1.1 429 Too many requests Content-Length: 273 X-RateLimit-Counter: 4 X-RateLimit-Limit: 3 X-RateLimit-Period: 5 X-Varnish: 32774 Via: 1.1 varnish-plus-v4
完整的 throttle.vtc 文件将会在 VMOD 处理前后输出时间戳信息,用以给出 Memcached 和 SQLite 查询引入 VMOD 的开销数据。在一个本地虚拟机上运行着 Memcached 服务的 varnishtest 上运行60个请求,返回如下的每个操作的时间信息(单位 ms):
-
SQLite SELECT,最大:0.32,最小:0.08,平均值:0.115
-
Memcached incr_set(),最大:1.23,最小:0.27平均值:0.29
这并不是个科学的结果,但是暗示了在大多数情况下,性能并不高。性能也是关于水平的能力。本文中给出的简单示例在需要的情况下,通过一个使用 Memcached 实例池的全局计数器,它的性能将会有规模性的扩展。
延伸阅读
现在已经有许多可用的 VMODs 了,VMODs Directory 是一个好的起点。这个目录中的一些亮点是关于cURL、Redis、Digest函数的VMODs和多种认证模块。
Varnish Plus,Varnish Cache 的完整商业支持版,已经经捆绑了一组高质量,有支持的 VMODs。对于开源版本,你可以手动下载和编译你需要的 VMODs。