Nginx實現Socket代理功能,根據Socket報文內容動態代理


近期有一需求:原有一Socket服務端(以下稱為A),可以處理一些固定類型的報文,在不能修改A的情況下,需要增加額外的報文類型處理支持。

考慮到A服務不能夠被修改,所以必須新增B服務來處理增量報文類型,但這樣客戶端就需要配置兩個Socket地址,並且根據報文類型來判斷應該發往哪個地址,這樣一來對於客戶端的修改非常大,並且不符合開閉原則,因此最終決定將B服務與A服務並列部署,並且在其上游增加反向代理服務,由反向代理服務來決定報文應該發往A還是B。

形成設計如下圖:

B服務的功能與A服務類似,就不展開多說了,下面主要對於反向代理的選型與設計進行介紹。

反向代理最初有兩種方案:1、代碼實現,2、Nginx轉發。

代碼實現初步設計如下:

代碼實現其實就是在一個Socket連接執行的過程中,內嵌一個Socket連接,去完成反向代理操作,需求看上去是可以滿足的,但是由於開發、測試工作量大,並且報文內容多的話,有可能出現客戶端Socket連接超時的情況,所以沒有被選做最優方案。

於是又開始進行了Nginx代理Socket請求的研究。

Nginx一般被用於代理Http、Https請求,1.9.0版本增加了對於Socket請求的處理——stream模塊。

但是只是代理Socket請求是不夠的,還需要根據Socket報文中的報文類型去動態分發到不同的服務端才行,於是又引入了Lua的概念——Lua是一種輕量小巧的腳本語言,可以很方便的嵌入別的程序里。

在部門開發前輩的建議下,最終采用了Openresty進行開發,OpenResty是一個基於 Nginx 與 Lua 的高性能 Web 平台,其內部集成了大量精良的 Lua 庫、第三方模塊以及大多數的依賴項。用於方便地搭建能夠處理超高並發、擴展性極高的動態 Web 應用、Web 服務和動態網關。

思路如下:

(1)在Nginx配置中引入stream模塊;

(2)在stream模塊中建立多個路由upstream,指向服務A與服務B;

(3)在stream模塊中使用Lua獲取Socket報文,並截取報文類型字段;

(4)根據報文類型字段確定最終使用的upstream;

(5)代理到相應路由

最終學習 + 開發使用了不到2天的時間,中間采坑記錄如下:

(1)Nginx的stream模塊中無法使用set $var方式建立變量

Nginx的stream模塊暫時不支持像Http模塊一樣,使用set $var的方式建立變量,否則會報錯。

在仔細研究了Openresty文檔后,發現Openresty針對這一情況提供了建立變量的方式——StreamLuaNginxModule模塊的lua_add_variable表達式。

lua_add_variable的作用域為Nginx的stream模塊中,使用方法如下:

stream {
    upstream serviceA {
      server 127.0.0.1:5432;
    }
    upstream serviceB {
      server 127.0.0.1:5433;
    }

    lua_add_variable $proxy;

    #...
}

使用lua_add_variable定義了變量后,即可在Nginx中直接調用,或者在Lua中使用ngx.var.proxy調用並修改變量值。

(2)Lua代碼在轉發前不執行

不執行代碼如下:

stream {
    #...

    server {		
        listen 5430;
        content_by_lua_block{
	    local sock = ngx.req.socket(true)
	    local reader = sock:receiveuntil("\r\n")
            local data, err, partial = reader()
            -- ...
	}
	proxy_pass $proxy;		
    }
}

經查資料,發現“content_by_lua_block是內容處理器,接受請求並輸出響應”,於是懷疑content_by_lua_block域的代碼是返回的時候執行的,將content_by_lua_block更換為preread_by_lua_block即可,preread_by_lua_block為預讀處理器,執行順序在轉發之前。

修改后代碼:

stream {
    #...

    server {		
        listen 5430;
        preread_by_lua_block{
	    local sock = ngx.req.socket(true)
	    local reader = sock:receiveuntil("\r\n")
            local data, err, partial = reader()
            -- ...
	}
	proxy_pass $proxy;		
    }
}

 

(3)Lua獲取Socket報文后,Socket連接中的報文會被消耗,判斷並轉發完成后,下游服務A / B無法再次獲取該Socket連接中的報文

Lua獲取Socket報文代碼如下:

stream {
    #...

    server {		
        listen 5430;
        preread_by_lua_block{
	    local sock = ngx.req.socket(true)
	    local reader = sock:receiveuntil("\r\n")
            local data, err, partial = reader()
            -- ...
	}
	proxy_pass $proxy;		
    }
}

 

經查看Openresty文檔發現:

If any request body data has been pre-read into the Nginx core request header buffer, the resulting cosocket object will take care of this to avoid potential data loss resulting from such pre-reading. Chunked request bodies are not yet supported in this API.

如果cosocket對象(ngx.req.socket(true)將返回cosocket對象)已經將請求報文數據讀取到Nginx core request header buffer中(已經使用receive / receiveunitl方法獲取報文),會出現此類因為預讀取導致的報文數據丟失。

所以cosocket對象在這個業務中不能使用receive相關方法獲取報文數據,經過研究發現了cosocket的另一個獲取報文方法:peek。

官方對於peek方法的說明如下:

Peeks into the preread buffer that contains downstream data sent by the client without consuming them. That is, data returned by this API will still be forwarded upstream in later phases.

瞥一眼預讀取緩沖區,該緩沖區包含客戶端發送的報文數據,而不消耗這些數據。也就是說,這個API返回的數據在以后的階段仍然會被轉發到下游。

所以將代碼修改為:

stream {

    #...

    server {
        listen 5430;

        preread_by_lua_block {
	    local sock = ngx.req.socket()
            local data = sock:peek(4) -- 瞥一眼報文的前4個字符
            
            --...
	}

        proxy_pass $proxy;
    }
}

最終代碼生效。

完整代碼如下:

stream {

    upstream serverA{
        server 127.0.0.1:5432;
    }

    upstream serverB{
        server 127.0.0.1:5431;
    }

    lua_add_variable $proxy;

    server {
        listen 5430;
        preread_by_lua_block {
	    local sock = ngx.req.socket()
            local data = sock:peek(4)
            --根據data判斷下游服務,將$proxy的值修改為serviceA或serviceB           
           if (data == "0000") then
		ngx.var.proxy= "serverA";
	    else
		ngx.var.proxy= "serverB";
            end
	}

        proxy_pass $proxy;
    }
}    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM