2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,OpenResty 软件基金会联合创始人王院生在活动上作了《APISIX 的高性能实践》的分享。
OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使用者的交流与学习,推进 OpenResty 开源项目的发展。活动将陆续在深圳、北京、武汉、上海、成都、广州、杭州等城市巡回举办。html
王院生,OpenResty 社区、OpenResty 软件基金会联合创始人,《OpenResty 最佳实践》主要做者,APISIX 项目发起人和主要做者。前端
如下是分享全文:nginx
你们好,我是王院生,很高兴来到上海。首先作下自我介绍,我于 2014 年加入奇虎 360,在那时认识了 OpenResty,此前我是一个纯粹的 C/C++ 语言开发者。在 360 工做期间,利用工做闲暇时间写了《OpenResty 最佳实践》,但愿能影响更多的人正确掌握 OpenResty 入门。2017 年我做为技术合伙人和春哥(章亦春,agentzh)一块儿创业。今年我我的的重心有所调整并在今年三月份离职,准备将更多精力投入到开源上,因而发起了 APISIX 这个项目,企业宗旨是依托开源社区,致力于微服务 API 相关技术的创新和实现。git
API 网关的地位愈来愈重要,它几乎劫持了全部流量,内外之间完成了用户的安全控制、审计,经过自定义插件的方式知足企业自身特定需求,最多见的自由身份认证等。随着服务在数量和复杂度上的不断增加,更多的企业采用了微服务的方式,这时经过 API 网关来完成统一的流量管理和调度就很是有必要。github
微服务网关和传统意义上的 API 网关有一些不一样,主要包括下面几点:编程
咱们要作的微服务 API 网关产品,除了上面的基本要求,还有一些是咱们区别于其余人的:json
上图是 APISIX 的基本架构,罗列用到的几个基本组件。其中包括 ETCD 能够完成配置存储,因为 ETCD 能够走集群,因此咱们能够借用它完成动态伸缩、高可用集群等。ETCD 数据支持经过 watch 的方式增量获取,使得 APISIX 节点规则更新能够作到毫秒级,甚至更低。APISIX 自身是无服务状态的,因此方便横向扩充。api
另外一个组件是 JSON Schema,它是一个标准协议,主要用来验证数据的有效性。JSON Schema 目前对外公开有四个不一样版本,咱们最终选用 RapidJSON,由于他对这四个版本都有相对完整的支持。缓存
图中的 Admin API 和 APISIX 能够放在一块儿,也能够分开。Admin API 接收用户提交的请求,在请求参数保存到 ETCD 以前,会使用 JSON Schema 作一次完整校验,有了校验能够肯定到 ETCD 里的都是有效数据。安全
上图右侧是接收外部用户的真实流量,APISIX 从 ETCD 中订阅全部配置规则,拿到配置规则后给到下面的路由引擎(libr3),目前默认使用的路由引擎是 libr3,我以前在武汉的分享中进行过详细介绍(https://www.upyun.com/opentalk/428.html)。 libr3 是一个路由引擎实现,基于前缀树,因为他还支持正则,因此效率很是高的,同时功能也很强大。
APISIX 的 v0.5 版本具有如下功能:
一般来讲,引入了前面提到的十几项功能,会伴随着性能的降低,那么究竟降低了多少呢?这里我作了一个性能的测试对比。如上图,右侧是我为了测试写的一个虚假的服务,这个服务里面空空如也,只是把 ngx_lua 里的一些变量拿出来,而后传给了什么都不作的 fake_fetch,后面的 http filter、log 阶段等同样,没有任何计算量。
而后对 APISIX 和右边的虚假服务分别跑压力测定,对比结果发现 APISIX 的性能仅仅降低了 15%,也就是说在接受了 15% 的性能降低的同时,就能够享受前面提到的全部功能。
说一下具体数值,这里使用的是阿里云的计算平台,单 worker 下能够跑到 23-24k QPS,4 worker 能够跑到 68k 的 QPS。
目前最新版本是 v0.5,架构是基于 ETCD+libr3+RapidJSON。这个版本加的最多的是代码覆盖率,v0.4 版本代码覆盖率不超过 5%,但最新版本中代码覆盖率达到 70%,这其中 95% 是核心代码,周边的代码覆盖率相对较低,主要是插件的相关测试有所欠缺。
本来计划在 0.5 版本上线管理界面功能,这样能够下降入门门槛,可是遗憾的是目前还没开发完成,这与咱们自身专业有关系,不擅长作前端界面,须要借助前端的专家帮咱们实现,咱们计划会在 0.6 的版本上线(注:目前已经发布了 v0.6 版本:https://github.com/iresty/apisix/blob/master/CHANGELOG_CN.md#060)。
我从 2014 年开始作 OpenResty 开发,至今已经有六年了。在 OpenResty 的领域里,它的哲学是要学会大事化小,小事化了,由于 Nginx 的内存管理方式是把全部的请求内存默认放到一个内存池里,请求退出的时再把内存池销毁。若是不能很快地一进一出,它就会不停申请,最后释放时资源损耗很大,这是 Nginx 不擅长的。因此用 OpenResty 作长链接就须要很是当心,避免把内存池搞大。
此外,要尽量少地建立临时对象。这里所指的临时对象有两类,一类是 table 类,一类是字符串拼接,好比某两个变量拼接产生新的字符串,这个看似在其余不少语言都没有问题,但在 OpenResty 里须要尽可能少作这种操做。Lua 语言虽然简单,但也是门高级语言,携带了优良的 GC ,让咱们无需关心全部变量的生命周期,只负责申请就行了,但若是滥用临时变量等,会让 GC 比较忙碌,付出代价是总体运行效能不高。Lua 擅长动态和流程控制,若是遇到硬核的 CPU 运算任务,仍是推荐交给 C/C++ 实现。
今天和你们分享优化技巧,主要仍是如何写好 Lua,毕竟他的受众群体更多。在 APISIX 的 core 中,咱们使用了一些比较特别的优化技巧,下面逐一给你们介绍。
先说一下场景:好比上面的这行日志调用,若是当前日志级别是 info ,咱们指望会正常 json encode;而当是 error 级别,咱们就不指望发生 json encode 操做,若是能自动跳过是最完美了。那咱们如何近似的实现这个目的呢?
咱们看一下 delay_encode 的实现源码,首先用元方法重载了 tostring ,下面 delay_encode 只是对 delay_tab 的两个对象 data 和 force 作了赋值,而后没有作其余的事情,这与你们平时看到的 json encode 方法都不同。由于真正在写日志时,若是给定的参数是 table,在 OpenResty 里会把他转成 string 的,过程是检查是否有 tostring 的元方法注册,若是有就调这个方法把它转换成字符串。有了上面的封装,咱们就在高性能和易用性上作了很好的平衡。
在 APISIX 的世界里,我把 HASH 和前缀树作了融合,若是你的请求和路由规则不包含高级规则匹配,会默认走 HASH 来保证效率;但若是有模糊匹配逻辑,则使用前缀树。
由于 ngx.log 是 NYI,因此咱们要尽可能减小下面这段代码的触发频率:
return ngx_log(log_level,…)
要降到最低,须要判断当前日志级别,若是当前的日志级别和你输入的日志级别存在大小比值关系,发现不须要输入就直接 return。避免出现日志处理完,传到 Nginx 内核后再发现不须要写日志,这样就会浪费很是多的资源。
前面提到的压力测试,都是把日志打到 error 级别,加了很是多的调试代码而且保留不删,这些测试代码的存在彻底不会影响性能结果。
场景:当某个 table 对象被系统回收时,但愿触发特定逻辑以释放关联资源。那么咱们如何给 table 注册 gc 呢?请参考下图示例:
当咱们没法控制 Lua table 的整个生命周期,能够用上图的方法去注册一个 GC,当 table 对象没有任何引用时会触发 GC,释放关联资源。
咱们在使用 r3 这个 C 库时遇到这么一个问题:咱们给 r3 添加不少路由规则,而后生成 r3 tree,若是规则没有变化 r3 将被反复使用,因为 r3 内部没有申请额外的内存存储,只是引用指针地址。但外面传入的 Lua 变量多是临时变量,引用计数为 0 后会被 Lua GC 自动回收。致使的现象是 r3 内部引用的原有内存地址内容忽然发生变化,最后导致路由匹配失败。
知道了问题缘由,解决方法就比较简单了,只须要避免变量 A 提早释放,让 Lua 里面变量 A 的生命周期和 r3 对象的生命周期保持一致便可。
你们知道 C 是不支持动态的,它是编译性语言。ngx.var.* 的内部实现能够查看 Nginx 源代码,或者经过火焰图的方式能够看到他内部的实现方式。为了完成动态获取变量,内部必须经过一次 hash 查找,到后用内部的规则把变量值读出。
解决方案是用上图这个库(http://github.com/iresty/lua-var-nginx-module),很是简单没有技术含量的办法。好比要获取客户端的 IP,在 C 里面直接把代码摘出来,而后经过 Lua FFI 方式读取变量的值,就是这么一段小代码可让 APISIX 性能有 5% 提高。这么作缺点是必需要对 OpenResty 编译时添加这个第三方模块,上手成本略高。
咱们要尽量下降每请求产生的垃圾对象的数量,做为 OpenResty 开发者,若是把这句话理解透彻,基本上能够进阶到前 50% 的行列。
减小没必要要的字符串的拼接,并不是意味着在须要作拼接字符串的时候不要拼接,而是须要在脑子里一直有这个意识,把无效的拼接下降下来,当这些小细节累积下来,性能提高就会很是大。
首先介绍下初级版的 table.clear。当须要使用一个临时 table,你们习惯性的写法是
local t ={}
咱们来聊聊这么作的缺点,若是在开头建立了一个临时的 table t,当函数退出的时候,t 会被回收;下次再进来这个函数,又会产生一个临时的 table t。在 Lua 世界,table 的产生和销毁是很是耗资源的,由于 table 是一个复杂对象,它不像 number、字符串等简单对象,申请和释放能够用一个结构体搞定,它会让你的 GC 一会儿变得很是忙碌。
若是 worker 里只须要一个惟一实例 table 对象,那么就可使用 table.clear 方式来反复使用这个临时表,好比上图的临时表 local_plugins_hash。
重用 table :进阶版 table.pool
有些 Lua table 的生命周期是每请求的,一般是请求进入申请对象,请求退出释放对象,这时候使用 table.pool 会很是合适。tablepool 中文翻译过来是表池,里面放的是能够重用的 table。官方文档能够到 https://github.com/openresty/lua-tablepool#synopsis 查看,结合 APISIX 的业务使用代码,更容易理解。
在 APISIX 中最集中使用的是两个地方,除了上图这里作回收,还有是申请的地方。在回收以后,这些 table 能够被其余请求所复用,由 tablepool 作统一控制,在 pool 里维持的对象可能就固定的几10、几百个,会反复使用,不存在销毁的状况。这个技巧的正确使用,性能至少能够提高 20%,提高效果很是明显。
简单介绍下 Irucache,Irucache 能够完成在 worker 内的数据的缓存和复用,Irucache 有一个很是大的优点是能够存储任何对象。而共享内存则是完成不一样 worker 之间的数据共享,但它只能存储简单对象,有些东西是不能跨 worker 共享,好比 function、cdata 对象等。
对 Irucache 进行二次封装,封装的内容主要包括:
上图是 lrucache 的封装,从下往上看,key 是 /routes,它跟的版本号是 conf_version,global 函数里作的事情是根据 key+version 的方式,去查找有无陈旧数据的缓存,若是有就直接返回,若是没有就调 creat_r3_router 完成建立,creat_r3_router 是负责建立一个新的对象,它只接受一个传参 routes,这个传参是由 routes.values 传进去的。
这层封装,把 Irucache new、数量等都隐藏起来,这样不少东西咱们看不到,当咱们须要自定义的时候可能仍是须要关心这些。APISIX 为了简化插件开发者对各类东西的理解,因此必需要作一层封装,简化使用。
△ lrucache 最佳实践⽤例
上图是用 version 下降垃圾缓存、重用 stale 状态的缓存数据,这 Irucache 的二次封装的代码。首先来看第二行,根据 key 去缓存里面取对象,而后把对象的 cache_ver 拿出来和当前传入的 version 作比较,若是相同则断定这个缓存对象必定是可用的。
往下多了 stale_obj,stale_obj 在文档里面说明的比较少,它只有在一种状况会发生:缓存对象在 Irucache 中已经被淘汰了,可是它只是到了淘汰的边缘,尚未彻底被扔掉。上图中经过陈旧数据的 cache_ver 与进来的 version 作比较,若是 version 一致那就是有效的。因此只要源头的数据没有变化,就能够再次使用。这样咱们就能够复用 stale_obj 从而避免再次建立新的对象。
到这里能够解释一下前面提到的:version 可下降垃圾缓存。若是没有 version,咱们须要把 version 写到 key 里面,每次 version 变化都会产生一个新的 key,那些被淘汰的旧数据会一直存在,没办法剔除掉。同时意味着 Irucache 里面的对象数会不停增长。而咱们前面的方式是保证 key 若是是一个对象,只会有一个 table 与它对应,不会根据不一样的 version 产生不一样的对象缓存,进而下降缓存总数。
以上是我今天的所有分享,谢谢你们!
演讲视频及PPT下载传送门: