整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

wireshark 书写lua插件

书写插件前

wireshark的协议不支持之前,报文几乎难以分析,举例如下

Wireshark插件路径

MAC

重新加载Wireshark中的lua插件

MAC

从一个新协议的样板开始

pulsar_protocol = Proto("Pulsar", "Pulsar Protocol")

pulsar_protocol.fields = {}

function pulsar_protocol.dissector(buffer, pinfo, tree)
    length = buffer:len()
    if length == 0 then
        return
    end
    pinfo.cols.protocol = pulsar_protocol.name
    local subtree = tree:add(pulsar_protocol, buffer(), "Pulsar Protocol Data")
end

local tcp_port = DissectorTable.get("tcp.port")
tcp_port:add(6650, pulsar_protocol)

我们从协议对象开始,命名为pulsar_protocol。构造函数两个参数分别为名称和描述。协议需要一个fields表和dissecotr函数。我们现在还没有任何field,所以fields表为空。对于每一个报文,dissctor函数都会被调用一次。

dissector函数有三个入参,bufferpinfo,和treebuffer包含了网络包的内容,是一个Tvb对象。pinfo包含了wireshark中展示packet的列信息,是一个Pinfo对象。tree是wireshark报文详情显示的内容,是TreeItem对象。

dissector函数中,我们检查buffer的长度,如果长度为0,则立即返回

pinfo对象包含着列信息,我们可以将pinfo的protocol设置为pulsar,显示在wireshark的界面中。接下来在packet的结构中创建一个子树,最后,我们把协议绑定到6650端口上。让我们加载这个lua插件

mkdir -p ~/.local/lib/wireshark/plugins
cp $DIR/../../pulsar_dissector.lua ~/.local/lib/wireshark/plugins/pulsar_dissector.lua

结果符合预期

添加长度字段

让我们添加一个长度字段,pulsar协议中,长度字段即就是前4个字节,定义字段

message_length = ProtoField.int32("pulsar.message_length", "messageLength", base.DEC)

pulsar_protocol.fields = { message_length }

pulsar.message_length可以用在过滤器字段中。messageLength是子树中的label。第三个字段决定了这个值会被如何展示

最后,我们把长度值加入到Wireshark的tree中

subtree:add(message_length, buffer(0,4))

pulsar的协议是大端序,我们使用add函数。如果协议是小端序,我们就可以使用addle函数。

我们添加的message_length字段已经可以显示在Wireshark中了

添加额外信息

protoc加入到wireshark

参考及附录

proto field函数列表

https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html#lua_fn_ProtoField_char_abbr___name____base____valuestring____mask____desc__

wireshark解析protobuf

https://ask.wireshark.org/question/15787/how-to-decode-protobuf-by-wireshark/

、什么是事务?

简单来说,事务(transaction)是指单个逻辑单元执行的一系列操作。

1.1、事务的四大特性ACID

事务有如下四大特性:

  • 1、原子性(Atomicity): 构成事务的所有操作都必须是一个逻辑单元,要么全部执行,要么全不执行
  • 2、一致性(Consistency): 数据库在事务执行前后状态都必须是稳定的或者一致的。A(1000)给B(200)转账100后A(900),B(300)总和保持一致。
  • 3、隔离性(Isolation): 事务之间相互隔离,互不影响。
  • 4、持久性(Durability): 事务执行成功后数据必须写入磁盘,宕机重启后数据不会丢失。

2、Redis中的事务

Redis中的事务通过multi,exec,discard,watch这四个命令来完成。

Redis的单个命令都是原子性的,所以确保事务的就是多个命令集合一起执行。

Redis命令集合打包在一起,使用同一个任务确保命令被依次有序且不被打断的执行,从而保证事务性。

Redis是弱事务,不支持事务的回滚。

2.1、事务命令

事务命令简介

  • 1、multi(开启事务)
    • 用于表示事务块的开始,Redis会将后续的命令逐个放入队列,然后使用exec后,原子化的执行这个队列命令。
    • 类似于mysql事务的begin
  • 2、exec(提交事务)
    • 执行命令队列
    • 类似于mysql事务的commit
  • 3、discard(清空执行命令)
    • 清除命令队列中的数据
    • 类似于mysql事务的rollback,但与rollback不一样 ,这里是直接清空队列所有命令,从而不执行。所以不是的回滚。就是个清除。
  • 4、watch
    • 监听一个redis的key 如果key发生变化,watch就能后监控到。如果一个事务中,一个已经被监听的key被修改了,那么此时会清空队列。
  • 5、unwatch
    • 取消监听一个redis的key

