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 端口的例子如下:

命名空间创建命令

shell 复制代码
ip netns add ns1
ip netns add ns2
go 复制代码
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 的方式如下

go 复制代码
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
}

实际应用的数据参考

打开的文件如下

shell 复制代码
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 核心,创建的线程如下

shell 复制代码
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
相关推荐
yuguo.im4 小时前
Dkron 架构与设计
架构·golang·go·dkron
桃园码工1 天前
2-测试bigcache做进程内缓存 --开源项目obtain_data测试
vscode·mysql·go·postman
ZQDesigned2 天前
在 Windows 和 macOS 上配置 Golang 语言环境
后端·go
楽码2 天前
只需一文:了解validator标签以轻松验证
后端·安全·go
煎鱼eddycjy3 天前
新提案:由迭代器启发的 Go 错误函数处理
go
煎鱼eddycjy3 天前
Go 语言十五周年!权力交接、回顾与展望
go
不爱说话郭德纲4 天前
聚焦 Go 语言框架,探索创新实践过程
go·编程语言
0x派大星5 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
IT书架5 天前
golang高频面试真题
面试·go
郝同学的测开笔记5 天前
云原生探索系列(十四):Go 语言panic、defer以及recover函数
后端·云原生·go