Go 如何利用 Linux 内核的负载均衡能力?
在測(cè)試 HTTP 服務(wù)時(shí),如果該進(jìn)程我們忘記關(guān)閉,而重新嘗試啟動(dòng)一個(gè)新的服務(wù)進(jìn)程,那么將會(huì)遇到類似以下的錯(cuò)誤信息:
$?go?run?main.go listen?tcp?:8000:?bind:?address?already?in?use這是由于默認(rèn)情況下,操作系統(tǒng)不允許我們打開(kāi)具有相同源地址和端口的套接字 socket。但如果我們想開(kāi)啟多個(gè)服務(wù)進(jìn)程去監(jiān)聽(tīng)同一個(gè)端口,這可以嗎?如果可以,這又能給我們帶來(lái)什么?
socket 五元組
socket 編程是每位程序員都應(yīng)該掌握的基礎(chǔ)知識(shí)。因此,大家應(yīng)該知道,socket 連接通過(guò)五元組唯一標(biāo)識(shí)。任意兩條連接,它的五元組不能完全相同。
{<protocol>,?<src?addr>,?<src?port>,?<dest?addr>,?<dest?port>}protocol 指的是傳輸層 TCP/UDP 協(xié)議,它在 socket 被創(chuàng)建時(shí)就已經(jīng)確定。src addr/port 與 dest addr/port ?分別標(biāo)識(shí)著請(qǐng)求方與服務(wù)方的地址信息。
因此,只要請(qǐng)求方的 dest addr/port 信息不相同,那么服務(wù)方即使是同樣的 src addr/port ,它仍然可以標(biāo)識(shí)唯一的 socket 連接。
基于這個(gè)理論基礎(chǔ),那實(shí)際上,我們可以在同一個(gè)網(wǎng)絡(luò)主機(jī)復(fù)用相同的 IP 地址和端口號(hào)。
Linux SO_REUSEPORT
為了滿足復(fù)用端口的需求,Linux 3.9 內(nèi)核引入了 SO_REUSEPORT選項(xiàng)(實(shí)際在此之前有一個(gè)類似的選項(xiàng) SO_REUSEADDR,但它沒(méi)有做到真正的端口復(fù)用,詳細(xì)可見(jiàn)參考鏈接1)。
SO_REUSEPORT 支持多個(gè)進(jìn)程或者線程綁定到同一端口,用于提高服務(wù)器程序的性能。它的特性包含以下幾點(diǎn):
允許多個(gè)套接字 bind 同一個(gè)TCP/UDP 端口
每一個(gè)線程擁有自己的服務(wù)器套接字
在服務(wù)器套接字上沒(méi)有了鎖的競(jìng)爭(zhēng)
內(nèi)核層面實(shí)現(xiàn)負(fù)載均衡
安全層面,監(jiān)聽(tīng)同一個(gè)端口的套接字只能位于同一個(gè)用戶下(same effective UID)
有了 SO_RESUEPORT 后,每個(gè)進(jìn)程可以 bind 相同的地址和端口,各自是獨(dú)立平等的。
讓多進(jìn)程監(jiān)聽(tīng)同一個(gè)端口,各個(gè)進(jìn)程中 accept socket fd 不一樣,有新連接建立時(shí),內(nèi)核只會(huì)調(diào)度一個(gè)進(jìn)程來(lái) accept,并且保證調(diào)度的均衡性。
其工作示意圖如下
有了 SO_REUSEADDR 的支持,我們不僅可以創(chuàng)建多個(gè)具有相同 IP:PORT 的套接字能力,而且我們還得到了一種內(nèi)核模式下的負(fù)載均衡能力。
Go 如何設(shè)置 SO_REUSEPORT
Linux 經(jīng)典的設(shè)計(jì)哲學(xué):一切皆文件。當(dāng)然,socket 也不例外,它也是一種文件。
如果我們想在 Go 程序中,利用上 linux 的 SO_REUSEPORT 選項(xiàng),那就需要有修改內(nèi)核 socket 連接選項(xiàng)的接口,而這可以依賴于 golang.org/x/sys/unix 庫(kù)來(lái)實(shí)現(xiàn),具體就在以下這個(gè)方法。
import?“"golang.org/x/sys/unix"” ... unix.SetsockoptInt(int(fd),?unix.SOL_SOCKET,?unix.SO_REUSEPORT,?1)因此,一個(gè)持有 SO_REUSEPORT 特性的完整 Go 服務(wù)代碼如下
package?mainimport?("context""fmt""net""net/http""os""syscall""golang.org/x/sys/unix" )var?lc?=?net.ListenConfig{Control:?func(network,?address?string,?c?syscall.RawConn)?error?{var?opErr?errorif?err?:=?c.Control(func(fd?uintptr)?{opErr?=?unix.SetsockoptInt(int(fd),?unix.SOL_SOCKET,?unix.SO_REUSEPORT,?1)});?err?!=?nil?{return?err}return?opErr}, }func?main()?{pid?:=?os.Getpid()l,?err?:=?lc.Listen(context.Background(),?"tcp",?"127.0.0.1:8000")if?err?!=?nil?{panic(err)}server?:=?&http.Server{}http.HandleFunc("/",?func(w?http.ResponseWriter,?r?*http.Request)?{w.WriteHeader(http.StatusOK)fmt.Fprintf(w,?"Client?[%s]?Received?msg?from?Server?PID:?[%d]?\n",?r.RemoteAddr,?pid)})fmt.Printf("Server?with?PID:?[%d]?is?running?\n",?pid)_?=?server.Serve(l) }我們將其編譯為 linux 可執(zhí)行文件 main
$?CGO_ENABLED=0?GOOS=linux?GOARCH=amd64?go?build?main.go在 linux 主機(jī)上開(kāi)啟三個(gè)同時(shí)監(jiān)聽(tīng) 8000 端口的進(jìn)程,我們可以看到三個(gè)服務(wù)進(jìn)程的 PID 分別是 32687 、32691 和 32697。
~?$?./main Server?with?PID:?[32687]?is?running ~?$?./main Server?with?PID:?[32691]?is?running ~?$?./main Server?with?PID:?[32697]?is?running最后,通過(guò) curl 命令,模擬多次 http 客戶端請(qǐng)求
~?$?for?i?in?{1..20};?do?curl?localhost:8000;?done Client?[127.0.0.1:56876]?Received?msg?from?Server?PID:?[32697] Client?[127.0.0.1:56880]?Received?msg?from?Server?PID:?[32687] Client?[127.0.0.1:56884]?Received?msg?from?Server?PID:?[32687] Client?[127.0.0.1:56888]?Received?msg?from?Server?PID:?[32687] Client?[127.0.0.1:56892]?Received?msg?from?Server?PID:?[32691] Client?[127.0.0.1:56896]?Received?msg?from?Server?PID:?[32697] Client?[127.0.0.1:56900]?Received?msg?from?Server?PID:?[32691] Client?[127.0.0.1:56904]?Received?msg?from?Server?PID:?[32691] Client?[127.0.0.1:56908]?Received?msg?from?Server?PID:?[32697] Client?[127.0.0.1:56912]?Received?msg?from?Server?PID:?[32697] Client?[127.0.0.1:56916]?Received?msg?from?Server?PID:?[32687] Client?[127.0.0.1:56920]?Received?msg?from?Server?PID:?[32691] Client?[127.0.0.1:56924]?Received?msg?from?Server?PID:?[32697] Client?[127.0.0.1:56928]?Received?msg?from?Server?PID:?[32697] Client?[127.0.0.1:56932]?Received?msg?from?Server?PID:?[32691] Client?[127.0.0.1:56936]?Received?msg?from?Server?PID:?[32697] Client?[127.0.0.1:56940]?Received?msg?from?Server?PID:?[32687] Client?[127.0.0.1:56944]?Received?msg?from?Server?PID:?[32691] Client?[127.0.0.1:56948]?Received?msg?from?Server?PID:?[32687] Client?[127.0.0.1:56952]?Received?msg?from?Server?PID:?[32697]可以看到,20 個(gè)客戶端請(qǐng)求被均衡地打到了三個(gè)服務(wù)進(jìn)程上。
總結(jié)
linux 內(nèi)核自 3.9 提供的 SO_REUSEPORT 選項(xiàng),可以讓多進(jìn)程監(jiān)聽(tīng)同一個(gè)端口。
這種機(jī)制帶來(lái)了什么:
提高服務(wù)器程序的吞吐性能:我們可以運(yùn)行多個(gè)應(yīng)用程序?qū)嵗?#xff0c;充分利用多核 CPU 資源,避免出現(xiàn)單核在處理數(shù)據(jù)包,其他核卻閑著的問(wèn)題。
內(nèi)核級(jí)負(fù)載均衡:我們不需要在多個(gè)實(shí)例前面添加一層服務(wù)代理,因?yàn)閮?nèi)核已經(jīng)提供了簡(jiǎn)單的負(fù)載均衡。
不停服更新:當(dāng)我們需要更新服務(wù)時(shí),可以啟動(dòng)新的服務(wù)實(shí)例來(lái)接受請(qǐng)求,再優(yōu)雅地關(guān)閉掉舊服務(wù)實(shí)例。
如果你們的 Go 項(xiàng)目,一到高峰期就有請(qǐng)求堆積問(wèn)題,這個(gè)時(shí)候就可以考慮采用 SO_REUSEPORT 選項(xiàng)。
參考鏈接:
【1. How do SO_REUSEADDR and SO_REUSEPORT differ?】https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ
【2. SO_REUSEPORT 性能測(cè)試】 http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html
【3. linux socket man-page】https://man7.org/linux/man-pages/man7/socket.7.html
感謝你的點(diǎn)贊和在看哦~
總結(jié)
以上是生活随笔為你收集整理的Go 如何利用 Linux 内核的负载均衡能力?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 云原生环境下对“多活”架构的思考
- 下一篇: linux 其他常用命令