事务操作

# 普通的执行多个命令
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name zhangsan
QUEUED
127.0.0.1:6379> hmset m_set name zhangsan age 20 
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK

# 执行命令前清空队列 将会导致事务执行不成功
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name_1 lisi
QUEUED
127.0.0.1:6379> hmset m_set_1 name lisi age 21
QUEUED
# 提交事务前执行了清空队列命令
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI

# 监听一个key,并且在事务提交之前改变在另一个客户端改变它的值,也会导致事务失败
127.0.0.1:6379> set m_name_2 wangwu01
OK
127.0.0.1:6379> watch m_name_2
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name_2 wangwu02
QUEUED
# 另外一个客户端在exec之前执行之后,这里会返回nil,也就是清空了队列,而不是执行成功
127.0.0.1:6379> exec
(nil)

# 另外一个客户端在exec之前执行
127.0.0.1:6379> set m_name_2 niuqi
OK

2.2、事务机制分析

我们前面总是在说,Redis的事务命令是打包放在一个队列里的。那么来看一下Redis客户端的数据结构吧。

client数据结构

typedef struct client {
    // 客户端唯一的ID
    uint64_t id;   
    // 客户端状态 表示是否在事务中
    uint64_t flags;         
    // 事务状态
    multiState mstate;
    // ...还有其他的就不一一列举了
} client;

multiState事务状态数据结构

typedef struct multiState {
    // 事务队列 是一个数组,按照先入先出顺序,先入队的命令在前 后入队的命令在后
    multiCmd *commands;     /* Array of MULTI commands */
    // 已入队命令数
    int count;              /* Total number of MULTI commands */
    // ...略
} multiState;

multiCmd事务命令数据结构

/* Client MULTI/EXEC state */
typedef struct multiCmd {
    // 命令的参数
    robj **argv;
    // 参数长度
    int argv_len;
    // 参数个数
    int argc;
    // redis命令的指针
    struct redisCommand *cmd;
} multiCmd;

Redis的事务执行流程图解

Redis的事务执行流程分析

  • 1、事务开始时,在Client中,有属性flags,用来表示是否在事务中,此时设置flags=REDIS_MULTI
  • 2、Client将命令存放在事务队列中,事务本身的一些命令除外(EXEC,DISCARD,WATCH,MULTI)
  • 3、客户端将命令放入multiCmd *commands,也就是命令队列
  • 4、Redis客户端将向服务端发送exec命令,并将命令队列发送给服务端
  • 5、服务端接受到命令队列后,遍历并一次执行,如果全部执行成功,将执行结果打包一次性返回给客户端。
  • 6、如果执行失败,设置flags=REDIS_DIRTY_EXEC, 结束循环,并返回失败。

2.3、监听机制分析

我们知道,Redis有一个expires的字典用于key的过期事件,同样,监听的key也有一个类似的watched_keys字典,key是要监听的key,值是一个链表,记录了所有监听这个key的客户端。

而监听,就是监听这个key是否被改变,如果被改变了,监听这个key的客户端的flags属性就设置为REDIS_DIRTY_CAS。

Redis客户端向服务器端发送exec命令,服务器判断Redis客户端的flags,如果为REDIS_DIRTY_CAS,则清空事务队列。

redis监听机制图解

redis监听key数据结构

回过头再看一下RedisDb类的watched_keys,确实是一个字典,数据结构如下:

