APISIX 的 proxy-cache 外掛可以對上游的查詢進行快取,這樣就不需要上游的應用服務自己實現快取了,或者也能少實現一部分快取,通用的交給外掛來做。
下面的操作都是基於 APISIX 3.2 版本進行設定,關於 proxy-cache 的詳細設定的可以參考檔案:https://apisix.apache.org/docs/apisix/3.2/plugins/proxy-cache/ 不過檔案很多地方說的不是太清楚,這裡把重點的地方補充一下,首先是外掛的引數:
根據官網的說明,有下面的幾點需要注意:
開啟外掛之前,首先需要在本地組態檔新增快取區域的設定,否則啟用外掛以及後續呼叫時會報錯,首先編輯 config.yaml
新增設定如下:
apisix:
# ...
proxy_cache:
# 磁碟快取時間 預設是 10s,可以在這裡修改
cache_ttl: 10s
zones:
# 磁碟的 cache_zone 的名稱
- name: disk_cache_one
# 索引需要在記憶體中儲存,設定記憶體的大小限制
memory_size: 50m
# 磁碟快取的大小限制
disk_size: 1G
# 快取檔案的路徑
disk_path: "/tmp/disk_cache_one"
# 快取級別設定
cache_levels: "1:2"
# 記憶體的 cache_zone 名稱
- name: memory_cache_one
# 記憶體快取的大小限制
memory_size: 512m
上面就分別設定了磁碟和記憶體的 cache_zone 當然可以設定多個,比如大小限制不一樣或者儲存路徑不一樣,針對於不同外掛的設定。再比如我們這裡只使用記憶體作為快取,所以也可以不設定磁碟的。總之,外掛中需要用到的設定,在組態檔中必須找得到才可以,修改好之後我們儲存設定,然後重啟 APISIX 服務。
我們這裡打算通過自定義一個 header 頭來判斷請求是否走快取,由於變數在 APISIX 或 nginx 的內建變數中不存在,所以我們編寫一個自定義變數的外掛來解決,外掛內容如下:
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local ngx = ngx
local core = require("apisix.core")
local plugin = require("apisix.plugin")
local schema = {
type = "object",
properties = {
name = {type = "string"},
label = {type = "integer"}
},
required = {"name"},
}
local plugin_name = "custom-vars"
local _M = {
version = 0.1,
priority = 99,
name = plugin_name,
schema = schema,
}
defined_var_names = {"custom_username", "cache_bypass"}
core.ctx.register_var("custom_username", function(ctx)
return get_custom_username()
end)
core.ctx.register_var("cache_bypass", function(ctx)
local bypass = core.request.header(ctx, "cache-bypass")
if not bypass then
return ""
end
return bypass
end)
function get_custom_username()
local req_headers = ngx.req.get_headers()
local username = req_headers.user
if username ~= "" then
return username
end
return nil
end
function _M.check_schema(conf, schema_type)
if schema_type == core.schema.TYPE_METADATA then
return core.schema.check(metadata_schema, conf)
end
return core.schema.check(schema, conf)
end
function _M.init()
-- call this function when plugin is loaded
local attr = plugin.plugin_attr(plugin_name)
if attr then
core.log.info(plugin_name, " get plugin attr val: ", attr.val)
end
end
function _M.destroy()
-- call this function when plugin is unloaded
end
-- sorted phase:
-- rewrite -> access -> before_proxy -> header_filter -> body_filter -> delayed_body_filter -> log
function _M.rewrite(conf, ctx)
-- core.log.warn("plugin rewrite phase, conf: ", core.json.encode(conf))
-- core.log.warn("plugin rewrite phase, ctx: ", core.json.encode(ctx, true))
-- core.log.warn("plugin rewrite phase, username: ", get_username())
end
function _M.access(conf, ctx)
-- core.log.warn("plugin access phase, conf: ", core.json.encode(conf))
-- core.log.warn("plugin access phase, ctx: ", core.json.encode(ctx, true))
-- core.log.warn("plugin access phase, ngx headers: ", core.json.encode(ngx.req.get_headers()))
end
function _M.before_proxy(conf, ctx)
-- After access and before the request goes upstream
end
function _M.header_filter(conf, ctx)
-- Response header filter
end
function _M.body_filter(conf, ctx)
-- Response body filter
end
function _M.delayed_body_filter(conf, ctx)
-- delayed_body_filter is called after body_filter
-- it is used by the tracing plugins to end the span right after body_filter
end
function _M.log(conf, ctx)
-- Log processing after response
end
local function list_vars()
local args = ngx.req.get_uri_args()
if args["json"] then
return 200, defined_var_names
else
return 200, table.concat(defined_var_names, "\n") .. "\n"
end
end
function _M.control_api()
return {
{
methods = {"GET"},
uris = {"/v1/plugin/custom-vars"},
handler = list_vars,
}
}
end
return _M
這裡外掛名稱我們叫 custom-vars
,是專門註冊自定義變數的外掛,我們註冊了 custom_username
和 cache_bypass
這兩個變數,並且新增了 Control API,我們將原始碼儲存為 custom-vars.lua
並放到 APISIX 的 plugins
目錄下,然後在組態檔中新增外掛,如果之前沒有新增過需要複製 config-default.yaml
中所有的外掛,然後再補充上我們的外掛。
具體如何載入外掛可以參考之前寫過的外掛開發的文章。
由於我們在外掛中註冊了全域性變數,只要外掛被載入就可以,我們無需使用它也可以使用其中的自定義變數,但是假如我們要存取外掛的 Control API 那麼則必須在某個路由上啟用外掛。
使用專門的自定義變數外掛的好處是我們不需要修改 proxy-cache 的原始碼在裡面註冊變數,這樣假如 APISIX 升級了並且 proxy-cache 的原始碼有所變化我們也不需要再進行更新,只需要加入我們的 custom-vars 外掛即可,對 APISIX 原有外掛不會有任何影響,也是為了解耦。
加入外掛後不要忘記重啟 APISIX,然後我們來新增一個路由:
curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/100 \
-H 'X-API-KEY: <api-key>' -d '
{
"uri": "/hello",
"name": "範例路由",
"plugins": {
"custom-vars": {
"name": "vars"
},
"proxy-cache": {
"cache_bypass": [
"$cache_bypass"
],
"cache_control": false,
"cache_http_status": [
200
],
"cache_key": [
"$uri",
"-cache-id"
],
"cache_method": [
"GET",
"PURGE"
],
"cache_strategy": "memory",
"cache_ttl": 30,
"cache_zone": "memory_cache_one",
"hide_cache_headers": false
}
},
"upstream": {
"nodes": [
{
"host": "10.0.1.12",
"port": 1980,
"weight": 1
}
],
"type": "roundrobin",
"hash_on": "vars",
"scheme": "http",
"pass_host": "pass"
},
"status": 1
}'
現在我們就新增了路由,然後我們存取路由新增 -i
引數就可以看到 APISIX 響應的欄位,比如:
curl localhost:9080/hello -i
第一次會看到 APISIX-Cache-Status: MISS
因為資料未快取,然後再次請求就可以看到 APISIX-Cache-Status: HIT
表示快取已經命中,同時會返回 Age
響應頭,表示當前快取的存活時間,當時間超過 TTL 時,快取就會被刪除。
然後我們也可以選擇不使用快取,比如:
curl localhost:9080/hello -i -H 'Cache-Bypass: 1'
這時候我們會看到 APISIX-Cache-Status: BYPASS
表示沒有使用快取,而是直接請求上游服務。
假如我們要快取 POST 之類的請求,那麼這個時候 $request_body
肯定也要作為 cache_key 的一部分,但是這個時候上下文中又沒有這個變數,那麼怎麼辦呢?可以換一種方式,由於 $request_body
本身可能比較大,我們可以使用它做一個 Hash,只要請求體內容不變,那麼 Hash 結果也是確定的,而且快取的 key 也比較小,由於同時有 $uri
進行區分,選用 md5 這樣的函數完全夠了,碰撞的概率也是極小的,我們可以在上面外掛中註冊一個標識請求體的變數,比如:
core.ctx.register_var("request_body_uuid", function(ctx)
local body = core.request.get_body()
if not body then
return ""
end
return ngx.md5(body)
end)
這樣我們就可以使用 $request_body_uuid
這樣的變數的,那麼我們在建立路由的時候 cache_key 設定如下:
{
"cache_key": [
"$uri",
"$request_body_uuid"
]
}
這樣就可以快取 POST 請求了,如果要快取帶引數的 GET 請求可以將 $uri
變數替換為 $request_uri
變數,後者是包含引數並且未規範化的。
最後我們還可以刪除路由的快取,使用 HTTP 的 PURGE 方法發起請求:
curl localhost:9088/hello -X PURGE -i
如果成功刪除快取會返回 200 OK,否則如果不存在快取則會返回 404,但是前提路由設定中一定要允許 PURGE 方法,我們上面建立時就指定了,如果不指定則無法使用上面的命令刪除快取,並且啟用了之後這個請求也是用於 APISIX 刪除快取,並不會請求上游的服務。
另外下面會給出 OpenResty 中兩個 Lua 模組的倉庫,其中有很多好用的函數可以參考,並且由於 APISIX 是基於 OpenResty 的,所以在 APISIX 外掛開發中都是可用的。
Reference: