命令行
服务启动
|
|
典型的项目目录:
├── 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中只有false
和nil
为false
,其余都等于true
number
string
|
|
table
table类型实现了一种抽象的“关联数组”
索引通常是string或number类型,但也可以是除nil意外的任意类型
内部实现上,table通常实现为一个哈希表、一个数组、或者两者的混合。具体的实现为何种形式,动态依赖于具体的table的键分布特点
lua中table赋值是引用,string/number属于值拷贝
推荐使用[]
访问元素
|
|
遍历
ipairs可以被jit编译,pairs只能被解释执行
|
|
排序
按照值排序: 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
和错误信息
|
|
面向对象
openresty
获取请求body
由于 Nginx 是为了解决负载均衡场景诞生的,所以它默认是不读取 body 的行为
|
|
日志输出
openresty标准日志输出语句是ngx.log(log_level,…)
对于应用开发,一般用到ngx.INFO或ngx.CRIT就够了。生产中一般开到ngx.ERR
|
|
子查询
Nginx 子请求是一种非常强有力的方式,它可以发起非阻塞的内部请求访问目标 location。目标 location 可以是配置文件中其他文件目录,或 任何 其他 nginx C 模块,包括 ngx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle,甚至 ngx_lua 自身等等 。
需要注意的是,子请求只是模拟 HTTP 接口的形式, 没有 额外的 HTTP/TCP 流量,也 没有 IPC (进程间通信) 调用。所有工作在内部高效地在 C 语言级别完成。
|
|
通过文件加载
|
|
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],诸如此类)
|
|
|
|
json捕获
如果需要在 Lua 中处理错误,必须使用函数 pcall(protected call)来包装需要执行的代码。 pcall 接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;返回值 true 或者或 false, errorinfo。pcall 以一种”保护模式”来调用第一个参数,因此 pcall 可以捕获函数执行中的任何错误
|
|
另外,可以使用 CJSON 2.1.0,该版本新增一个 cjson.safe 模块接口,该接口兼容 cjson 模块,并且在解析错误时不抛出异常,而是返回 nil
|
|
稀疏数组
|
|
如果把 data 的数组下标修改成 5 ,那么这个 json.encode 就会是成功的。
为什么下标是 1000 就失败呢?实际上这么做是 cjson 想保护你的内存资源。她担心这个下标过大直接撑爆内存
如果我们一定要让这种情况下可以 encode,就要尝试 encode_sparse_array API 了
encode空table
|
|
Lua nginx module
执行阶段
代码中使用 阶段名_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 函数,所以小日志会丢失。这个坑单纯是因为日志记录用错了阶段
|
|
如果我们的log里需要记录其他阶段的一些变量,两阶段之间怎么传递数据呢?推荐使用ngx.ctx
|
|
cosocket
cosocket = coroutine+socket
在上图中,用户的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;
|
|
2.lua LRU cache
lua LRU cache 是worker级别的,并且它是预先分配好key的数量,而shared dict需要自己用key和value的大小和数量,来估算需要把内存设置为多少
|
|
如何选择
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的库都是同步阻塞的
|
|
使用一个例子说明阻塞api对nginx并发性能的影响
|
|
➜ 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
自身的运行与当前请求并无关系
|
|
如何只启动一个timer工作
|
|
协程
可以用于模拟并发调用
|
|
|
|
禁止某些终端访问
- IP 地址
- 域名、端口
- Cookie 特定标识
- location
- body 中特定标识
|
|
geo
|
|
openresty中解决访问控制
|
|
请求返回后继续执行
ngx.eof()
提前返回响应
你不能任性的把阻塞操作加入代码,即使是ngx.eof之后。虽然已经返回了中断的请求,但是nginx的worker还在被你占用,所以在keep alive的情况下,本次请求的总时间,会把上一次eof之后的时间加上,如果你加上了阻塞的代码,高并发就是空谈。
记录日志
变量范围
- 全局变量
- 模块变量
- 局部变量
动态限速
- limit_rate 限制响应速度
- limit_conn 限制连接数
- limit_req 限制请求数
|
|
正确使用长连接
典型的应用场景
连接池
XXX池,原理和目的几乎是一样的,那就是复用
传统短连接带来的几个问题
- 性能普通上不去
- cpu大量资源被系统消耗
- 网络一旦抖动,会有大量TIME_WAIT产生,不得不定期重启服务或定期重启机器
- 服务器工作不稳定,QPS忽高忽低
可以优化的第一件事,就是把短连接改为长连接,也就是改成创建连接、手法数据、收发数据…拆除连接,这样我们可以减少大量创建连接、拆除连接的时间。从性能上要比短连接好不少。但是还是有比较大的浪费。
举例: 请求进入时,直接分配数据库长连接资源,假设80%时间在与关系型数据库通讯,20%时间是与nosql数据库通讯。当有50k个并行请求时,后端要分配50K*2=100K的长连接支撑请求。这时系统压力是非常大的。
连接池终于要出场了,它的解决思路是先把所有长连接存起来,谁需要使用,从这里取走,干完活立马放出来。那么按照这个思路,刚才的50K的并发请求,最多占用后端50K的长连接就够了。省了一半。
在 OpenResty 中,所有具备 set_keepalive 的类、库函数,说明他都是支持连接池的。
|
|
坑①:只有数据传输完毕了,才能放到池子里,系统无法帮你自动做这个事情
坑②:不能把状态未知的连接放回池子里,设想另一个请求如果获取到一个不能用的连接,他不得哭死啊
坑③:逗你玩,这个不是坑,是正确的
代码动态加载
从外部加载lua代码或字节码,然后通过loadstring
“eval”进Lua VM。
相似的有load
、loadfile
动态代码加载需要即时编译,开销很大,应尽量少的使用,如必须使用,应该进行缓存处理
|
|
接下来可以直接使用foo模块
|
|
模块热更新
首先说明下require
函数,使用require加载模块后会将模块存放到package.loaded[modulename]中,再次require模块则会复用package.loaded中缓存的模块,基于这种考虑,我们很容易想到一种模块热更新的方法
|
|
对于以下几种情况,lua模块不能通过package.loaded.modulename = nil来清除缓存
- 使用FFI加载了外部变量
- 使用FFI定义了新的C类型
- 使用FFI定义了新的函数原型
另一种方法就是利用nginx的HUP reload,达到平滑更新
Nginx worker间数据共享
通过require加载的模块内容会被缓存,之后同worker进程中的所有请求都会访问到同一个模块实例,达到worker级别的数据共享,注意require模块缓存是worker进程级的,每个worker进程有自己的缓存
如果想实现服务器级别的数据共享,请使用ngx.shared.DICT
共享内存
对于通常的lua全局变量(非模块内部),在各个请求间是相互独立的,这是由于
one-coroutine-per-request(每个请求一个协程)
隔离设计导致的
ngx.null
|
|
对MD5的值既不是nil,也不是空字符串
使用第三方库
|
|
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
- lua table组成分为
- 模块懒加载
- 避免使用table.insert
- 减少使用unpack
- docker环境下网卡使用宿主机网卡
追踪调试
关闭code cache
调试时最好关闭lua_code_cache 选项
12 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进程的某一共享内存使用情况(包含使用未使用情况)
stapxx
openresty-systemtap-toolkit
FlameGraph
什么是dbgsym文件
在缺省情况下,大多数程序和库都是带调试符号(使用 gcc 的 -g 选项)编译的。 当调试一个带调试符号的程序时,调试器不仅能给出内存地址,还能给出函数和变量的名字。但是,这些调试符号明显地增大了程序和库。于是很多软件将调试符号文件提拆分为独立的文件,其后缀名为dbgsym
FAQ
1.模块缓存
在lua_code_cache开启后,所有通过require加载的模块内容都会被缓存,所以需要我们注意不要在模块中存放任何状态相关的内容
|
|
面向对象编程时,我们肯定会设定对象的成员变量,这时如何不缓存这些成员变量呢?我们看官方库的写法
|
|
每次new()时通过setmetatable生成一个新的对象,新对象有width和height两个成员属性,除此之外对该对象的其他访问都是访问_M,而_M是缓存的,这样就兼顾了灵活性与性能
2.元表与…转换
|
|
3.Lua 变量的传递和内存的使用
Lua 里面的变量都是值的引用(或者说是值的别名),而并不是值的容器。所以 Lua 里的赋值和参数传递全部都是引用传递。
4.如果获取系统环境变量
如果你想在 Lua 中通过标准 Lua API os.getenv 来访问系统环境变量,例如 foo
, 那么你需要在你的 nginx.conf 中,通过 env 指令,把这个环境变量列出来。 例如:
|
|
5.lua中如何continue
LuaJIT跟进了一些Lua5.2的特性,开始支持goto
和::label::
机制,通过这个机制我们可以模拟出continue的效果。
|
|
6.为什么我们的域名不能被解析
我们可以在nginx配置中使用resolver
指令设置使用的nameserver
地址
|
|
不过这样的问题是nameserver
被写死在配置文件中,如果使用场景比较复杂或有内部dns
服务时维护比较麻烦
更好的方式是使用resolver
以及dnsmasq
来优化缓存DNS
7.缓存失效风暴
缓存失效的瞬间会突然有大量请求直接访问到数据库,导致数据库压力倍增,甚至直接打死。我们可以用加锁的方式来避免这种情况。
8.TIME WAIT问题
主动关闭的一方会进入TIME_WAIT状态
有没有nginx主动关闭连接的情况呢?
- 请求使用http1.0协议,如果请求头里没有connection选项,应答默认是connection:close
- User-Agent中浏览器版本过低,也会直接close
redis返回空数据判定问题
error日志中发现
|
|
代码如下:
|
|
查询得知lua读取redis结果为空时,返回结果不是nil
而是userdata
类型的null
,且不等同于nil
,需要使用特殊的ngx.null
来进行判定,代码调整后如下
|
|
定义方法时也要注意是否要用局部变量
比如redis模块中
local _M = {}
function _M.add_commands(…)
…
end
local close()
…
end
其中close表示模块内局部函数
_M已经是局部变量,故其成员赋值不用再加local
nil与false
nil与false在openresty中都表示”假”,但这并不代表两者是等价的
|
|
待整理
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跟踪到并进行优化。
同步非阻塞 15章
18章
共享内存
shared dict
queue
管理类
|
|
cosocket=coroutine+socket 遇到I/O阻塞,切换coroutine
lua 断点调试 vscode luaide