OpenResty学习指南(一)

浅谈synchronized

OpenResty学习指南(一)

我的博客: 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命令行

  1. 格式:nginx -s reload
  2. 帮助: -? -h
  3. 使用指定的配置文件: -c
  4. 指定配置指令:-g
  5. 指定运行目录:-p
  6. 发送信号:-s (stop / quit / reload / reopen)
  7. 测试配置文件是否有语法错误:-t -T
  8. 打印nginx的版本信息、编译信息等:-v -V

nginx信号

因为nginx是多进程的程序:
OpenResty学习指南(一)

所以信号分为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入门

  1. 创建工作目录
mkdir geektime
cd luoluo
mkdir logs/ conf/
  1. 在conf里面添加nginx.conf文件
events {
    worker_connections 1024;
}
http {
    server {
        listen 8080;
        location / {
            content_by_lua '
                ngx.say("hello, world")
            ';
        }
    }
}
  1. 启动openresty服务
openresty -p `pwd` -c conf/nginx.conf
指定运行目录:-p
使用指定的配置文件: -c

openresty后面跟随的命令和nginx是一样的

独立出Lua代码

  1. 我们先在luo的工作目录下,创建一个名为lua的目录
$ mkdir lua
$ cat lua/hello.lua
ngx.say("hello, world")
  1. 修改 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

  1. 重启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 库的几个函数:

OpenResty学习指南(一)

其中,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 对象。

需注意:

  1. 不能把发生错误的连接放入连接池
  2. 第二,要搞清楚连接的数量。连接池是 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 从入门到放弃

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享