惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
阮一峰的网络日志
阮一峰的网络日志
S
Secure Thoughts
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Threat Research - Cisco Blogs
P
Privacy & Cybersecurity Law Blog
The Hacker News
The Hacker News
H
Heimdal Security Blog
W
WeLiveSecurity
L
LINUX DO - 热门话题
Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
The Last Watchdog
The Last Watchdog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
D
DataBreaches.Net
I
Intezer
Webroot Blog
Webroot Blog
C
Cisco Blogs
AWS News Blog
AWS News Blog
博客园 - 聂微东
T
The Blog of Author Tim Ferriss
V
Vulnerabilities – Threatpost
罗磊的独立博客
Google DeepMind News
Google DeepMind News
N
Netflix TechBlog - Medium
Schneier on Security
Schneier on Security
宝玉的分享
宝玉的分享
博客园 - 叶小钗
PCI Perspectives
PCI Perspectives
D
Docker
Scott Helme
Scott Helme
NISL@THU
NISL@THU
J
Java Code Geeks
B
Blog RSS Feed
Google Online Security Blog
Google Online Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
The Exploit Database - CXSecurity.com
AI
AI
美团技术团队
Cloudbric
Cloudbric
月光博客
月光博客
P
Proofpoint News Feed
T
Tailwind CSS Blog
Google DeepMind News
Google DeepMind News
小众软件
小众软件
www.infosecurity-magazine.com
www.infosecurity-magazine.com
The Cloudflare Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org

Posts on WKLKEN THINKING

apisix 中的服务发现机制 apisix 中的负载均衡 apisix etcd机制 聊聊框架 关于 k8s 的 zero downtime deployment 一些建议 apisix 遇到的一些问题 关于在除夕前一天换了一个洗衣机的故事 Django DRF 性能优化 DRF 的一些实践 Part1: Serializer DRF继承关系图 Better Code: 关于接口的灵活性 新的仓库: wklken/naming 缓存使用的一些经验 Better Code: 抽象: 可扩展性与可维护性的抉择 Better Code: 异常时, 该提示用户哪些信息? Better Code: 更好的异常日志打印 Go: some libs Go: go-redis/cache升级的坑 Go: logrus性能提升 Go: gin validation 远程办公的一点总结 Go: 开发过程中的一些bug 项目管理实践: 风险驱动开发 Go: 一种error wrap调用链处理方式 漫谈技术选型 Go: 基于 apitest 做handler层单元测试 Go: go-sql-driver interpolateparams参数优化 [分享]深度工作 你需要更多的思考时间 Django项目重构小结 工作七年小结: 学习,生活及其他 [分享]bash日常: bash-utils 极客时间推广海报 2017总结: 予时光以意义 k8s APIServer源码: api注册详细细节 k8s APIServer源码: api注册主体流程 k8s APIServer源码: 服务启动 k8s APIServer源码: go-restful框架 重构 - 读书笔记(Python示例) 写给新人的沟通建议 vim 杂谈 - 关于快速编辑 vim 杂谈 - 关于移动 读书笔记-重构: 章11 处理概括关系 读书笔记-重构: 章10 简化函数调用 读书笔记-重构: 章9 简化表达式 读书笔记-重构: 章8 重新组织数据 读书笔记-重构: 章7 在对象之间搬移特性 读书笔记-重构: 章6 重新组织函数 Python 代码规范小结 [分享]关于vim ElasticSearch集群部署文档 Logstash+ElasticSearch处理mysql慢查询日志 [分享]关于代码调试DE那些事 Logstash+ElasticSearch+Kibana- 实现相对通用的数据收集分析 ELK维护的一些点(二) [分享]Python源码剖析-数据结构 一些Centos Python生产环境的部署命令 摘录<<6个月学会任何一种外语>> ELK 维护的一些点 也许是一个新的开始 一些vim的个性化配置 读书笔记-调试九法 这段时间的一些想法 Python 源码阅读 - 垃圾回收机制 我为什么要写博客 APUE笔记-第一章 UNIX基础知识 Python源码阅读-闭包的实现 Python源码阅读-内存管理机制(二) Python源码阅读-内存管理机制(一) Python-基础-数据结构小结 '活动'设计的一些trick 一些简单的Python测试题 我的tmux配置及说明【k-tmux】 Review and Restart 工作四周年小结 vim插件: surround & repeat[成对符号编辑] vim插件: gundo[时光机] vim插件: expand-region[区域选中] vim插件: quickrun[快速执行] vim插件: trailing-whitespace[行尾空格处理] vim插件: closetag[成对标签补全] vim插件: ctrlp[文件搜索] vim插件: airline[状态栏增强] vim插件: theme[主题] vim插件: tagbar[大纲式导航] vim插件: nerdcommenter[快速注释] vim插件: rainbow_parentheses[括号高亮] vim插件: syntastic[语法检查] vim插件: delimitmate[符号自动补全] vim插件: matchit[成对标签跳转] vim插件: easy-align[快速对齐] vim插件: multiple-cursors[多光标操作] vim插件: vim-signature[快速标记跳转] vim插件: easymotion[快速跳转] vim插件: vundle[管理插件] Elasticsearch几个问题的解决 分享一份 Vim 简介PPT k-vim 更新9.0版本 关于知识管理工具的思考 Logstash+ElasticSearch+Kibana处理nginx访问日志
apisix 中的 lrucache
2024-09-27 · via Posts on WKLKEN THINKING

