什麼是 LuaJIT?為什麼 Apache APISIX 選擇了 LuaJIT?

語言: CN / TW / HK

本文介紹了 LuaJIT 的高靈活性和高效能,以及 APISIX 作為雲原生 API 閘道器選擇 LuaJIT 的原因。

作者楊陶,API7.ai 技術工程師。

原文連結

什麼是 LuaJIT

定義

簡單地說,LuaJIT 是 Lua 這種程式語言的實時編譯(JIT,Just-In-Time Compilation)器的實現。 對於不太瞭解 LuaJIT 的讀者,我們可以將 LuaJIT 拆成 Lua 和 JIT 兩個部分來理解。

Lua

Lua 是一種優雅、易於學習的程式語言,具有自動記憶體管理、完整的詞法作用域、閉包、迭代器、協程、正確的尾部呼叫以及使用關聯陣列進行非常實用的資料處理。本文不會涉及 Lua 的語法,有關內容歡迎閱讀 Getting Started With Lua

Lua 的設計目標是能與 C 或其它常用的程式語言相互整合,這樣就可以利用其它語言已經做好的方面;而它提供的特性又恰好是 C 這類語言不太擅長的,比如相對於硬體層的高層抽象,動態的結構,簡易的測試等等。其袖珍的語言核心和只依賴於 ANSI C 標準的特點,使之在各平臺上的可移植性變得非常高。因此 Lua 不僅是一個可以作為獨立程式執行的指令碼語言,也是一個可以嵌入其它應用的嵌入式語言。

Apache APISIX 就是一個在底層同時使用 Lua 和 C 的極佳例子。

但此時的 Lua 還有傳統指令碼語言常見的兩個問題:效率低和程式碼暴露。而 LuaJIT 引入的 JIT 技術能夠有效地解決了這兩個問題。

JIT

JIT(Just-In-Time Compilation),實時編譯,是動態編譯的一種形式。在電腦科學中,動態編譯並不是唯一的編譯形式,比如現今仍然流行的 C 語言使用的就是另一種形式:靜態編譯。

需要指出的是,我們也常常將 C 語言的這種與動態編譯相反的編譯方式稱為提前編譯(AOT,Ahead-of-Time Compilation),但二者並不是完全對等的。AOT 僅是描述在執行程式前,將某種“高階”語言編譯為某種“低階”語言的行為。其編譯的目標語言並不一定特定於程式宿主機上的機器碼,而是任意定義的。比如將 Java 編譯為 C,或者將 JavaScript 編譯為 V8 等等這些行為也會被視為 AOT。由於所有靜態編譯在技術上都是提前執行的,所以在這種特定的上下文中使用時,我們可以將 AOT 視為與 JIT 相反的靜態編譯。

拋開這些冗雜的名詞,想到靜態編譯的產物,你可能會發現,Lua 語言面臨的問題也可以通過靜態編譯來解決。但事實上,這就丟失了 Lua 作為指令碼語言的優勢:熱更新的靈活性和良好的平臺相容性。所以目前除了有特殊需求的指令碼語言外,大部分指令碼語言都在使用 JIT 嘗試提高語言效能,比如 Chromium 平臺上使用 V8 的 JavaScript 和使用 YJIT 的 Ruby。

JIT 嘗試將 Lua 的動態解釋和 C 的靜態編譯兩者的優缺點相結合,在指令碼語言的執行期間,通過不斷地分析正在執行的程式碼片段,編譯或重新編譯這段程式碼,以得到執行效率的提升。此時,JIT 假設的目標是,由此得到的效能提升能夠高於編譯或重新編譯這段程式碼的開銷。理論上說,由於能夠進行動態地重新編譯,JIT 在此過程中,可以針對正在執行程式的特定平臺架構進行優化、加速,在某些情況下,能產生比靜態編譯更快的執行速度。

