Go 对多个网络命令空间中的端口进行监听的解决方案
作者:文一路挖坑侠
需求为 对多个命名空间内的端口进行监听和代理。
刚开始对 netns 的理解不够深刻,以为必须存在一个新的线程然后调用 setns(2) 切换过去,如果有新的 netns 那么需要再新建一个线程切换过去使用,这样带来的问题就是线程数量和 netns 的数量为 1:1,资源占用会比较多。
当时没有想到别的好办法,Go 里面也不能创建线程,只能想到使用一个 C 进程来实现这个功能,这里就多了 通信交互/协议解析处理/资源占用 的成本。
新方案
后面在 stackoverflow 中闲逛看到一篇文章 https://stackoverflow.com/questions/28846059/can-i-open-sockets-in-multiple-network-namespaces-from-my-python-code,看到了关键点 在套接字创建之前,切换到对应的命名空间,并不需要创建线程。
这样就可以一个线程下对多个命名空间的端口进行监听,可以减少线程本身资源的占用以及额外的管理成本。
原来 C 实现的改造比较好实现,删除创建线程那一步差不多就可以了。如何更进一步使用 Go 实现,减少维护的成本?
使用 Go 进行实现
保证套接字创建时在某个命名空间内,就可以完成套接字后续的操作,不必使用一个线程来持有一个命名空间,建立一个典型的 TCP 服务如下
- 获取并且保存默认网络命名空间
- 加锁防止多个网络命名空间同时切换,将 goroutine 绑定到当前的线程上防止被调度
- 获取需要操作的网络命名空间,并且切换过去 setns
- 监听套接字 net.Listen
- 切换到默认的命名空间(还原)
- 释放当前线程的绑定,释放锁
实现对 TCP 的监听
使用 github.com/vishvananda/netns 这个库对网络命名空间进行操作,一个同时在 默认/ns1/ns2 三个命名空间内监听 8000 端口的例子如下:
命名空间创建命令
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 的监听
UDP 监听和 TCP 无异,Go 会做好调度不会产生新线程。
SCTP 如果是使用库 github.com/ishidawataru/sctp,那么需要注意这个库就是简单的 fd 封装,并且其 Accept() 是一个阻塞的动作,在 for 循环内调用 Accept() 会导致 Go runtime 会创建一个新线程来防止阻塞。
解决方案如下,直接操作 fd
- 设置非阻塞
- 手动使用 epoll 封装(必须是 epoll,select/poll 在几百个fd的情况下性能很差,无连接的情况负载都很高)。
获取 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 }
实际应用的数据参考
打开的文件如下
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
业务机器CPU为 4 核心,创建的线程如下
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
到此这篇关于Go 如何对多个网络命令空间中的端口进行监听的文章就介绍到这了,更多相关Go 端口监听内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!