先来看一张简单的缓存流程图,如图10-1所示。
《缓存利器》一、worker进程的共享内存_第1张图片
图10-1 缓存流程图

CDN缓存和proxy_cache缓存在前面的章节中已经有过介绍,它们可以提升访问速度,减少回源流量,从而减少后端业务层的压力。

在业务层中,也有不少服务有自己的缓存,这些缓存既可以减少服务自身的运算量,也可以减轻后端数据库等服务的压力。常见的业务层缓存工具有Redis、Memcached、Couchbase等。它们可以用来完成大量的业务数据缓存和计算,除这些工具外,Ngx_Lua还提供了一套内部缓存系统,它是key/value类型的,有个很大的优点就是不会产生额外的TCP开销,请求在内部的缓存系统就能完成数据的读取。在数据结构或业务需求不复杂的情况下,使用Ngx_Lua内部缓存将是一个非常好的选择。

worker进程的共享内存

worker进程的共享内存,顾名思义就是Nginx的全部worker进程都共享这份内存数据,如果某个worker进程对该数据进行了修改,其他的worker进程看到的都将是修改后的数据。我们可以利用共享内存来存放缓存数据,提升吞吐率,减少到达后端的请求次数。

10.1.1 创建共享内存区域

在使用共享内存之前,需要先创建1块内存区域,即声明共享内存的大小和名字。这要用到指令lua_shared_dict。

语法:lua_shared_dict
默认:no
环境:http
含义:声明1个名字为name,大小为size的共享内存区域,用来存放基于Lua字典的共享内存。
例如,声明1个名字为shared_test、内存空间为1MB的共享内存区域,如下所示:
lua_shared_dict shared_test 1M;
lua_shared_dict的参数size支持KB和MB两种单位,并且允许设置的最小值为12KB,如果小于12KB启动就会报错。当size的值小于12KB且大于8KB时,报错如下:
nginx: [crit] ngx_slab_alloc() failed: no memory
当size的值小于8KB时,则报错内容如下:
nginx: [emerg] invalid lua shared dict size "2k" in /usr/local/nginx_1.12.2/conf/nginx.conf
通过lua_shared_dict声明了多少内存,Nginx就会在系统中占用多少内存。例如,如果声明1个500MB的共享内存区域,那么,当重载Nginx配置后,观察系统的内存使用情况,会发现Nginx的每个worker进程的内存占用量都在500MB以上(500MB共享内存+Nginx本身要占用的内存,会超过500MB)。
在配置共享内存区域的数据时须注意如下几点。

1.共享内存区域的数据是所有worker进程共享的,因此一旦对其进行修改,所有worker进程都会使用修改后的数据,worker进程之间同时读取或修改同一个共享内存数据会产生锁。

2.共享内存区域的大小是预先分配的,如果内存空间使用完了,会根据LRU(Least Recently Used,即最近最少使用)算法淘汰访问量少的数据。

3.在Nginx中可以配置多个共享内存区域。

10.1.2 操作共享内存

创建共享内存区域后,可以尝试对其进行读/写操作,示例如下:
--声明共享内存

lua_shared_dict shared_test 1m;
server {
    listen 80;
    server_name testnginx.com;
    location /set {
        content_by_lua_block {
            --获取指定共享内存的Lua字典对象
            local shared_test = ngx.shared.shared_test
            --在共享内存里存储URL参数a的值,类似于Redis的set指令
            shared_test:set("a",ngx.var.arg_a)
            ngx.say("STORED")
        }
    }
    location /get {
        content_by_lua_block {
            --获取指定共享内存的Lua字典对象
            local shared_test = ngx.shared.shared_test
            --读取共享内存的数据,获取a的值,类似于Redis的get指令
            ngx.say(shared_test:get("a"))
        }
    }
}

在上述配置中,先将参数a=123传递给/set的location,由set指令完成存储,然后再请求/get,读取共享内存中a的值,执行结果如下:

# curl   'http://testnginx.com/set?a=123'
STORED
# curl   'http://testnginx.com/get?a'
123

Ngx_Lua提供了很多操作共享内存的指令,下面将会介绍一些常用的指令。
注意:下面的DICT是变量,它指的是lua_shared_dict声明的共享内存区域的名字。


