• Go 如何对多个网络命令空间中的端口进行监听


    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 服务如下

    1. 获取并且保存默认网络命名空间
    2. 加锁防止多个网络命名空间同时切换,将 goroutine 绑定到当前的线程上防止被调度
    3. 获取需要操作的网络命名空间,并且切换过去 setns
    4. 监听套接字 net.Listen
    5. 切换到默认的命名空间(还原)
    6. 释放当前线程的绑定,释放锁

    实现对 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

    1. 设置非阻塞
    2. 手动使用 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
    
  • 相关阅读:
    题目 1067: 二级C语言-分段函数 sqrt、fabs、pow
    容器云平台监控告警体系(三)—— 使用Prometheus Operator部署并管理Prometheus Server
    HTML+CSS+JS静态网页设计【二十四节气】期末课程大作业
    在vue-cli项目中打包的 dist 静态文件不能直接双击打开访问解决方法-直接打开访问是空白的解决方法
    Cpp知识点系列-宏定义
    解密Elasticsearch:深入探究这款搜索和分析引擎
    目录遍历漏洞
    【力扣2656】K个元素的最大和
    C++中成员函数的重载覆盖与隐藏
    基于Springboot 游戏娱乐信息平台-计算机毕设 附源码 04691
  • 原文地址:https://www.cnblogs.com/shuqin/p/18282324