原文链接:OpenResty 实战之缓存应用
本文给大家介绍一下 OpenResty 的缓存应用,主要涉及 Shared_Dict,LruCache,多级缓存 mlcache。
缓存原则
缓存是一个大型系统中非常重要的一个组成部分。一个生产环境的缓存,需要根据自己的业务瓶颈制作合理的缓存方案。一般来说缓存有两个原则:
- 缓存越靠近用户的请求越好:能在用户本地的,就不要再发,能使用 cdn 的就不要回源服务器。
- 尽量使用本机和本进程的缓存解决,因为跨机器或者跨机房会造成带宽压力和延迟。
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 缓存,以便其他工作者进行检索。
参考:
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于