openresty学习笔记

命令行

服务启动

1
2
3
4
sudo openresty -c ~/project/openresty/conf/nginx.conf -p ~/project/openresty
-c 指定配置文件
-p 指定项目目录

典型的项目目录:

├── conf
│   ├── muxx.me.conf
│   ├── nginx.conf
├── lualib
├── logs
│   ├── access.log
│   └── error.log
├── run
│   └── nginx.pid
├── htdocs
│   └── demo
│       ├── app
│       └── lualib
├── shell
│   ├── reload.sh
│   ├── start.sh
│   └── stop.sh
├── ssl
├── proxy_temp
├── fastcgi_temp
└── uwsgi_temp

Lua基础

openresty的解释器目前实际使用的是luajit,它兼容lua语法(5.1),并且在此基础上增加JIT的特性

数据类型

可以使用type获取变量数据类型

nil

变量的默认值

boolean

lua中只有falsenilfalse,其余都等于true

number

string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
string.byte
string.char
string.len
string.dump
string.format(format, ...) 格式化字符串
string.gmatch
string.gsub(s, p, r [, n]) 将s中所有子串p替换成字符串r
string.match
string.rep(s, n) 返回字符串s的n次拷贝
string.reverse
string.find(s, p [, init [, plain]]) 在s中查找字符串p,匹配成功返回s中子串的起始位置和结束位置,匹配失败返回nil
string.sub(s, i [, j]) 返回s中索引从i到j的子字符串
string.lower
string.upper
#获取字符串长度

table

table类型实现了一种抽象的“关联数组”
索引通常是string或number类型,但也可以是除nil意外的任意类型
内部实现上,table通常实现为一个哈希表、一个数组、或者两者的混合。具体的实现为何种形式,动态依赖于具体的table的键分布特点

lua中table赋值是引用,string/number属于值拷贝

推荐使用[]访问元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
local corp = {
web = "www.google.com", --索引为字符串,key = "web",
-- value = "www.google.com"
telephone = "12345678", --索引为字符串
staff = {"Jack", "Scott", "Gary"}, --索引为字符串,值也是一个表
100876, --相当于 [1] = 100876,此时索引为数字
-- key = 1, value = 100876
100191, --相当于 [2] = 100191,此时索引为数字
[10] = 360, --直接把数字索引给出
["city"] = "Beijing" --索引为字符串
}
print(corp.web) -->output:www.google.com
print(corp["telephone"]) -->output:12345678
print(corp[2]) -->output:100191
print(corp["city"]) -->output:"Beijing"
print(corp.staff[1]) -->output:Jack
print(corp[10]) -->output:360
print(#corp) -->output:2
//合并两个table
local a = {1,2}
local b = {3,4}
for k,v in pairs(b) do
a[k] = v
end
//explode
function explode(div,str) -- credit: http://richard.warburton.it
if (div=='') then return false end
local pos,arr = 0,{}
-- for each divider found
for st,sp in function() return string.find(str,div,pos,true) end do
table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider
pos = sp + 1 -- Jump past current divider
end
table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider
return arr
end
//implode
local concatTable = table.concat(a,'&')
//插入数组
table.insert(table,element)
table[#table+1]=element
遍历

ipairs可以被jit编译,pairs只能被解释执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arr = {1,3,[5]=5,name="kaikai",age=12, 89}
--arr[4]= 23
--ipairs
--ipairs仅仅遍历值 按照索引从1开始升序遍历 索引中断停止遍历
for i,v in ipairs(arr) do
print(i,v)
end
输出:
1
3
89
ps: 注意索引为5的元素没有打印出来
--pairs遍历table所有元素
for k,v in pairs(arr) do
print(k,v)
end
输出:
1
3
89
kaikai
12
排序

按照值排序: table.sort
按照key排序: 如果是数组且数字索引是从1开始连续的 可以用ipairs,否则需要自行实现

判断元表是否为空

type(data) == ‘table ‘ and next(data) == nil

function

userdata

在Lua中可以通过自定义类型(user data)与C语言代码更高效、更灵活的交互,从而扩展Lua能够表达的类型。

类型转换

字符串类型和数字类型转换 tostring tonumber

错误处理

pcall(protected call),类似其他语言中的try-catch结构,可以捕获错误,保证程序正常执行

  • 当函数正常执行后,它返回true"被执行函数"的返回值
  • 当函数发生错误时,不会抛出错误,而是返回nil错误信息
1
2
3
4
5
-- 使用pcall引入类库
local status,ret = pcall(require,'util')
-- 使用pcall执行函数
local status,ret = pcall(function () ... end)

面向对象

openresty

获取请求body

由于 Nginx 是为了解决负载均衡场景诞生的,所以它默认是不读取 body 的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
# 默认读取 body
lua_need_request_body on;
location /test {
content_by_lua_block {
# 局部获取body
ngx.req.read_body()
local data = ngx.req.get_body_data()
ngx.say("hello ", data)
}
}
}

日志输出

openresty标准日志输出语句是ngx.log(log_level,…)

对于应用开发,一般用到ngx.INFO或ngx.CRIT就够了。生产中一般开到ngx.ERR

1
2
3
4
5
6
7
8
9
10
11
12
13
error_log log/error.log error; # 设定日志级别 大于等于error等级的错误会记录到日志中
http {
server {
listen 80;
location / {
content_by_lua_block {
local num = 55
ngx.log(ngx.ERR,"num: ",num)
}
}
}
}

子查询

Nginx 子请求是一种非常强有力的方式,它可以发起非阻塞的内部请求访问目标 location。目标 location 可以是配置文件中其他文件目录,或 任何 其他 nginx C 模块,包括 ngx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle,甚至 ngx_lua 自身等等 。

需要注意的是,子请求只是模拟 HTTP 接口的形式, 没有 额外的 HTTP/TCP 流量,也 没有 IPC (进程间通信) 调用。所有工作在内部高效地在 C 语言级别完成。

1
2
3
4
res = ngx.location.capture(
'/foo/bar',
{ method = ngx.HTTP_POST, body = 'hello, world' }
)

通过文件加载

1
2
3
location /test {
content_by_lua_file test.lua
}

redis

pipeline

script

script效果:将多个操作放到一个 TCP 请求中,做到减少 TCP 请求数量,减少网络延时。

EVAL 的第一个参数 script 是一段 Lua 脚本程序。这段 Lua 脚本不需要(也不应该)定义函数。它运行在 Redis 服务器中。 EVAL 的第二个参数 numkeys 是参数的个数,后面的参数 key(从第三个参数),表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1],KEYS[2],以此类推)。 在命令的最后,那些不是键名参数的附加参数 arg [arg …],可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1]、ARGV[2],诸如此类)

