1. 程式人生 > 實用技巧 >nginx+lua 記一次特殊字元導致丟包問題

nginx+lua 記一次特殊字元導致丟包問題

前言

&符號在http請求中,是作為引數分隔符使用的,如果傳入的傳入的引數裡面有&的話,那麼就會導致獲取引數的時獲取不到完整的值。

架構介紹

客戶端 ---> 代理程式(nginx+lua) ---> 服務端

lua發起http請求是使用resty.http這個模組

  1. 客戶端發起一個請求,如GET http://proxy.com/?url=baidu.com&userid=123
  2. 請求到了代理程式,代理程式先把url這個引數解開,發現是要攜帶userid=123 以GET方法去訪問baidu.com這個地址,於是代理程式就這樣去訪問了。
  3. 服務端(baidu.com)處理完請求後,返回結果。
  4. 代理程式拿到服務端結果後,返回給客戶端。

開始

測試同學反饋說有個大號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

  1. body 是body的長度,body作為引數傳入 send_body 這個方法裡面。
  2. 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