typedef struct redisDb {
    dict *dict;                 /* 存储所有的key-value */
    dict *expires;              /* 存储key的过期时间 */
    dict *blocking_keys;        /* blpop存储阻塞key和客户端对象*/
    dict *ready_keys;           /* 阻塞后push,响应阻塞的那些客户端和key */
    dict *watched_keys;         /* 存储watch监控的key和客户端对象 WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* 数据库的ID为0-15,默认redis有16个数据库 */
    long long avg_ttl;          /* 存储对象的额平均ttl(time in live)时间用于统计 */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
    clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;

2.4、Redis的弱事务性

为什么说Redis是弱事务性呢? 因为如果redis事务中出现语法错误,会暴力的直接清除整个队列的所有命令。

# 在事务外设置一个值为test
127.0.0.1:6379> set m_err_1 test
OK
127.0.0.1:6379> get m_err_1 
"test"
# 开启事务 修改值 但是队列的其他命令出现语法错误  整个事务会被discard
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> set m_err_1 test1
QUEUED
127.0.0.1:6379> sets m_err_1 test2
(error) ERR unknown command `sets`, with args beginning with: `m_err_1`, `test2`, 
127.0.0.1:6379> set m_err_1 test3
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

# 重新获取值
127.0.0.1:6379> get m_err_1
"test"

我们发现,如果命令队列中存在语法错误,是直接的清除队列的所有命令,并不是进行事务回滚,但是语法错误是能够保证原子性的

再来看一些,如果出现类型错误呢?比如开启事务后设置一个key,先设置为string, 然后再当成列表操作。

# 开启事务
127.0.0.1:6379> multi 
OK
# 设置为字符串
127.0.0.1:6379> set m_err_1 test_type_1
QUEUED
# 当初列表插入两个值
127.0.0.1:6379> lpush m_err_1 test_type_1 test_type_2
QUEUED
# 执行
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of valu
# 重新获取值,我们发现我们的居然被改变了,明明,事务执行失败了啊
127.0.0.1:6379> get m_err_1
"test_type_1"

直到现在,我们确定了redis确实不支持事务回滚。因为我们事务失败了,但是命令却是执行成功了。

弱事务总结

  • 1、大多数的事务失败都是因为语法错误(支持回滚)或者类型错误(不支持回滚),而这两种错误,再开发阶段都是可以遇见的
  • 2、Redis为了性能,就忽略了事务回滚。

那么,redis就没有办法保证原子性了吗,当然有,Redis的lua脚本就是对弱事务的一个补充。

3、Redis中的lua脚本

lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua应用场景:游戏开发、独立应用脚本、Web应用脚本、扩展和数据库插件。

OpenResty:一个可伸缩的基于Nginx的Web平台,是在nginx之上集成了lua模块的第三方服务器。

OpenResty是一个通过Lua扩展Nginx实现的可伸缩的Web平台,内部集成了大量精良的Lua库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发(日活千万级别)、扩展性极高的动态Web应用、Web服务和动态网 关。 功能和nginx类似,就是由于支持lua动态脚本,所以更加灵活,可以实现鉴权、限流、分流、日志记 录、灰度发布等功能。

OpenResty通过Lua脚本扩展nginx功能,可提供负载均衡、请求路由、安全认证、服务鉴权、流量控 制与日志监控等服务。

类似的还有Kong(Api Gateway)、tengine(阿里)

3.1、Lua安装(Linux)

lua脚本下载和安装http://www.lua.org/download.html

lua脚本参考文档:http://www.lua.org/manual/5.4/

# curl直接下载
curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
# 解压
tar zxf lua-5.4.4.tar.gz
# 进入,目录
cd lua-5.4.4
# 编译安装
make all test

编写lua脚本

编写一个lua脚本test.lua,就定义一个本地变量,打印出来即可。

local name = "zhangsan"

print("name:",name)

执行lua脚本

[root@VM-0-5-centos ~]# lua test.lua 
name:   zhangsan

3.2、Redis中使用Lua

Redis从2.6开始,就内置了lua编译器,可以使用EVAL命令对lua脚本进行求值。

脚本命令是原子性的,Redis服务器再执行脚本命令时,不允许新的命令执行(会阻塞,不在接受命令)。、

EVAL命令

通过执行redis的eval命令,可以运行一段lua脚本。

EVAL script numkeys key [key ...] arg [arg ...]

EVAL命令说明

  • 1、script:是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数。
  • 2、numkeys:指定键名参数的个数。
  • 3、key [key ...]:从EVAL的第三个参数开始算起,使用了numkeys个键(key),表示在脚本中所用到的哪些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)
  • 4、arg [arg ...]:可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似(ARGV[1] 、 ARGV[2] ,诸如此类)

简单来说,就是

eval lua脚本片段  参数个数(假设参数个数=2)  参数1 参数2  参数1值  参数2值