1
2
3
4
5
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
location /redisscript {
content_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local ok,err = red:connect("127.0.0.1",6379)
if not ok then
ngx.say("fail to connect")
end
-- eval
local id = 1
local res,err = red:eval([[
local info = redis.call('get',KEYS[1])
info = cjson.decode(info)
local g_id = info.gid
local g_info = redis.call('get',g_id)
return g_info
]],1,id)
if not res then
ngx.say("fail to get the group info:",err)
return
end
ngx.say(res)
-- put it into the connection pool of size 100,
-- with 10 seconds max idle time
local ok,err = red:set_keepalive(10000,100)
if not ok then
ngx.say("fail to set keepalive")
return
end
}
}

json捕获

如果需要在 Lua 中处理错误,必须使用函数 pcall(protected call)来包装需要执行的代码。 pcall 接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;返回值 true 或者或 false, errorinfo。pcall 以一种”保护模式”来调用第一个参数,因此 pcall 可以捕获函数执行中的任何错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local json = require("cjson")
local function _json_decode(str)
return json.decode(str)
end
function json_decode( str )
local ok, t = pcall(_json_decode, str)
if not ok then
return nil
end
return t
end

另外,可以使用 CJSON 2.1.0,该版本新增一个 cjson.safe 模块接口,该接口兼容 cjson 模块,并且在解析错误时不抛出异常,而是返回 nil

