nginx+lua 記一次特殊字元導致丟包問題
前言
&符號在http請求中,是作為引數分隔符使用的,如果傳入的傳入的引數裡面有&的話,那麼就會導致獲取引數的時獲取不到完整的值。
架構介紹
客戶端 ---> 代理程式(nginx+lua) ---> 服務端
lua發起http請求是使用resty.http這個模組
- 客戶端發起一個請求,如GET http://proxy.com/?url=baidu.com&userid=123
- 請求到了代理程式,代理程式先把url這個引數解開,發現是要攜帶userid=123 以GET方法去訪問baidu.com這個地址,於是代理程式就這樣去訪問了。
- 服務端(baidu.com)處理完請求後,返回結果。
- 代理程式拿到服務端結果後,返回給客戶端。
開始
測試同學反饋說有個大號json串 在通過代理程式的時候有問題,服務端返回的內容類似於 : 傳入的引數不是完整的 。於是我看這個POST請求的大號json串 有7K 長,而服務端日誌顯示他只收到了3.6K,丟了一半的資料。
我一開始看到這個問題,就想著“丟包”這方面去了,於是翻閱 resty。http的原始碼,地址:https://github.com/ledgetech/lua-resty-http/blob/master/lib/resty/http.lua , 我使用的是request_uri 方法來發起的請求,發起請求的程式碼如下:
res,err = httpc:request_uri(url, { method = method, body = body, headers = headers, keepalive_timeout = 60000, -- ms keepalive_pool = 20, })
排查http模組原始碼
於是,我在官網翻閱 http.lua 裡面的 request_uri 的原始碼,發現傳送請求引數的最終呼叫的 ngx.socket.tcp 來發送的 ,請參考在558行 的 send_body 方法。 我初步懷疑是 會不會socket 的限制了傳送長度呢,於是參考ngx.socket.tcp的官網介紹,說send在傳送玩資料之後,會返回傳送資料的長度,於是我就在 http.lua 的 576行打了一行日誌,看看到底傳送了多少資料以至於丟包。日誌程式碼如下:
573 local bytes, err = sock:send(body) 574 ngx_log(ngx_ERR,"chunk_len:",#body , " , send_length:", bytes) 575 if not bytes then 576 return nil, err 577 end
- body 是body的長度,body作為引數傳入 send_body 這個方法裡面。
- bytes 是 傳送位元組數的長度。
於是,再次訪問代理程式,很快啊,就把日誌打印出來了。日誌內容如下
2020/12/09 16:06:32 [error] 7855#0: *3364 [lua] http.lua:574: _send_body(): chunk_len:7131 , send_length:7131
一看日誌,發現socket並沒有因為buffer或者其他原因導致傳送的資料不完整,也就是傳入多少就傳送多少,這個沒問題的,關於buffer ,我特意在nginx的配置檔案設定了下,如下所示:
lua_shared_dict api_root_sysConfig 10240k;
lua_shared_dict kv_api_root_upstream 10240k;
lua_socket_connect_timeout 60s;
lua_socket_send_timeout 60s;
lua_socket_read_timeout 60s;
lua_socket_pool_size 400;
lua_socket_keepalive_timeout 60s;
lua_socket_buffer_size 64k; # 這是設定buffer大小
lua_code_cache on;
lua_ssl_verify_depth 4;
lua_ssl_trusted_certificate "/etc/ssl/certs/ca-lua.pem" ;
lua_package_path "/opt/nginx_2.3.2/lua_gray/?.lua;/opt/nginx_2.3.2/lua_gray/lib/?.lua;/opt/nginx_2.3.2/lua_gray/lib/lua-resty-core/lib/?.lua;/opt/nginx_2.3.2/lua_gray/lib/lua-resty-http/lib/?.lua;";
lua_need_request_body on;
抓包排查網路問題
可是還是懷疑丟包的問題,於是用tcpdump來抓包看看(tcpdump -i eth0 dst host 172.18.21.195 -w /tmp/max_length.pcap
),我們在代理程式的伺服器上(172.18.21.239)抓取了 目地址為 後端伺服器本地IP的包,開啟後發現,確確實實發出了7K多的資料:
於是我們又在伺服器端(172.18.21.195) 抓取來自於代理程式的(172.18.21.239)的包,發現也確確實實收到了7K的資料,也就是資料沒有丟失再網路中。
tcpdump -i eth0 src host 172.18.21.239 -w /tmp/src_21.239.pcap
那問題就來了,丟失的資料哪裡去了?
請求引數仔細核對
我想了一會,仔細看了傳輸的資料,發現數據中含有中文,並且幾個中文之間有 & ,例如 "淘寶&平多多" 這種格式,然後看了看伺服器收到的資料剛剛好就在 & 符號前面,頓時煥然大悟,原來是 & 符號在傳輸中變成了間隔符,所以伺服器端收到資料是完整的,壞就懷在 從這大json串裡面獲取一個值的時候由於&符號導致取到的資料不完整。
知道原因了,那就知道怎麼改了。改動的程式碼主要是把&轉義下,也就是url 編碼。如下所示:
v = string.gsub(v,"%&","%%26") -- 轉義與符號
完整的程式碼如下:
local mix_args = function(post_data,headers,post_body)
-- 混合引數,把table型別的引數變為 a=1&b=2,,用與符號連結
@post_data :提交的資料
@headers: 頭資訊
@post_body: 用於拼接的字串。適用於連續拼接請求體
if post_body == nil then
post_body = ""
end
for k,v in pairs(post_data) do
--log(ERR,"get k:",k,",v:",v)
if string.lower(k) == "url" then
-- log(ERR,"k is url ,v is",v)
if string.find(v,"?") ~= nil then
local url_array = split(v,"?") -- k is url
local url = url_array[1]
local arg = url_array[2]
local arg_array = split(arg,"=")
post_body = post_body ..tostring( arg_array[1] ).."="..tostring( arg_array[2] ).."&"
end
else
if type(v) == "string" then
if string.find(v,"Date") ~= nil then
v = string.gsub(v,"%+","%%2B") -- 轉義加號
end
v = string.gsub(v,"%&","%%26") -- 轉義與符號
end
post_body = post_body ..tostring(k).."="..tostring(v).."&"
end
end
local post_body_len = string.len(post_body)
post_body = string.sub(post_body,0,post_body_len-1) -- 去掉最後一位與符號&
--log(ERR,"mix_args post_body: ",post_body )
return post_body
end
這樣的引數體,推給伺服器端就沒問題了。
附贈特殊符號轉義
符號 | url中轉義結果 | 轉義碼 |
---|---|---|
+ | URL 中+號表示空格 | %2B |
空格 | URL中的空格可以用+號或者編碼 | %20 |
/ | 分隔目錄和子目錄 | %2F |
? | 分隔實際的URL和引數 | %3F |
% | 指定特殊字元 | %25 |
# | 表示書籤 | %23 |
& | URL 中指定的引數間的分隔符 | %26 |
= | URL 中指定引數的值 | %3D |