使用 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。