Golang實(shí)現(xiàn)四層負(fù)載均衡的示例代碼
本文代碼已經(jīng)上傳到github
https://github.com/HobbyBear/codelearning/tree/master/layer4balance
為了知識(shí)的完整性,我們也科普下七層負(fù)載均衡的概念,我們先簡(jiǎn)單了解下四層負(fù)載均衡和7層負(fù)載均衡的區(qū)別。
四層負(fù)載均衡和七層負(fù)載均衡
七層負(fù)載均衡
首先,我們來(lái)看下七層負(fù)載均衡,它一般是針對(duì)應(yīng)用層請(qǐng)求協(xié)議做請(qǐng)求轉(zhuǎn)發(fā),拿http請(qǐng)求舉例,有A,B兩臺(tái)服務(wù)器,如果采用輪詢(xún)的負(fù)載均衡策略,負(fù)載均衡器將第一個(gè)請(qǐng)求轉(zhuǎn)發(fā)給了A服務(wù)器,那么第二個(gè)請(qǐng)求到達(dá)時(shí),負(fù)載均衡器就會(huì)把請(qǐng)求轉(zhuǎn)發(fā)到B服務(wù)器。
在轉(zhuǎn)發(fā)時(shí),能夠在應(yīng)用協(xié)議層對(duì)請(qǐng)求做一些變動(dòng),拿http請(qǐng)求來(lái)說(shuō),可以對(duì)http的請(qǐng)求頭,http路徑做相應(yīng)的變動(dòng)。
四層負(fù)載均衡
再來(lái)看看四層負(fù)載均衡,它一般是指針對(duì)連接做的負(fù)載均衡,舉例說(shuō)明下,有A,B兩臺(tái)服務(wù)器,同樣采取輪詢(xún)的策略,某個(gè)客戶端發(fā)起一個(gè)新的連接,經(jīng)過(guò)均衡器連接到了A服務(wù)器,現(xiàn)在又來(lái)一個(gè)客戶端同樣發(fā)起連接,經(jīng)過(guò)均衡器后,此時(shí)就該和B服務(wù)器建立連接了。而在同一個(gè)連接里是能夠發(fā)送多個(gè)請(qǐng)求的,這也是和七層負(fù)載均衡最本質(zhì)的區(qū)別,它是針對(duì)連接做的負(fù)載均衡。
實(shí)現(xiàn)四層負(fù)載均衡器
實(shí)現(xiàn)四層負(fù)載均衡策略的方式有很多,比較著名的四層負(fù)載均衡軟件就有l(wèi)vs,它是通過(guò)修改數(shù)據(jù)包的ip地址或者mac地址實(shí)現(xiàn)四層負(fù)載均衡,性能較好,工作模式有好幾種,具體的就不在本文展開(kāi)了。
本文實(shí)現(xiàn)的四層負(fù)載均衡的原理和nginx四層負(fù)載類(lèi)似 ,通過(guò)均衡器在客戶端和服務(wù)端之前都維護(hù)一個(gè)連接來(lái)達(dá)到讓 客戶端在同一個(gè)連接里發(fā)送的請(qǐng)求都會(huì)被服務(wù)端同一個(gè)連接所接收的目的。如下圖所示:
以后client1 通過(guò)連接A發(fā)的請(qǐng)求都會(huì)由連接B發(fā)往服務(wù)器,而client2通過(guò)連接C發(fā)送的請(qǐng)求,都將經(jīng)過(guò)連接D發(fā)往另一臺(tái)服務(wù)器。
實(shí)現(xiàn)邏輯
現(xiàn)在讓我們來(lái)實(shí)現(xiàn)下這部分的邏輯,我將會(huì)以輪詢(xún)的策略實(shí)現(xiàn)連接的負(fù)載均衡。
并且這里還要考慮下實(shí)現(xiàn)數(shù)據(jù)復(fù)制的邏輯,我們需要在均衡器分別建立對(duì)客戶端和服務(wù)端的socket連接,并且將其中一個(gè)socket的數(shù)據(jù)轉(zhuǎn)移到另一個(gè)socket,如果每次都將某一個(gè)socket數(shù)據(jù)讀到用戶層,再寫(xiě)到另一個(gè)socket就會(huì)導(dǎo)致一些沒(méi)有必要的拷貝。偽代碼如下:
var ( src net.Conn // 一個(gè)socket 連接 dst net.Conn // 一個(gè)socket連接 ) // ... buf = make([]byte, size) nr, er := src.Read(buf) nw, ew := dst.Write(buf[0:nr])
有沒(méi)有什么技術(shù)讓內(nèi)核自動(dòng)將某個(gè)socket的數(shù)據(jù)轉(zhuǎn)移到另一個(gè)socket,不用將數(shù)據(jù)拷貝到應(yīng)用層來(lái),這正是零拷貝相關(guān)的技術(shù),關(guān)于零拷貝的技術(shù)原理我在之前這篇文章 有很詳細(xì)的介紹,內(nèi)核提供了一個(gè)splice的系統(tǒng)調(diào)用,專(zhuān)門(mén)用于socket連接間拷貝數(shù)據(jù),只需要調(diào)用時(shí)傳入對(duì)應(yīng)socket連接的文件描述符即可讓內(nèi)核自動(dòng)完成拷貝過(guò)程。
func?Splice(rfd?int,?roff?*int64,?wfd?int,?woff?*int64,?len?int,?flags?int)?(n?int64,?err?error)?
這個(gè)系統(tǒng)調(diào)用已經(jīng)被golang更深層次的封裝到了一個(gè)比較常用的方法io.Copy里,這個(gè)方法會(huì)自動(dòng)判斷reader和writer底層的類(lèi)型,如果都是socket連接則會(huì)調(diào)用splice系統(tǒng)調(diào)用實(shí)現(xiàn)零拷貝。
func Copy(dst Writer, src Reader) (written int64, err error) { return copyBuffer(dst, src, nil) }
接著我們看下均衡的代碼邏輯,運(yùn)行邏輯如下:
1, 監(jiān)聽(tīng)到新連接,啟動(dòng)一個(gè)協(xié)程去處理連接。
2 , 在新協(xié)程里與通過(guò)輪詢(xún)的策略,選擇一個(gè)后端服務(wù)器并與之建立連接。
3, 啟動(dòng)兩個(gè)協(xié)程分別進(jìn)行io.Copy ,將客戶端的socket寫(xiě)到服務(wù)端socket,將服務(wù)端socket返回的信息寫(xiě)到客戶端socket。代碼如下:
type Server struct { Li net.Listener Balance balancepolicy.Policy } func (s *Server) Run() { for { c, err := s.Li.Accept() if err != nil { log.Fatal(err) } go func(c net.Conn) { remoteAddr := c.RemoteAddr() backendIp := s.Balance.PickNode(remoteAddr.String()) serverConn, err := net.Dial("tcp", backendIp) if err != nil { log.Fatal(err) c.Close() return } fmt.Println("獲取到了新連接", remoteAddr, backendIp) go func() { _, err := io.Copy(serverConn, c) if err != nil { fmt.Println(err, 1) } c.Close() serverConn.Close() fmt.Println("結(jié)束1", err) }() go func() { _, err := io.Copy(c, serverConn) if err != nil { fmt.Println(err, 2) } c.Close() serverConn.Close() fmt.Println("結(jié)束2", err) }() }(c) } }
io.Copy 會(huì)不斷的拷貝源socket的數(shù)據(jù)到目的socket,直到連接關(guān)閉。
更好的方案
可以看到上述方案中維護(hù)一個(gè)客戶端的連接將會(huì)啟動(dòng)3個(gè)協(xié)程,當(dāng)連接量上去后,均衡器很可能成為瓶頸,有沒(méi)有辦法減少下協(xié)程的數(shù)量,可以直接采用epoll的方式監(jiān)聽(tīng)連接的讀寫(xiě),以及關(guān)閉事件(這樣能在一個(gè)協(xié)程里處理多個(gè)連接),當(dāng)連接可讀時(shí),直接使用splice系統(tǒng)調(diào)用對(duì)數(shù)據(jù)進(jìn)行拷貝直到返回syscall.EAGAIN 就停止,因?yàn)榉祷豷yscall.EAGAIN 說(shuō)明連接緩沖區(qū)內(nèi)的數(shù)據(jù)暫時(shí)被讀取完了,繼續(xù)下一次epoll wait的監(jiān)聽(tīng)循環(huán)。這樣能極大的減少協(xié)程數(shù)量。不過(guò)實(shí)現(xiàn)我就不準(zhǔn)備再繼續(xù)展開(kāi)了,后續(xù)有空再補(bǔ)充下這部分。對(duì)epoll的使用有興趣的同學(xué)也可以看看我之前一篇用epoll實(shí)現(xiàn)類(lèi)似redis的網(wǎng)絡(luò)模型框架這篇文章
測(cè)試負(fù)載均衡代碼
現(xiàn)在讓我們來(lái)測(cè)試下負(fù)載均衡的代碼,我會(huì)用docker-compose去啟動(dòng)兩個(gè)mysql,然后本地啟動(dòng)我們負(fù)載均衡器的代碼,之后用兩個(gè)mysql客戶端去連接負(fù)載均衡器,看下是不是mysql客戶端連接到了不同的mysql服務(wù)器。
docker-compose的配置文件如下:
version: '3' services: mysql1: restart: always image: amd64/mysql:latest container_name: mysql1 environment: - "MYSQL_ROOT_PASSWORD=1234567" - "MYSQL_DATABASE=test" ports: - "3306:3306" mysql2: restart: always image: amd64/mysql:latest container_name: mysql2 environment: - "MYSQL_ROOT_PASSWORD=1234567" - "MYSQL_DATABASE=test2" ports: - "3307:3306"
為了能驗(yàn)證不同客戶端的確連上了不同的mysql服務(wù)器,我在mysql1上創(chuàng)建了test數(shù)據(jù)庫(kù),在mysql2上創(chuàng)建了test2數(shù)據(jù)庫(kù)。到時(shí)候連上不同服務(wù)器數(shù)據(jù)庫(kù)是不一樣的。
均衡服務(wù)器監(jiān)聽(tīng)5555端口啟動(dòng)
s := &proxy.Server{}
li, err := net.Listen("tcp", ":5555")
if err != nil {
log.Fatal(err)
}
s.Li = li
s.Balance = balancepolicy.NewRoundRobin()
s.Balance.AddNode("127.0.0.1:3306", "mysql1")
s.Balance.AddNode("127.0.0.1:3307", "mysql2")
s.Run()
之后用mysql客戶端去連接均衡服務(wù)器
## client1
mysql -h 127.0.0.1 -u root -P 5555 -D test -p1234567
## client2
mysql -h 127.0.0.1 -u root -P 5555 -D test2 -p1234567
發(fā)現(xiàn)兩個(gè)mysql客戶端的確連接到了不同服務(wù)器,并且能正常執(zhí)行命令,over。
到此這篇關(guān)于Golang實(shí)現(xiàn)四層負(fù)載均衡的示例代碼的文章就介紹到這了,更多相關(guān)Golang四層負(fù)載均衡內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang中生成隨機(jī)字符串并復(fù)制到粘貼板的方法
這篇文章主要介紹了Golang中生成隨機(jī)字符串并復(fù)制到粘貼板的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12Go語(yǔ)言中未知異常捕獲的多種場(chǎng)景與實(shí)用技巧
在Go語(yǔ)言編程中,異常處理是確保程序健壯性的關(guān)鍵環(huán)節(jié),與一些其他編程語(yǔ)言不同,Go沒(méi)有傳統(tǒng)的try - catch結(jié)構(gòu)化異常處理機(jī)制,本文將深入探討Go語(yǔ)言中未知異常捕獲的多種場(chǎng)景與實(shí)用技巧,需要的朋友可以參考下2024-11-11golang連接池檢查連接失敗時(shí)如何重試(示例代碼)
在Go中,可以通過(guò)使用database/sql包的DB類(lèi)型的Ping方法來(lái)檢查數(shù)據(jù)庫(kù)連接的可用性,本文通過(guò)示例代碼,演示了如何在連接檢查失敗時(shí)進(jìn)行重試,感興趣的朋友一起看看吧2023-10-10Go中基本數(shù)據(jù)類(lèi)型和字符串表示之間轉(zhuǎn)換詳解
這篇文章主要為大家詳細(xì)介紹了Go中基本數(shù)據(jù)類(lèi)型和字符串表示之間轉(zhuǎn)換的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01Go并發(fā)同步Mutex典型易錯(cuò)使用場(chǎng)景
這篇文章主要為大家介紹了Go并發(fā)同步Mutex典型易錯(cuò)使用場(chǎng)景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08GO Cobra Termui庫(kù)開(kāi)發(fā)終端命令行小工具輕松上手
這篇文章主要為大家介紹了GO語(yǔ)言開(kāi)發(fā)終端命令行小工具,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01golang簡(jiǎn)易實(shí)現(xiàn)?k8s?的yaml上傳并應(yīng)用示例方案
這篇文章主要為大家介紹了golang簡(jiǎn)易實(shí)現(xiàn)?k8s?的yaml上傳并應(yīng)用示例方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07詳解如何為Go中的無(wú)限循環(huán)添加時(shí)間限制
在 Go 語(yǔ)言的開(kāi)發(fā)過(guò)程中,我們有時(shí)需要在后臺(tái)執(zhí)行長(zhǎng)時(shí)間運(yùn)行的任務(wù),例如監(jiān)聽(tīng)或輪詢(xún)某些資源,這篇文章將通過(guò)一個(gè)實(shí)例詳細(xì)介紹如何為 Go 語(yǔ)言中的無(wú)限循環(huán)設(shè)置時(shí)間限制,保證程序的健壯性和可控性,需要的朋友可以參考下2024-04-04