openresty學習筆記

本文檔參考極客時間openresty專欄和其他博客,中間也有一些坑,記錄下學習過程。

1. 概述

Openresty是基於nginx和lua的高性能web平臺,同時擁有腳本語言的開發效率和迭代速度,以及 NGINX C 模塊的高併發和高性能優勢。

2. 入門

0. 前提

需要對nginx.conf配置文件的結構有大致的瞭解

參考博客:https://juejin.im/post/5c1616186fb9a049a42ef21d

1. 下載和安裝

下載和安裝,在官方(https://openresty.org/cn/)進行下載和安裝,centos版本,最好不要直接下載源碼包,會有很多坑。

2. helloworld

首先創建工作目錄

mkdir /geektime
cd /geektime
mkdir logs/ conf/

其中 logs 目錄用於存放日誌,conf 用於存放配置文件。

接着,我們在 conf 目錄下創建一個 nginx.conf 文件,如下

worker_connections 1024;
http {
    server {
        listen 8080;
        location / {
            content_by_lua '
                ngx.say("<p>Hello, World!</p>")
            ';
        }
    }
}

啓動openresty

默認情況下 openresty 安裝在 /usr/local/openresty 目錄中,啓動命令爲:

/usr/local/openresty/nginx/sbin/nginx -p /root/geektime -c conf/nginx.conf

如果沒有任何輸出,說明啓動成功,-p 指定我們的項目目錄,-c 指定配置文件。

接下來我們可以使用 curl 來測試是否能夠正常返回:

curl http://localhost:8080/

輸出正常,結果

<p>Hello, World!</p>

3. 調用lua腳本

使用content_by_lua_file指令調用Lua腳本文件。

在geektime目錄下創建lua目錄,用來存放腳本,在lua文件夾下創建hello.lua:

ngx.say("hello, world")

停止已啓動的nginx進程

killall -9 nginx

啓動nginx進程

/usr/local/openresty/nginx/sbin/nginx -p /root/geektime/ -c conf/nginx.conf

再次用curl測試,nginx openresty content_by_lua_file 404,百度發現nginx.conf文件中需要添加 user root root;

worker_processes 1;
user root root;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8080;
        location / {
		content_by_lua_file 'lua/hello.lua';
        	}
	}
	
}

輸出正常。

lua腳本內容實時生效

如果修改lua腳本內容後,需要重啓nginx才能生效,如何使lua腳本實時生效?

在 nginx.conf 中關閉 lua_code_cache 。

worker_processes 1;
user root root;
events {
    worker_connections 1024;
}
http {
    server {
	lua_code_cache off;
        listen 8080;
        location / {
		content_by_lua_file 'lua/hello.lua';
        	}
	}	
}

重啓後可以實時修改,但是每次都會警告,生產環境最好不要這樣,影響性能。

3. 模塊

1. 自身項目

  • nginx C模塊 *-nginx-module命名的就是 NGINX 的 C 模塊。

nginx C模塊

openresty -V 命令可看到這些C模塊,--add-module= 後邊跟着的就是C模塊,最核心的就是 lua-nginx-module 和
stream-lua-nginx-module,前者用來處理七層流量,後者用來處理四層流量。 cosocket 功能加入之後,大部分C庫都已經被 lua-restyredis 和 lua-resty-memcached 替代,處於疏於維護的狀態。

lua-resty-周邊庫

包含 18 個 lua-resty-* 庫,涵蓋 Redis、MySQL、memcached、websocket、dns、流量控制、字符串處理、進程內緩存等常用庫。 非常重要。

自己維護的 LuaJIT 分支

LuaJIT 的作者Mike Pall 宣佈退休, 沒有找到合適的維護者, 新功能的開發也已經暫停,所以 OpenResty 維護着自己的 LuaJIT 分支。 相對於 Lua,LuaJIT 增加了不少獨有的函數,這些函數非常重要 。

2. 第三方類庫

OPM(OpenResty Package Manager )是 OpenResty 自帶的包管理器,可以試着去找找發送 http 請求的庫

opm search http

查看類庫的分類,可以參考awesome-resty 這個項目 https://github.com/bungle/awesome-resty

4. 基礎

1. 使用的nginx的知識

作用域

每個指令都有自己適用的上下文(Context),也就是NGINX 配置文件中指令的作用域 。

