你们下午好!首先作下自我介绍,我于 2014 年加入奇虎 360,后与温铭结识,当时他正在基于 OpenResty 作天擎服务端,用于提供 API 服务。2015 年咱们一块儿写了《OpenResty 最佳实践》,缘由是当时咱们团队想扩充,可是身边的同事都不知道如何学习 OpenResty,OpenResty 相关的学习资料也少。咱们完成这本书的写做后,就给身边的同事们使用,而再也不须要每次都经过口传和培训的方式来影响。意外的是咱们在公司内影响的人并很少,反而在公司外却经过这本书汇集了上万人的社区成员。《OpenResty 最佳实践》从无到有,彻底是以开源的方式公开的。2015 年 12 月,老罗在锤子科技产品发布会上宣布将门票收入所有捐赠给开源项目 OpenResty,此次也让更多的人知道了 OpenResty。2017 年 3 月,春哥(章亦春,网名:agentzh)准备创业,我就跟他一块儿做为技术合伙加入了 OpenResty Inc.。html
今天跟你们分享我最近在作的基于 OpenResty 的高性能路由实现。我了解到不少人用 OpenResty 作网关,也有作 Web Server,因为 OpenResty 成立到如今周边的库和基础设施并无很完善,因此咱们一直想经过社区的方式来提供一个你们比较认同的开发框架来作 Web Server。对于 Web 框架里面比较经典的是 MVC 结构,其中包括 Model、View 和 Controller 三层,目前 Model 和 View 层已经有了比较好的实现,而路由一直没有特别强大、高效的解决方案。此次与你们介绍 lua-resty-r3 路由实现 ,和你们分享我在与春哥合做以后的一个感悟,做为一个开发什么事情是值得咱们吹牛?程序员
首先说一说程序员的“牛皮”,之因此说这些是由于其中大部分是我曾经跟别人吹过的牛皮,好比“一天写了几千行代码”、“收入也不错”、“作过超大项目,几亿 PV 小意思”等,这些均可能是程序员跟别人炫耀的话,可是我认为程序员真正的牛皮逃不出两个点:“都在用个人代码”和“运行在更多计算机上”。金山的口号归纳地特别好“但愿咱们写的代码能够跑在每台计算机上”,咱们做为程序员,若是创造的每行代码可让全部人享受到这个代码的好处,这是一件很是荣耀的事情。bash
今天咱们汇集在一块儿讨论 OpenResty ,很大一部分缘由是春哥创造了 OpenResty,而咱们都在使用。春哥创造了咱们天天都在用的东西,这就是他最“牛”的地方。数据结构
若是咱们要写代码让更多的人使用,那么代码就必需要下沉到基础组件,由于基础组件的开发所要求的严谨程度远大于业务应用,基础组件的开发一般须要知足如下的要求:框架
上图是一条比较完整的基础组件开发的流程,这里列举的并不是包含了全部要素,而是我认为如今作基础组件时哪些是必备的。最中心的是书写代码,这个环节每每是重中之重,但若是要维持一个良好的基础组件,实际上从最开始的需求提出到最后发布版本全流程都须要注意。今天分享的议题是根据我对春哥以及 OpenResty 体系的研究,总结出最多见的基础组件的维护的完整流程。函数
首先需求、调研和项目目标,甚至包括最简单的测试用例,这些信息主要是用来肯定项目目标,咱们首先要知道要作什么?技术目标是什么?以及要暴露哪些 API?当暴露了 API 以后,须要讨论最小的使用迷你 case 是什么样子,从而梳理出前期的基本需求,这个环节一般开发能够本身拍板,主要涉及一些文档的工做。工具
下面介绍三种测试模式,大部分人会接触其中的 1-2 种。我认为其中能够适当轻松一下的是 Service Tests,而 Unit Tests(单元测试)是必需要有的,它能保证全部 API 的细节符合咱们的输入输出。End-to-End Test (端到端测试),其实是为了保证业务自己符合咱们一开始的设计,它是直接面对用户的,手机 App 点击菜单,输出各类各样的效果,都是有自动化的工具来实现的。单元测试提供的是开发内部,而端到端测试是对于外部完整的联动起来,这两者是必需要有的。性能
OpenResty 体系内使用了在其余领域不多见的测试框架 Test::Nginx,OpenResty单元测试
里面用了大量的 Lua ,而 Lua 的测试用例几乎都是使用 busted 来书写的,这两者之间有很大的区别。学习
OpenResty 能力很强的人,不必定能写 Test::Nginx 的测试用例,由于它的语言是 Perl,不少人不熟悉。此外 Test::Nginx 是一个通用的测试框架,并不只仅只服务于 OpenResty,它能够扩充延伸,甚至有其余不少不一样测试的用途。可是右边的 busted,明显是只能用于 Lua。
为何 OpenResty 要选 Test::Nginx 这个测试框架呢?缘由是由于使用场景,OpenResty 的测试场景既要可以测 C 模块,也要有能力测 Lua 模块,甚至有时候还要有能力测试一个服务,咱们不只须要测试进行内部,还须要测试进程的外部输出,好比 HTTP 请求查看结果。OpenResty 有运行阶段的概念,一样一串 Lua 代码在不一样的阶段行为是不同的。好比在 init 和 content 阶段,所可以使用的 API 彻底不同,可是这种模式在 Lua 层面是彻底作不到的。Test::Nginx 的功能点覆盖比较强,因为它是能够跨阶段,能够把 OpenResty 里面全部特殊的状况排列组合,达到测试目的。
Test::Nginx 有这么多优势,天然也会存在一些问题。首先是抽象的层次比较高,这就致使只看测试用例看不出它是用什么语言支撑的,由于它都是抽象的配置项,好比要测试访问码,它是由单独的配置项来实现的,和语言无关,这就须要专门看文档学习;第二个缺点是学习的难度比较高,尤为是须要作自定义修改时,好比扩展选项,这都是须要作二次开发或集成的。此外,Test::Nginx 是没有代码覆盖率的,由于代码覆盖率必需要在源码内部才有,因此 busted 是有代码覆盖率的。
选择 Test::Nginx 的测试框架,还有一个很重要的缘由是它是一个通用的测试框架,这意味着能够用这个测试框架测试现有的大部分的测试平台软件,好比 Java、Go 等。
如下是 Test::Nginx 测试框架的特色:
目前 Test::Nginx 测试框架已经很好地集成了 valgrind 和 ASAN 这两个内存整合工具,能够相互配合,他们都是用来作内存检测的,检查内存是否被正确释放、使用等状况。
我最近两年在写服务的时候都是用的 Test::Nginx 测试框架,也给你们推荐一下,虽然有它的不足,可是带来的好处也不少,最大的好处就是不须要在不一样的测试体系下来回地切换思惟。
接下来介绍一个技术细节,前面提到作基础组件的开发会分几步走,在书写完测试用例后会进行代码书写,我此次的路由书写代码用的框架是基于 r3 ,它是一个开源的项目。它能够把路由规则编译成一个前缀树,从而使匹配效率更高,能够直接用 Lua 调用 libr3.so 的库,这种代码结构会很是简单。可是缺点是若是经过 FFI 的方式来直接调用动态库,须要知道动态库调入时传入的参数的全部结构,若是它的入参只是一些字符串、数值,这样会很简单,直接包就能够。可是 libr3.so 的库比较复杂,它有很是强的内存结构,它的不少输入参数都是有结构体的,并且它也用了不少宏定义来实现数据结构,而后用这样的存储结构来作传参。当咱们用 FFI 的方式来描述全部参数的结构体时,须要在 FFI 的文件描述里完整地写出所用到的全部的头、导出的函数,以及依赖的结构体。
网上能够找到 Lua-resty-r3 的另外一个开源实现,关于 C 头文件描述用了 170 行代码,可是那个版本和 r3 最近的变化是冲突的,因而我尝试修改了项目的代码,把现有的结构体的声明、函数导出的声明都改一遍,修改到一半就遇到了问题,由于 r3 的结构体的实现一层套一层,并且里面还有各类宏的替换,致使人工来改的成本很高。
因而我对本来的 libr3 作一层封装,把他内部全部调用的结构体的传参所有藏起来,简单地说就是把全部是结构体的地方都换成了一个指针,若是里面的调用函数能够合并,就能够对外导出一个标准的函数。如上图右侧 ”Two steps“,Lua 调用咱们封装的 libr3.so,libr3.so 底层调用的是本来的实现 libr3.a ,中间套了一层以后就看不到原来 r3 的结构体。
上图是一段示例代码,能够看出大多数的封装只是把类型转了一下,并无特别复杂的封装,只是把结构体都变成 void* 。这样作的缺点是,对于结构体,C/C++ 编译器编译阶段很容易找到参数类型传错的问题,而当咱们换成 void* 的传参,因为 void* 能够被任意传参, 编译器只能帮咱们检测错误的可能,这个问题是开发者须要注意的。
可是优点也比较明显,FFI 只须要导出图中的函数:
void*
r3_create(int cap)
void
r3_free(void*tree)
复制代码
它们导出是不依赖任何的结构体,会让库写起来很是方便,因此会让 170 行代码变成 20 行。
前面已经有了测试框架,咱们须要有一种方式可以作对当前全部的业务请求作一个完整的测试用例的回归。
你们若是在用 Github 应该都会了解 Travis ,Travis 是对开源项目最友好的通用测试平台。服务一旦开启了 Travis ,每一次提交它均可以在平台上作自动的回归测试,固然咱们须要书写一个 .yml 文件,告诉它你要干什么事情、须要什么环境、怎么编译、怎么检测等,它会给你一个结果的反馈,利用这个结果,就能够跟进软件持续的开发。
上图是开启 Travis CI 的方法,登录本身的用户,点击 Settings,在用户的分组里面,找到一个具体的项目,点击勾选就开启了。
曾经不少人问过我,春哥的牛皮究竟是什么?其实这个问题,我在不一样的阶段也有不一样的回答。现阶段我认为春哥的测试体系很是厉害。OpenResty 这个软件若是有哪一个人能把春哥全部的东西以及这些子项目之间的关系所有搞清楚,我以为已经很厉害了,而春哥却以一己之力把这些东西玩的很转,其中很大一部分缘由是他把测试体系看的很重要,他在测试体系上的积累可以让他把 OpenResty 这个项目可持续地往前推动,因此你们之后要对测试体系要额外地重视,尤为是作一个比较复杂的组件。
下面详细介绍一下 C / C++ 的测试工具,我用这种方式发现了 r3 的两个 bug。其中两个测试都是与内存泄露相关,使用 ASAN mode 和 valgrind mode ,ASAN 是使用 clang 加编译参数完成;valgrind 运行以前须要经过 valgrind 命令行的方式,它会模拟 CPU 完成内存的管理,帮咱们检查是否有内存泄露等状况。
wrk 和火焰图是辅助工具,也是辅助咱们发现问题。wrk 是一个压测的工具,它和 OpenResty 存在的方式几乎是如出一辙的,都是经过 C + Lua 实现,不过这里的 Lua 和 OpenResty 里面的 Lua 是两回事,毕竟 Lua 是一门寄宿语言,是由它的宿主决定它具备什么扩展性。火焰图主要能够肯定性能瓶颈,好比 CPU 占时、内存持续泄露等问题。若是是性能问题,能够根据火焰图横坐标的长度,肯定问题大概在什么位置,是哪段代码占用过多 CPU 时间,若是时间消耗不是符合预期,就能够着手修他了。
在个人开发的习惯中,除了使用 ASAN 和 valgrind 来检查内存问题,到最后必定会跑性能,跑完性能用后面的辅助工具来检验,观察性能的指标是否符合预期。指标是一部分,还有是观察火焰图中看它表现出来的行为和预期的是否同样,好比如今作的库是 r3 路由,咱们指望它全部的 CPU 消耗都在路由的预算上。两个点能够关注:第一,是否是把大部分的时间都确实花在路由预算上,第二,路由预算的方法过程自己,是否是还有可优化的空间,这两个问题均可以在火焰图中找到很是好的答案。
最后须要建立里程碑,完成了一个阶段若是没有里程碑,就没有办法跟领导申请立项,也拿不到项目的预算资金,因此里程碑很是重要,它能够关注每一个阶段的东西。除此以外,当咱们作一个开源项目的时候,它的做用就更加明显,它表明的是项目对外的稳定版本。
上图是我对 lua-resty-r3 项目打的一个 tag ,这个版本是一个相对比较重要的稳定性阶段,这个项目相关的全部东西我都会放到 Issues 里面,目前这仍是一个私有项目,不过过不了多久就会开源给你们。
今天分享的内容主要侧重在开发流程上,纯粹的技术细节不是不少,谢谢你们!
演讲视频及PPT:
lua-resty-r3 高性能 OpenResty 路由实现www.upyun.com