lua-resty-r3 高性能 OpenResty 路由實現

你們下午好!首先作下自我介紹,我於 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 路由實現 ,和你們分享我在與春哥合做以後的一個感悟,做爲一個開發什麼事情是值得咱們吹牛?程序員

image

首先說一說程序員的「牛皮」,之因此說這些是由於其中大部分是我曾經跟別人吹過的牛皮,好比「一天寫了幾千行代碼」、「收入也不錯」、「作過超大項目,幾億 PV 小意思」等,這些均可能是程序員跟別人炫耀的話,可是我認爲程序員真正的牛皮逃不出兩個點:「都在用個人代碼」和「運行在更多計算機上」。金山的口號歸納地特別好「但願咱們寫的代碼能夠跑在每臺計算機上」,咱們做爲程序員,若是創造的每行代碼可讓全部人享受到這個代碼的好處,這是一件很是榮耀的事情。bash

今天咱們彙集在一塊兒討論 OpenResty ,很大一部分緣由是春哥創造了 OpenResty,而咱們都在使用。春哥創造了咱們天天都在用的東西,這就是他最「牛」的地方。數據結構

基礎組件開發特色

若是咱們要寫代碼讓更多的人使用,那麼代碼就必需要下沉到基礎組件,由於基礎組件的開發所要求的嚴謹程度遠大於業務應用,基礎組件的開發一般須要知足如下的要求:框架

image

  • 小而美,任何人都不但願基礎組件是一個很龐大的東西;
  • 需求變化小;
  • 穩定性要求高;
  • 小需求,可能大改動;
  • 可以處理異常分支。這裏的處理要大於正常的業務邏輯,須要把全部的異常都包括在內;
  • 技術難度大,甚至維護的難度也大。優秀的程序員可以把一個難事作的很簡單;
  • 關係咬合比較緊密,偶爾重構。因爲基礎組件位於業務的底層,因此它須要支持的場景本身是不知道的;
  • 基礎組件在迭代的時候,針對不兼容的狀況,老鳥一般是改,新鳥喜歡新增 API。

image

上圖是一條比較完整的基礎組件開發的流程,這裏列舉的並不是包含了全部要素,而是我認爲如今作基礎組件時哪些是必備的。最中心的是書寫代碼,這個環節每每是重中之重,但若是要維持一個良好的基礎組件,實際上從最開始的需求提出到最後發佈版本全流程都須要注意。今天分享的議題是根據我對春哥以及 OpenResty 體系的研究,總結出最多見的基礎組件的維護的完整流程。函數

首先需求、調研和項目目標,甚至包括最簡單的測試用例,這些信息主要是用來肯定項目目標,咱們首先要知道要作什麼?技術目標是什麼?以及要暴露哪些 API?當暴露了 API 以後,須要討論最小的使用迷你 case 是什麼樣子,從而梳理出前期的基本需求,這個環節一般開發能夠本身拍板,主要涉及一些文檔的工做。工具

測試模式

image

下面介紹三種測試模式,大部分人會接觸其中的 1-2 種。我認爲其中能夠適當輕鬆一下的是 Service Tests,而 Unit Tests(單元測試)是必需要有的,它能保證全部 API 的細節符合咱們的輸入輸出。End-to-End Test (端到端測試),其實是爲了保證業務自己符合咱們一開始的設計,它是直接面對用戶的,手機 App 點擊菜單,輸出各類各樣的效果,都是有自動化的工具來實現的。單元測試提供的是開發內部,而端到端測試是對於外部完整的聯動起來,這兩者是必需要有的。性能

單元測試

OpenResty 體系內使用了在其餘領域不多見的測試框架 Test::Nginx,OpenResty單元測試

裏面用了大量的 Lua ,而 Lua 的測試用例幾乎都是使用 busted 來書寫的,這兩者之間有很大的區別。學習

image

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::Base;
  • Perl 語⾔(上⼿難);
  • 語⾔無關的測試框架;
  • 很強的擴展性(雖然難),既是優點,也是劣勢;
  • 可搭配 valgrind;
  • 可搭配 ASAN;

