当前位置: 首页 > article >正文

containerd系统分析(五)-网络分析

 containerd系列文章:

containerd系统分析(一)-系统组成-CSDN博客

containerd系统分析(二)-镜像管理-CSDN博客

containerd系统分析(三)-容器创建流程分析-CSDN博客

containerd系统分析(四)-容器启动流程分析-CSDN博客

containerd系统分析(五)-网络分析-CSDN博客

containerd系统分析(六)-CRI接口-CSDN博客

1 网络创建简介

若是手工执行网络的安装可参考如下文章:

containerd系列(五):containerd 的CNI 网络配置_51CTO博客_containerD

ctr和nerdctl安装网络的过程略有区别,最终都是通过libcni.setup进行网络的安装,功能主要是由客户端侧实现。

本文侧重于ctr的网络创建过程的分析。最终执行的安装的代码是一样的,在安装的顺序上有所区别。

网络创建遵循cni标准。

2 主要流程

2.1 ctr安装cni

1 当ctr启用cni时,那么在containerd的准备创建容器的时候,需要准备网络的配置信息。从本地/etc/cni/net.d目录下加载文件

if enableCNI {
   if network, err = gocni.New(gocni.WithDefaultConf); err != nil {
      return err
   }
}
func loadFromConfDir(c *libcni, max int) error {
   files, err := cnilibrary.ConfFiles(c.pluginConfDir, []string{".conf", ".conflist", ".json"})
   switch {
   case err != nil:

2 在加载的时候,主要是加载.conf,.conflist,.json文件,通过插件配置信息列表,从配置文件中获得的信息,如果是多个文件,就多个多个网络配置描述信息,每个网络配置描述信息中会有多个插件。配置文件的格式如下(该示例文件是由nerdctl生成的):

{
  "cniVersion": "1.0.0",
  "name": "bridge", //网络类型的名称
  "nerdctlID":  "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121",
  "nerdctlLabels": {},
  "plugins": [ //插件的列表,在生成网络的时候,要根据type去调用相应的插件,插件路径默认是在/opt/cni/bin目录
    {
      "type": "bridge",
      "bridge": "nerdctl0",
      "isGateway": true,
      "ipMasq": true,
      "hairpinMode": true,
      "ipam": {
        "ranges": [
          [
            {
              "gateway": "10.4.0.1",
              "subnet": "10.4.0.0/24"
            }
          ]
        ],
        "routes": [
          {
            "dst": "0.0.0.0/0"
          }
        ],
        "type": "host-local"
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    },
    {
      "type": "firewall",
      "ingressPolicy": "same-bridge"
    },
    {
      "type": "tuning"
    }
  ]
}

3 获得net namespace的路径,在newTask之后可以安装网络了。为什么必须在newTask之后,因为newTask之后,生成了pid

if enableCNI {
   netNsPath, err := getNetNSPath(ctx, task) //netNsPath的路径为"/proc/{pid}/ns/net

   if err != nil {
      return err
   }

   if _, err := network.Setup(ctx, commands.FullID(ctx, container), netNsPath); err != nil {
      return err
   }
}

4 在获得netsPath之后,就可以进行网络的安装了

func (c *libcni) Setup(ctx context.Context, id string, path string, opts ...NamespaceOpts) (*Result, error) {
   if err := c.Status(); err != nil {//如果提供/etc/cni/net.d下的配置的网络的数量<期望的网络数量,那就不对了。就是说配置的要生成的网卡数小于期望的网卡数,那就返回错误
      return nil, err
   }
   //构建一个namespace对象,由net namespace的path, fullId,以及net opts构成
   ns, err := newNamespace(id, path, opts...)
   if err != nil {
      return nil, err
   }
   //在这里正式的进行网络的安装
   result, err := c.attachNetworks(ctx, ns)
   if err != nil {
      return nil, err
   }
   return c.createResult(result)
}

5 attachNetworks中进行网络的安装,这里是根据/etc/cni/net.d下的网卡的配置,逐个调用,在所有的调用完成后,生成应答。针对每种某种网卡,是通过go routine是同时进行安装的

func (c *libcni) attachNetworks(ctx context.Context, ns *Namespace) ([]*types100.Result, error) {
   var wg sync.WaitGroup
   var firstError error
   //构建一个通知,如果有多个网络配置的话,那就构建多个,可能是会有多网卡的情况
   results := make([]*types100.Result, len(c.Networks()))
   //异步通知的chanel
   rc := make(chan asynchAttachResult)

   //针对每种网卡,去进行attach,也就是在每个容器对相应的网卡都进行绑定
   for i, network := range c.Networks() {
      wg.Add(1)
      go asynchAttach(ctx, i, network, ns, &wg, rc)
   }

   for range c.Networks() {
      rs := <-rc
      if rs.err != nil && firstError == nil {
         firstError = rs.err
      }
      results[rs.index] = rs.res
   }
   wg.Wait()

   return results, firstError
}

6 对每种网卡执行attach操作

func asynchAttach(ctx context.Context, index int, n *Network, ns *Namespace, wg *sync.WaitGroup, rc chan asynchAttachResult) {
   defer wg.Done()
   r, err := n.Attach(ctx, ns)
   rc <- asynchAttachResult{index: index, res: r, err: err}
}

7 在attach的时候,调用标准的cni接口AddNetworkList

func (n *Network) Attach(ctx context.Context, ns *Namespace) (*types100.Result, error) {
   //这里的n.confg就是/etc/cni/config下面的东西了,这个n.ifName指的是要绑定的网卡的名称
   //ns.config生成网络命名空间的绑定信息,将容器的网卡的名称,和网络命名空间的地址相绑定,设定在外面预置好的,args和capabilityArgs参数
   r, err := n.cni.AddNetworkList(ctx, n.config, ns.config(n.ifName))
   if err != nil {
      return nil, err
   }
   return types100.NewResultFromResult(r)
}

8 cni调用网络配置中的每种插件,根据plugin的参数进行网络的安装,在安装完成后,缓存网卡的安装结果,结果存储的位置是在/拼接出唯一的名称:/var/lib/cni/result/{netName}-{ns-containerId}-{ifName}

func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
   var err error
   var result types.Result
   //这里调用的是/etc/cni/config下的配置文件中所述的各种网络插件参数,应该是根据所述的网络参数,然后就调/opt/cni/bin下面的参数吧
   for _, net := range list.Plugins {
      //这个list.Name指的是网络配置中的name, net指的网络配置中的网络插件的参数
      result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
      if err != nil {
         return nil, fmt.Errorf("plugin %s failed (add): %w", pluginDescription(net.Network), err)
      }
   }

   //缓存创建的结果
   if err = c.cacheAdd(result, list.Bytes, list.Name, rt); err != nil {
      return nil, fmt.Errorf("failed to set network %q cached result: %w", list.Name, err)
   }

   return result, nil

9 实际执行安装网卡的过程,是通过addNetwork调用/opt/cni/bin下面的可执行程序进行安装,在adddNetwork函数里进行参数拼装

func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
   c.ensureExec()
   //根据插件配置参数的type的类型和cni的插件的路径,去找网络插件
   pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
   if err != nil {
      return nil, err
   }

   //container id为命名空间+容器的id
   if err := utils.ValidateContainerID(rt.ContainerID); err != nil {
      return nil, err
   }
   //网络名称
   if err := utils.ValidateNetworkName(name); err != nil {
      return nil, err
   }
   //检验网卡的名称
   if err := utils.ValidateInterfaceName(rt.IfName); err != nil {
      return nil, err
   }

   //生成一个配置,net也就是原始的/etc/cni/net.d下的网络配置参数
   newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
   if err != nil {
      return nil, err
   }

   //最后调用这个插件执行add,来生成这个参数
   return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
}

2.2 nerdctl安装cni

1 对nerdctl的网络安装,是通过oci-hook回调的方式生成的

在生成spec参数的时候,设置了oci-hook在创建的时候执行回调。这样runc等运行时,在执行createRuntime阶段就会执行oci-hook回调指定的程序。

//在这里生成了网络参数
netOpts, netSlice, ipAddress, ports, err := generateNetOpts(cmd, dataStore, stateDir, ns, id)
if err != nil {
   return nil, nil, err
}
opts = append(opts, netOpts...)

//生成hookOpt
hookOpt, err := withNerdctlOCIHook(cmd, id)
func withNerdctlOCIHook(cmd *cobra.Command, id string) (oci.SpecOpts, error) {
   selfExe, f := globalFlags(cmd)
   args := append([]string{selfExe}, append(f, "internal", "oci-hook")...)
   return func(_ context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error {
      if s.Hooks == nil {
         s.Hooks = &specs.Hooks{}
      }
      crArgs := append(args, "createRuntime")
      s.Hooks.CreateRuntime = append(s.Hooks.CreateRuntime, specs.Hook{
         Path: selfExe,
         Args: crArgs,
         Env:  os.Environ(),
      })
      argsCopy := append([]string(nil), args...)
      psArgs := append(argsCopy, "postStop")
      s.Hooks.Poststop = append(s.Hooks.Poststop, specs.Hook{
         Path: selfExe,
         Args: psArgs,
         Env:  os.Environ(),
      })
      return nil
   }, nil
}

2 对于nerdctl在oci回调的时候,进行网络的安装和hosts文件的生成

func Run(stdin io.Reader, stderr io.Writer, event, dataStore, cniPath, cniNetconfPath string) error {
   if stdin == nil || event == "" || dataStore == "" || cniPath == "" || cniNetconfPath == "" {
      return errors.New("got insufficient args")
   }

   var state specs.State
   if err := json.NewDecoder(stdin).Decode(&state); err != nil {
      return err
   }

   if containerStateDir := state.Annotations[labels.StateDir]; containerStateDir == "" {
      return errors.New("state dir must be set")
   } else {
      if err := os.MkdirAll(containerStateDir, 0700); err != nil {
         return fmt.Errorf("failed to create %q: %w", containerStateDir, err)
      }
      logFilePath := filepath.Join(containerStateDir, "oci-hook."+event+".log")
      logFile, err := os.Create(logFilePath)
      if err != nil {
         return err
      }
      defer logFile.Close()
      logrus.SetOutput(io.MultiWriter(stderr, logFile))
   }

   //根据存储的网络的信息,得到创建网络所需要的的opts
   opts, err := newHandlerOpts(&state, dataStore, cniPath, cniNetconfPath)
   if err != nil {
      return err
   }

   switch event {
   //在创建时的回调函数,这里根据返回的state信息执行回调
   case "createRuntime":
      return onCreateRuntime(opts)
   case "postStop":
      return onPostStop(opts)
   default:
      return fmt.Errorf("unexpected event %q", event)
   }
}

3 在这里实际执行安装的过程就和ctr的安装的网络的过程是一样的了

onCreateRuntime(opts *handlerOpts) error {
   loadAppArmor()

   if opts.cni != nil {
      portMapOpts, err := getPortMapOpts(opts)
      if err != nil {
         return err
      }
      //获得net namespace path,实际是在/proc/{pid}/ns/net
      nsPath, err := getNetNSPath(opts.state)
      if err != nil {
         return err
      }
      ctx := context.Background()
      //etchosts目录,这个目录下存储hosts文件和meta文件信息
      hs, err := hostsstore.NewStore(opts.dataStore)
      if err != nil {
         return err
      }
      //获得静态ip地址信息,如果没有,那就是空
      ipAddressOpts, err := getIPAddressOpts(opts)
      if err != nil {
         return err
      }
      var namespaceOpts []gocni.NamespaceOpts
      namespaceOpts = append(namespaceOpts, portMapOpts...)
      namespaceOpts = append(namespaceOpts, ipAddressOpts...)
      hsMeta := hostsstore.Meta{
         Namespace:  opts.state.Annotations[labels.Namespace],
         //容器id
         ID:         opts.state.ID,
         //网络信息
         Networks:   make(map[string]*types100.Result, len(opts.cniNames)),
         //主机名
         Hostname:   opts.state.Annotations[labels.Hostname],
         ExtraHosts: opts.extraHosts,
         //显示的容器的名称
         Name:       opts.state.Annotations[labels.Name],
      }
      //应该是在这里创建网卡,fullId为命名空间+容器id
      //Setup主要分为三步:newNamespace初始化一个namespace对象;attachNetworks:初始化网络;createResult:构造返回结果
      cniRes, err := opts.cni.Setup(ctx, opts.fullID, nsPath, namespaceOpts...)
      if err != nil {
         return fmt.Errorf("failed to call cni.Setup: %w", err)
      }

3 总结

在获得namespace之后,即容器执行环境准备好后,开始实际安装容器网络。不同的容器的执行客户端的触发方式有所不同。可能遵循oci的标准,根据说置的post hook进行部署,也可以由客户端自行根据部署的阶段进行显示安装。最终调用的是libcni的库进行网络的部署。

4 相关开源项目或标准

oci标准:

GitHub - opencontainers/runtime-spec: OCI Runtime Specification

cni标准:GitHub - containernetworking/cni: Container Network Interface - networking for Linux containers


http://www.kler.cn/a/501043.html

相关文章:

  • 初学stm32 --- DAC输出三角波和正弦波
  • 微服务的配置共享
  • acwing-3194 最大的矩形
  • SQLAlchemy: python类的属性值为None,数据为JSON类型,插入数据库为‘ NULL‘字符串,而不是真正的NULL
  • electron 启动警告
  • 八、系统托盘与配置面板
  • 判断两个字符串是不是旋转字符串
  • 《探索鸿蒙Next上开发人工智能游戏应用的技术难点》
  • 【MySQL】NOT IN需要外部套一层SELECT
  • Linux 发行版介绍与对比:Red Hat、Ubuntu、Kylin、Debian
  • 百度视频搜索架构演进
  • Open FPV VTX开源之第一次出图
  • 【面试题】技术场景 4、负责项目时遇到的棘手问题及解决方法
  • 【mysql】约束的基本使用
  • 《跟我学Spring Boot开发》系列文章索引❤(2025.01.09更新)
  • ollama教程(window系统)
  • element plus 使用 el-tree 组件设置默认选中和获取所有选中节点id
  • Q_OBJECT宏报错的问题
  • Liunx-搭建安装VSOMEIP环境教程 执行 运行VSOMEIP示例demo
  • 网络安全-防火墙
  • DolphinScheduler自身容错导致的服务器持续崩溃重大问题的排查与解决
  • 第41章 使用 Docker Compose 进行容器迁移的技术指南及优势
  • 二十三种设计模式-原型模式
  • 深度学习张量的秩、轴和形状
  • HarmonyOS鸿蒙开发 弹窗及加载中指示器HUD功能实现
  • Redis数据库——Redis快的原因