浅谈synchronized
我的博客: https://www.luozhiyun.com/archives/217
想要学好 OpenResty,你必须理解下面 8 个重点:
- 同步非阻塞的编程模式;
- 不同阶段的作用;
- LuaJIT 和 Lua 的不同之处;
- OpenResty API 和周边库;
- 协程和 cosocket;
- 单元测试框架和性能测试工具;
- 火焰图和周边工具链;
- 性能优化。
你不应该使用任何 Lua 世界的库来解决上述问题,而是应该使用 cosocket 的 lua-resty-* 库。Lua 世界的库很可能会带来阻塞,让原本高性能的服务,直接下降几个数量级。
OpenResty阶段
和nginx一样,都有阶段的概念,并且每个阶段都有自己不同的作用:
- set_by_lua,用于设置变量;
- rewrite_by_lua,用于转发、重定向等;
- access_by_lua,用于准入、权限等;
- content_by_lua,用于生成返回内容;
- header_filter_by_lua,用于应答头过滤处理;
- body_filter_by_lua,用于应答体过滤处理;
- log_by_lua,用于日志记录。
OpenResty 的 API 是有阶段使用限制的。每一个 API 都有一个与之对应的使用阶段列表,如果你超范围使用就会报错。
具体的API可以查阅文档:https://github.com/openresty/lua-nginx-module
跨阶段的变量
有些情况下,我们需要的是跨越阶段的、可以读写的变量。
OpenResty 提供了 ngx.ctx,来解决这类问题。它是一个 Lua table,可以用来存储基于请求的 Lua 数据,且生存周期与当前请求相同。我们来看下官方文档中的这个示例:
location /test {
rewrite_by_lua_block {
ngx.ctx.foo = 76
}
access_by_lua_block {
ngx.ctx.foo = ngx.ctx.foo + 3
}
content_by_lua_block {
ngx.say(ngx.ctx.foo)
}
}
最终输出79
包管理
OPM
OPM(OpenResty Package Manager)是 OpenResty 自带的包管理器
opm search lua-resty-http
LUAROCKS
不同于 OPM 里只包含 OpenResty 相关的包,LuaRocks 里面还包含 Lua 世界的库。
luarocks search lua-resty-http
我们还可以去网站上看包的详细信息:https://luarocks.org/modules/pintsized/lua-resty-http,这里面包含了作者、License、GitHub 地址、下载次数、功能简介、历史版本、依赖等。
AWESOME-RESTY
awesome-resty 这个项目,就维护了几乎所有 OpenResty 可用的包,并且都分门别类地整理好了。
nginx
nginx命令行
- 格式:nginx -s reload
- 帮助: -? -h
- 使用指定的配置文件: -c
- 指定配置指令:-g
- 指定运行目录:-p
- 发送信号:-s (stop / quit / reload / reopen)
- 测试配置文件是否有语法错误:-t -T
- 打印nginx的版本信息、编译信息等:-v -V
nginx信号
因为nginx是多进程的程序:
所以信号分为Master进程信号和worker进程信号。
Master进程:
- 监控worker进程: CHILD ,如果worker进程出现了故障而挂掉了,那么master可以通过这个信号将worker进程迅速拉起
- 管理worker进程:
- TERM,INT:表示立刻停止nginx进程
- QUIT:表示优雅停止nginx进程
- HUP:重载配置文件
- USR1:表示重新打开日志文件
- USR2、WINCH:专门针对热部署使用
worker进程:与master进程命令一一对应
- TERM,INT:表示立刻停止nginx进程
- QUIT:表示优雅停止nginx进程
- USR1:表示重新打开日志文件
- WINCH:专门针对热部署使用
Nginx命令行,相当于直接向master进程发送命令
- reload:HUP
- reopen:USR1
- stop:TERM
- quit:QUIT
openresty入门
- 创建工作目录
mkdir geektime
cd luoluo
mkdir logs/ conf/
- 在conf里面添加nginx.conf文件
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
content_by_lua '
ngx.say("hello, world")
';
}
}
}
- 启动openresty服务
openresty -p `pwd` -c conf/nginx.conf
指定运行目录:-p
使用指定的配置文件: -c
openresty后面跟随的命令和nginx是一样的
独立出Lua代码
- 我们先在luo的工作目录下,创建一个名为lua的目录
$ mkdir lua
$ cat lua/hello.lua
ngx.say("hello, world")
- 修改 nginx.conf 的配置
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
content_by_lua_file lua/hello.lua;
}
}
}
}
这里把 content_by_lua_block 改为 content_by_lua_file
- 重启OpenResty
$ sudo kill -HUP `cat logs/nginx.pid`
我这里使用了发送信号的方式 -HUP表示重载配置文件
NYI
NYI,全称为 Not Yet Implemented。LuaJIT 中 JIT 编译器的实现还不完善,有一些原语它还无法编译,因为这些原语实现起来比较困难,再加上 LuaJIT 的作者目前处于半退休状态。这些原语包括常见的 pairs() 函数、unpack() 函数、基于 Lua CFunction 实现的 Lua C 模块等。这样一来,当 JIT 编译器在当前代码路径上遇到它不支持的操作时,便会退回到解释器模式。这些不能编译的函数称为NYI。
NYI函数都在:http://wiki.luajit.org/NYI
在开发中,可以先去找OpenResty的API:https://github.com/openresty/lua-nginx-module
例如,NYI 列表中 string 库的几个函数:
其中,string.byte 对应的能否被编译的状态是 yes,表明可以被 JIT。
string.char 对应的编译状态是 2.1,表明从 LuaJIT 2.1 开始支持。我们知道,OpenResty 中的 LuaJIT 是基于 LuaJIT 2.1 的,所以你也可以放心使用。
string.dump 对应的编译状态是 never,即不会被 JIT,会退回到解释器模式。
string.find 对应的编译状态是 2.1 partial,意思是从 LuaJIT 2.1 开始部分支持,后面的备注中写的是 只支持搜索固定的字符串,不支持模式匹配。
JDBC的学习笔记-手动实现
如何检测函数
LuaJIT 自带的 jit.dump 和 jit.v 模块。它们都可以打印出 JIT 编译器工作的过程。前者会输出非常详细的信息,可以用来调试 LuaJIT 本身;后者的输出比较简单,每行对应一个 trace,通常用来检测是否可以被 JIT。
使用resty:
$resty -j v -e
其中,resty 的 -j 就是和 LuaJIT 相关的选项;后面的值为 dump 和 v,就对应着开启 jit.dump 和 jit.v 模式。
如下例子:
$resty -j v -e 'local t = {}
for i=1,100 do
t[i] = i
end
for i=1, 1000 do
for j=1,1000 do
for k,v in pairs(t) do
--
end
end
end'
上面的pairs是NYI的语句,不能被JIT,所以结果里面就会显示:
[TRACE 1 (command line -e):2 loop]
[TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]
shdict get API
shared dict(共享字典)是基于 NGINX 共享内存区的 Lua 字典对象,它可以跨多个 worker 来存取数据,一般用来存放限流、限速、缓存等数据。
例子:
http {
lua_shared_dict dogs 10m;
server {
location /demo {
content_by_lua_block {
local dogs = ngx.shared.dogs
dogs:set("Jim", 8)
local v = dogs:get("Jim")
ngx.say(v)
}
}
}
}
简单说明一下,在 Lua 代码中使用 shared dict 之前,我们需要在 nginx.conf 中用 lua_shared_dict 指令增加一块内存空间,它的名字是 dogs,大小为 10M。
也可以使用resty CLI:
$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
dogs:set("Jim", 8)
local v = dogs:get("Jim")
ngx.say(v)'
共享内存使用阶段
context: 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*
可以看出, init 和 init_worker 两个阶段不在其中,也就是说,共享内存的 get API 不能在这两个阶段使用。
get函数返回多个值
value, flags = ngx.shared.DICT:get(key)
正常情况下:
第一个参数value 返回的是字典中 key 对应的值;但当 key 不存在或者过期时,value 的值为 nil。
第二个参数 flags 就稍微复杂一些了,如果 set 接口设置了 flags,就返回,否则不返回。
一旦 API 调用出错,value 返回 nil,flags 返回具体的错误信息。
cosocket
cosocket 是把协程和网络套接字的英文拼在一起形成的,即 cosocket = coroutine + socket。
遇到网络 I/O 时,它会交出控制权(yield),把网络事件注册到 Nginx 监听列表中,并把权限交给 Nginx;当有 Nginx 事件达到触发条件时,便唤醒对应的协程继续处理(resume),最终实现了非阻塞网络 I/O。
API
- 创建对象:ngx.socket.tcp。
- 设置超时:tcpsock:settimeout 和 tcpsock:settimeouts。
- 建立连接:tcpsock:connect。
- 发送数据:tcpsock:send。
- 接受数据:tcpsock:receive、tcpsock:receiveany 和 tcpsock:receiveuntil。
- 连接池:tcpsock:setkeepalive。
- 关闭连接:tcpsock:close。
上下文:
rewrite_by_lua*,
access_by_lua*,
content_by_lua*,
ngx.timer.*,
ssl_certificate_by_lua*,
ssl_session_fetch_by_lua*_
cosocket API 在 set_by_lua, log_by_lua, header_filter_by_lua* 和 body_filter_by_lua* 中是无法使用的。init_by_lua* 和 init_worker_by_lua* 中暂时也不能用。
与这些API相应的Nginx指令:
- lua_socket_connect_timeout:连接超时,默认 60 秒。
- lua_socket_send_timeout:发送超时,默认 60 秒。
- lua_socket_send_lowat:发送阈值(low water),默认为 0。
- lua_socket_read_timeout: 读取超时,默认 60 秒。
- lua_socket_buffer_size:读取数据的缓存区大小,默认 4k/8k。
- lua_socket_pool_size:连接池大小,默认 30。
- lua_socket_keepalive_timeout:连接池 cosocket 对象的空闲时间,默认 60 秒。
- lua_socket_log_errors:cosocket 发生错误时,是否记录日志,默认为 on。
例子
$ resty -e 'local sock = ngx.socket.tcp()
sock:settimeout(1000) -- one second timeout
local ok, err = sock:connect("www.baidu.com", 80)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local req_data = "GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n"
local bytes, err = sock:send(req_data)
if err then
ngx.say("failed to send: ", err)
return
end
local data, err, partial = sock:receive()
if err then
ngx.say("failed to receive: ", err)
return
end
sock:close()
ngx.say("response is: ", data)'
- 首先,通过 ngx.socket.tcp() ,创建 TCP 的 cosocket 对象,名字是 sock。
- 然后,使用 settimeout() ,把超时时间设置为 1 秒。注意这里的超时没有区分 connect、receive,是统一的设置。
- 接着,使用 connect() 去连接指定网站的 80 端口,如果失败就直接退出。
- 连接成功的话,就使用 send() 来发送构造好的数据,如果发送失败就退出。
- 发送数据成功的话,就使用 receive() 来接收网站返回的数据。这里 receive() 的默认参数值是 l,也就是只返回第一行的数据;如果参数设置为了a,就是持续接收数据,直到连接关闭;
- 最后,调用 close() ,主动关闭 socket 连接。
超时时间
在上面settimeout() ,作用是把连接、发送和读取超时时间统一设置为一个值。如果要想分开设置,就需要使用 settimeouts() 函数:
sock:settimeouts(1000, 2000, 3000)
接收数据
receive 接收指定大小:
local data, err, partial = sock:receiveany(10240)
这段代码就表示,最多只接收 10K 的数据。
关于 receive,还有另一个很常见的用户需求,那就是一直获取数据,直到遇到指定字符串才停止。
ocal reader = sock:receiveuntil("\r\n")
while true do
local data, err, partial = reader(4)
if not data then
if err then
ngx.say("failed to read the data stream: ", err)
break
end
ngx.say("read done")
break
end
ngx.say("read chunk: [", data, "]")
end
这段代码中的 receiveuntil 会返回 \r\n 之前的数据,并通过迭代器每次读取其中的 4 个字节。
连接池
没有连接池的话,每次请求进来都要新建一个连接,就会导致 cosocket 对象被频繁地创建和销毁,造成不必要的性能损耗。
在你使用完一个 cosocket 后,可以调用 setkeepalive() 放到连接池中:
local ok, err = sock:setkeepalive(2 * 1000, 100)
if not ok then
ngx.say("failed to set reusable: ", err)
end
这段代码设置了连接的空闲时间为 2 秒,连接池的大小为 100。在调用 connect() 函数时,就会优先从连接池中获取 cosocket 对象。
需注意:
- 不能把发生错误的连接放入连接池
- 第二,要搞清楚连接的数量。连接池是 worker 级别的,每个 worker 都有自己的连接池。所以,如果你有 10 个 worker,连接池大小设置为 30,那么对于后端的服务来讲,就等于有 300 个连接。
定时任务
OpenResty 的定时任务可以分为下面两种:
- ngx.timer.at,用来执行一次性的定时任务;
- ngx.time.every,用来执行固定周期的定时任务。但是在启动了一个 timer 之后,你就再也没有机会来取消这个定时任务了
如下:
init_worker_by_lua_block {
local function handler()
local sock = ngx.socket.tcp()
local ok, err = sock:connect(“www.baidu.com", 80)
end
local ok, err = ngx.timer.at(0, handler)
}
启动了一个延时为 0 的定时任务。它启动了回调函数 handler,并在这个函数中,用 cosocket 去访问一个网站
Docker Swarm 从入门到放弃