最上層的是 main,裏面是和具體業務無關的一些指令,比如上面出現的 worker_processes、pid 和error_log,都屬於 main 這個上下文。另外,上下文是有層級關係的,比如 location 的上下文是 server,server 的上下文是 http,http 的上下文是 main。

多進程模式

一個 Master 進程和多個 Worker 進程。Master 進程,一如其名,扮演“管理者”的角色,並不負責處理終端的請求。它是用來管理Worker 進程的,包括接受管理員發送的信號量、監控 Worker 的運行狀態。當 Worker 進程異常退出時,Master 進程會重新啓動一個新的 Worker 進程。

Worker 進程則是“一線員工”,用來處理終端用戶的請求。它是從 Master 進程 fork 出來的,彼此之間相互獨立,互不影響。多進程的模式比 Apache 多線程的模式要先進很多,沒有線程間加鎖,也方便調試。即使某個進程崩潰退出了,也不會影響其他 Worker 進程正常工作。

而 OpenResty 在 NGINX Master-Worker 模式的前提下,又增加了獨有的特權進程(privileged agent)。這個進程並不監聽任何端口,和 NGINX 的 Master 進程擁有同樣的權限,所以可以做一些需要高權限才能完成的任務,比如對本地磁盤文件的一些寫操作等。

減少對外部程序的依賴,儘量在 OpenResty 進程內解決問題,不僅方便部署、降低運維成本,也可以降低程序出錯的概率。可以說,OpenResty 中的特權進程、ngx.pipe 等功能,都是出於這個目的。

2. 執行階段

最好根據不同的功能拆分業務代碼:

  • set_by_lua:設置變量;
  • rewrite_by_lua:轉發、重定向等;
  • access_by_lua:准入、權限等;
  • content_by_lua:生成返回內容;
  • header_filter_by_lua:應答頭過濾處理;
  • body_filter_by_lua:應答體過濾處理;
  • log_by_lua:日誌記錄。

3. lua的常用數據類型

1. 字符串

單引號、雙引號,以及長括號([[]])

長括號中的字符串不會做任何的轉義處理。

 resty -e 'print([[string has \n and \r]])'

結果:string has \n and \r

如果上面那段字符串中包括了長括號本身,又該怎麼處理呢? 在長括號中間增加一個或者多個 = 符號:

resty -e 'print([=[ string has a [[]]. ]=])'

結果: string has a [[]].

2. 布爾值

只有 nil 和 false 爲假,其他都爲真,包括 0 和空字符串也爲真

3. 數字

Lua 的 number 類型,是用雙精度浮點數來實現的。 LuaJIT 支持 dual-number(雙數)模式,也就是說, LuaJIT 會根據上下文來用整型來存儲整數,而用雙精度浮點數來存放浮點數。

LuaJIT 還支持⻓整型的大整數 。

4. 函數

可以把函數存放在一個變量中,也可以當作另外一個函數的入參和出參。

5. table

是 Lua 中唯一的數據結構

6. 空值

在 Lua 中,空值就是 nil。如果你定義了一個變量,但沒有賦值,它的默認值就是 nil

4. 常用標準庫

OpenResty的API > LuaJIT的庫函數 > 標準Lua的函數

1. string庫

如果涉及到正則表達式的,要使用 OpenResty 提供的 ngx.re.* 來解決 ,不要用 Lua 的 string.* 處理。

  • string.byte(s [, i [, j ]]) 返回字符s[i]、s[i + 1]、s[i + 2]、······、s[j] 對應的ASCII碼,i的默認值爲1
print(string.byte("abc", 1, 3))
print(string.byte("abc", 3)) -- 缺少第三個參數,第三個參數默認與第二個相同,此時爲 3
print(string.byte("abc"))    -- 缺少第二個和第三個參數,此時這兩個參數都默認爲 1

-->output
97    98    99
99
97

table庫

lua自帶的table庫,只推薦table.concat、table.sort。

table.concat用於字符串拼接,避免生成很多無用的字符串。

$ resty -e 'local a = {"A", "b", "C"}
print(table.concat(a))'

math庫

隨機數 math.random()和math.randomseed()較常用

resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'

虛變量