基于 3.10.0 版本

apisix 的 lrucache 封装了 resty-lrucacheresty-lock

机制

代码不多,逐行分析

先看下新建一个 lrucache时支持的选项 opts

type  类型, 如果是插件是 plugin
count 容量, 如果没有设置, 类型为 plugin 的默认 count=8,其他类型默认 count=1024
ttl   过期时间,如果没有设置,类型为 plugin 默认 ttl=5min, 其他类型默认 ttl=60min

release  缓存命中,但是val.version不是预期的,会调用hook函数, 做一些额外的行为
invalid_stale    是否允许获取已过期的值, key 对应value过期,但是val.version是一致的,是的话,会重新将这个已过期值设置到 lrucache中 (即,会命中缓存,不会调用 create_obj_fun
serial_creating  确保缓存对象是串行创建的,即创建流程中会上锁避免竟态场景下创建同一个对象

new_lru_fun

local function new_lru_fun(opts)
    -- 选项
    local item_count, item_ttl
    if opts and opts.type == 'plugin' then
        item_count = opts.count or PLUGIN_ITEMS_COUNT
        item_ttl = opts.ttl or PLUGIN_TTL
    else
        item_count = opts and opts.count or GLOBAL_ITEMS_COUNT
        item_ttl = opts and opts.ttl or GLOBAL_TTL
    end

    local item_release = opts and opts.release
    local invalid_stale = opts and opts.invalid_stale
    local serial_creating = opts and opts.serial_creating
  
    -- 创建一个 lrucache 对象, 大小为 item_count
    local lru_obj = lru_new(item_count)

    -- 返回一个fun, 其参数为 key, version, create_obj_fun, 以及 create_obj_fun 的参数
    return function (key, version, create_obj_fun, ...)
        -- 非 serial_creating ,或者 phase 不是 ssl_session_fetch、ssl_session_store、rewrite、access、content、timer 之一
        -- nginx.get_phase: https://github.com/openresty/lua-nginx-module#ngxget_phase
        if not serial_creating or not can_yield_phases[get_phase()] then
            -- 获取缓存
            local cache_obj = fetch_valid_cache(lru_obj, invalid_stale,
                                item_ttl, item_release, key, version)
            -- 命中直接返回
            if cache_obj then
                return cache_obj.val
            end
      
            -- 主意,这里没有上锁; 重复调用 create_obj_fun 以及 lru_obj:set 是允许的
            -- 没上锁的好处:1. 不必要的复杂度 2. 性能

            -- 未命中,获取数据,设置缓存,返回
            local obj, err = create_obj_fun(...)
            if obj ~= nil then
                -- 注意这里 set 的时候,值 = 对象+version
                lru_obj:set(key, {val = obj, ver = version}, item_ttl)
            end

            return obj, err
        end

        -- 获取缓存
        local cache_obj = fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
                            item_release, key, version)
        -- 命中,直接返回
        if cache_obj then
            return cache_obj.val
        end

        -- 否则,进入 获取-设置流程
        -- 1. 获取 lock
        local lock, err = resty_lock:new(lock_shdict_name)
        if not lock then
            return nil, "failed to create lock: " .. err
        end

        -- 2. 上锁
        local key_s = tostring(key)
        log.info("try to lock with key ", key_s)

        local elapsed, err = lock:lock(key_s)
        if not elapsed then
            return nil, "failed to acquire the lock: " .. err
        end

        -- 3. 再次获取缓存: 避免从 获取缓存失败 到 上锁成功 这段时间内 其他地方已经获取缓存并成功
        cache_obj = fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
                        nil, key, version)
        --- 3.1 命中则解锁,返回
        if cache_obj then
            lock:unlock()
            log.info("unlock with key ", key_s)
            return cache_obj.val
        end
       
        -- 4. 调用 create_obj_fun, 获取数据
        local obj, err = create_obj_fun(...)
        --- 4.1 获取成功,设置
        if obj ~= nil then
            lru_obj:set(key, {val = obj, ver = version}, item_ttl)
        end
        -- 如果获取成功, obj正确, err=nil; 如果获取失败,obj nil, err!=nil
    
        --- 5. 解锁
        lock:unlock()
        log.info("unlock with key ", key_s)
        
        -- 6. 返回
        return obj, err
    end
end

说明:

  1. 如果 opts.serial_creating = true, 在lrucache流程中会上锁,如果 opts.serial_creating = false或者没有设置,则不会上锁
  2. 如果当前的 phase nginx.get_phase() 不是 ssl_session_fetch、ssl_session_store、rewrite、access、content、timer 之一(can_yield_phases, 即属于不能 yield的 phase), 例如 rewrite 阶段、log 阶段等,那么 lrucache流程也不会上锁;
    1. 即,如果配置了 serial_creating = true, 当前 phase 也必须是 can_yield_phases: ssl_session_fetch、ssl_session_store、rewrite、access、content、timer,才会是附带上锁的逻辑
    2. 如果phase被yield,那么必须上锁,不上锁的话,执行一半被yeild, 当恢复执行时,获取的缓存对象可能不是最新,使用过程中会发现缓存被设置回旧的值。