目前 Test::Nginx 測試框架已經很好地集成了 valgrind 和 ASAN 這兩個內存整合工具,能夠相互配合,他們都是用來作內存檢測的,檢查內存是否被正確釋放、使用等狀況。

我最近兩年在寫服務的時候都是用的 Test::Nginx 測試框架,也給你們推薦一下,雖然有它的不足,可是帶來的好處也不少,最大的好處就是不須要在不一樣的測試體系下來回地切換思惟。

書寫代碼

image

接下來介紹一個技術細節,前面提到作基礎組件的開發會分幾步走,在書寫完測試用例後會進行代碼書寫,我此次的路由書寫代碼用的框架是基於 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 的結構體。

image

上圖是一段示例代碼,能夠看出大多數的封裝只是把類型轉了一下,並無特別複雜的封裝,只是把結構體都變成 void* 。這樣作的缺點是,對於結構體,C/C++ 編譯器編譯階段很容易找到參數類型傳錯的問題,而當咱們換成 void* 的傳參,因爲 void* 能夠被任意傳參, 編譯器只能幫咱們檢測錯誤的可能,這個問題是開發者須要注意的。

可是優點也比較明顯,FFI 只須要導出圖中的函數:

void*
r3_create(int cap)
void
r3_free(void*tree)
複製代碼

它們導出是不依賴任何的結構體,會讓庫寫起來很是方便,因此會讓 170 行代碼變成 20 行。

持續集成

前面已經有了測試框架,咱們須要有一種方式可以作對當前全部的業務請求作一個完整的測試用例的迴歸。

image

image

你們若是在用 Github 應該都會了解 Travis ,Travis 是對開源項目最友好的通用測試平臺。服務一旦開啓了 Travis ,每一次提交它均可以在平臺上作自動的迴歸測試,固然咱們須要書寫一個 .yml 文件,告訴它你要幹什麼事情、須要什麼環境、怎麼編譯、怎麼檢測等,它會給你一個結果的反饋,利用這個結果,就能夠跟進軟件持續的開發。

image

image

上圖是開啓 Travis CI 的方法,登錄本身的用戶,點擊 Settings,在用戶的分組裏面,找到一個具體的項目,點擊勾選就開啓了。

曾經不少人問過我,春哥的牛皮究竟是什麼?其實這個問題,我在不一樣的階段也有不一樣的回答。現階段我認爲春哥的測試體系很是厲害。OpenResty 這個軟件若是有哪一個人能把春哥全部的東西以及這些子項目之間的關係所有搞清楚,我以爲已經很厲害了,而春哥卻以一己之力把這些東西玩的很轉,其中很大一部分緣由是他把測試體系看的很重要,他在測試體系上的積累可以讓他把 OpenResty 這個項目可持續地往前推動,因此你們之後要對測試體系要額外地重視,尤爲是作一個比較複雜的組件。

測試工具

image

下面詳細介紹一下 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 消耗都在路由的預算上。兩個點能夠關注:第一,是否是把大部分的時間都確實花在路由預算上,第二,路由預算的方法過程自己,是否是還有可優化的空間,這兩個問題均可以在火焰圖中找到很是好的答案。

建立里程碑

image

最後須要建立里程碑,完成了一個階段若是沒有里程碑,就沒有辦法跟領導申請立項,也拿不到項目的預算資金,因此里程碑很是重要,它能夠關注每一個階段的東西。除此以外,當咱們作一個開源項目的時候,它的做用就更加明顯,它表明的是項目對外的穩定版本。

上圖是我對 lua-resty-r3 項目打的一個 tag ,這個版本是一個相對比較重要的穩定性階段,這個項目相關的全部東西我都會放到 Issues 裏面,目前這仍是一個私有項目,不過過不了多久就會開源給你們。

今天分享的內容主要側重在開發流程上,純粹的技術細節不是不少,謝謝你們!

演講視頻及PPT:

lua-resty-r3 高性能 OpenResty 路由實現​www.upyun.com

圖標
相關文章
相關標籤/搜索