Go?對多個網(wǎng)絡(luò)命令空間中的端口進(jìn)行監(jiān)聽的解決方案
需求為 對多個命名空間內(nèi)的端口進(jìn)行監(jiān)聽和代理。
剛開始對 netns 的理解不夠深刻,以為必須存在一個新的線程然后調(diào)用 setns(2) 切換過去,如果有新的 netns 那么需要再新建一個線程切換過去使用,這樣帶來的問題就是線程數(shù)量和 netns 的數(shù)量為 1:1,資源占用會比較多。
當(dāng)時沒有想到別的好辦法,Go 里面也不能創(chuàng)建線程,只能想到使用一個 C 進(jìn)程來實現(xiàn)這個功能,這里就多了 通信交互/協(xié)議解析處理/資源占用 的成本。
新方案
后面在 stackoverflow 中閑逛看到一篇文章 https://stackoverflow.com/questions/28846059/can-i-open-sockets-in-multiple-network-namespaces-from-my-python-code,看到了關(guān)鍵點(diǎn) 在套接字創(chuàng)建之前,切換到對應(yīng)的命名空間,并不需要創(chuàng)建線程。
這樣就可以一個線程下對多個命名空間的端口進(jìn)行監(jiān)聽,可以減少線程本身資源的占用以及額外的管理成本。
原來 C 實現(xiàn)的改造比較好實現(xiàn),刪除創(chuàng)建線程那一步差不多就可以了。如何更進(jìn)一步使用 Go 實現(xiàn),減少維護(hù)的成本?
使用 Go 進(jìn)行實現(xiàn)
保證套接字創(chuàng)建時在某個命名空間內(nèi),就可以完成套接字后續(xù)的操作,不必使用一個線程來持有一個命名空間,建立一個典型的 TCP 服務(wù)如下
- 獲取并且保存默認(rèn)網(wǎng)絡(luò)命名空間
- 加鎖防止多個網(wǎng)絡(luò)命名空間同時切換,將 goroutine 綁定到當(dāng)前的線程上防止被調(diào)度
- 獲取需要操作的網(wǎng)絡(luò)命名空間,并且切換過去 setns
- 監(jiān)聽套接字 net.Listen
- 切換到默認(rèn)的命名空間(還原)
- 釋放當(dāng)前線程的綁定,釋放鎖
實現(xiàn)對 TCP 的監(jiān)聽
使用 github.com/vishvananda/netns 這個庫對網(wǎng)絡(luò)命名空間進(jìn)行操作,一個同時在 默認(rèn)/ns1/ns2 三個命名空間內(nèi)監(jiān)聽 8000 端口的例子如下:
命名空間創(chuàng)建命令
ip netns add ns1 ip netns add ns2
package main
import (
"net"
"runtime"
"sync"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/vishvananda/netns"
)
var (
mainNetnsHandler netns.NsHandle
mainNetnsMutex sync.Mutex
)
func mustInitMainNetnsHandler() {
nh, err := netns.Get()
if err != nil {
panic(err)
}
mainNetnsHandler = nh
}
func ListenInsideNetns(ns, network, address string) (net.Listener, error) {
if ns == "" {
return net.Listen(network, address)
}
var set bool
mainNetnsMutex.Lock()
runtime.LockOSThread()
defer func() {
if set {
err := netns.Set(mainNetnsHandler)
if err != nil {
logrus.WithError(err).Warn("Fail to back to main netns")
}
}
runtime.UnlockOSThread()
mainNetnsMutex.Unlock()
}()
nh, err := netns.GetFromName(ns)
if err != nil {
return nil, errors.Wrap(err, "netns.GetFromName")
}
defer nh.Close()
err = netns.Set(nh)
if err != nil {
return nil, errors.Wrap(err, "netns.Set")
}
set = true
return net.Listen(network, address)
}
func serve(listener net.Listener) error {
for {
conn, err := listener.Accept()
if err != nil {
return err
}
logrus.WithFields(logrus.Fields{"local": conn.LocalAddr(), "remote": conn.RemoteAddr()}).Info("New conn")
conn.Write([]byte("hello"))
conn.Close()
}
}
func main() {
mustInitMainNetnsHandler()
wg := sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
lis, err := ListenInsideNetns("", "tcp", ":8000")
if err != nil {
panic(err)
}
logrus.WithFields(logrus.Fields{"netns": "", "addr": lis.Addr()}).Info("Listen on")
serve(lis)
}()
go func() {
defer wg.Done()
lis, err := ListenInsideNetns("ns1", "tcp", ":8000")
if err != nil {
panic(err)
}
logrus.WithFields(logrus.Fields{"netns": "ns1", "addr": lis.Addr()}).Info("Listen on")
serve(lis)
}()
go func() {
defer wg.Done()
lis, err := ListenInsideNetns("ns2", "tcp", ":8000")
if err != nil {
panic(err)
}
logrus.WithFields(logrus.Fields{"netns": "ns2", "addr": lis.Addr()}).Info("Listen on")
serve(lis)
}()
wg.Wait()
}UDP/SCTP 的監(jiān)聽
UDP 監(jiān)聽和 TCP 無異,Go 會做好調(diào)度不會產(chǎn)生新線程。
SCTP 如果是使用庫 github.com/ishidawataru/sctp,那么需要注意這個庫就是簡單的 fd 封裝,并且其 Accept() 是一個阻塞的動作,在 for 循環(huán)內(nèi)調(diào)用 Accept() 會導(dǎo)致 Go runtime 會創(chuàng)建一個新線程來防止阻塞。
解決方案如下,直接操作 fd
- 設(shè)置非阻塞
- 手動使用 epoll 封裝(必須是 epoll,select/poll 在幾百個fd的情況下性能很差,無連接的情況負(fù)載都很高)。
獲取 fd 的方式如下
type sctpWrapListener struct {
*sctp.SCTPListener
fd int
}
func listenSCTP(network, address string) (*sctpWrapListener, error) {
addr, err := parseSCTPAddr(address)
if err != nil {
return nil, err
}
sctpFd := 0
sc := sctp.SocketConfig{
InitMsg: sctp.InitMsg{NumOstreams: sctp.SCTP_MAX_STREAM},
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
err := syscall.SetNonblock(int(fd), true)
if err != nil {
syscall.Close(int(fd))
return
}
sctpFd = int(fd)
})
},
}
l, err := sc.Listen(network, addr)
if err != nil {
return nil, err
}
return &sctpWrapListener{SCTPListener: l, fd: sctpFd}, nil
}實際應(yīng)用的數(shù)據(jù)參考
打開的文件如下
root@localhost:~# lsof -p $(pidof fake_name) | tail fake_name 1599860 root 1203u sock 0,8 0t0 20374830 protocol: UDP fake_name 1599860 root 1204u pack 20375161 0t0 ALL type=SOCK_RAW fake_name 1599860 root 1205u sock 0,8 0t0 20374831 protocol: SCTPv6 fake_name 1599860 root 1206u sock 0,8 0t0 20375156 protocol: TCP fake_name 1599860 root 1207u sock 0,8 0t0 20375157 protocol: UDP fake_name 1599860 root 1208u sock 0,8 0t0 20375158 protocol: SCTPv6 fake_name 1599860 root 1209u pack 20381769 0t0 ALL type=SOCK_RAW fake_name 1599860 root 1210u sock 0,8 0t0 20381764 protocol: TCP fake_name 1599860 root 1211u sock 0,8 0t0 20381765 protocol: UDP fake_name 1599860 root 1212u sock 0,8 0t0 20381766 protocol: SCTPv6 root@localhost:~# lsof -p $(pidof fake_name) | wc -l 1216
業(yè)務(wù)機(jī)器CPU為 4 核心,創(chuàng)建的線程如下
root@localhost:~# ll /proc/$(pidof fake_name)/task total 0 dr-xr-xr-x 13 root root 0 Jul 3 14:51 ./ dr-xr-xr-x 9 root root 0 Jul 3 14:51 ../ dr-xr-xr-x 7 root root 0 Jul 3 14:51 1599860/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1599861/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1599862/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1599863/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1599864/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1599865/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1600021/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1600033/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1600056/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1600058/ dr-xr-xr-x 7 root root 0 Jul 3 14:57 1602524/ root@localhost:~# ll /proc/$(pidof fake_name)/task | wc -l 14
到此這篇關(guān)于Go 如何對多個網(wǎng)絡(luò)命令空間中的端口進(jìn)行監(jiān)聽的文章就介紹到這了,更多相關(guān)Go 端口監(jiān)聽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
輕松入門:使用Golang開發(fā)跨平臺GUI應(yīng)用
Golang是一種強(qiáng)大的編程語言,它的并發(fā)性和高性能使其成為開發(fā)GUI桌面應(yīng)用的理想選擇,Golang提供了豐富的標(biāo)準(zhǔn)庫和第三方庫,可以輕松地創(chuàng)建跨平臺的GUI應(yīng)用程序,通過使用Golang的GUI庫,開發(fā)人員可以快速構(gòu)建具有豐富用戶界面和交互功能的應(yīng)用程序,需要的朋友可以參考下2023-10-10
Golang利用channel協(xié)調(diào)協(xié)程的方法詳解
go?當(dāng)中的并發(fā)編程是通過goroutine來實現(xiàn)的,利用channel(管道)可以在協(xié)程之間傳遞數(shù)據(jù),所以本文就來講講Golang如何利用channel協(xié)調(diào)協(xié)程吧2023-05-05
Golang unsafe.Sizeof函數(shù)代碼示例使用解析
這篇文章主要為大家介紹了Golang unsafe.Sizeof函數(shù)代碼示例使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12