當一個函數返回多個值,有些返回值不需要,如何接收這些值?lua提供虛變量dummy variable的概念,按照慣例以一個下劃線來命名,表示丟棄不需要的數值,起到佔位的作用。

以string.find函數爲例,它會返回兩個值,分別代表開始和結束的下標。

如果只需要獲取開始的下標,只聲明一個變量接收返回值 :

resty -e 'local start = string.find("hello", "he")
print(start)'

如果你只想獲取結束的下標,那就必須使用虛變量了

resty -e 'local _, end_pos = string.find("hello", "he")
print(end_pos)'

虛變量還經常用於循環中

resty -e 'for _, v in ipairs({4,5,6}) do
print(v)
end'

而當有多個返回值需要忽略時,你可以重複使用同一個虛變量 .

5. luaJIT

標準Lua 和 LuaJIT 是兩回事兒,LuaJIT 只是兼容了 Lua 5.1 的語法。 而OpenResty 維護了自己的 LuaJIT 分支,並擴展了很多獨有的 API。

爲什麼用luaJIT

最主要的原因,還是LuaJIT的性能優勢。 所謂 LuaJIT 的性能優化,本質上就是讓儘可能多的 Lua 代碼可以被 JIT 編譯器生成機器碼,而不是回退到 Lua 解釋器的解釋執行模式。

兼容 Lua 5.1 的語法並支持 JIT 外,LuaJIT 還緊密結合了 FFI(Foreign Function Interface),可以讓你直接在 Lua 代碼中調用外部的 C 函數和使用 C 的數據結構。

local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")

直接在 Lua 中調用 C 的 printf 函數,打印出 Hello world! 可以用 FFI 來調用 NGINX、OpenSSL 的 C 函數,來完成更多的功能。實際上,FFI 方式比傳
統的 Lua/C API 方式的性能更優,這也是 lua-resty-core 項目存在的意義。

出於性能方面的考慮,LuaJIT 還擴展了 table 的相關函數:table.new 和 table.clear。這是兩個在性能優化方面非常重要的函數,在 OpenResty 的 lua-resty 庫中會被頻繁使用。

檢測內存泄露

使用FFI時,需要注意內存泄露。

測試,使用Valgrind檢測內存泄露問題。測試框架test::nginx,有專門的內存泄露檢測模式,去運行單元測試案例集。只需要設置環境變量 TEST_NGINX_USE_VALGRIND=1 。

而 OpenResty 的 CLI resty 也有 --valgrind 選項,方便你單獨運行某段 Lua 代碼,即使你沒有寫測試案例也是沒問題的。

調試工具:提供基於 systemtap 的擴展,來對 OpenResty 程序進行活體的動態分析。你可以在這個項目的工具集中,搜索 gc 這個關鍵字,會看到 lj-gc 和 lj-gc-objs 這兩個工具;對於 core dump 這種離線分析,OpenResty 提供了 GDB 的工具集,同樣你可以在裏面搜索 gc,找到lgc、lgcstat 和 lgcpath 三個工具。

6. lua的特別之處

1.下標從1開始

2. 使用 … 來拼接字符串

3. 只有 table 這一種數據結構

不同於 Python 這種內置數據結構豐富的語言,Lua 中只有一種數據結構,那就是 table,它裏面可以包括數組和哈希表:

local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"]) --> output: red
print(color[1]) --> output: blue
print(color["third"]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: nil

如果不顯式地用_鍵值對_的方式賦值,table 就會默認用數字作爲下標,從 1 開始。所以 color[1] 就是blue。

另外,想在 table 中獲取到正確長度,也是一件不容易的事情 。

local t1 = { 1, 2, 3 }
print("Test1 " .. table.getn(t1))

local t2 = { 1, a = 2, 3}
print("Test2 " .. table.getn(t2))

local t3 = { 1, nil }
print("Test3 " .. table.getn(t3))

local t4 = { 1, nil, 2 }
print("Test4 " .. table.getn(t4))

resty運行結果

Test1 3
Test2 2
Test3 1
Test4 1

除了第一個返回長度爲 3 的測試案例外,後面的測試都是我們預期之外的結果。 想要在Lua 中獲取 table 長度,必須注意到,只有在 table 是 序列 的時候,才能返回正確的值。

什麼是序列呢? 首先序列是數組(array)的子集,也就是說,table 中的元素都可以用正整數下標訪問到,不存在鍵值對的情況。其次,序列中不包含空洞(hole),即 nil。 上面的 table 中, t1 是一個序列,而 t3 和 t4
是 array,卻不是序列(sequence)。

爲什麼 t4 的長度會是 1 呢? 在遇到 nil 時,獲取長度的邏輯就不繼續往下運行,而是直接返回了。

table的內部結構參考博客https://blog.csdn.net/wwlcsdn000/article/details/81291756

4. 默認是全局變量

除非你相當確定,否則在 Lua 中聲明變量時,前面都要加上 local。 強烈建議你總是使用 local 來聲明變量 。

7. NYI

如果你的項目中只用到了 OpenResty 提供的 API,沒有自己調用C 函數的需求,那麼 FFI 對你而言並沒有那麼重要,你只需要確保開啓了 lua-resty-core 即可。

你可以很快使用 OpenResty 寫出邏輯正確的代碼,但不明白 NYI,你就不能寫出高效的代碼,無法發揮OpenResty 真正的威力。

JIT 編譯器不支持的這些原語,其實就是 NYI,全稱爲Not Yet Implemented。 當 JIT 編譯器在當前代碼路徑上遇到它不支持的操作時,便會退回到解釋器模式。

如何檢測NYI?

LuaJIT 自帶的 jit.dump 和 jit.v 模塊。它們都可以打印出 JIT 編譯器工作的過程。

resty 的 -j 就是和 LuaJIT 相關的選項;後面的值爲 dump 和 v,就對應着開啓 jit.dump 和jit.v 模式。

在 jit.v 模塊的輸出中,每一行都是一個成功編譯的 trace 對象。剛剛是一個能夠被 JIT 的例子,而如果遇到NYI 原語,輸出裏面就會指明 NYI,比如下面這個 pairs 的例子:

resty -j v -e 'local t = {}
for i=1,100 do
t[i] = i
end
for i=1, 1000 do
for j=1,1000 do
for k,v in pairs(t) do
--
end
end
end'

它就不能被 JIT,所以結果裏,指明瞭第 7 行中有 NYI 原語。

8. table和metatable特性

可以用ipairs 函數,只遍歷數組部分的內容。

openresty的table擴展函數

OpenResty 自己維護的 LuaJIT 分支,也對 table 做了擴展,它新增了幾個API:table.isempty、table.isarray、 table.nkeys 和 table.clone。

table.nkeys:獲取 table 長度的函數,返回的是 table 的元素個數,包括數組和哈希部分的元素。因此,我們可以用它來替代 table.getn 。

LuaJIT 的 table 擴展函數

table.new(narray, nhash) 新建 table ;table.clear() 清空 table ,避免反覆創建和銷燬 table 的開銷。

local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
t[i] = i
end

標準table 庫函數

table.getn 獲取元素個數 ,不推薦使用,時間複雜度O(n),用table.nkeys替換。

table.remove 刪除指定元素,只能刪除數組,如何刪除 table 中的哈希部分呢?把key 對應的 value 設置爲 nil 即可。

table.concat 元素拼接函數 。

table.insert 插入一個元素,儘量少用,時間複雜度也是O(n)。

元表

由 table 引申出來的 元表(metatable)。

元表的表現行爲類似於操作符重載,比如我們可以重載 __add,來計算兩個 Lua 數組的並集;或者重載__tostring,來定義轉換爲字符串的函數。

Lua 提供了兩個處理元表的函數:

  • setmetatable(table, metatable), 用於爲一個 table 設置元表;
  • setmetatable(table, metatable), 用於爲一個 table 設置元表;
local version = {
major = 1,
minor = 1,
patch = 1
}
print(tostring(version))
version = setmetatable(version, {
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})

結果:

table: 0x7f585889a588
1.1.1

除了__tostring之外,在實際項目中,我們還經常重載元表中的以下兩個元方法(metamethod)。

其中一個是__index。我們在 table 中查找一個元素時,首先會直接從 table 中查詢,如果沒有找到,就繼續到元表的 __index 中查詢。

local version = {
major = 1,
minor = 1
}
version = setmetatable(version, {
__index = function(t, key)
if key == "patch" then 
return 2
end
end,
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(tostring(version))

另一個元方法則是__call。它類似於仿函數,可以讓 table 被調用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章