再谈 APISIX 高性能实践

2019 年 8 月 31 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·成都站,APISIX 主要做者王院生在活动上作了《APISIX 高性能实践》的分享。html

OpenResty × Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使用者的交流与学习,推进 OpenResty 开源项目的发展。nginx

王院生,APISIX 项目发起人和主要做者,OpenResty 社区、OpenResty 软件基金会发起人,《OpenResty 最佳实践》主要做者。算法

如下是分享全文:编程

首先作下自我介绍,我大学毕业后在传统金融行业工做九年,2014 年加入奇虎 360,期间撰写了《OpenResty 最佳实践》。我我的比较喜欢研究技术和开源,多是受老罗影响,喜欢尝试理想化的事情。今年 3 月份与志同道合的伙伴一块儿创办了深圳支流科技公司,这是一家以开源方式创业的科技公司,在国内屈指可数,APISIX 是咱们目前的主要项目。json

APISIX 是微服务 API 网关产品,今年 7 月份我在上海作过一次关于“ APISIX 高性能实践”的分享,此次的内容是在上次分享的基础上,并会将最近的新积累分享给你们。api

什么是 API 网关

API 网关的地位愈来愈重要,它是全部流量的出入口,从图中能够看到请求方可能来自于浏览器、loT 设备以及移动设备等,API 网关做为中间管控层须要作安全控制、流量以及日志记录等。愈来愈多的企业采用了微服务的方式,以此完成内部解耦、灵活部署、弹性伸缩等技术特性从而知足业务需求。微服务的数量和复杂度也都随之水涨船高,经过 API 网关来完成统一的流量管理调度就很是必要,并对 API 网关提出了更高要求。数组

APISIX 概述

上图是 APISIX 的基本构架,因为要支持集群和高可用,因此在任何一个节点都须要包含 adminAPI 或 APISIX 内核,使用时能够只启用其中一部分或都启用。admin API 主要用于接收管理员的提交信息,经过 json schema 完成参数的校验,防止非法参数落到存储的配置中心。APISIX 内部部分处理外部请求,根据请求特征,匹配到具体路由规则,执行插件,而后把流量转发到指定上游服务。浏览器

APISIX 每月会发布一个版本,在 0.7 版本支持了路由插件化,很自豪地说这是目前惟一容许自定义路由的 API 网关实现。除了以前已有的 r3 路由,APISIX 新增了专门高性能的前缀匹配 radixtree,radixtree 是由 Redix 的做者开源出来的。radixtree 代码的匹配效率是 r3 的 10 倍甚至更高,一些生产用户升级 radixtree 后 CPU 使用率确实降低明显。缓存

上图显示的是两个月前 APISIX 已有的功能。安全

最近的两个月,APISIX 增长了以上新功能,每月大概都会有 五、6 个大的新特性,若是我只准备 APISIX 里的一些新特性与你们分享,各位受益可能会比较小,因此今天我给你们分享一些通用的 OpenResty 编程技巧。

APISIX 主打的是高性能,咱们与 OpenResty 对比性能,这样更能突出 APISIX 性能的极致。首先用 APISIX 完整服务来压测,对比一个没有任何功能的空 OpenResty 服务,发现 APISIX 在加载了全部功能的状况下只降低了 15% 的性能。换言之,你若是能接受 15% 的性能降低,就能够直接享受上图的全部功能。

OpenResty 优化技巧

路由:radixtree vs r3

既然已经有了 r3 ,为何咱们还要继续用 resty-radixtree 实现新的路由呢?

先介绍 r3 的问题:r3 的学习复杂度比较高(正则自己就有学习难度),而且不支持经过迭代器的方式迭代匹配结果,效率相比前缀树实现低很多。相反这些问题在 resty-radixtree 上都有完美解决方案,性能、稳定性天然也就提高不少。目前的 resty-radixtree 是基于 antirez/rax 实现的,也是 Redis 的做者写的,站在巨人肩膀可让咱们少走很多弯路。

从数据结构上看,前缀树理论上是比哈希算法更快,缘由是哈希算法的真正复杂度是O(K),K 是指查询的 Key 的长度,Key 越长哈希算法把字符串变成整数就越复杂,而前缀树是层层递进,最坏的复杂度就是 O(K),所以前缀树的最坏效率与哈希算法是同样的。