EVAL命令执行

# 执行一段lua脚本 就是把传入的参数和对应的值返回回去
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 name age zhangsan 20
1) "name"
2) "age"
3) "zhangsan"
4) "20"

lua脚本中调用redis

我们直到了如何接受和返回参数了,那么lua脚本中如何调用redis呢?

  • 1、redis.call
    • 返回值就是redis命令执行的返回值
    • 如果出错,则返回错误信息,不继续执行
  • 2、redis.pcall
    • 返回值就是redis命令执行的返回值
    • 如果出错,则记录错误信息,继续执行

其实就是redis.call会把异常抛出来,redis.pcall则时捕获了异常,不会抛出去。

lua脚本调用redis设置值

# 使用redis.call设置值
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 eval_01 001
OK
127.0.0.1:6379> get eval_01
"001"

EVALSHA命令

前面的eval命令每次都要发送一次脚本本身的内容,从而每次都会编译脚本。

Redis提供了一个缓存机制,因此不会每次都重新编译脚本,可能在某些场景,脚本传输消耗的带宽可能是不必要的。

为了减少带宽的西消耗,Redis实现了evaklsha命令,它的作用和eval一样,只是它接受的第一个参数不是脚本,而是脚本的SHA1校验和(sum)。

所以如何获取这个SHA1的值,就需要提到Script命令。

  • 1、SCRIPT FLUSH :清除所有脚本缓存。
  • 2、SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存。
  • 3、SCRIPT LOAD :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它。
  • 4、SCRIPT KILL :杀死当前正在运行的脚本

执行evalsha命令

# 使用script load将脚本内容加载到缓存中,返回sha的值
127.0.0.1:6379> script load "return redis.call('set',KEYS[1],ARGV[1])"
"c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
# 使用evalsha和返回的sha的值 + 参数个数 参数名称和值执行
127.0.0.1:6379> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 eval_02 002
OK
# 获取结果
127.0.0.1:6379> get eval_02
"002"

我们上面都是将脚本写在代码行里面,可以不可以将脚本内容写在xxx.lua中,直接执行呢? 当然是可以的。

使用redis-cli运行外置lua脚本

编写外置脚本test2.lua, 设置值到redis中。

# 脚本内容 也就是设置一个值
return redis.call('set',KEYS[1],ARGV[1])

# 执行结果,可以使用./redis-cli -h 127.0.0.1 -p 6379 指定redis ip、端口等
root@62ddf68b878d:/data# redis-cli --eval /data/test2.lua eval_03 , test03       
OK

利用Redis整合lua脚本,主要是为了保证性能是事务的原子性,因为redis的事务功能确实有些差劲!

4、Redis的脚本复制

Redis如果开启了主从复制,脚本是如何从主服务器复制到从服务器的呢?

首先,redis的脚本复制有两种模式,脚本传播模式和命令传播模式。

在开启了主从,并且开启了AOF持久化的情况下。

4.1、脚本传播模式

其实就是主服务器执行什么脚本,从服务器就执行什么样的脚本。但是如果有当前事件,随机函数等会导致差异。

主服务器执行命令

# 执行多个redis命令并返回
127.0.0.1:6379> eval "local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_01 eval_test_02 0001 0002
1) OK
2) OK
127.0.0.1:6379> get eval_test_01
"0001"
127.0.0.1:6379> get eval_test_02
"0002"

那么主服务器将向从服务器发送完全相同的eval命令:

eval "local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_01 eval_test_02 0001 0002

注意:在这一模式下执行的脚本不能有时间、内部状态、随机函数等。执行相同的脚本以及参数必须产生相同的效果。在Redis5,也是处于同一个事务中。

4.2、命令传播模式

处于命令传播模式的主服务器会将执行脚本产生的所有写命令用事务包裹起来,然后将事务复制到AOF文件以及从服务器里面.

因为命令传播模式复制的是写命令而不是脚本本身,所以即使脚本本身包含时间、内部状态、随机函数等,主服务器给所有从服务器复制的写命令仍然是相同的。

为了开启命令传播模式,用户在使用脚本执行任何写操作之前,需要先在脚本里面调用以下函数:

redis.replicate_commands()

