APISIX proxy-cache 插件用法

APISIX 的 proxy-cache 插件可以对上游的查询进行缓存,这样就不需要上游的应用服务自己实现缓存了,或者也能少实现一部分缓存,通用的交给插件来做。

下面的操作都是基于 APISIX 3.2 版本进行配置,关于 proxy-cache 的详细配置的可以参考文档:https://apisix.apache.org/docs/apisix/3.2/plugins/proxy-cache/ 不过文档很多地方说的不是太清楚,这里把重点的地方补充一下,首先是插件的参数:

  1. cache_strategy 这个表示我们插件的缓存策略,支持配置 disk 或 memory,默认是 disk
  2. cache_zone 这个表示我们使用的存储区域,对于内存或者磁盘都可以详细配置,这个需要在配置文件中进行配置。
  3. cache_key 这个是我们要缓存请求的 key,key 是判断是否缓存的依据,可以指定多个 APISIX 或者 nginx 的变量,也可以指定常量字符串。需要注意的是不是所有的变量都可以使用,比如 request_body 变量就是不能使用的,因为如果 body 太大,上下文传递会有比较大的开销,所以设置了结果也是空的。
  4. cache_bypass 这个指定不进行缓存的情况,也是一个数组,可以写多个变量,如果至少有 1 个变量的不为空并且不等于 0,那么就会跳过缓存。这个配置不太好理解,具体是什么变量并没有说,通过查看插件源码发现取的是 ctx.var 中的值,所以其实这个并不是请求的 URL 参数,也不是请求的 Header 内容,而是 APISIX 里面的变量,当然也包括 nginx 的变量,当变量存在时就会自动绕过请求,如果内置变量不满足要求,我们可以通过实现自定义变量来解决。
  5. cache_method 这个好理解,就是哪些方法会被缓存,可以指定一个数组。
  6. cache_http_status 这个表示上游的哪些状态码会被缓存,也是一个数组。
  7. hide_cache_headers 如果设置为 true,会将响应的 Expires 和 Cache-Control 头响应到客户端中,默认是会去掉的。
  8. cache_control 如果设置为 true,将按照 HTTP 规范中的行为进行缓存,这个仅对于内存策略生效。
  9. no_cache 这个和 cache_bypass 非常类似,同样是配置一个变量列表,不过这个是在响应阶段处理,也就是上游服务主动告诉 APISIX 这个请求是否缓存,变量的含义和上面一样,支持内置变量和自定义变量。
  10. cache_ttl 缓存的过期时间,单位是秒,当上面的 cache_control 未启用或者服务器未返回缓存控制头时生效,如果启动了 cache_control 则以响应的控制头为准,同样这个仅对内存策略生效。

根据官网的说明,有下面的几点需要注意:

  1. 如果是基于磁盘的缓存,无法在插件中设置过期时间,默认就是 10s,但是可以通过服务的响应头 Expires 和 Cache-Control 设置过期时间。
  2. 如果上游服务不可用时,那么 APISIX 会返回 502 和 504 状态码,这个时候缓存时间是默认的 10s。
  3. 在 cache_key, cache_bypass 以及 no_cache 中指定的变量,如果变量值不存在,则结果为空字符串。如果其中写了常量,结果会将变量值和常量一块拼接起来。

开启插件之前,首先需要在本地配置文件添加缓存区域的配置,否则启用插件以及后续调用时会报错,首先编辑 config.yaml 添加配置如下:

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 的内置变量中不存在,所以我们编写一个自定义变量的插件来解决,插件内容如下:

lua 复制代码
--
-- 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_usernamecache_bypass 这两个变量,并且添加了 Control API,我们将源码保存为 custom-vars.lua 并放到 APISIX 的 plugins 目录下,然后在配置文件中添加插件,如果之前没有添加过需要复制 config-default.yaml 中所有的插件,然后再补充上我们的插件。

具体如何加载插件可以参考之前写过的插件开发的文章。

由于我们在插件中注册了全局变量,只要插件被加载就可以,我们无需使用它也可以使用其中的自定义变量,但是假如我们要访问插件的 Control API 那么则必须在某个路由上启用插件。

使用专门的自定义变量插件的好处是我们不需要修改 proxy-cache 的源码在里面注册变量,这样假如 APISIX 升级了并且 proxy-cache 的源码有所变化我们也不需要再进行更新,只需要加入我们的 custom-vars 插件即可,对 APISIX 原有插件不会有任何影响,也是为了解耦。

加入插件后不要忘记重启 APISIX,然后我们来添加一个路由:

bash 复制代码
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 响应的字段,比如:

shell 复制代码
curl localhost:9080/hello -i

第一次会看到 APISIX-Cache-Status: MISS 因为数据未缓存,然后再次请求就可以看到 APISIX-Cache-Status: HIT 表示缓存已经命中,同时会返回 Age 响应头,表示当前缓存的存活时间,当时间超过 TTL 时,缓存就会被删除。

然后我们也可以选择不使用缓存,比如:

shell 复制代码
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 这样的函数完全够了,碰撞的概率也是极小的,我们可以在上面插件中注册一个标识请求体的变量,比如:

lua 复制代码
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 配置如下:

json 复制代码
{
    "cache_key": [
       "$uri",
       "$request_body_uuid"
     ]
}

这样就可以缓存 POST 请求了,如果要缓存带参数的 GET 请求可以将 $uri 变量替换为 $request_uri 变量,后者是包含参数并且未规范化的。

最后我们还可以删除路由的缓存,使用 HTTP 的 PURGE 方法发起请求:

shell 复制代码
curl localhost:9088/hello -X PURGE -i

如果成功删除缓存会返回 200 OK,否则如果不存在缓存则会返回 404,但是前提路由配置中一定要允许 PURGE 方法,我们上面创建时就指定了,如果不指定则无法使用上面的命令删除缓存,并且启用了之后这个请求也是用于 APISIX 删除缓存,并不会请求上游的服务。

另外下面会给出 OpenResty 中两个 Lua 模块的仓库,其中有很多好用的函数可以参考,并且由于 APISIX 是基于 OpenResty 的,所以在 APISIX 插件开发中都是可用的。

Reference:

  1. APISIX 官方中文版文档参考
  2. OpenResty 提供的 Lua 模块
  3. OpenResty 的 Lua core API 参考
相关推荐
查士丁尼·绵1 个月前
apisix云原生网关
apisix
看不见鲸鱼的鼻子3 个月前
Apisix自定义httpcode 请求重试
网关·nginx·lua·apisix
VinciYan4 个月前
Apache APISIX遇到504超时的解决办法
网络·网关·微服务·apache·apisix·开源网关
VinciYan5 个月前
开源网关Apache APISIX启用JWT身份验证
docker·微服务·apache·jwt·apisix·开源网关
VinciYan5 个月前
Ubuntu部署开源网关Apache APISIX
ubuntu·docker·微服务·apache·apisix·开源网关
老陈聊架构7 个月前
『Apisix安全篇』APISIX 加密传输实践:SSL/TLS证书的配置与管理实战指南
安全·证书·ssl·apisix
老陈聊架构7 个月前
『Apisix系列』破局传统架构:探索新一代微服务体系下的API管理新范式与最佳实践
网关·云原生·实战·apisix
老陈聊架构7 个月前
『Apisix安全篇』探索Apache APISIX身份认证插件:从基础到实战
安全认证·apisix·keyauth
老陈聊架构7 个月前
『Apisix入门篇』从零到一掌握Apache APISIX:架构解析与实战指南
网关·架构·apisix