笔者在往年的 COSCUP 大会做分享时,曾有观众问这样的问题,为什么 APISIX、Kong 和 3scale 这些网关都采纳 Lua 来编写逻辑?
是啊,Lua 并不是一门广为人知的语言,离“支流编程语言”的圈子大略还差个十万八千里吧。甚至有一次,我在跟他人交换的时候,对方在说到 Lua 之前,先进展了片刻,随后终于打定主意,以“L”“U”“A”一一字母发音的形式表白了对这一常见之物的称说。
所以,为什么 APISIX 和其余出名的网关会抉择用 Lua 呢?
事实上,APISIX 采纳的技术栈并不是纯正的 Lua,精确来说,应该是 Nginx + Lua。APISIX 以底下的 Nginx 为根基,以上层的 Lua 代码为枝叶。
LuaJIT VS Go
谨严认真的读者必然会指出,APISIX 并非基于 Nginx + Lua 的技术栈,而是 Nginx + LuaJIT(又称 OpenResty,以下为了防止凌乱,会仅仅采纳 Nginx + Lua 这样的称说)。
LuaJIT 是 Lua 的一个 JIT 实现,性能比 Lua 好很多,而且额定增加了 FFI 的性能,能不便高效地调用 C 代码。
因为现行的支流 API 网关,如果不是基于 OpenResty 实现,就是应用 Go 编写,所以时不时会看到各种 Go 和 Lua 谁的性能更好的比拟。
就我个人观点看,脱离场景比拟语言的性能,是没有意义的。
首先明确一点,APISIX 是基于 Nginx + Lua 的技术栈,只是外层代码用的是 Lua。所以如果要论证哪种网关性能更好,正确的比拟对象是 C + LuaJIT 跟 Go 的比拟。网关的性能的大头,在于代理 HTTP 申请和响应,这一块的工作次要是 Nginx 在做。所以假使要比试比试性能,无妨比拟 Nginx 和 Go 规范库的 HTTP 实现。
家喻户晓,Nginx 是一个 bytes matter 的高性能服务器实现,对内存应用十分抠门。轻易举两个例子:
- Nginx 外面的 request header 在大多数时候都只是指向原始的 HTTP 申请数据的一个指针,只有在批改的时候才会创立正本。
- Nginx 代理上游响应时对 buffer 的复用逻辑非常复杂,是我读过的最为烧脑的代码之一。
凭借这种抠门,Nginx 得以耸立在高性能服务器之巅。
相同的,Go 规范库的 HTTP 实现,是一个滥用内存的典型反例。这可不是我的一面之辞,Fasthttp,一个从新实现 Go 规范库外面的 HTTP 包的我的项目,就举了两个例子:
- 规范库的 HTTP Request 构造体没法复用
- headers 总是被提前解析好,存储成
map[string][]string
,即便没有用到(原文见:https://github.com/valyala/fa…)
Fasthttp 文档外面还提到一些 bytes matter 的优化技巧,倡议大家能够浏览下。
事实上,即便不去比拟作为网关外围的代理性能,用 LuaJIT 写的代码不肯定比 Go 差多少。起因有二。
其一,拜 Lua 跟 C 良好的亲和力所赐,许多 Lua 的库外围其实是用 C 写的。
比方 lua-cjson 的 json 编解码,lua-resty-core 的 base64 编解码,实际上大头是用 C 实现的。
而 Go 的库,当然是大部分用 Go 实现的。尽管有 CGO 这种货色,然而受限于 Go 的协程调度和工具链的限度,它在 Go 的生态圈外面只能处于隶属的位置。
对于 LuaJIT 和 Go 对于 C 的亲和力的比拟,举荐 Hacker News 上的这篇文章:《Comparing the C FFI overhead in various programming languages》(链接 https://news.ycombinator.com/…)
于是咱们比拟 Lua 的某些性能,其实还是会回到 C 和 Go 的比拟中。
其二,LuaJIT 的 JIT 优化无出其右
探讨动静语言的性能,能够把动静语言分成两类,带 JIT 和不带 JIT 的。JIT 优化可能把动静语言的代码在运行时编译成机器码,进而把原来的代码的性能晋升一个数量级。
带 JIT 的语言还能够分成两类,能充沛 JIT 的和只反对局部 JIT 的。而 LuaJIT 属于前者。
人所皆知,Lua 是一门非常简单的语言。
绝对鲜为人知的是,LuaJIT 的作者 Mike Pall 是一个十分厉害的程序员。
这两者的联合,诞生了 LuaJIT 这种能跟 V8 比肩的作品。(对于 LuaJIT 和 V8 到底谁更快,始终是长盛不衰的争执话题。)
开展讲 LuaJIT 的 JIT 曾经超过了本文想要探讨的领域。简略来说,JIT 加持的 LuaJIT 跟事后编译好的 Go 性能差异并不大。
至于谁比谁慢,慢多少,那就是个见仁见智的问题了。这里我举个例子,
local text = {"The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog", "at", "a", "restaurant", "near", "the", "lake", "of", "a", "new", "era"}
local map = {}
local times = 1e8
local n = #text
for i = 1, n do
map] = 0
for _ = 1, times do
map] = map] + 1
end
end
for i = 1, n do
io.write(text[i], "", map],"\n")
end
``````
package main
import "fmt"
func main() {text := []string{"The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog", "at", "a", "restaurant", "near", "the", "lake", "of", "a", "new", "era"}
m := map[string]int{}
times := int(1e8)
for _, t := range text {m[t] = 0
for i := 0; i < times; i++ {m[t]++
}
}
for _, t := range text {fmt.Println(t, " ", m[t])
}
}
下面两段代码是等价的。你猜是第一个 Lua 版本的快,还是第二个 Go 版本的快?
在我的机器上,第一个用时不到 1 秒,第二个花了 23 秒多。
举这个例子并不是想证实 LuaJIT 比 Go 快 20 倍。我只想阐明用 micro benchmark 证实某个语言比另一个语言快的意义不大,因为影响性能的因素很多。一个简略的 micro benchmark 很有可能过分强调某一个因素,导致出其不意的后果。
Nginx + Lua:高性能 + 灵便
让咱们转回 APISIX 的 Nginx + Lua 的技术栈。Nginx + Lua 的技术栈给咱们带来的,不仅仅是高性能。
常常有人问咱们,既然你们是基于 Nginx 开源版本,而 Nginx 并不反对动静配置,为什么 APISIX 宣称本人能够实现动静配置?你们是不是改了点货色?
是的,咱们的确有在保护本人的 Nginx 发行版,不过 APISIX 的大部分性能在官网的 Nginx 上就能应用。咱们之所以能做到动静配置,全靠把配置放到 Lua 代码外面来实现。
举路由零碎作为一个例子。Nginx 的路由须要在配置文件外面进行配置。每次更改路由,都须要 reload 之后能力失效。这是因为 Nginx 的路由散发只反对动态配置,不能动静增减路由。
为了实现路由动静配置,APISIX 做了两件事:
- 在 Nginx 配置文件外面配置单个 server,这个 server 外面只有一个 location。咱们把这个 location 作为主入口,这样所有的申请都会走到这个中央上来。
- 咱们用 Lua 实现路由散发的工作。APISIX 的路由散发模块,反对在运行时增减路由,这样就能动静配置路由了。
你可能会问,在 Lua 外面做路由散发,会比 Nginx 的实现慢吗?
就像后面提到过的一样,但凡对性能要求比拟高的,咱们会把外围代码用 C 改写。咱们的路由散发模块就是这么干的。路由散发模块在匹配路由时,会采纳一个前缀树来匹配。而这个前缀树是用 C 实现的。感兴趣的读者能够看下代码:https://github.com/api7/lua-r…。
实现了 C 层面上的前缀树匹配,接下来就该 Lua 施展灵活性的时刻了。对于匹配同一前缀的各个路由,咱们反对通过许多别的形式来进行下一级的匹配,其中就蕴含通过一个特定的表达式来匹配。只管硬着头皮,也能在 C 层面上接入一个表达式引擎,然而纯 C 实现做不了非常灵活地自定义表达式外面的变量。
举个例子,上面是 APISIX 用来匹配 GraphQL 申请的 route 配置:
{"methods": ["POST"],
"upstream": {
"nodes": {"127.0.0.1:1980": 1},
"type": "roundrobin"
},
"uri": "/hello",
"vars": [["graphql_name", "==", "repo"]]
}
它会匹配这样的 GraphQL 申请:
query repo {
owner {name}
}
这里的 graphql\_name 并非 Nginx 内置变量,而是通过 Lua 代码定义的。APISIX 一共定义了三个 GraphQL 相干的变量,连同解析 GraphQL body 在内不过 62 行 Lua 代码。如果要通过 Nginx C 模块来定义变量,62 行可能只不过是把相干办法的样板代码搭建起来,都还没有到真正的解析 GraphQL 的逻辑呢。
采纳 Lua 代码来做路由还有一个益处:它减低了二次开发的门槛。如果在路由过程中须要有非凡的逻辑,用户能够实现成自定义的变量和运算符,比方通过 IP 库匹配到的地理位置来决定采纳哪条路由。用户只须要写一些 Lua 代码,这要比批改 Nginx C module 的难度小多了。
在 APISIX 外面,不仅仅路由是动静的,咱们的 TLS 服务端证书和上游节点配置都是动静的,而且无需批改 Nginx —— 上述性能能够跑在官网的 Nginx + Lua 技术栈上。当然通过批改 Nginx,咱们还实现了更多的高级性能,比方动静的 gzip 配置和动静的客户端申请大小限度。后续咱们将推广本人的 Nginx 发行版,这样开源用户也能轻松用上这些高级性能。
对于 Apache APISIX
Apache APISIX 是一个动静、实时、高性能的开源 API 网关,提供负载平衡、动静上游、灰度公布、服务熔断、身份认证、可观测性等丰盛的流量治理性能。Apache APISIX 能够帮忙企业疾速、平安的解决 API 和微服务流量,包含网关、Kubernetes Ingress 和服务网格等。
寰球已有数百家企业应用 Apache APISIX 解决要害业务流量,涵盖金融、互联网、制作、批发、运营商等等,比方美国航空航天局(NASA)、欧盟的数字工厂、中国航信、中国移动、腾讯、华为、微博、网易、贝壳找房、360、泰康、奈雪的茶等。
200 余位贡献者,一起缔造了 Apache APISIX 这个世界上最沉闷的开源网关我的项目。聪慧的开发者们!快来退出这个沉闷而多样化的社区,一起来给这个世界带来更多美妙的货色吧!
- Apache APISIX GitHub:https://github.com/apache/apisix
- Apache APISIX 官网:https://apisix.apache.org/
- Apache APISIX 文档:https://apisix.apache.org/zh/…