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