固然这只是原理上的,通过专门测试发现 Lua table 的哈希查找速度秒杀前缀树,这是由于在编译 LuaJIT 的时候,它使用了 CPU 指令集来计算哈希值,这样能够完美的作到 O(1),因此 LuaJIT table 的哈希是效率是最高的,其次才是前缀树。

在 LuaJIT 世界匹配效率最高,永远都是先优先使用 Lua table 的哈希匹配。咱们最终也没直接使用前缀树(trietree),由于它比较消耗内存,而是采用了基数树(radixtree),在性能相差很少的状况下,内存占用更小。

OpenResty VS Golang、HTTP VS gRPC

2015 年我没有选择 Golang 而选择 OpenResty,缘由是我认为 OpenResty 能够思考地更深刻,而 Golang 只能站在应用层去解决问题,出于这个缘由我选择了 OpenResty。

APISIX 支持了这样一个场景:HTTP(s) -> APISIX -> gRPC server,把 REST API 转成 gRPC 请求。完成该功能后,须要作些压力测试验证效果。为了方便对比,用 Golang 的方式也写了一个协议转换网关。测试发现 APISIX 的版本比 Golang 的版本性能略还好一点,个人电脑上都是单核 1 万左右的 QPS。本觉得在 gRPC 领域 Golang 的性能应当是最好的,没想到 APISIX 有机会略胜一筹。

咱们以前粗浅地认为 HTTP 的性能必定没有 gRPC 的性能好,如今看有点武断。gRPC 的不少优点是 HTTP 不具有的,好比它的体积更小且内置 schema 检查等。但若是你的请求体比较小,在 HTTP 上使用 json 加 json schema,它们俩的性能几乎相同,尤为是在内网环境下相差仍是很是小的。若是请求体比较大编码复杂,那么 gRPC 会有明显优点。

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 语言中宏定义的味道,对性能和易用是个极好的平衡。

静态代码检测工具

目前 APISIX 进行 CI 回归,都会运行代码检查工具进行检查:Lua -check 和 lj-releng。对当前代码目录的内容作静态检测,好比有没有加全局的变量,代码行的长度是否是超了等。

rapid json 的生命周期

调试过程当中发现的一个特别有意思的关于 rapid json 声明周期的 bug。关于这个周期的缘由能够看一下上图的最后一行,咱们真正使用的是 validator,并且只调用了validator 的一个验证,它是从上边的 create-validator 得来的。这里值得注意的是,为何用一个数组缓存住另外一个叫 sd 的对象呢?

由于 validator 是个 cdata ,内部有对 sd 对象的指针引用依赖,他们两个也就必需要有相同的声明周期,不能有某一项提早释放的状况。若是咱们须要让两个对象有相同的生命周期,那么把它们放到同一个 table 中是最简单的方法。

第三方库使用 pcre

若是你选择效率最高的 C 库,而这个 C 库里还引用了 pcre 这个库,那就须要考虑到一个问题,这个对象的跨请求就会有很是大的风险,为此必需要单独给这个库建立独立的内存池,决不能使用当前请求的内存池,由于当前请求很快就被释放。

怎么解决这个问题呢?若是如今 OpenResty 有相关的 API,那么直接去申请内存池是最好的,可是惋惜 OpenResty 并不具有。看看 Ningx 源码,能够看到建立 Nginx 的内存池函数定义是 ngxcreatepool(size_tsize, void *log) ,只要能获取到全局日志句柄便可。

咱们选择从全局的 ngxcycle 获取 log 对象,这里我定义了一个虚假的 fakengxcycle 结构体,这个结构体和 Nginx 的 ngxcycle 构体的前三项是同样的,可是截掉了后面的部分,而后咱们作内存拷贝,从而获得了 log 对象指针位置。

开启 prometheus 插件

我当时在研究 Kong 的 prometheus 插件时粗略看了一下它的代码,发现他的实现逻辑是有问题的,会很是影响性能。因此没有直接使用 Kong 的方式,在 APISIX 中开启这个 插件,性能只会降低5% 左右。这个插件咱们比 Kong 高近 10 倍性能。我也和 Kong 的技术负责人聊过这里,后续会把 APISIX 的一些作法贡献给 Kong,相互学习一块儿成长。

以上是我今天的所有分享,谢谢你们!

演讲PPT下载及视频观看:

Apache APISIX 微服务网关极致性能架构解析

本文由博客一文多发平台 OpenWrite 发布!

相关文章
相关标签/搜索