在apisix中编写lua必看

4,230 阅读7分钟

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

1: 再谈 APISIX 高性能实践

ngx.var 的加速

对获取 Nginx 变量的加速,最简单的就是用 iresty/lua-var-nginx-module 仓库,把它作为一个 lua module 编译到 OpenResty 项目里。当我们提取对应的 ngx.var 的时,使用库里提供的方法来获取,可以让 APISIX 整体有 5% 的性能提升,单纯某个变量性能对比,至少有 10 倍差别。当然也可以把这个模块编译成动态库,然后用动态方式加载,这样就不用重新编译 OpenResty。

APISIX 网关会从 ngx.var 里获取大量变量信息,比如 host 地址等变量更是可能会被反复获取,每次都与 Nginx 交互效率会比较低。因此我们在 APISIX/core 里加了一层 ctx 缓存,也就是第一次与 Nginx 交互获取变量,后面将直接使用缓存。

题外篇:再次推荐大家多参考借鉴 APISIX/core 中的代码,这些代码是通用的,对大多数项目都应该有借鉴意义。

fail to json encode

当我们用 json 的方式去 encode 一个 table时,可能会失败。失败原因有以下几种:比如 table 中包含 cdata 或者 userdata 无法 encode ,又或者包含 function 等,但实际上我们做 encode 并不是想要一个可以完美支持序列化/反序列化的结果,有时候只是为了调试。

所以我在 APISIX 的 core/json_encode 增加了一个布尔参数,表示是否进行强制转码,这样当遇到不能转码时就把强制它变成一个字符串。此外 table 套 table 是一个常见的情况,即有一个 table A,在 A 的 table 里面的内部又引用了A 自身,形成了一个循环嵌套。这个问题的解决比较简单,在发生嵌套时,到达某一个位置点后就不要再往里嵌了。这两个场景下允许强制 table encode 对我们开发调试非常有用。

在调试时,如果需要打一下 table 结果,当日志级别不够时,不应该触发无意义的 jsonencode 行为,这时候推荐使用 delay_encode 来调试日志,只有当日志真正需要写到磁盘上时,才会触发 json encode,避免那些不需要 encode 。这个问题在APISIX 里面效果非常好,终于不需要注释代码就可以完成不同级别日志的测试,有点 C 语言中宏定义的味道,对性能和易用是个极好的平衡。

2:OpenResty 社区王院生:APISIX 的高性能实践

技巧九:Irucache 的正确姿势

简单介绍下 Irucache,Irucache 可以完成在 worker 内的数据的缓存和复用,Irucache 有一个非常大的优势是可以存储任何对象。而共享内存则是完成不同 worker 之间的数据共享,但它只能存储简单对象,有些东西是不能跨 worker 共享,比如 function、cdata 对象等。

对 Irucache 进行二次封装,封装的内容主要包括:

  • key 要尽量短、简单:我们在写 key 时重要的是要简单,key 糟糕的设计是里面东西很长,但是有用信息不多。key 理论上大家都喜欢用字符串,但他可以是 table 等对象,key 尽量做到明确,只包含你感兴趣的内容,能省略的尽量省略,降低拼接成本。
  • version 可降低垃圾缓存:这点算是我在做 APISIX 的突破:提取出了 version, Irucache+ version 这套组合,可以极大地降低垃圾缓存。
  • 重用 stale 状态的缓存数据。

version 可降低垃圾缓存。如果没有 version,我们需要把 version 写到 key 里面,每次 version 变化都会产生一个新的 key,那些被淘汰的旧数据会一直存在,没办法剔除掉。同时意味着 Irucache 里面的对象数会不停增加。而我们前面的方式是保证 key 如果是一个对象,只会有一个 table 与它对应,不会根据不同的 version 产生不同的对象缓存,进而降低缓存总数。

3:worker进程的共享内存

worker进程的共享内存

worker进程的共享内存,顾名思义就是Nginx的全部worker进程都共享这份内存数据,如果某个worker进程对该数据进行了修改,其他的worker进程看到的都将是修改后的数据。我们可以利用共享内存来存放缓存数据,提升吞吐率,减少到达后端的请求次数。

1.共享内存区域的数据是所有worker进程共享的,因此一旦对其进行修改,所有worker进程都会使用修改后的数据,worker进程之间同时读取或修改同一个共享内存数据会产生锁。

2.共享内存区域的大小是预先分配的,如果内存空间使用完了,会根据LRU(Least Recently Used,即最近最少使用)算法淘汰访问量少的数据。

3.在Nginx中可以配置多个共享内存区域。

worker进程的共享内存,它利用丰富的指令使数据的缓存操作变得非常简单,但它也存在一些缺点。

1.worker进程之间会有锁竞争,在高并发的情况下会增加性能开销。

2.只支持Lua布尔值、数字、字符串和nil类型的数据,无法支持table类型的数据。

3.在读取数据时有反序列化操作,会增加CPU开销。

ngx.shared共享内存reload会全部失效

在apisix里面有一个全局的配置,就是通过lrucache做的

具体实现:LRU 缓存

4:Lua模块下的共享内存

lua-resty-lrucache是基于Ngx_Lua的缓存利器,它拥有如下优点。

1.支持更丰富的数据类型,可以把table存放在value中,这对数据结构复杂的业务非常有用。

2.可以预先分配key的数量,不用设置固定的内存空间,在内存的使用上更为灵活。

3.每个worker进程独立缓存,所以当worker进程同时读取同一个key 时不存在锁竞争。

它与lua_shared_dict相比也有一些缺点。

1.因为数据不在worker之间共享,所以无法保证在更新数据时,数据在同一时间的不同worker进程上完全一致。

2.虽然可以支持复杂的数据结构,但可使用的指令却很少,如不支持消息队列功能。

3.重载Nginx配置时,缓存数据会丢失。如果使用lua_shared_dict,则不会如此。

5:shared.dict和LruCache如何选择?

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 的情况下,内存占用比较少。

附录

在编写的过程中充满了各种异常码,可参考

HTTP 响应代码