ngx.shared.DICT
语法:dict = ngx.shared.DICT或dict = ngx.shared[name_var]
环境:init_by_lua、init_worker_by_lua、set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua、ngx.timer.,balancerby lua、ssl_certificate_by_lua、ssl_session_fetch_by_lua、ssl_session_store_by_lua
含义:获取指定的共享内存(即Lua字典)中的数据。


ngx.shared.DICT.set
语法:success, err, forcible = ngx.shared.DICT:set(key, value, exptime?, flags?)
含义:设置key/value的值,并存入共享内存重,其中value可以是Lua布尔值、数字、字符串,或nil类型的,不可以是table类型的。
该请求会返回如下3个值。
success:布尔值,用来表示是否成功set。
err:如果success的值是false,就会将错误信息存放在err中。
forcible:布尔值,用来表示是否有数据被强制删除。在存储数据的过程中,如果内存空间不够就会使用LRU算法清除数据,此时,forcible为是true。
除key/value外,set中其他的参数作用如下。
exptime为可选参数,作用是设置key/value的有效期(单位是秒),默认是不过期的。
flags为可选参数,用来做用户标识,可以标识set操作的执行者,默认是0。只能设置number类型,例如,可以使用开发人员的工位号作为flags,这样就可以知道每条set指令的操作者了,方便后期排查问题。


ngx.shared.DICT.safe_set
语法:ok, err = ngx.shared.DICT:safe_set(key, value, exptime?, flags?)
含义:与set类似,但在内存不足的情况下,它不会覆盖已经存在且没有过期的数据,而会报“no memory”错误并返回nil。


ngx.shared.DICT.get
语法:value, flags = ngx.shared.DICT:get(key)
含义:获取指定key对应的value,value的数据类型与存储时的原始数据类型一致,但如果key不存在或过期就返回nil。
flags用来获取是否对用户做了标识,一般在set时都会加入标识,如果没有,则默认为0。


ngx.shared.DICT.get_stale
语法:value, flags, stale = ngx.shared.DICT:get_stale(key)
含义:与get类似,但返回的是过期的值,过期的值无法保证一直存在,如当内存不够时会被自动清理。
flags,获取标识,一般在set时会加入此标识,如果没有,则默认为0。
stale,代表获取的value是否已经过期,如果是,则返回true。


ngx.shared.DICT.add
语法:success, err, forcible = ngx.shared.DICT:add(key, value, exptime?, flags?)
含义:与set类似,但如果key在缓存里存在且没有过期就不会执行add指令,此时err会返回“exist”;如果key不存在或已过期,则会执行add指令并把key/value的值存入共享内存中。


ngx.shared.DICT.safe_add
语法:ok, err = ngx.shared.DICT:safe_add(key, value, exptime?, flags?)
含义:与set类似,但在内存不足的情况下,它不会覆盖已经存在且没有过期的数据,而会报“no memory”错误并返回nil。


ngx.shared.DICT.replace
语法:success, err, forcible = ngx.shared.DICT:replace(key, value, exptime?, flags?)
含义:和set类似,但只有当key存在时,才会把key/value的值存入共享内存中(key过期属于不存在的情况)。


ngx.shared.DICT.delete
语法:ngx.shared.DICT:delete(key)
含义:删除指定的key。


ngx.shared.DICT.incr
语法:newval, err, forcible? = ngx.shared.DICT:incr(key, value, init?)
含义:让指定key的值递增,步长是value。适合用来在请求时做计数操作。newval是数字累加后的值。如果key不存在,并且init参数为空,则请求会返回“not found”并赋值给err;如果key不存在,并且init是number类型,那么会创建1个init+value的新key。
注意:这里的key、value、init,在正常情况下都应该是number类型。


ngx.shared.DICT.flush_all
语法:ngx.shared.DICT:flush_all()
含义:清空共享内存中的所有数据,方法是让所有的数据都过期,这意味着虽然可以用get_stale的方式去读取数据,但不一定会有值存在。