JIT 分為傳統的 Method JIT 和 LuaJIT 正在使用的 Trace JIT 兩種。Method JIT 是將每一個方法(Method)翻譯為機器碼;而如下圖所示,更先進的Trace JIT 假定 “對只執行一兩次的程式碼,解釋執行比 JIT 編譯執行要快”,以此為依據對傳統 JIT 進行優化,具體表現為將頻繁執行的程式碼片段(即熱路徑上的程式碼)認定為需要跟蹤的程式碼,將這部分程式碼編譯成機器碼執行。

LuaJIT 的原理

LuaJIT

而 LuaJIT(2.x 版本)在 Trace JIT 的基礎上,集成了使用匯編編寫的高速直譯器和基於 SSA 並進行優化的程式碼生成器後端,大幅提高了 JIT 的表現,最終使得 LuaJIT 成為最快的動態語言實現之一。

除此之外,相對於原生 Lua 中為了與 C 互動而需要編寫 Lua 與 C 的繁複繫結,LuaJIT 還實現了 FFI(外部函式介面,Foreign Function Interface)。該技術允許了我們在不清楚引數個數和型別的情況下,從 Lua 程式碼中直接呼叫外部的 C 函式和使用 C 的資料結構。由此功能,我們也可以直接使用 FFI 實現所需的資料結構,而非 Lua 原生的 Table 型別,進一步在效能敏感的場景下,提升程式執行的速度。有關使用 FFI 提高效能的技巧並非本文討論的範疇,更深入的內容可以參閱 Why Does lua-resty-core Perform Better?

總而言之,LuaJIT 在 Lua 語法的基礎上,實現了迄今為止指令碼語言中最快的 Trace JIT 之一,並提供了 FFI 等功能,解決了 Lua 效率低和程式碼暴露的問題,讓 Lua 真正成為了高靈活性、高效能和超低記憶體佔用的指令碼語言和嵌入式語言。

與其它語言、WASM 的對比

相對於 Lua 和 LuaJIT,我們可能對其它的一些語言更加熟悉,比如 JavaScript (Node.js),Python,Golang,Java 等。對比這些大眾化的語言,我們可以看到更多 LuaJIT 的特性和優勢,下面簡單羅列一些 Lua/LuaJIT 與這些語言的對比:

  • Lua 的語法設計是針對非軟體工程師所設計的。所以像 R 語言一樣,Lua 也擁有陣列下標從 1 開始等適合普通人的設計。
  • Lua 非常適合作為嵌入式語言。Lua 本身擁有一個輕量的 VM,而 LuaJIT 在新增各種功能和優化後,也仍然很輕量。所以相對 Node.js 和 Python 之類龐大的執行環境,LuaJIT 直接整合到 C 編寫的程式中後也不會增大太多體積。因此,實際上 Lua 是所有嵌入式語言中使用量比較大且主流的選擇。
  • Lua 也很適合做“膠水”語言。類似 JavaScript(Node.js) 和 Python,Lua 也能很好地連線不同的庫和程式碼。但稍有不同的是,Lua 與底層生態的耦合性更高,所以在不同的領域中,Lua 的生態可能並不通用。

WASM(Web Assembly)是一種新興的跨平臺技術。這種起初設計為補充而非取代 JavaScript 的技術,因為能夠將其它的語言編譯成 WASM 位元組碼,同時還能作為安全沙箱執行程式碼,使得越來越多的程式也在考慮使用 WASM 作為嵌入或者膠水的平臺。即便如此,Lua/LuaJIT 在對比新興的 WASM 時,也仍然有不少優勢:

  • WASM 的效能是受限的,無法達到彙編的水準。普遍場景下的效能,WASM 肯定好過 Lua,但與 LuaJIT 有所差距。
  • WASM 與宿主程式的資料傳遞效率比較低。而 LuaJIT 可以通過 FFI 進行高效率的資料傳遞。

為什麼 Apache APISIX 選擇 LuaJIT

儘管上文描述了 LuaJIT 自身的諸多優勢,但對於大部分開發者而言,Lua 不是一門大眾的語言,LuaJIT 更不是一個大眾的選擇。那為什麼 Apache 基金會所屬的雲原生 API 閘道器 Apache APISIX 還是選擇了 LuaJIT 呢?

