OpenResty 实战之缓存应用

本贴最后更新于 2352 天前,其中的信息可能已经水流花落

原文链接:OpenResty 实战之缓存应用

本文给大家介绍一下 OpenResty缓存应用,主要涉及 Shared_DictLruCache,多级缓存 mlcache。

缓存原则

缓存是一个大型系统中非常重要的一个组成部分。一个生产环境的缓存,需要根据自己的业务瓶颈制作合理的缓存方案。一般来说缓存有两个原则:

  1. 缓存越靠近用户的请求越好:能在用户本地的,就不要再发,能使用 cdn 的就不要回源服务器。
  2. 尽量使用本机和本进程的缓存解决,因为跨机器或者跨机房会造成带宽压力和延迟。

shared_dict

OpenResty 自带的缓存机制,是提前创建了一块固定大小的共享内存,所有的 worker 都可以(加锁)使用。内部使用 LRU 算法,过期的数据将被清除。

完整示例:

worker_processes 1; error_log logs/error.log info; events { worker_connections 512; } http { log_format myformat '$remote_addr $status $time_local'; access_log logs/access.log myformat; lua_shared_dict mycache 8M; server { listen 8080; charset utf-8; location /redis { content_by_lua_file lua/redis_exp.lua; } location /mixed { content_by_lua_block { function get_cache(key) local cache = ngx.shared.mycache local value = cache:get(key) return value end function set_cache(key, value, expire) if not expire then expire = 0 end local cache = ngx.shared.mycache local succ, err, forcible = cache:set(key, value, expire) return succ end local sum = get_cache("sum") if not sum then set_cache("sum", 1) sum = 1 end sum = sum + 1 set_cache("sum", sum) ngx.say("sum ", sum) } } } }

Lua LRU cache

这个引入了一个 Lua 的外部库,在单个 worker 中各个请求之间进行数据共享,这里尤其是要注意库的使用方式,因为不同的调用方式会导致缓存失效。

我们先给出正确的版本:

worker_processes 1; error_log logs/error.log info; events { worker_connections 512; } http { log_format myformat '$remote_addr $status $time_local'; access_log logs/access.log myformat; lua_package_path "/home/zhoulihai/Desktop/work/lua/?.lua;;"; server { listen 8080; charset utf-8; location /redis { content_by_lua_file lua/redis_exp.lua; } location /mixed { content_by_lua ' require("lru").go() '; } } }

这里主要注意两点:lua_package_path "/home/zhoulihai/Desktop/work/lua/?.lua;;";content_by_lua

第一个参数是指定 lua 脚本的位置,作用是为第二句话打基础,直接使用 content_by_lua_file 来调用 lua 脚本使用的位置变量和 content_by_lua 脚本的位置变量是不同的。这里需要声明一下位置。

第二个地方应该是这样使用 content_by_lua,因为这个缓存不能每次都创建一个新的,利用的是 OpenResty 的只加载一次 lua 代码的特性,使创建缓存的代码只运行一次。还有一种方法是另创建一个 lua 脚本来调用这句话。

lru.lua 文件:

local _M = {} local lrucache = require "resty.lrucache" local c, err = lrucache.new(200) if not c then return error("error on new lrucache: ", err or "unknow") end function _M:go() local sum = c:get("sum") if not sum then c:set("sum", 0) sum = 0 end sum = sum + 1 c:set("sum", sum) ngx.say("sum ", sum) end return _M

这样每访问一次都会增加一个计数。

另一个演示

location /mixed { content_by_lua_file lua/lru_t.lua; }

lru_t.lua 文件:

require("lru").go()

“LRU cache 的目的”

做一个东西一定是有它的目的的,不能为了炫技术而去做一点东西。首先,为什么做缓存,答案是节省时间。为了实现一个缓存,你可能会这样做:

cache = [] ### set cache[key] = value ### get return cache[key]