redis.replicate_commands() 只对调用该函数的脚本有效:在使用命令传播模式执行完当前脚本之后,服务器将自动切换回默认的脚本传播模式。

执行脚本

eval "redis.replicate_commands();local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_03 eval_test_04 0003 0004

appendonly.aof文件内容

*1
$5
MULTI
*3
$3
set
$12
eval_test_03
$4
0003
*3
$3
set
$12
eval_test_04
$4
0004
*1
$4
EXEC

可以看到,在一个事务里面执行了我们脚本执行的命令。

同样的道理,主服务器只需要向从服务器发送这些命令就可以实现主从脚本数据同步了。

5、Redis的管道/事务/脚本

  • 1、管道其实就是一次性执行一批命令,不保证原子性,命令都是独立的,属于无状态操作(也就是普通的批处理)
  • 2、事务和脚本是有原子性的,但是事务是弱原子性,lua脚本是强原子性。
  • 3、lua脚本可以使用lua语言编写比较复杂的逻辑。
  • 4、lua脚本的原子性强于事务,脚本执行期间,另外的客户端或其他的任何脚本或命令都无法执行。所以lua脚本的执行事件应该尽可能的短,不然会导致redis阻塞不能做其他工作。

6、小结

Redis的事务是弱事务,多个命令开启事务一起执行性能比较低,且不能一定保证原子性。所以lua脚本就是对它的补充,它主要就是为了保证redis的原子性。

比如有的业务(接口Api幂等性设计,生成token,(取出toker并判断是否存在,这就不是原子操作))我们需要获取一个key, 并且判断这个key是否存在。就可以使用lua脚本来实现。

还有很多地方,我们都需要redis的多个命令操作需要保证原子性,此时lua脚本可能就是一个不二选择。

7、相关文章

本人还写了Redis的其他相关文章,有兴趣的可以点击查看!

  • <<Redis持久化机制分析>>
  • <<Redis的事件处理机制分析>>
  • <<Redis客户端和服务端如何通信?>>
  • <<redis的淘汰机制分析>>
  • <<Redis的底层数据结构分析>>
  • <<Redis的8种数据类型,什么场景使用?>>
  • <<缓存是什么?缓存有哪些分类?使用它的代价是什么?>>
  • <<缓存的6种常见的使用场景>>

ua是一门脚本动态语言,并不太适合做复杂业务逻辑的程序开发,但是,在高并发场景下,Nginx Lua编程是解决性能问题的利器。

Nginx Lua编程主要的应用场景如下:

  • API网关:实现数据校验前置、请求过滤、API请求聚合、AB测试、灰度发布、降级、监控等功能,著名的开源网关Kong就是基于Nginx Lua开发的。
  • 高速缓存:可以对响应内容进行缓存,减少到后端的请求,从而提升性能。比如,Nginx Lua可以和Java容器、Redis整合,由Java容器进行业务处理和数据缓存,而Nginx负责读缓存并进行响应,从而解决Java容器的性能瓶颈
  • 简单的动态Web应用:可以完成一些业务逻辑处理较少但耗费CPU的简单应用,比如模板页面的渲染。一般的Nginx Lua页面渲染处理流程为:从Redis获取业务处理结果数据,从本地加载XML/HTML页面模板,然后进行页面渲染。
  • 网关限流:缓存、降级、限流是解决高并发的三大利器,Nginx内置了令牌限流的算法,但是对于分布式的限流场景,可以通过Nginx Lua编程定制自己的限流机制

ngx_lua是Nginx的一个扩展模块,将Lua VM嵌入Nginx,请求时创建一个VM,请求结束时回收VM,这样就可以在Nginx内部运行Lua脚本,使得Nginx变成一个Web容器。以OpenResty为例,其提供了一些常用的ngx_lua开发模块:

  • lua-resty-memcached:通过Lua操作memcache
  • lua-resty-mysql:通过Lua操作MySQL
  • lua-resty-redis:通过Lua操作Redis缓存
  • lua-resty-dns:通过Lua操作DNS域名服务器
  • lua-resty-limit-traffic:通过Lua进行限流
  • lua-resty-template:通过Lua进行模板渲染
  • lua-resty-jwt:通过Lua生成jwt
  • lua-resty-kafka:通过Lua操作kafka