作為雲原生的 API 閘道器,Apache APISIX 兼具動態、實時、高效能等特點,提供了負載均衡、動態上游、灰度釋出(金絲雀釋出)、服務熔斷、身份認證、可觀測性等豐富的流量管理功能。我們可以使用 Apache APISIX 來處理傳統的南北向流量,也可以處理服務間的東西向流量,還可以用作 k8s 的 Ingress Controller。

而這一切都建立在 Apache APISIX 所選擇的 NGINX 和 LuaJIT 技術棧之上。

LuaJIT 與 NGINX 結合帶來的優勢

NGINX 是一個知名的高效能 HTTP 、TCP/UDP 代理和反向代理的 Web 伺服器。

但在使用中,我們會發現很惱人的是,每次修改 NGINX 的配置檔案後,都需要使用 nginx -s reload 重新載入 NGINX 配置。

不僅如此,頻繁地使用該命令重新載入配置可能會造成連線的不穩定,增加業務丟失的可能性;而在某些情況下,NGINX 過載配置的機制也可能會造成舊程序的回收時間過長,影響正常的業務。對於該問題的深入討論,可以閱讀 為什麼 NGINX 的 reload 不是熱載入?,這裡不進行深入展開。

Apache APISIX 誕生的目的之一就是解決 NGINX 的動態配置問題,LuaJIT 的高靈活性、高效能和超低記憶體佔用帶來了這種可能性。 以最具普遍性的路由為例,Apache APISIX 通過在 NGINX 配置檔案中只配置單個 location 作為主入口,而後續的路由分發則由 APISIX 的路由分發模組完成,以此實現了路由的動態配置。

為了實現足夠高的效能,Apache APISIX 使用 C 編寫了基於字首樹的匹配路由演算法,並在此基礎上使用 LuaJIT 提供的 FFI 編寫了適用於 Lua 的介面。而 Lua 的靈活性,也使得 Apache APISIX 的路由分發模組,可以輕易地支援通過特定的表示式等方法,對同一字首的下級路由進行匹配。最終在替代 NGINX 原生路由分發功能的前提下,實現了兼具高效能、高靈活性的動態配置功能。有關這部分功能的詳細實現,可以檢視 lua-resty-radixtreeroute.lua

另外,不只是路由,從負載均衡、健康檢查,到上游節點配置、服務端證書,以及擴充套件 APISIX 能力的外掛本身,都能在 APISIX 不重啟的情況下重新載入。

同時,除了在使用 LuaJIT 進行外掛等功能的開發,Apache APISIX 還支援了 Java、Go、Node、Python 以及 WASM 等多種方式開發外掛,也讓 Apache APISIX 的二次開發門檻大大降低,使 Apache APISIX 獲得了豐富的外掛生態和活躍的開源社群。

Apache APISIX 的外掛原理和生態

總結

LuaJIT 是 Lua 的實時編譯器實現。

Apache APISIX 作為一個動態、實時、高效能的開源 API 閘道器,基於 NGINX 與 LuaJIT 帶來的高效能、高靈活等特性,提供了負載均衡、動態上游、灰度釋出、服務熔斷、身份認證、可觀測性等豐富的流量管理功能。

目前 Apache APISIX 已經來到了全新的 3.x 版本,並帶來了更多的開源專案整合、雲供應商整合,原生的 gRPC 支援,更多的外掛開發方式選擇,以及服務網格支援等功能。歡迎加入 Apache APISIX 社群,瞭解更多 LuaJIT 在雲原生 API 閘道器中的應用。

關於 API7.ai 與 APISIX

API7.ai(支流科技 )是一家提供 API 處理和分析的開源基礎軟體公司,於 2019 年開源了新一代雲原生 API 閘道器 -- APISIX 並捐贈給 Apache 軟體基金會。此後,API7.ai 一直積極投入支援 Apache APISIX 的開發、維護和社群運營。與千萬貢獻者、使用者、支持者一起做出世界級的開源專案,是 API7.ai 努力的目標。