wireshark的協議不支持之前,報文幾乎難以分析,舉例如下
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函數有三個入參,buffer,pinfo,和tree。buffer包含了網絡包的內容,是一個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中了
https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html#lua_fn_ProtoField_char_abbr___name____base____valuestring____mask____desc__
https://ask.wireshark.org/question/15787/how-to-decode-protobuf-by-wireshark/
簡單來說,事務(transaction)是指單個邏輯單元執行的一系列操作。
事務有如下四大特性:
Redis中的事務通過multi,exec,discard,watch這四個命令來完成。
Redis的單個命令都是原子性的,所以確保事務的就是多個命令集合一起執行。
Redis命令集合打包在一起,使用同一個任務確保命令被依次有序且不被打斷的執行,從而保證事務性。
Redis是弱事務,不支持事務的回滾。
事務命令簡介
事務操作
# 普通的執行多個命令
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
我們前面總是在說,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的事務執行流程分析
我們知道,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;
為什么說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確實不支持事務回滾。因為我們事務失敗了,但是命令卻是執行成功了。
弱事務總結
那么,redis就沒有辦法保證原子性了嗎,當然有,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(阿里)
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
Redis從2.6開始,就內置了lua編譯器,可以使用EVAL命令對lua腳本進行求值。
腳本命令是原子性的,Redis服務器再執行腳本命令時,不允許新的命令執行(會阻塞,不在接受命令)。、
EVAL命令
通過執行redis的eval命令,可以運行一段lua腳本。
EVAL script numkeys key [key ...] arg [arg ...]
EVAL命令說明
簡單來說,就是
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呢?
其實就是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命令。
執行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的事務功能確實有些差勁!
Redis如果開啟了主從復制,腳本是如何從主服務器復制到從服務器的呢?
首先,redis的腳本復制有兩種模式,腳本傳播模式和命令傳播模式。
在開啟了主從,并且開啟了AOF持久化的情況下。
其實就是主服務器執行什么腳本,從服務器就執行什么樣的腳本。但是如果有當前事件,隨機函數等會導致差異。
主服務器執行命令
# 執行多個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,也是處于同一個事務中。
處于命令傳播模式的主服務器會將執行腳本產生的所有寫命令用事務包裹起來,然后將事務復制到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
可以看到,在一個事務里面執行了我們腳本執行的命令。
同樣的道理,主服務器只需要向從服務器發送這些命令就可以實現主從腳本數據同步了。
Redis的事務是弱事務,多個命令開啟事務一起執行性能比較低,且不能一定保證原子性。所以lua腳本就是對它的補充,它主要就是為了保證redis的原子性。
比如有的業務(接口Api冪等性設計,生成token,(取出toker并判斷是否存在,這就不是原子操作))我們需要獲取一個key, 并且判斷這個key是否存在。就可以使用lua腳本來實現。
還有很多地方,我們都需要redis的多個命令操作需要保證原子性,此時lua腳本可能就是一個不二選擇。
本人還寫了Redis的其他相關文章,有興趣的可以點擊查看!
ua是一門腳本動態語言,并不太適合做復雜業務邏輯的程序開發,但是,在高并發場景下,Nginx Lua編程是解決性能問題的利器。
Nginx Lua編程主要的應用場景如下:
ngx_lua是Nginx的一個擴展模塊,將Lua VM嵌入Nginx,請求時創建一個VM,請求結束時回收VM,這樣就可以在Nginx內部運行Lua腳本,使得Nginx變成一個Web容器。以OpenResty為例,其提供了一些常用的ngx_lua開發模塊:
Lua腳本需要通過Lua解釋器來解釋執行,除了Lua官方的默認解釋器外,目前使用廣泛的Lua解釋器叫做LuaJIT。LuaJIT采用C語言編寫,被設計成全兼容標準Lua 5.1,因此LuaJIT代碼的語法和標準Lua的語法沒多大區別。但是LuaJIT的運行速度比標準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就會更少。
ngx_lua定義的Nginx配置指令大致如下:
ngx_lua配置指令在Nginx的HTTP請求處理階段所處的位置如圖:
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 lua-script-str。
格式為: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 $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 $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 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
調試日志調用成功
內置變量
內置常量
內置常量基本是見名知意的,可以根據后面的實戰案例,加深理解。
核心常量
HTTP方法常量
HTTP狀態碼常量
日志類型常量
Nginx+LUA基礎到此結束,下一篇開始實戰!并在實戰中掌握基礎。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。