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

Go 如何对多个网络命令空间中的端口进行监听的更多相关文章

  1. linux网络连接的查看和端口的监听

    网络软件都是由客户端和服务端组成,由服务端在服务器上监听指定的端口处理接收到的数据,而客户端是向服务器端监听的端口发送数据,并由服务器端对该数据进行处理,然后将处理结果返回给客户端. 那么我们在lin ...

  2. oracle 11G创建表空间、用户、配置监听和TNS

    最近总在安装各种版本的oralce数据库做测试,11G,32位的,64位的,12C的,每次都折腾表空间,用户.tns啥的,这里记录下,再也不用现用现百度找了 一.创建表空间.用户  在plsql工具中 ...

  3. 解开一个疑惑,为什么LVS开放的端口,使用netstat或ss命令,不能查找到其监听的端口呢?

    RT, 这个疑问,本周一直在心里,今天找到一个说法. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 另外LVS规则算是内核方法,用netstat -ntulp也显 ...

  4. 深入理解 Neutron -- OpenStack 网络实现(4):网络名字空间

    问题导读1.如何查看网络名字空间?2.网络名字空间开头的名字有什么规律?3.dhcp服务是如何实现的?4.router的实现是通过iptables进行的是否正确?5.SNAT和DNAT规则有什么作用? ...

  5. 网络名称空间 实例研究 veth处于不同网络的路由问题

    相关命令详细介绍参见 http://www.cnblogs.com/Dream-Chaser/p/7077105.html .问题: 两个网络名称空间中的两个接口veth0和veth1,如何配置net ...

  6. Linux网络命令与脚本使用

    作为系统管理员,经常需要诊断和解决网络问题,而配置.监控与保护网络有助于发现问题并在事情范围扩大前得意解决,并且网络的性能与安全也是管理与诊断网络的重要部分.这里总结一下常用与Linux网络管理的命令 ...

  7. Linux常用命令之网络命令

    write命令 write命令用于向指定登录用户终端上发送信息.通过write命令可传递信息给另一位登入系统的用户,当输入完毕后,键入EOF表示信息结束,write命令就会将信息传给对方.如果接收信息 ...

  8. [Linux] 017 网络命令与挂载命令

    1. 网络命令:write 命令名称:write 命令所在路径:/usr/bin/write 执行权限:所有用户 语法:write [用户名] 功能描述:给用户发信息,以 Ctrl-d 保存结束 范例 ...

  9. (办公)记事本_linux网络命令

    参考谷粒学院的linux视频教程:http://www.gulixueyuan.com/course/300/task/7091/show 阿里云ECS云服务器更换公网IP的方法:https://yq ...

  10. Android开发之使用广播监听网络状态变化

    我们经常需要判断网络状态的变化,如有无网络,所以需要监听网络状态的变化,比如网络断开,网络连接给予友好提示.如何监听网络状态的变化呢,最近工作中需要用到这个,于是就用广播机制来实现了网络状态的监听. ...

随机推荐

  1. sqli-labs-master 导入导出 + 第七关

    1.load_file()导出文件 load_file(file_name):读取文件并返回该文件内容作为一个字符串. 使用条件: A:必须有权限读取并且文件完全可读 B:预读取文件必修在服务器上 C ...

  2. 02 elasticsearch学习笔记-ES核心概念

    一. 前序 sh Elasticsearch是一个基于Apache Lucene(TM)的开源搜索引擎.无论在开源还是专有领域,Lucene 可以被认为是迄今为止最先进.性能最好的.功能最全的搜索引擎 ...

  3. ubuntu下安装php pdo扩展和导入数据库

    默认安装的php不存在pdo扩展,因此在使用到的时候会报错,直接使用这个命令 apt-get install php-mysql 就可以成功安装pdo扩展 安装完数据库后需要导入sql语句,先进入数据 ...

  4. List<T> 根据对象中的属性处理数据

    一.创建测试数据 UserEntity user1 = UserEntity.builder().id(1).name("张三").sex(0).build(); UserEnti ...

  5. 如何在 Ubuntu 服务器上安装桌面环境 (GUI)

    先以VNC方式远程登录服务器 执行命令 sudo apt update && sudo apt upgrade # 选择1---使用tasksel安装 sudo apt install ...

  6. Unity Shader中的常见流控制指令

    在Shader中处理流控制语句时,常加上一些宏去处理流控制指令.例如: [UNITY_UNROLL] for (int i = 0; i < 10; i++) { //do something. ...

  7. 4G 信令中的 PCO 字段

    目录 文章目录 目录 Protocol Configuration Option Protocol Configuration Option PCO(Protocol Configuration Op ...

  8. docker 修改运行容器环境变量,如何修改容器中的环境变量env使长期有效

    @ 目录 前言 第一步:查看Docker Root目录 第二步:查到容器的长id(container id) 第三步:停止容器 第四步:编辑修改环境变量env 第五步:重载服务的配置文件 第六步:重启 ...

  9. VSCode:让VSCode终端面板显示到右边

    很简单,右击下方终端面板,选中"将视图移动到侧面板": 可以看到终端成功显示到右边了:

  10. Istio(六):Istio弹性(超时&重试)和故障注入

    目录 一.模块概览 二.系统环境 三.弹性(超时&重试) 3.1 弹性 四.故障注入 4.1 故障注入 五.实战:观察错误注入 5.1 在 Grafana.Zipkin 和 Kiali 中观察 ...