1
2
3
4
5
6
7
local json = require("cjson.safe")
local str = [[ {"key:"value"} ]]
local t = json.decode(str)
if t then
ngx.say(" --> ", type(t))
end

稀疏数组

1
2
3
4
5
6
7
8
9
10
local json = require("cjson")
local data = {1, 2}
data[1000] = 99
-- ... do the other things
ngx.say(json.encode(data))
content_by_lua(nginx.conf:219):5: Cannot serialise table: excessively sparse array
stack traceback:... 无法序列化table,极度稀疏数组

如果把 data 的数组下标修改成 5 ,那么这个 json.encode 就会是成功的。
为什么下标是 1000 就失败呢?实际上这么做是 cjson 想保护你的内存资源。她担心这个下标过大直接撑爆内存

如果我们一定要让这种情况下可以 encode,就要尝试 encode_sparse_array API 了

encode空table

1
2
3
4
5
local json = require("cjson")
-- 默认情况{dogs={}} 会encode为{"dogs":{}}
-- json.encode_empty_table_as_object(false) 设定不会encode为对象 结果是{"dogs":[]}
ngx.say(json.encode({dogs={}})

Lua nginx module

执行阶段

20180312152083695913469.png
代码中使用 阶段名_block来向不同阶段挂载对应的处理逻辑,如access_by_lua_block,content_by_lua_nginx等

  • set_by_lua*: 流程分支处理判断变量初始化
  • rewrite_by_lua*: 转发、重定向、缓存等功能(例如特定请求代理到外网)
  • access_by_lua*: IP 准入、接口权限等情况集中处理(例如配合 iptable 完成简单防火墙)
  • content_by_lua*: 内容生成
  • header_filter_by_lua*: 响应头部过滤处理(例如添加头部信息)
  • body_filter_by_lua*: 响应体过滤处理(例如完成应答内容统一成大写)
  • log_by_lua*: 会话完成后本地异步完成日志记录(日志可以记录在本地,还可以同步到其他机器)

实际上我们只使用其中一个阶段 content_by_lua*,也可以完成所有的处理。但这样做,会让我们的代码比较臃肿,越到后期越发难以维护。把我们的逻辑放在不同阶段,分工明确,代码独立,后期发力可以有很多有意思的玩法。

正确记录日志

在content_by_lua阶段也可以记录日志,那么为什么推荐在log_by_lua阶段记录日志呢?

  • log_by_lua 是一个请求经历的最后阶段。由于记日志跟应答内容无关,Nginx 通常在结束请求之后才更新访问日志。如果我们有日志输出的情况,最好统一到 log_by_lua 阶段。如果我们把记日志的操作放在 content_by_lua* 阶段,那么将线性的增加请求处理时间

  • 在使用lua-resty-logger-socket时,如果将flush_limit值调的稍微大一些,会导致某些体积比较小的日志的丢失。这是由于content_by_lua*阶段变量的生命周期会随着请求的终结而结束,所以当日志量小于 flush_limit 的情况下这些日志就不能被累积,也不会触发 _flush_buffer 函数,所以小日志会丢失。这个坑单纯是因为日志记录用错了阶段

1
2
3
4
5
6
7
8
lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;";
server {
location / {
content_by_lua_file lua/content.lua;
log_by_lua_file lua/log.lua;
}
}

如果我们的log里需要记录其他阶段的一些变量,两阶段之间怎么传递数据呢?推荐使用ngx.ctx

1
2
3
4
5
6
7
8
9
10
11
12
location /test {
rewrite_by_lua_block {
ngx.say("foo = ", ngx.ctx.foo)
ngx.ctx.foo = 76
}
access_by_lua_block {
ngx.ctx.foo = ngx.ctx.foo + 3
}
content_by_lua_block {
ngx.say(ngx.ctx.foo)
}
}

cosocket

cosocket = coroutine+socket

20190419155566154827629.png

在上图中,用户的Lua脚本每触发一个网络操作,都会有协程的yield以及resume,以为请求的Lua脚本实际上都运行在独享协程之上,可以在任何需要的时候 暂停自己(yield),也可以在任何需要的时候被唤醒(resume)

暂停自己,把网络事件注册到Nginx监听列表中,并把运行权限交给nginx。当有nginx注册网络事件达到触发条件时,唤醒对应的协程继续处理。

可以看到cosocket是依赖协程+Nginx事件通知两个重要特性结合而成的。

cosocket特性:

  • 同步
  • 非阻塞
  • 全双工

阻塞操作

openresty对外宣传是同步非阻塞,基于事件通知的 Nginx 给我们带来了足够强悍的高并发支持,但是也对我们的编码有特殊要求。这个特殊要求就是我们的代码,也必须是非阻塞的

openresty中的阻塞函数

  • 高 CPU 的调用(压缩、解压缩、加解密等)
  • 高磁盘的调用(所有文件操作)
  • 非 OpenResty 提供的网络操作(luasocket 等)
  • 系统命令行调用(os.execute 等)

这些函数我们应该尽可能降低使用频次

缓存

缓存的原则

一般来说缓存有两个原则

  • 越靠近用户的请求越好
    • 比如能用本地缓存的就不要发送 HTTP 请求,能用 CDN 缓存的就不要打到 Web 服务器,能用 Nginx 缓存的就不要用数据库的缓存
  • 尽量使用本进程和本机的缓存解决,因为跨了进程和机器甚至机房,缓存的网络开销就会非常大,在高并发的时候会非常明显。

单机闭环思想

openresty的缓存

1.使用lua shared dict

lua shared dict是nginx所有worker之间共享的,内部使用LRU算法(最近最少使用)来判断缓存是否在内存占满时被清除

需要在nginx.conf中配置
lua_shared_dict my_cache 128m;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function get_from_cache(key)
local cache_ngx = ngx.shared.my_cache
local value = cache_ngx:get(key)
return value
end
function set_to_cache(key, value, exptime)
if not exptime then
exptime = 0
end
local cache_ngx = ngx.shared.my_cache
local succ, err, forcible = cache_ngx:set(key, value, exptime)
return succ
end
2.lua LRU cache

lua LRU cache 是worker级别的,并且它是预先分配好key的数量,而shared dict需要自己用key和value的大小和数量,来估算需要把内存设置为多少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local _M = {}
-- alternatively: local lrucache = require "resty.lrucache.pureffi"
local lrucache = require "resty.lrucache"
-- we need to initialize the cache on the Lua module level so that
-- it can be shared by all the requests served by each nginx worker process:
local c = lrucache.new(200) -- allow up to 200 items in the cache
if not c then
return error("failed to create the cache: " .. (err or "unknown"))
end
function _M.go()
c:set("dog", 32)
c:set("cat", 56)
ngx.say("dog: ", c:get("dog"))
ngx.say("cat: ", c:get("cat"))
c:set("dog", { age = 10 }, 0.1) -- expire in 0.1 sec
c:delete("dog")
end
return _M
如何选择

shared.dict 使用的是共享内存,每次操作都是全局锁,如果高并发环境,不同 worker 之间容易引起竞争。所以单个 shared.dict 的体积不能过大。lrucache 是 worker 内使用的,由于 Nginx 是单进程方式存在,所以永远不会触发锁,效率上有优势,并且没有 shared.dict 的体积限制,内存上也更弹性,但不同 worker 之间数据不同享,同一缓存数据可能被冗余存储。

你需要考虑的,一个是 Lua lru cache 提供的 API 比较少,现在只有 get、set 和 delete,而 ngx shared dict 还可以 add、replace、incr、get_stale(在 key 过期时也可以返回之前的值)、get_keys(获取所有 key,虽然不推荐,但说不定你的业务需要呢);第二个是内存的占用,由于 ngx shared dict 是 workers 之间共享的,所以在多 worker 的情况下,内存占用比较少。

sleep

在openresty里选择库的一个原则是:尽量使用openresty的库函数,尽量不使用lua库函数,因为lua的库都是同步阻塞的

1
2
3
4
5
6
7
8
9
lua_package_path "/path/to/lua-resty-redis/lib/?.lua;;";
server {
location /non_block {
content_by_lua_block {
ngx.sleep(0.1)
}
}
}

使用一个例子说明阻塞api对nginx并发性能的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
location /sleep_1 {
default_type 'text/plain';
content_by_lua_block {
ngx.sleep(0.01)
ngx.say("ok")
}
}
location /sleep_2 {
default_type 'text/plain';
content_by_lua_block {
function sleep(n)
os.execute("sleep " .. n)
end
sleep(0.01)
ngx.say("ok")
}
}
➜  nginx git:(master) ab -c 10 -n 20  http://127.0.0.1/sleep_1
...
Requests per second:    860.33 [#/sec] (mean)
...
➜  nginx git:(master) ab -c 10 -n 20  http://127.0.0.1/sleep_2
...
Requests per second:    56.87 [#/sec] (mean)
...

两者性能相差15倍

为什么会这样?
原因是sleep_1使用非阻塞api,sleep_2使用阻塞api。前者只会引起进程内协程的切换,但是进程还是处于运行状态(其他协程还在运行),而后者却会触发进程切换,当前进程会变成睡眠状态,结果cpu就进入空闲状态。很明显,非阻塞的性能更高。

定时任务

ngx.timer.at 会创建一个nginx timer。在事件循环中,nginx会找出到期的timer,并且在一个独立的协程中执行对应的lua回调函数。nginx worker退出时会触发当前所有有效的timer,回调参数接收参数premature,如果为true,表示是nginx worker退出触发了当前timer。

ngx.timer.every

有些nginx_lua api不能再这里使用,比如子请求、ngx.req.*和向下游输出的API(ngx.print、ngx.flush),原因是这些调用需要依赖具体请求。但是ngx.timer.at自身的运行与当前请求并无关系

1
2
3
4
5
6
7
8
9
10
local function handler(premature)
if premature then
return
end
ngx.log(ngx.ERR,"timer works.")
end
local delay = 1
local ok,err = ngx.timer.at(delay,handler)

如何只启动一个timer工作

1
2
3
4
5
6
7
8
9
10
-- 只在worker.id为0的进程上运行后台timer
init_worker_by_lua_block{
if 0 == ngx.worder.id() then
local ok,err = new_timer(delay,check)
if not ok then
log(ERR,"failed to create timre:",err)
return
end
end
}

协程

可以用于模拟并发调用

1
2
3
ngx.thread.spawn
ngx.thread.wait
ngx.thread.kill
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
local util = require "util"
function cap(uri)
return ngx.location.capture(uri)
end
local co = ngx.thread.spawn(cap, "/getrandom2.json")
local co2 = ngx.thread.spawn(cap, "/getrandom.json")
-- 等待co co2任意一个返回
--local ok, res = ngx.thread.wait(co, co2)
--
--ngx.say(res.body)
-- 等到全部返回 公共时间窗口
local threads = {
ngx.thread.spawn(cap, "/getrandom.json"),
ngx.thread.spawn(cap, "/getrandom2.json")
}
for i=1, #threads do
local ok,res = ngx.thread.wait(threads[i])
if not ok then
ngx.say(i, ": failed to run: ", res)
else
ngx.say(i, ": status: ", res.status)
ngx.say(i, ": body: ", res.body)
end
end
ngx.exit(200)

官方文档

禁止某些终端访问

  • IP 地址
  • 域名、端口
  • Cookie 特定标识
  • location
  • body 中特定标识
1
2
3
4
5
6
7
location / {
deny 192.168.1.1;
allow 192.168.1.0/24;
allow 10.1.1.0/16;
allow 2001:0db8::/32;
deny all;
}

geo

1
2
3
4
5
6
7
8
9
10
11
12
13
geo $country {
default ZZ;
proxy 192.168.100.0/24;
127.0.0.0/24 US;
127.0.0.1/32 RU;
10.1.0.0/16 RU;
192.168.1.0/24 UK;
}
if ($country == ZZ){
return 403;
}

openresty中解决访问控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
init_by_lua_block {
local iputils = require("resty.iputils")
iputils.enable_lrucache()
local whitelist_ips = {
"127.0.0.1",
"10.10.10.0/24",
"192.168.0.0/16",
}
-- WARNING: Global variable, recommend this is cached at the module level
-- https://github.com/openresty/lua-nginx-module#data-sharing-within-an-nginx-worker
whitelist = iputils.parse_cidrs(whitelist_ips)
}
access_by_lua_block {
local iputils = require("resty.iputils")
if not iputils.ip_in_cidrs(ngx.var.remote_addr, whitelist) then
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}

请求返回后继续执行

ngx.eof()提前返回响应

你不能任性的把阻塞操作加入代码,即使是ngx.eof之后。虽然已经返回了中断的请求,但是nginx的worker还在被你占用,所以在keep alive的情况下,本次请求的总时间,会把上一次eof之后的时间加上,如果你加上了阻塞的代码,高并发就是空谈。

记录日志

变量范围

  • 全局变量
  • 模块变量
  • 局部变量

动态限速

  • limit_rate 限制响应速度
  • limit_conn 限制连接数
  • limit_req 限制请求数
1
2
3
access_by_lua_block{
ngx.var.limit_rate = "300k"
}

流量控制模块

正确使用长连接

典型的应用场景

连接池

XXX池,原理和目的几乎是一样的,那就是复用

传统短连接带来的几个问题

  • 性能普通上不去
  • cpu大量资源被系统消耗
  • 网络一旦抖动,会有大量TIME_WAIT产生,不得不定期重启服务或定期重启机器
  • 服务器工作不稳定,QPS忽高忽低

可以优化的第一件事,就是把短连接改为长连接,也就是改成创建连接、手法数据、收发数据…拆除连接,这样我们可以减少大量创建连接、拆除连接的时间。从性能上要比短连接好不少。但是还是有比较大的浪费。
举例: 请求进入时,直接分配数据库长连接资源,假设80%时间在与关系型数据库通讯,20%时间是与nosql数据库通讯。当有50k个并行请求时,后端要分配50K*2=100K的长连接支撑请求。这时系统压力是非常大的。
连接池终于要出场了,它的解决思路是先把所有长连接存起来,谁需要使用,从这里取走,干完活立马放出来。那么按照这个思路,刚才的50K的并发请求,最多占用后端50K的长连接就够了。省了一半。

在 OpenResty 中,所有具备 set_keepalive 的类、库函数,说明他都是支持连接池的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
location /test {
content_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
-- red:set_keepalive(10000, 100) -- 坑①
ok, err = red:set("dog", "an animal")
if not ok then
-- red:set_keepalive(10000, 100) -- 坑②
return
end
-- 坑③
red:set_keepalive(10000, 100)
}
}
}

坑①:只有数据传输完毕了,才能放到池子里,系统无法帮你自动做这个事情
坑②:不能把状态未知的连接放回池子里,设想另一个请求如果获取到一个不能用的连接,他不得哭死啊
坑③:逗你玩,这个不是坑,是正确的

代码动态加载

从外部加载lua代码或字节码,然后通过loadstring“eval”进Lua VM。

相似的有loadloadfile

动态代码加载需要即时编译,开销很大,应尽量少的使用,如必须使用,应该进行缓存处理

foo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
local lua_m = [[
local _M = {}
function _M.run() --注意这里不是local
ngx.say("hello world")
end
return _M
]]
local f = loadstring(lua_m)
local mod = f()
--缓存模块foo
package.loaded.foo = mod

接下来可以直接使用foo模块

1
2
local foo = require "foo"
foo.run()

模块热更新

首先说明下require函数,使用require加载模块后会将模块存放到package.loaded[modulename]中,再次require模块则会复用package.loaded中缓存的模块,基于这种考虑,我们很容易想到一种模块热更新的方法

1
2
3
4
--强制清空模块缓存
function flushModule(modulename)
package.loaded.modulename = nil
end

对于以下几种情况,lua模块不能通过package.loaded.modulename = nil来清除缓存

  • 使用FFI加载了外部变量
  • 使用FFI定义了新的C类型
  • 使用FFI定义了新的函数原型

另一种方法就是利用nginx的HUP reload,达到平滑更新

lua脚本热更新

Nginx worker间数据共享

通过require加载的模块内容会被缓存,之后同worker进程中的所有请求都会访问到同一个模块实例,达到worker级别的数据共享,注意require模块缓存是worker进程级的,每个worker进程有自己的缓存

如果想实现服务器级别的数据共享,请使用ngx.shared.DICT共享内存

对于通常的lua全局变量(非模块内部),在各个请求间是相互独立的,这是由于one-coroutine-per-request(每个请求一个协程)隔离设计导致的

ngx.null

1
{"result":[{"url":"\/www.baidu.com\/img\/bdlogo.gif","result":0,"md5":null,"putflag":"remote"}]}

对MD5的值既不是nil,也不是空字符串

nil、null与ngx.null

使用第三方库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
resolver 8.8.8.8
location /thirdpart {
content_by_lua_block {
local http = require "resty.http"
local httpc = http.new()
local res,err = httpc:request_uri("http://www.baidu.com")
if err then
ngx.say(err)
ngx.exit(res.status)
end
if res.status == ngx.HTTP_OK then
ngx.say(res.body)
else
ngx.exit(res.status)
end
}
}
}

ngx.worker.id()

返回当前 Nginx 工作进程的一个顺序数字(从 0 开始)。
所以,如果工作进程总数是 N,那么该方法将返回 0 和 N - 1 (包含)的一个数字。
该方法只对 Nginx 1.9.1+ 版本返回有意义的值。更早版本的 nginx,将总是返回 nil

我们可以用它来确定这个 worker 的内部身份,并且这个身份是相对稳定的。即使当前 Nginx 进程因为某些原因 crash 了,新 fork 出来的 Nginx worker 是会继承这个 worker id 的

性能提升

  • 使用局部变量(根据网上资料,局部变量比全局变量快30%)
  • 元表预定义长度(Lua中table类型变量的容量扩增代价很高)
    • lua table组成分为数组部分哈希部分。数组部分索引的key是1~n的整数,哈希部分一个哈希表。
    • 向table插入数据,如果table已经满了,这时会调整table中数组部分或哈希部分的大小,容量是成倍增加的,并且哈希部分还要对哈希表中的数据进行整理
    • 特别的,对于没有赋初值的table,其容量为0
  • 模块懒加载
  • 避免使用table.insert
  • 减少使用unpack
  • docker环境下网卡使用宿主机网卡

追踪调试

关闭code cache

调试时最好关闭lua_code_cache 选项

1
2
> lua_code_cache off;
>

>

关闭 lua_code_cache 之后,OpenResty 会给每个请求创建新的 Lua VM。由于没有 Lua module 的缓存,新的 VM 会去加载刚最新的 Lua 文件。 这样,你修改完代码后,不用 reload Nginx 就可以生效了。在生产环境下记得打开这个选项。

当然,由于每个请求运行在独立的 Lua VM 里,lua_code_cache off 会带来以下几个问题:

  • 每个请求都会有独立的 module,独立的 lrucache,独立的 timer,独立的线程池。
    跟请求无关的模块,由于不会被新的请求加载,并不会主动更新。比如 init_by_lua_file 引用的文件就不会被更新。
  • *_by_lua_block 里面的代码,由于不在 Lua 文件里面,设置 lua_code_cache 对其没有意义。

在systemtap基础上,openresty官方封装了方便使用的类库脚本

需要安装systemtap

需要安装openresty的调用符号包openresty-dbgsym(可以使用官方源安装,如需)

stapxx

​ ./samples/ngx-rps.sxx -x 19647 获取指定worker进程qps信息
​ ./samples/ngx-req-latency-distr.sxx -x 28078 获取指定worker进程请求延迟信息

openresty-systemtap-toolkit

​ ./sample-bt -p 8736 -t 5 -u > a.bt 对指定进程用户态进行抽样追踪
​ ./sample-bt -p 8736 -t 5 -k > a.bt 对指定进程内核态进行抽样追踪
​ ./ngx-shm -p 15218 分析指定worker进程的共享内存使用情况
​ ./ngx-shm -p 15218 -n mycache 分析指定worker进程的某一共享内存使用情况(包含使用未使用情况)

20190426155625282466255.png

stapxx
openresty-systemtap-toolkit
FlameGraph

什么是dbgsym文件

在缺省情况下,大多数程序和库都是带调试符号(使用 gcc-g 选项)编译的。 当调试一个带调试符号的程序时,调试器不仅能给出内存地址,还能给出函数和变量的名字。但是,这些调试符号明显地增大了程序和库。于是很多软件将调试符号文件提拆分为独立的文件,其后缀名为dbgsym

FAQ

1.模块缓存

在lua_code_cache开启后,所有通过require加载的模块内容都会被缓存,所以需要我们注意不要在模块中存放任何状态相关的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
--模块t
local _M = {}
return _M
--set.lua
local m = require "t"
m.a=1
--get.lua
local m = require "t"
ngx.say(m.a)
ngx.exit(200)
接下来依次执行
curl "127.0.0.1/get"
nil
curl "127.0.0.1/set"
curl "127.0.0.1/get" -- 说明取到了刚才set后缓存的模块内容
1

面向对象编程时,我们肯定会设定对象的成员变量,这时如何不缓存这些成员变量呢?我们看官方库的写法

1
2
3
4
5
6
7
8
local _M = {}
local mt = {__index=_M}
function _M.new(w,h)
return setmetatable({width=width,height=height},mt)
end
return _M

每次new()时通过setmetatable生成一个新的对象,新对象有width和height两个成员属性,除此之外对该对象的其他访问都是访问_M,而_M是缓存的,这样就兼顾了灵活性与性能

2.元表与…转换

1
2
3
function(...)
local a = {...}
end

3.Lua 变量的传递和内存的使用

Lua 里面的变量都是值的引用(或者说是值的别名),而并不是值的容器。所以 Lua 里的赋值和参数传递全部都是引用传递。

4.如果获取系统环境变量

如果你想在 Lua 中通过标准 Lua API os.getenv 来访问系统环境变量,例如 foo , 那么你需要在你的 nginx.conf 中,通过 env 指令,把这个环境变量列出来。 例如:

1
env foo;

5.lua中如何continue

LuaJIT跟进了一些Lua5.2的特性,开始支持goto::label::机制,通过这个机制我们可以模拟出continue的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for i=1, 3 do
if i <= 2 then
ngx.say(i, "yes continue")
goto continue
end
ngx.say(i, " no continue")
::continue::
ngx.say([[i'm end]])
end
$ luajit test.lua
1 yes continue
i'm end
2 yes continue
i'm end
3 no continue
i'm end

6.为什么我们的域名不能被解析

我们可以在nginx配置中使用resolver指令设置使用的nameserver地址

1
resolver 8.8.8.8 valid=3600s;

不过这样的问题是nameserver被写死在配置文件中,如果使用场景比较复杂或有内部dns服务时维护比较麻烦

更好的方式是使用resolver以及dnsmasq来优化缓存DNS

7.缓存失效风暴

缓存失效的瞬间会突然有大量请求直接访问到数据库,导致数据库压力倍增,甚至直接打死。我们可以用加锁的方式来避免这种情况。

cache-locks

8.TIME WAIT问题

主动关闭的一方会进入TIME_WAIT状态

有没有nginx主动关闭连接的情况呢?

  • 请求使用http1.0协议,如果请求头里没有connection选项,应答默认是connection:close
  • User-Agent中浏览器版本过低,也会直接close

redis返回空数据判定问题

error日志中发现

1
[error] 7#7: *12030 lua entry thread aborted: runtime error: /data/share/apps/lua/access_check.lua:133: bad argument #1 to 'decode' (string expected, got userdata)

代码如下:

1
2
3
4
5
6
7
8
local access_token = redis_client:read_by_key(token_key)
if access_token == nil then
-- do something...
return false
end
local obj_token = cjson.decode(access_token)
-- do something

查询得知lua读取redis结果为空时,返回结果不是nil而是userdata类型的null,且不等同于nil,需要使用特殊的ngx.null来进行判定,代码调整后如下

1
2
3
4
5
6
7
8
local access_token = redis_client:read_by_key(token_key)
if access_token == ngx.null or access_token == nil then
-- do something...
return false
end
local obj_token = cjson.decode(access_token)
-- do something

定义方法时也要注意是否要用局部变量

比如redis模块中
local _M = {}
function _M.add_commands(…)

end

local close()

end

其中close表示模块内局部函数
_M已经是局部变量,故其成员赋值不用再加local

nil与false

nil与false在openresty中都表示”假”,但这并不代表两者是等价的

1
2
if nil==false then ngx.say(1) else ngx.say(2) end
result: 2

待整理

resty -e ‘print(“hello world”)’’

,LuaJIT 的解释器会在执行字节码的同时,记录一些运行时的统计信息,比如每个 Lua 函数调用入口的实际运行次数,还有每个 Lua 循环的实际执行次数。当这些次数超过某个随机的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环足够热,这时便会触发 JIT 编译器开始工作。 JIT 编译器会从热函数的入口或者热循环的某个位置开始,尝试编译对应的 Lua 代码路径。编译的过程,是把 LuaJIT 字节码先转换成 LuaJIT 自己定义的中间码(IR),然后再生成针对目标体系结构的机器码。 所以,所谓 LuaJIT 的性能优化,本质上就是让尽可能多的 Lua 代码可以被 JIT 编译器生成机器码,而不是回退到 Lua 解释器的解释执行模式。明白了这个道理,你才能理解后面学到的 OpenResty 性能优化的本质。

在 OpenResty 层面,Lua 的协程会与 NGINX 的事件机制相互配合。如果 Lua 代码中出现类似查询 MySQL 数据库这样的 I/O 操作,就会先调用 Lua 协程的 yield 把自己挂起,然后在 NGINX 中注册回调;在 I/O 操作完成(也可能是超时或者出错)后,再由 NGINX 回调 resume 来唤醒 Lua 协程。这样就完成了 Lua 协程和 NGINX 事件驱动的配合,避免在 Lua 代码中写回调。

开启require "resty.core" 带来大幅性能提升 https://time.geekbang.org/column/article/100564
lua调用C函数有两种方式
Lua C API
FFI
在lua-nginx-module中,调用C函数的API使用的是Lua C API来完成的;而在lua-resty-core中,则是把lua-nginx-module已有的部分API,使用FFI的模式重新实现了一遍
Lua C API对于LuaJIT来说是黑盒,无法进行优化。luaJIT FFI则不同,FFI的交互部分是用lua实现的,可以被JIT跟踪到并进行优化。

20190721156371553580722.png

同步非阻塞 15章

18章
共享内存
shared dict
queue
管理类

1
2
3
4
5
require "resty.core.shdict"
local cats = ngx.shared.cats
local capacity_bytes = cats:capacity()
local free_page_bytes = cats:free_space()

20190721156371911745084.png

cosocket=coroutine+socket 遇到I/O阻塞,切换coroutine

lua 断点调试 vscode luaide