Lua脚本需要通过Lua解释器来解释执行,除了Lua官方的默认解释器外,目前使用广泛的Lua解释器叫做LuaJIT。LuaJIT采用C语言编写,被设计成全兼容标准Lua 5.1,因此LuaJIT代码的语法和标准Lua的语法没多大区别。但是LuaJIT的运行速度比标准Lua快数十倍。

Nginx Lua的执行原理

在OpenResty中,每个Worker进程使用一个Lua VM,当请求被分配到Worker时,将在这个Lua VM中创建一个协程,协程之间数据隔离,每个协程都具有独立的全局变量。

ngx_lua是将Lua VM嵌入Nginx,让Nginx执行Lua脚本,并且高并发、非阻塞地处理各种请求Lua内置协程可以很好地将异步回调转换成顺序调用的形式。ngx_lua在Lua中进行的IO操作都会委托给Nginx的事件模型,从而实现非阻塞调用。开发者可以采用串行的方式编写程序,ngx_lua会在进行阻塞的IO操作时自动中断,保存上下文,然后将IO操作委托给Nginx事件处理机制,在IO操作完成后,ngx_lua会恢复上下文,程序继续执行,这些操作对用户程序都是透明的。

每个Worker进程都持有一个Lua解释器或LuaJIT实例,被这个Worker处理的所有请求共享这个实例。每个请求的context上下文会被Lua轻量级的协程分隔,从而保证每个请求是独立的。

ngx_lua采用one-coroutine-per-request的处理模型,对于每个用户请求,ngx_lua会唤醒一个协程用于执行用户代码处理请求,当请求处理完成后,这个协程会被销毁。每个协程都有一个独立的全局环境,继承于全局共享的、只读的公共数据。所以,被用户代码注入全局空间的任何变量都不会影响其他请求的处理,并且这些变量在请求处理完成后会被释放,这样就保证所有的用户代码都运行在一个sandbox(沙箱)中,这个沙箱与请求具有相同的生命周期。

得益于Lua协程的支持,ngx_lua在处理10000个并发请求时,只需要很少的内存。根据测试,ngx_lua处理每个请求只需要2KB的内存,如果使用LuaJIT就会更少

Nginx Lua配置指令

ngx_lua定义的Nginx配置指令大致如下:

  • lua_package_path:配置Lua外部库的搜索路径,搜索的文件类型为.lua。
  • lua_package_cpath:配置Lua外部搜索库的搜索路径,搜索C语言编写的外部库文件。
  • init_by_lua:Master进程启动时挂载的Lua代码块,常用于导入公共模块。
  • init_by_lua_file:Master进程启动时挂载的Lua脚本文件。
  • init_worker_by_lua:Worker进程启动时挂载的Lua代码块,常用于执行一些定时任务
  • init_worker_by_lua_file:Worker进程启动时挂载的Lua文件,常用于执行一些定时任务
  • set_by_lua:类似于rewrite模块的set指令,将Lua代码块的返回结果设置在Nginx的变量中。
  • set_by_lua_file:同上,执行的是脚本Lua脚本文件。
  • rewrite_by_lua:执行在rewrite阶段的Lua代码块,完成转发、重定向、缓存等功能。
  • rewrite_by_lua_file:同上,执行的是Lua脚本文件。
  • access_by_lua:执行在access阶段的Lua代码块,完成IP准入、接口权限等功能。
  • access_by_lua_file:同上,执行的是Lua脚本文件。
  • content_by_lua:执行在content阶段的Lua代码块,执行结果将作为请求响应的内容。
  • content_by_lua_file:同上,执行的是Lua脚本文件。
  • content_by_lua_block:content_by_lua的升级款,在一对花括号中编写Lua代码,而不需要做特殊字符转译。
  • header_filter_by_lua:响应头部过滤处理的Lua代码块,可以用于添加设置响应头部信息,如Cookie相关属性。
  • body_filter_by_lua:响应体过滤处理的Lua代码块,例如加密响应体。
  • log_by_lua:异步完成日志记录的Lua代码块,例如既可以在本地记录日志,也可以记录到ETL集群。

ngx_lua配置指令在Nginx的HTTP请求处理阶段所处的位置如图:

