apisix实际应⽤_OpenResty社区王院⽣:APISIX的⾼性能实
2019 年 7 ⽉ 6 ⽇,OpenResty 社区联合⼜拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,OpenResty 软件基⾦会联合创始⼈王院⽣在活动上做了《APISIX 的⾼性能实践》的分享。
OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、⼜拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使⽤者的交流与学习,推动 OpenResty 开源项⽬的发展。活动将陆续在深圳、北京、武汉、上海、成都、⼴州、杭州等城市巡回举办。
王院⽣,OpenResty 社区、OpenResty 软件基⾦会联合创始⼈,《OpenResty 最佳实践》主要作者,APISIX 项⽬发起⼈和主要作者。以下是分享全⽂:
⼤家好,我是王院⽣,很⾼兴来到上海。⾸先做下⾃我介绍,我于 2014 年加⼊奇虎 360,在那时认识了 OpenResty,此前我是⼀个纯粹的 C/C++ 语⾔开发者。在 360 ⼯作期间,利⽤⼯作闲暇时间写了《OpenResty 最佳实践》,希望能影响更多的⼈正确掌握OpenResty ⼊门。2017 年我作为技术合伙⼈和春哥(章亦春,agentzh)⼀起创业。今年我个⼈的重⼼有所调整并在今年三⽉份离职,准备将更多精⼒
投⼊到开源上,于是发起了 APISIX 这个项⽬,企业宗旨是依托开源社区,致⼒于微服务 API 相关技术的创新和实现。
什么是 API ⽹关
API ⽹关的地位越来越重要,它⼏乎劫持了所有流量,内外之间完成了⽤户的安全控制、审计,通过⾃定义插件的⽅式满⾜企业⾃⾝特定需求,最常见的⾃由⾝份认证等。随着服务在数量和复杂度上的不断增长,更多的企业采⽤了微服务的⽅式,这时通过 API ⽹关来完成统⼀的流量管理和调度就⾮常有必要。
微服务⽹关和传统意义上的 API ⽹关有⼀些不同,主要包括下⾯⼏点:
动态更新:在微服务之前,服务不像现在这样经常来回地变化。⽐如微服务需要做横向扩充,或者故障恢复、热备、切换等,IP 、节点等变动更加频繁。举例如微博上⼀旦出现了爆点事件,就急速扩充计算点,必须要⾮常快地扩充新机器来扛压。波峰波⾕变化明显,分钟级别的机器动态管理,已经越发是常态。
更低延迟:通常动态就意味着可能会做⼀些延迟(复杂度增加),在微服务⾥⾯,对于延迟要求⽐较⾼,尤其对于现在的⽤户体验,超过 1 秒以上的延迟是完全不可接受的。
⽤户⾃定义插件:API ⽹关是给企业⽤户使⽤的,它⼀定存在私有逻辑(⽐如特殊的认证授权等),所以微服务⽹关必须能够⽀持企业⽤户⾃定义插件。
更集中的管理 API:如前⾯所说 API ⽹关劫持了⽤户的所有流量,所以⽤⽹关来做统⼀的 API 管理是⾮常必要的。在⽹关⾓度可以看到API 是如何设计,是否存在延迟、安全问题,以及响应速度和健康信息等。
我们要做的微服务 API ⽹关产品,除了上⾯的基本要求,还有⼀些是我们区别于其他⼈的:
通过社区聚焦:通过开源⽅式聚焦有共同需求的⼈,让更多不同公司的⼈可以⼀起协作,共同打磨更好的产品,减少冗余开发。
简洁的 core:产品的内核必须是⾮常简洁的,如果内核复杂,会使得⼤家的上⼿成本⾼很多,望⽽却步肯定不是我们期望的。
可扩展性、顶级性能、低延迟:这⼏项都是要同时严格保障的,也是我们会花主要精⼒保证的。⽬前 APISIX 项⽬的性能⽐空跑OpenResty 只低 15%,这点还是⾮常值得傲娇的。
APISIX ⾼性能微服务⽹关
APISIX 架构与功能
上图是 APISIX 的基本架构,罗列⽤到的⼏个基本组件。其中包括 ETCD 可以完成配置存储,由于 ETCD 可以⾛集,所以我们可以借⽤它完成动态伸缩、⾼可⽤集等。ETCD 数据⽀持通过 watch 的⽅式增量获取,使得 APISIX 节点规则更新可以做到毫秒级,甚⾄更低。APISIX ⾃⾝是⽆服务状态的,所以⽅便横向扩充。
另⼀个组件是 JSON Schema,它是⼀个标准协议,主要⽤来验证数据的有效性。JSON Schema ⽬前对外公开有四个不同版本,我们最终选⽤ RapidJSON,因为他对这四个版本都有相对完整的⽀持。
图中的 Admin API 和 APISIX 可以放在⼀起,也可以分开。Admin API 接收⽤户提交的请求,在请求参数保存到 ETCD 之前,会使⽤JSON Schema 做⼀次完整校验,有了校验可以确定到 ETCD ⾥的都是有效数据。
APISIX 的 v0.5 版本具备以下功能:
APISIX 的性能
通常来说,引⼊了前⾯提到的⼗⼏项功能,会伴随着性能的下降,那么究竟下降了多少呢?这⾥我做了⼀个性能的测试对⽐。如上图,右侧是我为了测试写的⼀个虚假的服务,这个服务⾥⾯空空如也,只是把 ngx_lua ⾥的⼀些变量拿出来,然后传给了什么都不做的
fake_fetch,后⾯的 http filter、log 阶段等⼀样,没有任何计算量。
然后对 APISIX 和右边的虚假服务分别跑压⼒测定,对⽐结果发现 APISIX 的性能仅仅下降了 15%,也就是说在接受了 15% 的性能下降的同时,就可以享受前⾯提到的所有功能。
说⼀下具体数值,这⾥使⽤的是阿⾥云的计算平台,单 worker 下可以跑到 23-24k QPS,4 worker 可以跑到 68k 的 QPS。
APISIX ⽬前的状态
⽬前最新版本是 v0.5,架构是基于 ETCD+libr3+RapidJSON。这个版本加的最多的是代码覆盖率,v0.4 版本代码覆盖率不超过 5%,但最新版本中代码覆盖率达到 70%,这其中 95% 是核⼼代码,周边的代码覆盖率相对较低,主要是插件的相关测试有所⽋缺。
OpenResty 编程哲学与优化技巧
我从 2014 年开始做 OpenResty 开发,⾄今已经有六年了。在 OpenResty 的领域⾥,它的哲学是要学会⼤事化⼩,⼩事化了,因为Nginx 的内存管理⽅式是把所有的请求内存默认放到⼀个内存池⾥,请求退出的时再把内存池销毁。如果不能很快地⼀进⼀出,它就会不停申请,最后释放时资源损耗很⼤,这是 Nginx 不擅长的。所以⽤ OpenResty 做长连接就需要⾮常⼩⼼,避免把内存池搞⼤。
此外,要尽可能少地创建临时对象。这⾥所指的临时对象有两类,⼀类是 table 类,⼀类是字符串拼接,⽐如某两个变量拼接产⽣新的字符串,这个看似在其他很多语⾔都没有问题,但在 OpenResty ⾥需要尽量少做这种操作。Lua 语⾔虽然简单,但也是门⾼级语⾔,携带了优良的 GC ,让我们⽆需关⼼所有变量的⽣命周期,只负责申请就好了,但如果滥⽤临时变量等,会让 GC ⽐较忙碌,付出代价是整体运⾏效能不⾼。Lua 擅长动态和流程控制,如果遇到硬核的 CPU 运算任务,还是推荐交给 C/C++ 实现。
今天和⼤家分享优化技巧,主要还是如何写好 Lua,毕竟他的受众体更多。在 APISIX 的 core 中,我们使⽤了⼀些⽐较特别的优化技巧,下⾯逐⼀给⼤家介绍。nginx和网关怎么配合使用
技巧⼀:delay_json
先说⼀下场景:⽐如上⾯的这⾏⽇志调⽤,如果当前⽇志级别是 info ,我们期望会正常 json encode;⽽当是 error 级别,我们就不期望发⽣ json encode 操作,如果能⾃动跳过是最完美了。那我们如何近似的实现这个⽬的呢?
我们看⼀下 delay_encode 的实现源码,⾸先⽤元⽅法重载了 tostring ,下⾯ delay_encode 只是对 delay_tab 的两个对象 data 和force 做了赋值,然后没有做其他的事情,这与⼤家平时看到的 json encode ⽅法都不⼀样。因为真正在写⽇志时,如果给定的参数是table,在 OpenResty ⾥会把他转成
string 的,过程是检查是否有 tostring 的元⽅法注册,如果有就调这个⽅法把它转换成字符串。有了上⾯的封装,我们就在⾼性能和易⽤性上做了很好的平衡。
技巧⼆:HASH vs 前缀树 vs 遍历
Lua table 的 HASH:性能最好的匹配⽅式,缺点是只能做全量匹配。
前缀树:借助 libr3 完成前缀等⾼级匹配(⽀持正则)。
遍历:永远是最糟糕的。
在 APISIX 的世界⾥,我把 HASH 和前缀树做了融合,如果你的请求和路由规则不包含⾼级规则匹配,会默认⾛ HASH 来保证效率;但如果有模糊匹配逻辑,则使⽤前缀树。
技巧三:ngx.log 是 NYI
因为 ngx.log 是 NYI,所以我们要尽量减少下⾯这段代码的触发频率:
return ngx_log(log_level,…)
要降到最低,需要判断当前⽇志级别,如果当前的⽇志级别和你输⼊的⽇志级别存在⼤⼩⽐值关系,
发现不需要输⼊就直接 return。避免出现⽇志处理完,传到 Nginx 内核后再发现不需要写⽇志,这样就会浪费⾮常多的资源。
前⾯提到的压⼒测试,都是把⽇志打到 error 级别,加了⾮常多的调试代码并且保留不删,这些测试代码的存在完全不会影响性能结果。
技巧四:gc for cdata and table
场景:当某个 table 对象被系统回收时,希望触发特定逻辑以释放关联资源。那么我们如何给 table 注册 gc 呢?请参考下图⽰例:
当我们⽆法控制 Lua table 的整个⽣命周期,可以⽤上图的⽅法去注册⼀个 GC,当 table 对象没有任何引⽤时会触发 GC,释放关联资源。
技巧五:如何保护常驻内存的 cdata 对象
我们在使⽤ r3 这个 C 库时遇到这么⼀个问题:我们给 r3 添加很多路由规则,然后⽣成 r3 tree,如果规则没有变化 r3 将被反复使⽤,由于 r3 内部没有申请额外的内存存储,只是引⽤指针地址。但外⾯传⼊的 Lua 变量可能是临时变量,引⽤计数为 0 后会被 Lua GC ⾃动回收。导致的现象是 r3 内部引⽤的原有内存地址内容突然发⽣变化,最后致使路由匹配失败。
知道了问题原因,解决⽅法就⽐较简单了,只需要避免变量 A 提前释放,让 Lua ⾥⾯变量 A 的⽣命周期和 r3 对象的⽣命周期保持⼀致即可。
技巧六:ngx.var.* 是⽐较慢的
⼤家知道 C 是不⽀持动态的,它是编译性语⾔。ngx.var.* 的内部实现可以查看 Nginx 源代码,或者通过⽕焰图的⽅式可以看到他内部的实现⽅式。为了完成动态获取变量,内部必须通过⼀次 hash 查,到后⽤内部的规则把变量值读出。
技巧七:减少每请求的垃圾对象
我们要尽可能降低每请求产⽣的垃圾对象的数量,作为 OpenResty 开发者,如果把这句话理解透彻,基本上可以进阶到前 50% 的⾏列。
减少不必要的字符串的拼接,并⾮意味着在需要做拼接字符串的时候不要拼接,⽽是需要在脑⼦⾥⼀直有这个意识,把⽆效的拼接降低下来,当这些⼩细节累积下来,性能提升就会⾮常⼤。
技巧⼋:重⽤ table
⾸先介绍下初级版的 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
在 APISIX 中最集中使⽤的是两个地⽅,除了上图这⾥做回收,还有是申请的地⽅。在回收之后,这些 table 可以被其他请求所复⽤,由tablepool 做统⼀控制,在 pool ⾥维持的对象可能就固定的⼏⼗、⼏百个,会反复使⽤,不存在销毁的情况。这个技巧的正确使⽤,性能⾄少可以提升 20%,提升效果⾮常明显。
技巧九:Irucache 的正确姿势
简单介绍下 Irucache,Irucache 可以完成在 worker 内的数据的缓存和复⽤,Irucache 有⼀个⾮常⼤的优势是可以存储任何对象。⽽共享内存则是完成不同 worker 之间的数据共享,但它只能存储简单对象,有些东西是不能跨 worker 共享,⽐如 function、cdata 对象等。
对 Irucache 进⾏⼆次封装,封装的内容主要包括:
key 要尽量短、简单:我们在写 key 时最重要的是要简单,key 最糟糕的设计是⾥⾯东西很长,但是有⽤信息不多。key 理论上⼤家都喜欢⽤字符串,但他可以是 table 等对象,key 尽量做到明确,只包含你感兴趣的内容,能省略的尽量省略,降低拼接成本。
version 可降低垃圾缓存:这点算是我在做 APISIX 的突破:提取出了 version, Irucache+ version 这套组合,可以极⼤地降低垃圾缓存。
重⽤ stale 状态的缓存数据。
上图是 lrucache 的封装,从下往上看,key 是 /routes,它跟的版本号是 conf_version,global 函数⾥做的事情是根据 key+version 的⽅式,去查有⽆陈旧数据的缓存,如果有就直接返回,如果没有就调 creat_r3_router 完成创建,creat_r3_router 是负责创建⼀个新的对象,它只接受⼀个传参 routes,这个传参是由 routes.values 传进去的。
这层封装,把 Irucache new、数量等都隐藏起来,这样很多东西我们看不到,当我们需要⾃定义的时候可能还是需要关⼼这些。APISIX 为了简化插件开发者对各种东西的理解,所以必须要做⼀层封装,简化使⽤。
△ lrucache 最佳实践⽰例
△ 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下载传送门:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。