插件与缓存交互
有时插件需要在请求或者响应中频繁访问自定义实体。通常情况下,我们会在第一次访问时加载它们,然后将其缓存在内存中,这样会显著提高性能,同时确保数据库不会因为请求过多而负载过重,继而影响其他kong节点的性能。
例如在一个api-key身份验证插件场景,它需要在每个请求上验证api-key,从而在每个请求上从数据存储加载自定义凭据对象。当客户端随请求一起提供api密钥时,通常会查询数据存储以检查该密钥是否存在,然后阻止请求或检索Consumer ID以标识用户。这将在每个请求中发生,并且效率非常低:
- 查询数据存储会增加每个请求的延迟,使请求处理变慢。
- 数据存储也会受到负载增加的影响,可能会崩溃或变慢,这反过来又会影响到每个Kong节点。
为了避免每次都查询数据存储,我们可以在节点的内存中缓存自定义实体,这样频繁的实体查找就不会每次都触发数据存储查询(只是第一次),而是发生在内存中,这比从数据存储查询要快得多,也可靠得多(特别是在高负载情况下)。
缓存对应的模块:kong.plugins.<plugin_name>.daos
一、缓存自定义实体
一旦定义了自定义实体,就可以使用PDK提供的kong.cache模块将它们缓存到代码的内存中。
local cache = kong.cache
kong中包含两级缓存:
- L1: Lua内存缓存,Nginx worker进程中的本地缓存。它可以保存任何类型的Lua值。
- L2:共享内存缓存(SHM) ,Nginx节点的本地缓存,但在所有worker之间共享。这只能保存标量值,因此需要对更复杂的类型(如Lua表)进行(反)序列化。
当从数据库中获取数据时,它将同时存储在两个缓存中。如果同一个worker进程再次请求数据,它将从Lua内存缓存中检索先前反序列化的数据。如果同一个Nginx节点内的不同worker请求该数据,它将在共享内存缓存(SHM) 中找到数据,对其进行反序列化(并将其存储在自己的Lua内存缓存中),然后返回它。kong.cache模块暴露的方法如表1所示:
方法 | 描述 |
---|---|
value, err = cache:get(key, opts?, cb, ...) | 从缓存中检索值。如果缓存没有值(miss),则以保护模式异步调用cb。Cb必须返回一个(且只有一个)将被缓存的值。它可以抛出错误,因为这些错误将被捕获并被Kong正确地记录在ngx上。这个函数缓存负结果(nil)。因此,在检查错误时必须依赖它的第二个参数err。 |
ttl, err, value = cache:probe(key) | 检查值是否被缓存。如果是,则返回剩余的ttl。否則返回nil。被缓存的值也可以是nil。第三个返回值是被缓存的值本身。 |
cache:invalidate_local(key) | 从节点的缓存中刪除一个键对应的值。 |
cache:invalidate(key) | 从节点的缓存中删除值,并将删除事件传播到集群中的所有其他节点。 |
cache:purge() | 从节点的缓存中移除所有值。 |
使用key-auth插件进行举例:
-- handler.lua
local CustomHandler = {
VERSION = "1.0.0",
PRIORITY = 10,
}
local kong = kong
local function load_credential(key)
local credential, err = kong.db.keyauth_credentials:select_by_key(key)
if not credential then
return nil, err
end
return credential
end
function CustomHandler:access(config)
-- retrieve the apikey from the request querystring
local key = kong.request.get_query_arg("apikey")
-- 先创建一个缓存cache_key键,然后使用该键从缓存中检索该键(如果缓存未命中,则从数据库中检索)
local credential_cache_key = kong.db.keyauth_credentials:cache_key(key)
-- We are using cache.get to first check if the apikey has been already
-- stored into the in-memory cache. If it's not, then we lookup the datastore
-- and return the credential object. Internally cache.get will save the value
-- in-memory, and then return the credential.
local credential, err = kong.cache:get(credential_cache_key, nil,
load_credential, credential_cache_key)
if err then
kong.log.err(err)
return kong.response.exit(500, {
message = "Unexpected error"
})
end
if not credential then
-- no credentials in cache nor datastore
return kong.response.exit(401, {
message = "Invalid authentication credentials"
})
end
-- set an upstream header if the credential exists and is valid
kong.service.request.set_header("X-API-Key", credential.apikey)
end
return CustomHandler
二、缓存失效
在kong的官方文档中,对缓存失效的描述如下:
Every time a cached custom entity is updated or deleted in the data store (i.e. using the Admin API), it creates an inconsistency between the data in the data store, and the data cached in the Kong nodes’ memory. To avoid this inconsistency, we need to evict the cached entity from the in-memory store and force Kong to request it again from the data store. We refer to this process as cache invalidation.
1、kong的自动缓存失效原理
如果开发者是依赖定义实体对象时,使用cache_key
属性实现的缓存机制,那么缓存失效功能时开箱即用的。在kong框架的插件(二)中提到的key-auth的插件daos实体和之后表格中对cache_key的描述可以看到,key-auth插件的daos实体的cache_key为字段key,这里使用key是因为它的唯一性约束,不会有两个实体生成相同的缓存键。
我们可以使用DAO对象的cache_key方法生成缓存键:
cache_key = kong.db.<dao>:cache_key(arg1, arg2, arg3, ...)
该方法中的参数必须时实体中cache_key属性指定的字段,并按照指定顺序排列。开发者需要确保生成的缓存键是唯一的。
对于该方法生成的缓存,其缓存失效是一个自动过程。CRUD操作会使kong将缓存标记为已受影响,并将其广播到集群中的其他节点,以便它们能在缓存中删除这个特定值(这里主要使用了kong的worker.events和cluster_events回调函数)。处理下一个请求时,kong网关会从数据库中获取新值。如果操作父对象时键-值发生变化,kong网关会对父对象和子对象同时执行缓存失效操作。
注意:Kong提供的负缓存。在上面的示例中,如果给定key在数据库中没有找到,缓存模块将该key存储一个miss标记,就像它是命中一样。同样该“创建”事件也由Kong传播到各个节点。等将来经过CRUD操作后,该key有更新真正有效的值,此时各个节点可以将缓存里存储的miss标记驱逐,并从数据库中获取新创建的apikey。
2、手工执行缓存失效
在某些情况下,实体模式的cache_key属性不够灵活,必须手动使其缓存无效。造成这种情况的原因可能是插件没有通过传统的foreign = "parent_entity:parent_attribute"语法定义与另一个实体的关系,或者因为它没有使用DAO中的cache_key方法。无论哪种原因,开发者都需要订阅kong正在监听的缓存失效事件,并执行开发者自定义的缓存失效逻辑。
要监听kong内部的缓存失效通道,我们需要在插件的init_worker钩子方法中实现以下内容:
function MyCustomHandler:init_worker()
-- 监听 Consumer对象上的所有CRUD操作
kong.worker_events.register(function(data)
end, "crud", "consumers")
-- 或者监听单个操作
kong.worker_events.register(function(data)
kong.log.inspect(data.operation) -- "update"
kong.log.inspect(data.old_entity) -- old entity table (only for "update")
kong.log.inspect(data.entity) -- new entity table
kong.log.inspect(data.schema) -- entity's schema
end, "crud", "consumers:update")
end
一旦上述监听器为所需的实体设置好,您就可以对插件缓存的任何实体执行手动失效操作。例如:
kong.worker_events.register(function(data)
if data.operation == "delete" then
local cache_key = data.entity.id
kong.cache:invalidate("prefix:" .. cache_key)
end
end, "crud", "consumers")
参考资料:
- 《Kong网关 --入门、实战与进阶》
- Kong官方文档