常用配置指令

  • lua_package_path指令:用于设置".lua"外部库的搜索路径,此指令的上下文为http配置块,默认值为LUA_PATH环境变量内容或者lua编译的默认值。
    • 格式:lua_package_path lua-style-path-str。
    • lua_package_cpath指令:用于设置Lua的C语言块外部库".so"(Linux)或".dll"(Windows)的搜索路径,此指令的上下文为http配置块。
    • 格式:lua_package_cpath lua-style-cpath-str
http {
  ...
  #设置“.lua”外部库的搜索路径,此指令的上下文为http配置块
	#";;"常用于表示原始的搜索路径
	lua_package_path	"/foo/bar/?.lua;/blah/?.lua;;";
	lua_package_cpath	"/usr/local/openresty/lualib/?/?.so;/usr/local/openresty/lualib/?.so;;";
}

对于以上两个指令,OpenResty可以在搜索路径中使用插值变量。例如,可以使用插值变量$prefix或${prefix}获取虚拟服务器server的前缀路径,server的前缀路径通常在Nginx服务器启动时通过-p PATH命令在指定。

  • init_by_lua指令:只能用于http上下文,运行在配置加载阶段。当Nginx的master进程在加载Nginx配置文件时,在全局Lua VM级别上运行由参数lua-script-str指定的Lua脚本块。若使用init_by_lua_file指令,后面跟lua文件的路径( lua_file_path),则在全局Lua VM 级别上运行lua_file_path文件指定的lua脚本。如果Lua脚本的缓存是关闭的,那么每一次请求都运行一次init_by_lua处理程序。

格式为:init_by_lua lua-script-str。

  • lua_load_cache指令:用于启用或禁止Lua脚本缓存。可以使用的上下文为http、server、location配置块。默认开启。

格式为:lua_code_cache on | off

http {
  ...
	#项目初始化
  init_by_lua_file	conf/luaScript/initial/loading_config.lua;
  	
  #调试模式,关闭lua脚本缓存
  lua_code_cache on;
  ...
}

在缓存关闭的时,set_by_lua_file、content_by_lua_file、access_by_lua_file、content_by_lua_file等指令中引用的Lua脚本都将不会被缓存,所有的Lua脚本都将从头开始加载。

  • set_by_lua指令:将Lua脚本块的返回结果设置在Nginx变量中。

格式为:set_by_lua $destVar lua-script-str params

location /set_by_lua_demo {
	#set 指令定义两个Nginx变量
  set $foo 1;
  set $bar 2;
  			
  #调用Lua内联代码,将结果放入Nginx变量$sum
  #Lua脚本的含义是,将两个输入参数$foo、$bar累积起来,然后相加的结果设置Nginx变量$sum中
  set_by_lua $sum 'return tonumber(ngx.arg[1]) + tonumber(ngx.arg[2])' $foo $bar;
  
  echo "$foo + $bar = $sum";
}

运行结果:

➜  work curl http://localhost/set_by_lua_demo
1 + 2 = 3
  • access_by_lua指令:执行在HTTP请求处理11个阶段的access阶段,使用Lua脚本进行访问控制。运行于access阶段的末尾,总是在allow和deny这样的指令之后运行。

格式为:access_by_lua $destVar lua-script-str

location /access_demo {
  access_by_lua	'ngx.log(ngx.DEBUG, "remote_addr = "..ngx.var.remote_addr);
  if ngx.var.remote_addr == "192.168.56.121" then
  	return;
  end
  ngx.exit(ngx.HTTP_UNAUTHORIZED);
  ';
  echo "hello world";
}
  		
location /access_demo_2 {
  allow "192.168.56.121";
  deny all;
  echo "hello world";
}

运行结果:

➜  work curl http://localhost/access_demo
<html>
<head><title>401 Authorization Required</title></head>
<body bgcolor="white">
<center><h1>401 Authorization Required</h1></center>
<hr><center>openresty/1.13.6.2</center>
</body>
</html>

#上述案例运行日志:
2022/02/15 10:32:17 [debug] 26293#0: *17 [lua] access_by_lua(nginx-lua-demo.conf:85):1: remote_addr = 127.0.0.1
2022/02/15 10:32:17 [info] 26293#0: *17 kevent() reported that client 127.0.0.1 closed keepalive connection

➜  work curl http://localhost/access_demo_2
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>openresty/1.13.6.2</center>
</body>
</html>