但是久而久之,如果 key 足够多,占用的内存会越来越大。你会想着删除一点 key,但是这里遇到问题了,删哪些部分呢?

于是一般人都会想,很久不用的 key 就可以删掉了,最近使用过的 key 一定是要保留的。这就是 lrucache 的想法了:key 个数可以定义最多数目,超过数目了,删除最老的数据,保留最新的。

“实现原理”

为了实现 LRUCache,一定要有一个 cache[key]=value 这种 kv 的存储结构,但是怎么维护顺序呢?resty 采用的是双向链表维护顺序。

队列和节点

lrucache 有两个节点队列,一个是 free_queue,另一个是 cache_queue。set 操作为从 free_queue 中取出节点放到 cache 队列中;get 操作为从 cache 队列中取 node。

因为 lua 和 c 语言可以借助 ffi 很方便地互相嵌入,节点的定义是使用 c 代码定义的:

typedef struct lrucache_queue_s lrucache_queue_t; struct lrucache_queue_s { double expire; /* in seconds */ lrucache_queue_t *prev; lrucache_queue_t *next; };

一开始初始化 queue。

local mt = { __index = _M } function _M.new(size) if size < 1 then return nil, "size too small" end local self = { hasht = {}, free_queue = queue_init(size), cache_queue = queue_init(), key2node = {}, node2key = {}, } return setmetatable(self, mt) end

set 操作

如果 free_queue 是有 node 的,set 一个 key 的步骤如下:

  • 从 free 队列中取一个 node

  • 把 node 和 key 加入到 node2key 和 key2node 中

node2key[ptr2num(node)] = key key2node[key] = node
  • 把 node 从 free 队列中删除,queue_remove(node),删除队列 node 代码如下:
local function queue_remove(x) local prev = x.prev local next = x.next next.prev = prev prev.next = next -- for debugging purpose only: x.prev = NULL x.next = NULL end
  • 把 node 加入到 cache 队列中,queue_insert_head(self.cache_queue, node)
-- (bt) h[0] as a header node without any data, so queue length is size+1 local function queue_insert_head(h, x) x.next = h[0].next x.next.prev = x x.prev = h h[0].next = x end

如果 free_queue 没有空闲的 node 了,set 一个 key 的步骤如下:

与上面第一步的”从 free 队列中取一个 node”不同,这里取 node 的方法为从 cache 队列的最后摘一个 node 下来。

local free_queue = self.free_queue local node2key = self.node2key -- (bt) if free_queue is empty, delete the last node. if queue_is_empty(free_queue) then -- evict the least recently used key -- assert(not queue_is_empty(self.cache_queue)) node = queue_last(self.cache_queue) local oldkey = node2key[ptr2num(node)] -- print(key, ": evicting oldkey: ", oldkey, ", oldnode: ", -- tostring(node)) if oldkey then hasht[oldkey] = nil key2node[oldkey] = nil end else -- take a free queue node node = queue_head(free_queue) -- print(key, ": get a new free node: ", tostring(node)) end

这里说下 queue_last 函数 O(1)的实现:

local function queue_last(h) return h[0].prev end

除了去 node 不一致之外,其他步骤与 free queue 有空闲 node 是一样的。

get 操作

和 set 相比,get 的操作比较简单。如果有 key 存在,则把 node 移到 cache 队列的最前面。

function _M.get(self, key) local hasht = self.hasht local val = hasht[key] if not val then return nil end local node = self.key2node[key] -- print(key, ": moving node ", tostring(node), " to cache queue head") -- (bt) remove it from queue and insert to head later. local cache_queue = self.cache_queue queue_remove(node) queue_insert_head(cache_queue, node) if node.expire >= 0 and node.expire < ngx_now() then -- (bt) expired -- print("expired: ", node.expire, " > ", ngx_now()) return nil, val end return val end

delete 操作

如果删除某一个 key 后,需要回收 cache_queue 中的 node 到 free_queue 中。