ngx.shared.DICT.flush_expired
语法:flushed = ngx.shared.DICT:flush_expired(max_count?)
含义:清空共享内存中所有过期的数据,方法是释放掉这些内存,彻底清除数据,并返回所清除数据的数量。
max_count为可选参数,如果不设置此参数或设为0,则会清除全部过期数据;如果参数值大于0,则会清除相应数量的数据。


ngx.shared.DICT.get_keys
语法:keys = ngx.shared.DICT:get_keys(max_count?)
含义:获取共享内存中的全部数据,返回的是table类型的数据。
max_count设置的是需要返回的数据的数量,默认是1024。如果设置为0,则表示返回全部数据。在实际运用中要尽量避免设置为0,因为如果数据量非常大,此操作会阻塞所有访问这个共享内存数据的worker进程。
在实践中需要特别注意的地方有如下几点。
1.在存储key/value时
如果set的 forcible = true或safe_set的err = "no memory",则可以判断出共享内存的空间不够了,需要进行异常报警。
2.在获取key/value时
get和get_stale 中的flags来自set指令的添加,在进行团队开发时建议不要使用默认值,而要定制每个开发人员的flags。
3.关于incr自增长的初始化值
如果把对数据的初始化放在init_by_lua阶段执行,那么,每次重载Nginx配置时都会导致数据再次被初始化。
4.在获取全部key的数据时
get_keys默认值是1024,如果无法确定key的数量,千万不要使用get_keys(0)来读取key,因为这就像在MySQL中执行没有limit的select
from table一样。
针对上述指令,示例代码如下:

location /set {
    content_by_lua_block {
        local shared_test = ngx.shared.shared_test
        --自增长操作
        local newval, err = shared_test:incr("incr_init_n",1,2)
        ngx.say("newval:",newval, " err:",err)  --输出 newval:3 err:nil
        shared_test:set("a",ngx.var.arg_a,100,2001)
        shared_test:set("b",ngx.var.arg_a,100,2001)
        local  success, err, forcible = shared_test:set("d",ngx.var.arg_a, 100,2001)
        --输出success:true err:nil forcible:false
        ngx.say("success:",success, " err:",err, " forcible:",forcible)
        local  ok, err = shared_test:safe_set("c",ngx.var.arg_a,100,2001)
        ngx.say("ok:",ok," err:",err)           --输出ok:true err:nil
        local value, flags, stale = shared_test:get_stale("a")
        --输出 value:123s flags:2001 stale:false,因为值没有过期,所以stale为false
        ngx.say("value:",value," flags:",flags," stale:",stale)
        --读取共享里面的2个key
        local getall_key = shared_test:get_keys(2)
        if type(getall_key) == 'table' then
           ngx.say(#getall_key)                 --输出key的数量
           for _, k in ipairs(getall_key) do
                                                --输出这2个数据
             -- key:b value:123s2001  key:d value:123s2001
             ngx.say("key:",k ," value:",shared_test:get(k))
           end
        end                                
        shared_test:flush_all()                 --清除共享内存中的数据
        local getall_key = shared_test:get_keys()
        --判断有几个key,输出为type :table,llen_list: 0
        ngx.say("type:",type(getall_key),  " llen_list: ",#getall_key)
    }
}

10.1.3 制造消息队列

ngx.shared.DICT还支持消息队列功能,与消息队列相关的指令及其使用方式见表10-1。

表10-1 与消息队列相关的指令及其使用方式
《缓存利器》一、worker进程的共享内存_第2张图片
示例如下:

lua_shared_dict shared_test_1 1m; 
server {
    listen 80;
    server_name testnginx.com;
    location /lpush {
        content_by_lua_block {
            local shared_test_1 = ngx.shared.shared_test_1
           --每次请求都会产生1个自增的值,可以把它作为测试数据,当作每次lpush的值
            local newval, err = shared_test_1:incr("incr_init_n",1,0)
           --将自增的值从列表的头部插入,每次插入都会返回当前列表的长度length
            local length, err = shared_test_1:lpush("push_abc", newval)
           --输出当前请求时,显示队列当前的长度和插入的值
            ngx.say("length:",length," lpush_value:",newval)
        }
    }
    location /lpop {
        content_by_lua_block {
            local shared_test_1 = ngx.shared.shared_test_1
            --从队列的末尾取出数据并存放在val上
            local val, err = ngx.shared.shared_test_1:lpop("push_abc")
            --获取当前key的队列长度
            local len, err = ngx.shared.shared_test_1:llen("push_abc")
            --输出当前请求的队列长度和值
            ngx.say("length:",len, " val:" ,val)
        }
    }
}

发送HTTP请求进行队列的存取,如下所示:

# curl   'http://testnginx.com/lpush'
length:1 lpush_value:1
[root@testnginx ~]# curl   'http://testnginx.com/lpush'
length:2 lpush_value:2
[root@testnginx ~]# curl   'http://testnginx.com/lpush'
length:3 lpush_value:3
[root@testnginx ~]# curl   'http://testnginx.com/lpop'
length:2 val:3
[root@testnginx ~]# curl   'http://testnginx.com/lpop'
length:1 val:2
[root@testnginx ~]# curl   'http://testnginx.com/lpop'
length:0 val:1
[root@testnginx ~]# curl   'http://testnginx.com/lpop'
length:0 val:nil

上述示例是在当前key不存在或为空的情况下进行的测试,分析其操作步骤如下。

1.先从列表的头部插入3条数据,输出显示列表的长度和值都在增加。

2.再从尾部取出4条数据,从数据的输出顺序可以看出数据是从尾部取出来的,因为val的值是按照3、2、1的顺序输出的。

3.最后1条输出为nil,因为数据被取完了。
上面只是一个示例,如果在真实的开发环境下做pop操作,不断地发送HTTP请求肯定不是首选,可以使用如下方式解决这个问题。
启动1个定时任务,让它不断去pop数据,这样会节省外部HTTP建联的开销。
根据业务的实时性进行判断,评估消息的生成速度,以此来确定定时任务之间的间隔时间,定时任务可以在毫秒级别执行,采用for循环读取数据,每次取固定数量的数据进行处理。

10.1.4 lua-resty-core

当对共享内存中的key设置有效期后,需要使用与lua-resty-core模块有关的指令才可以获取有效期,但使用lua-resty-core模块需要安装很多依赖包,并且这些包的版本必须一致,否则会出现不兼容的问题,所以更建议使用OpenResty,因为它已经包含了这些安装包。
下面是与lua-resty-core有关的指令。


ngx.shared.DICT.ttl
语法:ttl, err = ngx.shared.DICT:ttl(key)
含义:获取共享内存中的key的有效期。


ngx.shared.DICT.expire
语法:success, err = ngx.shared.DICT:expire(key, exptime)
含义:修改共享内存中的key的有效期。


ngx.shared.DICT.free_space
语法:free_page_bytes = ngx.shared.DICT:free_space()
含义:确认共享内存的剩余空间大小。

10.1.5 配置环境

共享内存ngx.shared.DICT.*的指令的执行环境都是一样的,它们的配置环境分别如下。

  1. dict = ngx.shared.DICT
    支持的环境有initby lua、init_worker_by_lua、set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua、ngx.timer.、balancer_by_lua、ssl_certificate_by_lua、ssl_session_fetch _by_lua、ssl_session_store_by_lua

2.ngx.shared.DICT:get_stale和ngx.shared.DICT:get
不支持在init_by_lua, init_worker_by_lua中执行。
支持的环境有set_by_lua、rewrite_by_lua、access_by_lua、content_by_lua、header_filter_by_lua、body_filter_by_lua、log_by_lua、ngx.timer.、balancer_by_lua、ssl_certificate_by_lua、ssl_session_fetch _by_lua、ssl_session_store_by_lua

3.其他的ngx.shared.DICT.指令
不支持在init_worker_by_lua
中执行。

支持的环境有init_by_lua、set_by_lua、rewrite_by_lua、access_by_lua、contentby lua、header_filter_by_lua、body_filter_by_lua、log_by_lua、ngx.timer.、balancerby lua、ssl_certificate_by_lua、ssl_session_fetch _by_lua、ssl_session_store_by_lua*。
共享内存提供了很多类似于Redis的操作指令,这些指令可以基本满足日常的开发需求,并且能够减少网络I/O,缓存的数据也可以在每个执行阶段被方便地使用。在本书后面的架构设计上会大量使用共享内存来实现动态化配置。