#上述案例运行日志
2022/02/15 10:33:11 [error] 26293#0: *18 access forbidden by rule, client: 127.0.0.1, server: localhost, request: "GET /access_demo_2 HTTP/1.1", host: "localhost"
2022/02/15 10:33:11 [info] 26293#0: *18 kevent() reported that client 127.0.0.1 closed keepalive connection
  • content_by_lua/content_by_lua_block指令:用于设置执行在content阶段的Lua代码块,执行结果将作为请求响应的内容。该指令用于location上下文。

格式为:content_by_lua lua-script-str

location /errorLog {
  content_by_lua '
    ngx.log(ngx.ERR, "this is an error log ");
  	ngx.say("错误日志调用成功");
  ';
}
  		
location /infoLog {
	content_by_lua '
		ngx.log(ngx.ERR, "this is an info log ");
  	ngx.say("业务日志调用成功");
  ';
}

location /debugLog {
  content_by_lua '
    ngx.log(ngx.ERR, "this is an debug log ");
  	ngx.say("调试日志调用成功");
  ';
}

OpenResty v0.9.17版本以后,使用content_by_lua_block指令代替content_by_lua指令,避免对代码块中的字符串进行转译。

运行结果:

➜  work curl http://localhost/errorLog
错误日志调用成功
➜  work curl http://localhost/infoLog 
业务日志调用成功
➜  work curl http://localhost/debugLog
调试日志调用成功

Nginx Lua的内置常量和变量

内置变量

  • ngx.arg:类型为Lua table,ngx.arg.VARIABLE用于获取ngx_lua配置指令后面的调用参数。
  • ngx.var:类型为Lua table,ngx.var.VARIABLE用于引用某个Nginx变量。前提是Nginx变量必须提前声明
  • ngx.ctx:类型为Lua table,可以用来访问当前请求的Lua上下文数据,其生存周期与当前请求相同
  • ngx.header:类型为Lua table,用于访问HTTP响应头,可以通过ngx.header.HEADER形式引用某个头
  • ngx.status:用于设置当前请求的HTTP响应码

内置常量

内置常量基本是见名知意的,可以根据后面的实战案例,加深理解。

核心常量

    • ngx.OK(0)
    • ngx.ERROR(-1)
    • ngx.AGAIN(-2)
    • ngx.DONE(-4)
    • ngx.DECLINED(-5)
    • ngx.nil

HTTP方法常量

    • ngx.HTTP.GET
    • ngx.HTTP.HEAD
    • ngx.HTTP.PUT
    • ngx.HTTP.POST
    • ngx.HTTP.DELETE
    • ngx.HTTP.OPTIONS
    • ngx.HTTP.MKCOL
    • ngx.HTTP.MOVE
    • ngx.HTTP.PROPFIND
    • ngx.HTTP.PROPPATCH
    • ngx.HTTP.LOCK
    • ngx.HTTP.UNLOCK
    • ngx.HTTP.PATH
    • ngx.HTTP.TRACE

HTTP状态码常量

    • ngx.HTTP_OK(200)
    • ngx.HTTP_CREATED(201)
    • ngx.HTTP_SPECIAL_RESPONSE(300)
    • ngx.HTTP_MOVED_PERMANENTLY(301)
    • ngx.HTTP_MOVER_TEMPORARILY(302)
    • ngx.HTTP_SEE_OTHER(303)
    • ngx.HTTP_NOT_MODIFIED(304)
    • ngx.HTTP_BAD_REQUEST(400)
    • ngx.HTTP_UNAUTHORIZED(401)
    • ngx.HTTP_FORBIDDEN(403)
    • ngx.HTTP_NOT_FOUND(404)
    • ngx.HTTP_NOT_ALLOWED(405)
    • ngx.HTTP_GONE(410)
    • ngx.HTTP_INTERNAL_SERVER_ERROR(500)

日志类型常量

    • ngx.STDERR
    • ngx.EMERG
    • ngx.ALERT
    • ngx.CRIT
    • ngx.ERR
    • ngx.WARE
    • ngx.NOTICE
    • ngx.INFO
    • ngx.DEBUG

Nginx+LUA基础到此结束,下一篇开始实战!并在实战中掌握基础。