整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          wireshark 書寫lua插件

          書寫插件前

          wireshark的協議不支持之前,報文幾乎難以分析,舉例如下

          Wireshark插件路徑

          MAC

          重新加載Wireshark中的lua插件

          MAC

          從一個新協議的樣板開始

          pulsar_protocol = Proto("Pulsar", "Pulsar Protocol")
          
          pulsar_protocol.fields = {}
          
          function pulsar_protocol.dissector(buffer, pinfo, tree)
              length = buffer:len()
              if length == 0 then
                  return
              end
              pinfo.cols.protocol = pulsar_protocol.name
              local subtree = tree:add(pulsar_protocol, buffer(), "Pulsar Protocol Data")
          end
          
          local tcp_port = DissectorTable.get("tcp.port")
          tcp_port:add(6650, pulsar_protocol)

          我們從協議對象開始,命名為pulsar_protocol。構造函數兩個參數分別為名稱和描述。協議需要一個fields表和dissecotr函數。我們現在還沒有任何field,所以fields表為空。對于每一個報文,dissctor函數都會被調用一次。

          dissector函數有三個入參,bufferpinfo,和treebuffer包含了網絡包的內容,是一個Tvb對象。pinfo包含了wireshark中展示packet的列信息,是一個Pinfo對象。tree是wireshark報文詳情顯示的內容,是TreeItem對象。

          dissector函數中,我們檢查buffer的長度,如果長度為0,則立即返回

          pinfo對象包含著列信息,我們可以將pinfo的protocol設置為pulsar,顯示在wireshark的界面中。接下來在packet的結構中創建一個子樹,最后,我們把協議綁定到6650端口上。讓我們加載這個lua插件

          mkdir -p ~/.local/lib/wireshark/plugins
          cp $DIR/../../pulsar_dissector.lua ~/.local/lib/wireshark/plugins/pulsar_dissector.lua

          結果符合預期

          添加長度字段

          讓我們添加一個長度字段,pulsar協議中,長度字段即就是前4個字節,定義字段

          message_length = ProtoField.int32("pulsar.message_length", "messageLength", base.DEC)
          
          pulsar_protocol.fields = { message_length }

          pulsar.message_length可以用在過濾器字段中。messageLength是子樹中的label。第三個字段決定了這個值會被如何展示

          最后,我們把長度值加入到Wireshark的tree中

          subtree:add(message_length, buffer(0,4))

          pulsar的協議是大端序,我們使用add函數。如果協議是小端序,我們就可以使用addle函數。

          我們添加的message_length字段已經可以顯示在Wireshark中了

          添加額外信息

          protoc加入到wireshark

          參考及附錄

          proto field函數列表

          https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html#lua_fn_ProtoField_char_abbr___name____base____valuestring____mask____desc__

          wireshark解析protobuf

          https://ask.wireshark.org/question/15787/how-to-decode-protobuf-by-wireshark/

          、什么是事務?

          簡單來說,事務(transaction)是指單個邏輯單元執行的一系列操作。

          1.1、事務的四大特性ACID

          事務有如下四大特性:

          • 1、原子性(Atomicity): 構成事務的所有操作都必須是一個邏輯單元,要么全部執行,要么全不執行
          • 2、一致性(Consistency): 數據庫在事務執行前后狀態都必須是穩定的或者一致的。A(1000)給B(200)轉賬100后A(900),B(300)總和保持一致。
          • 3、隔離性(Isolation): 事務之間相互隔離,互不影響。
          • 4、持久性(Durability): 事務執行成功后數據必須寫入磁盤,宕機重啟后數據不會丟失。

          2、Redis中的事務

          Redis中的事務通過multi,exec,discard,watch這四個命令來完成。

          Redis的單個命令都是原子性的,所以確保事務的就是多個命令集合一起執行。

          Redis命令集合打包在一起,使用同一個任務確保命令被依次有序且不被打斷的執行,從而保證事務性。

          Redis是弱事務,不支持事務的回滾。

          2.1、事務命令

          事務命令簡介

          • 1、multi(開啟事務)
            • 用于表示事務塊的開始,Redis會將后續的命令逐個放入隊列,然后使用exec后,原子化的執行這個隊列命令。
            • 類似于mysql事務的begin
          • 2、exec(提交事務)
            • 執行命令隊列
            • 類似于mysql事務的commit
          • 3、discard(清空執行命令)
            • 清除命令隊列中的數據
            • 類似于mysql事務的rollback,但與rollback不一樣 ,這里是直接清空隊列所有命令,從而不執行。所以不是的回滾。就是個清除。
          • 4、watch
            • 監聽一個redis的key 如果key發生變化,watch就能后監控到。如果一個事務中,一個已經被監聽的key被修改了,那么此時會清空隊列。
          • 5、unwatch
            • 取消監聽一個redis的key

          事務操作

          # 普通的執行多個命令
          127.0.0.1:6379> multi
          OK
          127.0.0.1:6379> set m_name zhangsan
          QUEUED
          127.0.0.1:6379> hmset m_set name zhangsan age 20 
          QUEUED
          127.0.0.1:6379> exec
          1) OK
          2) OK
          
          # 執行命令前清空隊列 將會導致事務執行不成功
          127.0.0.1:6379> multi
          OK
          127.0.0.1:6379> set m_name_1 lisi
          QUEUED
          127.0.0.1:6379> hmset m_set_1 name lisi age 21
          QUEUED
          # 提交事務前執行了清空隊列命令
          127.0.0.1:6379> discard
          OK
          127.0.0.1:6379> exec
          (error) ERR EXEC without MULTI
          
          # 監聽一個key,并且在事務提交之前改變在另一個客戶端改變它的值,也會導致事務失敗
          127.0.0.1:6379> set m_name_2 wangwu01
          OK
          127.0.0.1:6379> watch m_name_2
          OK
          127.0.0.1:6379> multi
          OK
          127.0.0.1:6379> set m_name_2 wangwu02
          QUEUED
          # 另外一個客戶端在exec之前執行之后,這里會返回nil,也就是清空了隊列,而不是執行成功
          127.0.0.1:6379> exec
          (nil)
          
          # 另外一個客戶端在exec之前執行
          127.0.0.1:6379> set m_name_2 niuqi
          OK

          2.2、事務機制分析

          我們前面總是在說,Redis的事務命令是打包放在一個隊列里的。那么來看一下Redis客戶端的數據結構吧。

          client數據結構

          typedef struct client {
              // 客戶端唯一的ID
              uint64_t id;   
              // 客戶端狀態 表示是否在事務中
              uint64_t flags;         
              // 事務狀態
              multiState mstate;
              // ...還有其他的就不一一列舉了
          } client;

          multiState事務狀態數據結構

          typedef struct multiState {
              // 事務隊列 是一個數組,按照先入先出順序,先入隊的命令在前 后入隊的命令在后
              multiCmd *commands;     /* Array of MULTI commands */
              // 已入隊命令數
              int count;              /* Total number of MULTI commands */
              // ...略
          } multiState;

          multiCmd事務命令數據結構

          /* Client MULTI/EXEC state */
          typedef struct multiCmd {
              // 命令的參數
              robj **argv;
              // 參數長度
              int argv_len;
              // 參數個數
              int argc;
              // redis命令的指針
              struct redisCommand *cmd;
          } multiCmd;

          Redis的事務執行流程圖解

          Redis的事務執行流程分析

          • 1、事務開始時,在Client中,有屬性flags,用來表示是否在事務中,此時設置flags=REDIS_MULTI
          • 2、Client將命令存放在事務隊列中,事務本身的一些命令除外(EXEC,DISCARD,WATCH,MULTI)
          • 3、客戶端將命令放入multiCmd *commands,也就是命令隊列
          • 4、Redis客戶端將向服務端發送exec命令,并將命令隊列發送給服務端
          • 5、服務端接受到命令隊列后,遍歷并一次執行,如果全部執行成功,將執行結果打包一次性返回給客戶端。
          • 6、如果執行失敗,設置flags=REDIS_DIRTY_EXEC, 結束循環,并返回失敗。

          2.3、監聽機制分析

          我們知道,Redis有一個expires的字典用于key的過期事件,同樣,監聽的key也有一個類似的watched_keys字典,key是要監聽的key,值是一個鏈表,記錄了所有監聽這個key的客戶端。

          而監聽,就是監聽這個key是否被改變,如果被改變了,監聽這個key的客戶端的flags屬性就設置為REDIS_DIRTY_CAS。

          Redis客戶端向服務器端發送exec命令,服務器判斷Redis客戶端的flags,如果為REDIS_DIRTY_CAS,則清空事務隊列。

          redis監聽機制圖解

          redis監聽key數據結構

          回過頭再看一下RedisDb類的watched_keys,確實是一個字典,數據結構如下:

          typedef struct redisDb {
              dict *dict;                 /* 存儲所有的key-value */
              dict *expires;              /* 存儲key的過期時間 */
              dict *blocking_keys;        /* blpop存儲阻塞key和客戶端對象*/
              dict *ready_keys;           /* 阻塞后push,響應阻塞的那些客戶端和key */
              dict *watched_keys;         /* 存儲watch監控的key和客戶端對象 WATCHED keys for MULTI/EXEC CAS */
              int id;                     /* 數據庫的ID為0-15,默認redis有16個數據庫 */
              long long avg_ttl;          /* 存儲對象的額平均ttl(time in live)時間用于統計 */
              unsigned long expires_cursor; /* Cursor of the active expire cycle. */
              list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
              clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
          } redisDb;

          2.4、Redis的弱事務性

          為什么說Redis是弱事務性呢? 因為如果redis事務中出現語法錯誤,會暴力的直接清除整個隊列的所有命令。

          # 在事務外設置一個值為test
          127.0.0.1:6379> set m_err_1 test
          OK
          127.0.0.1:6379> get m_err_1 
          "test"
          # 開啟事務 修改值 但是隊列的其他命令出現語法錯誤  整個事務會被discard
          127.0.0.1:6379> multi 
          OK
          127.0.0.1:6379> set m_err_1 test1
          QUEUED
          127.0.0.1:6379> sets m_err_1 test2
          (error) ERR unknown command `sets`, with args beginning with: `m_err_1`, `test2`, 
          127.0.0.1:6379> set m_err_1 test3
          QUEUED
          127.0.0.1:6379> exec
          (error) EXECABORT Transaction discarded because of previous errors.
          
          # 重新獲取值
          127.0.0.1:6379> get m_err_1
          "test"

          我們發現,如果命令隊列中存在語法錯誤,是直接的清除隊列的所有命令,并不是進行事務回滾,但是語法錯誤是能夠保證原子性的

          再來看一些,如果出現類型錯誤呢?比如開啟事務后設置一個key,先設置為string, 然后再當成列表操作。

          # 開啟事務
          127.0.0.1:6379> multi 
          OK
          # 設置為字符串
          127.0.0.1:6379> set m_err_1 test_type_1
          QUEUED
          # 當初列表插入兩個值
          127.0.0.1:6379> lpush m_err_1 test_type_1 test_type_2
          QUEUED
          # 執行
          127.0.0.1:6379> exec
          1) OK
          2) (error) WRONGTYPE Operation against a key holding the wrong kind of valu
          # 重新獲取值,我們發現我們的居然被改變了,明明,事務執行失敗了啊
          127.0.0.1:6379> get m_err_1
          "test_type_1"

          直到現在,我們確定了redis確實不支持事務回滾。因為我們事務失敗了,但是命令卻是執行成功了。

          弱事務總結

          • 1、大多數的事務失敗都是因為語法錯誤(支持回滾)或者類型錯誤(不支持回滾),而這兩種錯誤,再開發階段都是可以遇見的
          • 2、Redis為了性能,就忽略了事務回滾。

          那么,redis就沒有辦法保證原子性了嗎,當然有,Redis的lua腳本就是對弱事務的一個補充。

          3、Redis中的lua腳本

          lua是一種輕量小巧的腳本語言,用標準C語言編寫并以源代碼形式開放, 其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。

          Lua應用場景:游戲開發、獨立應用腳本、Web應用腳本、擴展和數據庫插件。

          OpenResty:一個可伸縮的基于Nginx的Web平臺,是在nginx之上集成了lua模塊的第三方服務器。

          OpenResty是一個通過Lua擴展Nginx實現的可伸縮的Web平臺,內部集成了大量精良的Lua庫、第三方模塊以及大多數的依賴項。用于方便地搭建能夠處理超高并發(日活千萬級別)、擴展性極高的動態Web應用、Web服務和動態網 關。 功能和nginx類似,就是由于支持lua動態腳本,所以更加靈活,可以實現鑒權、限流、分流、日志記 錄、灰度發布等功能。

          OpenResty通過Lua腳本擴展nginx功能,可提供負載均衡、請求路由、安全認證、服務鑒權、流量控 制與日志監控等服務。

          類似的還有Kong(Api Gateway)、tengine(阿里)

          3.1、Lua安裝(Linux)

          lua腳本下載和安裝http://www.lua.org/download.html

          lua腳本參考文檔:http://www.lua.org/manual/5.4/

          # curl直接下載
          curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
          # 解壓
          tar zxf lua-5.4.4.tar.gz
          # 進入,目錄
          cd lua-5.4.4
          # 編譯安裝
          make all test

          編寫lua腳本

          編寫一個lua腳本test.lua,就定義一個本地變量,打印出來即可。

          local name = "zhangsan"
          
          print("name:",name)

          執行lua腳本

          [root@VM-0-5-centos ~]# lua test.lua 
          name:   zhangsan

          3.2、Redis中使用Lua

          Redis從2.6開始,就內置了lua編譯器,可以使用EVAL命令對lua腳本進行求值。

          腳本命令是原子性的,Redis服務器再執行腳本命令時,不允許新的命令執行(會阻塞,不在接受命令)。、

          EVAL命令

          通過執行redis的eval命令,可以運行一段lua腳本。

          EVAL script numkeys key [key ...] arg [arg ...]

          EVAL命令說明

          • 1、script:是一段Lua腳本程序,它會被運行在Redis服務器上下文中,這段腳本不必(也不應該)定義為一個Lua函數。
          • 2、numkeys:指定鍵名參數的個數。
          • 3、key [key ...]:從EVAL的第三個參數開始算起,使用了numkeys個鍵(key),表示在腳本中所用到的哪些Redis鍵(key),這些鍵名參數可以在Lua中通過全局變量KEYS數組,用1為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)
          • 4、arg [arg ...]:可以在Lua中通過全局變量ARGV數組訪問,訪問的形式和KEYS變量類似(ARGV[1] 、 ARGV[2] ,諸如此類)

          簡單來說,就是

          eval lua腳本片段  參數個數(假設參數個數=2)  參數1 參數2  參數1值  參數2值

          EVAL命令執行

          # 執行一段lua腳本 就是把傳入的參數和對應的值返回回去
          127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 name age zhangsan 20
          1) "name"
          2) "age"
          3) "zhangsan"
          4) "20"

          lua腳本中調用redis

          我們直到了如何接受和返回參數了,那么lua腳本中如何調用redis呢?

          • 1、redis.call
            • 返回值就是redis命令執行的返回值
            • 如果出錯,則返回錯誤信息,不繼續執行
          • 2、redis.pcall
            • 返回值就是redis命令執行的返回值
            • 如果出錯,則記錄錯誤信息,繼續執行

          其實就是redis.call會把異常拋出來,redis.pcall則時捕獲了異常,不會拋出去。

          lua腳本調用redis設置值

          # 使用redis.call設置值
          127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 eval_01 001
          OK
          127.0.0.1:6379> get eval_01
          "001"

          EVALSHA命令

          前面的eval命令每次都要發送一次腳本本身的內容,從而每次都會編譯腳本。

          Redis提供了一個緩存機制,因此不會每次都重新編譯腳本,可能在某些場景,腳本傳輸消耗的帶寬可能是不必要的。

          為了減少帶寬的西消耗,Redis實現了evaklsha命令,它的作用和eval一樣,只是它接受的第一個參數不是腳本,而是腳本的SHA1校驗和(sum)。

          所以如何獲取這個SHA1的值,就需要提到Script命令。

          • 1、SCRIPT FLUSH :清除所有腳本緩存。
          • 2、SCRIPT EXISTS :根據給定的腳本校驗和,檢查指定的腳本是否存在于腳本緩存。
          • 3、SCRIPT LOAD :將一個腳本裝入腳本緩存,返回SHA1摘要,但并不立即運行它。
          • 4、SCRIPT KILL :殺死當前正在運行的腳本

          執行evalsha命令

          # 使用script load將腳本內容加載到緩存中,返回sha的值
          127.0.0.1:6379> script load "return redis.call('set',KEYS[1],ARGV[1])"
          "c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
          # 使用evalsha和返回的sha的值 + 參數個數 參數名稱和值執行
          127.0.0.1:6379> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 eval_02 002
          OK
          # 獲取結果
          127.0.0.1:6379> get eval_02
          "002"

          我們上面都是將腳本寫在代碼行里面,可以不可以將腳本內容寫在xxx.lua中,直接執行呢? 當然是可以的。

          使用redis-cli運行外置lua腳本

          編寫外置腳本test2.lua, 設置值到redis中。

          # 腳本內容 也就是設置一個值
          return redis.call('set',KEYS[1],ARGV[1])
          
          # 執行結果,可以使用./redis-cli -h 127.0.0.1 -p 6379 指定redis ip、端口等
          root@62ddf68b878d:/data# redis-cli --eval /data/test2.lua eval_03 , test03       
          OK

          利用Redis整合lua腳本,主要是為了保證性能是事務的原子性,因為redis的事務功能確實有些差勁!

          4、Redis的腳本復制

          Redis如果開啟了主從復制,腳本是如何從主服務器復制到從服務器的呢?

          首先,redis的腳本復制有兩種模式,腳本傳播模式和命令傳播模式。

          在開啟了主從,并且開啟了AOF持久化的情況下。

          4.1、腳本傳播模式

          其實就是主服務器執行什么腳本,從服務器就執行什么樣的腳本。但是如果有當前事件,隨機函數等會導致差異。

          主服務器執行命令

          # 執行多個redis命令并返回
          127.0.0.1:6379> eval "local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_01 eval_test_02 0001 0002
          1) OK
          2) OK
          127.0.0.1:6379> get eval_test_01
          "0001"
          127.0.0.1:6379> get eval_test_02
          "0002"

          那么主服務器將向從服務器發送完全相同的eval命令:

          eval "local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_01 eval_test_02 0001 0002

          注意:在這一模式下執行的腳本不能有時間、內部狀態、隨機函數等。執行相同的腳本以及參數必須產生相同的效果。在Redis5,也是處于同一個事務中。

          4.2、命令傳播模式

          處于命令傳播模式的主服務器會將執行腳本產生的所有寫命令用事務包裹起來,然后將事務復制到AOF文件以及從服務器里面.

          因為命令傳播模式復制的是寫命令而不是腳本本身,所以即使腳本本身包含時間、內部狀態、隨機函數等,主服務器給所有從服務器復制的寫命令仍然是相同的。

          為了開啟命令傳播模式,用戶在使用腳本執行任何寫操作之前,需要先在腳本里面調用以下函數:

          redis.replicate_commands()

          redis.replicate_commands() 只對調用該函數的腳本有效:在使用命令傳播模式執行完當前腳本之后,服務器將自動切換回默認的腳本傳播模式。

          執行腳本

          eval "redis.replicate_commands();local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_03 eval_test_04 0003 0004

          appendonly.aof文件內容

          *1
          $5
          MULTI
          *3
          $3
          set
          $12
          eval_test_03
          $4
          0003
          *3
          $3
          set
          $12
          eval_test_04
          $4
          0004
          *1
          $4
          EXEC

          可以看到,在一個事務里面執行了我們腳本執行的命令。

          同樣的道理,主服務器只需要向從服務器發送這些命令就可以實現主從腳本數據同步了。

          5、Redis的管道/事務/腳本

          • 1、管道其實就是一次性執行一批命令,不保證原子性,命令都是獨立的,屬于無狀態操作(也就是普通的批處理)
          • 2、事務和腳本是有原子性的,但是事務是弱原子性,lua腳本是強原子性。
          • 3、lua腳本可以使用lua語言編寫比較復雜的邏輯。
          • 4、lua腳本的原子性強于事務,腳本執行期間,另外的客戶端或其他的任何腳本或命令都無法執行。所以lua腳本的執行事件應該盡可能的短,不然會導致redis阻塞不能做其他工作。

          6、小結

          Redis的事務是弱事務,多個命令開啟事務一起執行性能比較低,且不能一定保證原子性。所以lua腳本就是對它的補充,它主要就是為了保證redis的原子性。

          比如有的業務(接口Api冪等性設計,生成token,(取出toker并判斷是否存在,這就不是原子操作))我們需要獲取一個key, 并且判斷這個key是否存在。就可以使用lua腳本來實現。

          還有很多地方,我們都需要redis的多個命令操作需要保證原子性,此時lua腳本可能就是一個不二選擇。

          7、相關文章

          本人還寫了Redis的其他相關文章,有興趣的可以點擊查看!

          • <<Redis持久化機制分析>>
          • <<Redis的事件處理機制分析>>
          • <<Redis客戶端和服務端如何通信?>>
          • <<redis的淘汰機制分析>>
          • <<Redis的底層數據結構分析>>
          • <<Redis的8種數據類型,什么場景使用?>>
          • <<緩存是什么?緩存有哪些分類?使用它的代價是什么?>>
          • <<緩存的6種常見的使用場景>>

          ua是一門腳本動態語言,并不太適合做復雜業務邏輯的程序開發,但是,在高并發場景下,Nginx Lua編程是解決性能問題的利器。

          Nginx Lua編程主要的應用場景如下:

          • API網關:實現數據校驗前置、請求過濾、API請求聚合、AB測試、灰度發布、降級、監控等功能,著名的開源網關Kong就是基于Nginx Lua開發的。
          • 高速緩存:可以對響應內容進行緩存,減少到后端的請求,從而提升性能。比如,Nginx Lua可以和Java容器、Redis整合,由Java容器進行業務處理和數據緩存,而Nginx負責讀緩存并進行響應,從而解決Java容器的性能瓶頸
          • 簡單的動態Web應用:可以完成一些業務邏輯處理較少但耗費CPU的簡單應用,比如模板頁面的渲染。一般的Nginx Lua頁面渲染處理流程為:從Redis獲取業務處理結果數據,從本地加載XML/HTML頁面模板,然后進行頁面渲染。
          • 網關限流:緩存、降級、限流是解決高并發的三大利器,Nginx內置了令牌限流的算法,但是對于分布式的限流場景,可以通過Nginx Lua編程定制自己的限流機制

          ngx_lua是Nginx的一個擴展模塊,將Lua VM嵌入Nginx,請求時創建一個VM,請求結束時回收VM,這樣就可以在Nginx內部運行Lua腳本,使得Nginx變成一個Web容器。以OpenResty為例,其提供了一些常用的ngx_lua開發模塊:

          • lua-resty-memcached:通過Lua操作memcache
          • lua-resty-mysql:通過Lua操作MySQL
          • lua-resty-redis:通過Lua操作Redis緩存
          • lua-resty-dns:通過Lua操作DNS域名服務器
          • lua-resty-limit-traffic:通過Lua進行限流
          • lua-resty-template:通過Lua進行模板渲染
          • lua-resty-jwt:通過Lua生成jwt
          • lua-resty-kafka:通過Lua操作kafka

          Lua腳本需要通過Lua解釋器來解釋執行,除了Lua官方的默認解釋器外,目前使用廣泛的Lua解釋器叫做LuaJIT。LuaJIT采用C語言編寫,被設計成全兼容標準Lua 5.1,因此LuaJIT代碼的語法和標準Lua的語法沒多大區別。但是LuaJIT的運行速度比標準Lua快數十倍。

          Nginx Lua的執行原理

          在OpenResty中,每個Worker進程使用一個Lua VM,當請求被分配到Worker時,將在這個Lua VM中創建一個協程,協程之間數據隔離,每個協程都具有獨立的全局變量。

          ngx_lua是將Lua VM嵌入Nginx,讓Nginx執行Lua腳本,并且高并發、非阻塞地處理各種請求Lua內置協程可以很好地將異步回調轉換成順序調用的形式。ngx_lua在Lua中進行的IO操作都會委托給Nginx的事件模型,從而實現非阻塞調用。開發者可以采用串行的方式編寫程序,ngx_lua會在進行阻塞的IO操作時自動中斷,保存上下文,然后將IO操作委托給Nginx事件處理機制,在IO操作完成后,ngx_lua會恢復上下文,程序繼續執行,這些操作對用戶程序都是透明的。

          每個Worker進程都持有一個Lua解釋器或LuaJIT實例,被這個Worker處理的所有請求共享這個實例。每個請求的context上下文會被Lua輕量級的協程分隔,從而保證每個請求是獨立的。

          ngx_lua采用one-coroutine-per-request的處理模型,對于每個用戶請求,ngx_lua會喚醒一個協程用于執行用戶代碼處理請求,當請求處理完成后,這個協程會被銷毀。每個協程都有一個獨立的全局環境,繼承于全局共享的、只讀的公共數據。所以,被用戶代碼注入全局空間的任何變量都不會影響其他請求的處理,并且這些變量在請求處理完成后會被釋放,這樣就保證所有的用戶代碼都運行在一個sandbox(沙箱)中,這個沙箱與請求具有相同的生命周期。

          得益于Lua協程的支持,ngx_lua在處理10000個并發請求時,只需要很少的內存。根據測試,ngx_lua處理每個請求只需要2KB的內存,如果使用LuaJIT就會更少

          Nginx Lua配置指令

          ngx_lua定義的Nginx配置指令大致如下:

          • lua_package_path:配置Lua外部庫的搜索路徑,搜索的文件類型為.lua。
          • lua_package_cpath:配置Lua外部搜索庫的搜索路徑,搜索C語言編寫的外部庫文件。
          • init_by_lua:Master進程啟動時掛載的Lua代碼塊,常用于導入公共模塊。
          • init_by_lua_file:Master進程啟動時掛載的Lua腳本文件。
          • init_worker_by_lua:Worker進程啟動時掛載的Lua代碼塊,常用于執行一些定時任務
          • init_worker_by_lua_file:Worker進程啟動時掛載的Lua文件,常用于執行一些定時任務
          • set_by_lua:類似于rewrite模塊的set指令,將Lua代碼塊的返回結果設置在Nginx的變量中。
          • set_by_lua_file:同上,執行的是腳本Lua腳本文件。
          • rewrite_by_lua:執行在rewrite階段的Lua代碼塊,完成轉發、重定向、緩存等功能。
          • rewrite_by_lua_file:同上,執行的是Lua腳本文件。
          • access_by_lua:執行在access階段的Lua代碼塊,完成IP準入、接口權限等功能。
          • access_by_lua_file:同上,執行的是Lua腳本文件。
          • content_by_lua:執行在content階段的Lua代碼塊,執行結果將作為請求響應的內容。
          • content_by_lua_file:同上,執行的是Lua腳本文件。
          • content_by_lua_block:content_by_lua的升級款,在一對花括號中編寫Lua代碼,而不需要做特殊字符轉譯。
          • header_filter_by_lua:響應頭部過濾處理的Lua代碼塊,可以用于添加設置響應頭部信息,如Cookie相關屬性。
          • body_filter_by_lua:響應體過濾處理的Lua代碼塊,例如加密響應體。
          • log_by_lua:異步完成日志記錄的Lua代碼塊,例如既可以在本地記錄日志,也可以記錄到ETL集群。

          ngx_lua配置指令在Nginx的HTTP請求處理階段所處的位置如圖:

          常用配置指令

          • lua_package_path指令:用于設置".lua"外部庫的搜索路徑,此指令的上下文為http配置塊,默認值為LUA_PATH環境變量內容或者lua編譯的默認值。
            • 格式:lua_package_path lua-style-path-str。
            • lua_package_cpath指令:用于設置Lua的C語言塊外部庫".so"(Linux)或".dll"(Windows)的搜索路徑,此指令的上下文為http配置塊。
            • 格式:lua_package_cpath lua-style-cpath-str
          http {
            ...
            #設置“.lua”外部庫的搜索路徑,此指令的上下文為http配置塊
          	#";;"常用于表示原始的搜索路徑
          	lua_package_path	"/foo/bar/?.lua;/blah/?.lua;;";
          	lua_package_cpath	"/usr/local/openresty/lualib/?/?.so;/usr/local/openresty/lualib/?.so;;";
          }
          

          對于以上兩個指令,OpenResty可以在搜索路徑中使用插值變量。例如,可以使用插值變量$prefix或${prefix}獲取虛擬服務器server的前綴路徑,server的前綴路徑通常在Nginx服務器啟動時通過-p PATH命令在指定。

          • init_by_lua指令:只能用于http上下文,運行在配置加載階段。當Nginx的master進程在加載Nginx配置文件時,在全局Lua VM級別上運行由參數lua-script-str指定的Lua腳本塊。若使用init_by_lua_file指令,后面跟lua文件的路徑( lua_file_path),則在全局Lua VM 級別上運行lua_file_path文件指定的lua腳本。如果Lua腳本的緩存是關閉的,那么每一次請求都運行一次init_by_lua處理程序。

          格式為:init_by_lua lua-script-str。

          • lua_load_cache指令:用于啟用或禁止Lua腳本緩存。可以使用的上下文為http、server、location配置塊。默認開啟。

          格式為:lua_code_cache on | off

          http {
            ...
          	#項目初始化
            init_by_lua_file	conf/luaScript/initial/loading_config.lua;
            	
            #調試模式,關閉lua腳本緩存
            lua_code_cache on;
            ...
          }

          在緩存關閉的時,set_by_lua_file、content_by_lua_file、access_by_lua_file、content_by_lua_file等指令中引用的Lua腳本都將不會被緩存,所有的Lua腳本都將從頭開始加載。

          • set_by_lua指令:將Lua腳本塊的返回結果設置在Nginx變量中。

          格式為:set_by_lua $destVar lua-script-str params

          location /set_by_lua_demo {
          	#set 指令定義兩個Nginx變量
            set $foo 1;
            set $bar 2;
            			
            #調用Lua內聯代碼,將結果放入Nginx變量$sum
            #Lua腳本的含義是,將兩個輸入參數$foo、$bar累積起來,然后相加的結果設置Nginx變量$sum中
            set_by_lua $sum 'return tonumber(ngx.arg[1]) + tonumber(ngx.arg[2])' $foo $bar;
            
            echo "$foo + $bar = $sum";
          }

          運行結果:

          ?  work curl http://localhost/set_by_lua_demo
          1 + 2 = 3
          • access_by_lua指令:執行在HTTP請求處理11個階段的access階段,使用Lua腳本進行訪問控制。運行于access階段的末尾,總是在allow和deny這樣的指令之后運行。

          格式為:access_by_lua $destVar lua-script-str

          location /access_demo {
            access_by_lua	'ngx.log(ngx.DEBUG, "remote_addr = "..ngx.var.remote_addr);
            if ngx.var.remote_addr == "192.168.56.121" then
            	return;
            end
            ngx.exit(ngx.HTTP_UNAUTHORIZED);
            ';
            echo "hello world";
          }
            		
          location /access_demo_2 {
            allow "192.168.56.121";
            deny all;
            echo "hello world";
          }

          運行結果:

          ?  work curl http://localhost/access_demo
          <html>
          <head><title>401 Authorization Required</title></head>
          <body bgcolor="white">
          <center><h1>401 Authorization Required</h1></center>
          <hr><center>openresty/1.13.6.2</center>
          </body>
          </html>
          
          #上述案例運行日志:
          2022/02/15 10:32:17 [debug] 26293#0: *17 [lua] access_by_lua(nginx-lua-demo.conf:85):1: remote_addr = 127.0.0.1
          2022/02/15 10:32:17 [info] 26293#0: *17 kevent() reported that client 127.0.0.1 closed keepalive connection
          
          ?  work curl http://localhost/access_demo_2
          <html>
          <head><title>403 Forbidden</title></head>
          <body bgcolor="white">
          <center><h1>403 Forbidden</h1></center>
          <hr><center>openresty/1.13.6.2</center>
          </body>
          </html>
          
          #上述案例運行日志
          2022/02/15 10:33:11 [error] 26293#0: *18 access forbidden by rule, client: 127.0.0.1, server: localhost, request: "GET /access_demo_2 HTTP/1.1", host: "localhost"
          2022/02/15 10:33:11 [info] 26293#0: *18 kevent() reported that client 127.0.0.1 closed keepalive connection
          • content_by_lua/content_by_lua_block指令:用于設置執行在content階段的Lua代碼塊,執行結果將作為請求響應的內容。該指令用于location上下文。

          格式為:content_by_lua lua-script-str

          location /errorLog {
            content_by_lua '
              ngx.log(ngx.ERR, "this is an error log ");
            	ngx.say("錯誤日志調用成功");
            ';
          }
            		
          location /infoLog {
          	content_by_lua '
          		ngx.log(ngx.ERR, "this is an info log ");
            	ngx.say("業務日志調用成功");
            ';
          }
          
          location /debugLog {
            content_by_lua '
              ngx.log(ngx.ERR, "this is an debug log ");
            	ngx.say("調試日志調用成功");
            ';
          }

          OpenResty v0.9.17版本以后,使用content_by_lua_block指令代替content_by_lua指令,避免對代碼塊中的字符串進行轉譯。

          運行結果:

          ?  work curl http://localhost/errorLog
          錯誤日志調用成功
          ?  work curl http://localhost/infoLog 
          業務日志調用成功
          ?  work curl http://localhost/debugLog
          調試日志調用成功

          Nginx Lua的內置常量和變量

          內置變量

          • ngx.arg:類型為Lua table,ngx.arg.VARIABLE用于獲取ngx_lua配置指令后面的調用參數。
          • ngx.var:類型為Lua table,ngx.var.VARIABLE用于引用某個Nginx變量。前提是Nginx變量必須提前聲明
          • ngx.ctx:類型為Lua table,可以用來訪問當前請求的Lua上下文數據,其生存周期與當前請求相同
          • ngx.header:類型為Lua table,用于訪問HTTP響應頭,可以通過ngx.header.HEADER形式引用某個頭
          • ngx.status:用于設置當前請求的HTTP響應碼

          內置常量

          內置常量基本是見名知意的,可以根據后面的實戰案例,加深理解。

          核心常量

            • ngx.OK(0)
            • ngx.ERROR(-1)
            • ngx.AGAIN(-2)
            • ngx.DONE(-4)
            • ngx.DECLINED(-5)
            • ngx.nil

          HTTP方法常量

            • ngx.HTTP.GET
            • ngx.HTTP.HEAD
            • ngx.HTTP.PUT
            • ngx.HTTP.POST
            • ngx.HTTP.DELETE
            • ngx.HTTP.OPTIONS
            • ngx.HTTP.MKCOL
            • ngx.HTTP.MOVE
            • ngx.HTTP.PROPFIND
            • ngx.HTTP.PROPPATCH
            • ngx.HTTP.LOCK
            • ngx.HTTP.UNLOCK
            • ngx.HTTP.PATH
            • ngx.HTTP.TRACE

          HTTP狀態碼常量

            • ngx.HTTP_OK(200)
            • ngx.HTTP_CREATED(201)
            • ngx.HTTP_SPECIAL_RESPONSE(300)
            • ngx.HTTP_MOVED_PERMANENTLY(301)
            • ngx.HTTP_MOVER_TEMPORARILY(302)
            • ngx.HTTP_SEE_OTHER(303)
            • ngx.HTTP_NOT_MODIFIED(304)
            • ngx.HTTP_BAD_REQUEST(400)
            • ngx.HTTP_UNAUTHORIZED(401)
            • ngx.HTTP_FORBIDDEN(403)
            • ngx.HTTP_NOT_FOUND(404)
            • ngx.HTTP_NOT_ALLOWED(405)
            • ngx.HTTP_GONE(410)
            • ngx.HTTP_INTERNAL_SERVER_ERROR(500)

          日志類型常量

            • ngx.STDERR
            • ngx.EMERG
            • ngx.ALERT
            • ngx.CRIT
            • ngx.ERR
            • ngx.WARE
            • ngx.NOTICE
            • ngx.INFO
            • ngx.DEBUG

          Nginx+LUA基礎到此結束,下一篇開始實戰!并在實戰中掌握基礎。


          主站蜘蛛池模板: 国产成人高清视频一区二区| 无码一区二区三区亚洲人妻| 成人精品一区二区三区电影| 一区二区三区四区在线观看视频| 午夜福利av无码一区二区 | 国产一区二区三区亚洲综合| 欧美日韩综合一区二区三区| 亚洲AV午夜福利精品一区二区| 国产成人一区二区三区高清| 国产精品久久一区二区三区| 人妻少妇AV无码一区二区| 国产高清在线精品一区二区| 国产免费av一区二区三区| 毛片无码一区二区三区a片视频| 亚洲成AV人片一区二区| 亚洲国产一区二区视频网站| 国产福利一区二区三区在线视频 | 日韩精品一区二区三区影院| 免费播放一区二区三区| 美女毛片一区二区三区四区| 国精产品一区一区三区有限公司| 91久久精品无码一区二区毛片| 黄桃AV无码免费一区二区三区| 国产aⅴ精品一区二区三区久久| 福利国产微拍广场一区视频在线| 国产精品揄拍一区二区久久| 色婷婷综合久久久久中文一区二区| 视频在线一区二区| 国产一区内射最近更新| 日本精品一区二区三区在线视频一| 久久精品免费一区二区喷潮| 日本精品视频一区二区| 成人精品一区久久久久| 视频在线观看一区| 制服中文字幕一区二区| 3d动漫精品成人一区二区三| 人妻无码久久一区二区三区免费| 91一区二区在线观看精品| 国产自产对白一区| 中日韩精品无码一区二区三区| 在线观看日本亚洲一区|