大部分的使用场景,都只设置了 count/ttl/type, 所以使用时几乎都没有上锁

fetch_valid_cache

local function fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
                                 item_release, key, version)
    -- 从 lru 中获取 key 对应值
    --- 如果存在且未过期, obj非空是, 否则obj = nil
    --- 如果存在但过期了, obj=nil, stale_obj != nil
    local obj, stale_obj = lru_obj:get(key)
    -- 1. 命中,并且 version 一致,直接返回
    if obj and obj.ver == version then
        return obj
    end
    
    -- 2. 没有命中(如果有已过期的值, stale_obj非空),或者 version 不一致
  
    --- 2.1 如果配置允许使用陈旧的值, 并且缓存中存在过期的值,且 version 一致
    if not invalid_stale and stale_obj and stale_obj.ver == version then
        -- 重新设置为陈旧的值,并且返回
        lru_obj:set(key, stale_obj, item_ttl)
        return stale_obj
    end

    -- 没有命中
    --- a. key 对应 value 不存在, 此时 obj = nil
    --- b. key 对应 value 存在,但是已过期,没有配置 invalid_stale = true, 此时 obj = nil
    --- c. key 对应的 value 存在,但是 version不一致, 此时 obj != nil
  
    --- 2.2 如果配置了 item_release, 并且 obj 非空(value存在但是version不一致),调用 item_release(是一个 hoook?)
    ---- 目的是,发现 version不一致,主动调用缓存主动刷新接口?
    ---- 目前项目中没有调用点
    if item_release and obj then
        item_release(obj.val)
    end

    --- 2.3 返回 nil
    return nil
end

接口

lrucache中提供了一个快捷创建插件 lrucache的函数

plugin_ctx

---
--  Cache some objects for plugins to avoid duplicate resources creation.
--
-- @function core.lrucache.plugin_ctx
-- @tparam table lrucache LRUCache object instance.
-- @tparam table api_ctx The request context.
-- @tparam string extra_key Additional parameters for generating the lrucache identification key.
-- @tparam function create_obj_func Functions for creating cache objects.
-- If the object does not exist in the lrucache, this function is
-- called to create it and cache it in the lrucache.
-- @treturn table The object cached in lrucache.
-- @usage
-- local function create_obj() {
-- --   create the object
-- --   return the object
-- }
-- local obj, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_obj)
-- -- obj is the object cached in lrucache
local function plugin_ctx(lrucache, api_ctx, extra_key, create_obj_func, ...)
    local key, ver = plugin_ctx_key_and_ver(api_ctx, extra_key)
    return lrucache(key, ver, create_obj_func, ...)
end

-- 生成 key 和 version
-- key = api_ctx.conf_type .. "#" .. api_ctx.conf_id = route#123  每个路由唯一
-- version = api_ctx.conf_version  =  = route.modifiedIndex, 只要 route 配置不变,这个version 不变
local function plugin_ctx_key_and_ver(api_ctx, extra_key)
    local key = api_ctx.conf_type .. "#" .. api_ctx.conf_id

    if extra_key then
        key = key .. "#" .. extra_key
    end

    return key, api_ctx.conf_version
end

使用示例

local lrucache = core.lrucache.new({
    type = "plugin",
})

-- 这里创建一个携带 limit object 的 lrucache
-- 其中 
-- 1. lrucache
-- 2. 没有设置 extra_key
-- 3. ctx, conf 为 apisix plugin 机制中的 ctx, conf
-- 4. create_limit_obj 为创建函数
local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_limit_obj, conf)

注意,这里会存在一个非常容易导致 bug 的点:假设我在公共模块配置了一个lrucache,然后在多个插件中调用,那么你会发现不同插件 lrucache 获取到的对象了, api_ctx.conf_type/api_ctx.conf_id 对于同一个资源是一致的, 所以同一个资源如果没有指定 extra_key,那么多个插件调用生成的key是一样地!

解决:公共模块中如果使用 plugin_ctx, 需要额外指定 extra_key = plugin_name

-- ratelimit/init.lua

local lrucache = core.lrucache.new(
    {
        type = "plugin",
        serial_creating = true,
    }
)

# bug here
function _M.rate_limit(conf, ctx, key, count, time_window)
      ......
      local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_limit_obj, plugin_name)

end

# ok here
function _M.rate_limit(conf, ctx, plugin_name, key, count, time_window)
      ......
      local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, plugin_name, create_limit_obj, plugin_name)

end

其他同类库

Fast and automated layered caching for OpenResty.

GitHub - thibaultcha/lua-resty-mlcache: Layered caching library for OpenResty 两级缓存库

默认lrucache是 worker 维度的, 这个库实现了两集缓存,二级缓存走的 lua_shared_dict, 进程维度,多个worker 共享