function _M.delete(self, key) self.hasht[key] = nil local key2node = self.key2node local node = key2node[key] if not node then return false end key2node[key] = nil self.node2key[ptr2num(node)] = nil -- (bt) 把node回收到free_queue中 queue_remove(node) queue_insert_tail(self.free_queue, node) return true end

队列操作

lru-cache 中使用链表操作,remove, insert_head 等,耗时都是 O(1)。

-- (bt) queue is a double-pointer-list. local function queue_init(size) if not size then size = 0 end local q = ffi_new(queue_arr_type, size + 1) ffi_fill(q, ffi_sizeof(queue_type, size + 1), 0) if size == 0 then q[0].prev = q q[0].next = q else local prev = q[0] for i = 1, size do local e = q[i] prev.next = e e.prev = prev prev = e end -- (bt) it is a loop local last = q[size] last.next = q q[0].prev = last end return q end -- (bt) if queue is empty, header is tail. local function queue_is_empty(q) -- print("q: ", tostring(q), "q.prev: ", tostring(q), ": ", q == q.prev) return q == q[0].prev end local function queue_remove(x) local prev = x.prev local next = x.next next.prev = prev prev.next = next -- for debugging purpose only: x.prev = NULL x.next = NULL end -- (bt) h[0] as a header node without any data, so queue length is size+1 local function queue_insert_head(h, x) x.next = h[0].next x.next.prev = x x.prev = h h[0].next = x end local function queue_last(h) return h[0].prev end local function queue_head(h) return h[0].next end

新的多级缓存库 lua-resty-mlcache

lua-resty-mlcache 用于 OpenResty 的快速自动分层缓存。

这个库可以作为键 / 值存储缓存的标量 Lua 类型和表来操作,结合了 Lua 共享的 dict API 和 Lua-resty-lucache 的威力,从而得到一个非常高性能和灵活的缓存解决方案。

特点:

使用 TTLs 进行缓存和负缓存。

通过 lua-resty-lock 内置 mutex,可以防止数据库 / 后端在缓存未命中时对数据库 / 后端的狗堆效应。

内置的工作人员之间的通信,以宣传高速缓存无效,并允许工作人员更新他们的 L1(lua-resty-lucache)缓存的更改(set () ,delete ())。

支持分离命中和未命中的缓存队列。

可以创建多个独立的实例来保存各种类型的数据,同时依赖于相同的共享型 lua L2 高速缓存。

这个库中各种缓存级别的说明:

┌─────────────────────────────────────────────────┐ │ Nginx │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │worker │ │worker │ │worker │ │ │ L1 │ │ │ │ │ │ │ │ │ Lua cache │ │ Lua cache │ │ Lua cache │ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌───────────────────────────────────────┐ │ │ │ │ │ │ L2 │ lua_shared_dict │ │ │ │ │ │ │ └───────────────────────────────────────┘ │ │ │ mutex │ │ ▼ │ │ ┌──────────────────┐ │ │ │ callback │ │ │ └────────┬─────────┘ │ └───────────────────────────┼─────────────────────┘ │ L3 │ I/O fetch ▼ Database, API, DNS, Disk, any I/O...

缓存级别的层次结构是:

L1: 最近最少使用的 Lua VM 缓存,使用了 Lua-resty-lucache。 如果填充,则提供最快的查找,并且避免耗尽工作人员的 Lua VM 内存。

L2: 所有工作者共享的共享存储区。 只有当 L1 未命中时才能访问这个级别,并防止 worker 请求 L3 缓存。

L3: 一个自定义函数,它只能由一个工作者来运行,以避免数据库 / 后端上的狗堆效应(通过 lua-resty-lock)。 通过 l 3 获取的值将被设置为 l 2 缓存,以便其他工作者进行检索。

参考:

OpenResty 缓存

resty_lrucache 解读

  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 53 关注
  • 缓存
    42 引用 • 70 回帖
  • Shared_Dict
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...