0%

CNI系列之规范学习

CNI是什么吗?

  • 全称是Container Network Interface
  • 也是CNCF管理的一个项目;
  • 该项目包含规范和实现库(golang版本);
  • CNI只关注于容器启动时网络的联接以及容器退出时网络资源的释放;

本文主要是学习CNI规范(其实就是翻译😀),golang版本的实现库,后续文章继续学习。

版本

当前CNI版本号为0.4.0,已发布的版本如下:

版本 主要变更
spec v0.4.0 引入CHECK命令;DEL命令需要传递prevResult
spec v0.3.1
spec v0.3.0 丰富result类型,支持链式插件
spec v0.2.0 支持VERSION命令
spec v0.1.0 初始版本

概述

本文旨在说明为Linux上的应用容器提供一种通用的基于插件的网络解决方案,即容器网络接口(Container network interface),简称CNI。源自rkt网络解决方案,rkt旨在适应多种网络设计思路。

为了更好的描述该规范,约定下面两个术语,本文后续使用networkcontainer描述:

  • 容器(container):可以认为等同于Linux网络命名空间。具体的承载单元,取决于特定的容器runtime实现。例如,rkt中每一个pod都是运行在一个独一无二的网络命名空间中;而docker中,每一个独立的Docker容器都存在一个独立的网络命名空间;
  • 网络(network):指一组实体,这些实体都是独一无二可寻址的而且可以相互通信的。单个实体可以是一个独立的容器container,一台机器或者其他网络设备(例如一个路由器)。逻辑上containers可以添加到一个或者多个网络network,也可以从中退出。

本文旨在规范runtimesplugins之间的接口。尽管有一些众所周知的字段,runtimes可能希望增加一些额外的信息给plugins。这些拓展不是规范的一部分,而是归属于约定

关键字”must”, “must not”, “required”, “shall”, “shall not”, “should”, “should not”, “recommended”, “may”和”optional”都是遵循规范RFC 2119。注意:本文对应于必须,必须不等等。

基本准则

  • 为容器执行任何插件之前,容器runtime必须创建一个新的网络命名空间;
  • runtime必须决定这个容器应该属于那些networks,而且对于每个network那些插件必须执行;
  • network配置是JSON格式,而且容易存储到文件。network配置包含强制性字段,如”name”和”type”;”type”对应于插件名。network配置允许字段在调用过程中变更值。为此,可选配置”args”,必须包含可变信息;
  • 容器runtime必须依次把容器添加到network中,通过执行相应的插件实现;
  • 容器生命周期结束之后,runtime必须以逆序(相对于加入network的操作顺序)执行插件,把容器从这些networks中退出;
  • 容器runtime必须不能对同一个容器并发执行network操作,但是允许对不同容器并发执行network操作;
  • 容器runtime必须保证对容器的ADDDEL操作是对应的,ADD之后总是对应一个DEL操作。DEL之后可能有其他的DELS,但是插件应该允许处理多个DEL操作,即插件的DEL应该是幂等的;
  • 容器必须通过独一无二的容器ID来标识。插件存储状态信息,应该使用(network name, CNI_CONTAINERID, CNI_IFNAME)作为主键;
  • 容器runtime必须不能对同一个(network name, container id, name of the interface inside the container)执行两次ADD操作(没有执行相应的DEL操作)。这意味着对同一个容器ID添加到特定的网络多次,只能是每次添加操作针对不同的接口名;
  • CNI结构(Network Configuration 和CNI Plugin Result)中的字段,除非标记为可选的,否则是必须的;

CNI插件

CNI插件必须以可执行文件的方式存在,由容器管理系统(rkt或者K8S)调用。

CNI插件负责把网络接口(例如veth对的一端)插入到容器网络命名空间,并且在host上面做必要的修改(例如把veth对的另外一端链接到bridge上面)。然后CNI插件需要给网络接口分配一个IP,并且通过调用适当的IPAM插件设置与IP地址管理部分一致的路由配置。

ADD操作参数

作用:把容器加入到network中。

支持的参数如下:

参数 作用
Container ID runtime分配的独一无二的明文标识符,必须以字母数字开头,后面可以跟一个或者多个字母数字、下划线_、点.或者中划线-字符的任意组合。
Network namespace path 表示加入的网络命名空间的路径,例如/proc/[pid]/ns/net或者bind-mount/link的路径。
Network configuration 描述容器将加入的network的JSON文档,下面会有章节详细描述。
Extra arguments 提供了一个替代的机制,允许对每个基础容器上的插件进行简单的配置。
Name of the interface inside the container 指定容器(网络命名空间)内创建的接口名称;该名称必须遵守Linux对接口名称的标准规范,必须不为空,必须不为”.”或者”..”,必须小于16个字符,而且不能包含”/“、”:”或者其他任何空白字符。

返回结果结构如下:

字段 描述
Interfaces list 取决于插件实现,可以包含sandbox(如容器或者hypervisor)中的接口名(如网卡名)或者主机上的接口名,每个接口的物理地址,以及这些接口所在的sandbox的详细信息。
IP configuration assigned to each interface 分配给sandbox或/和主机的接口的IPV4/IPV6的地址,网关和路由信息。
DNS information 包含DNS信息(nameservers,domain,search domains,options)的字典

DEL操作参数

作用:把容器从network中退出。

支持参数如下:

参数 作用
Container ID 同ADD
Network namespace path 同ADD
Network configuration 同ADD
Extra arguments 同ADD
Name of the interface inside the container 同ADD

功能规范要求:

  • 传递给DEL的参数,必须和对应的ADD的一样;
  • DEL操作应该释放container对已配置network持有的所有资源;
  • 如果已知对container的前一个ADD操作,那么runtime必须把前一个ADD返回的Result传递给当前JSON配置的插件(或者插件链的所有插件)的prevResult字段。runtime可能希望使用libcni提供的Result cache
  • 如果没有提供CNI_NETNS和/或prevResult,插件应该尽可能的释放资源(如释放IPAM分配的IP),并返回成功响应;
  • 如果runtime cache中缓存了给定容器的上一个ADD操作的Result,那么必须在DEL操作成功之后,删除该缓存的Result

即使某些资源丢失,插件一般也应该无错误的完成DEL操作。例如,即使容器网络命名空间不存在,IPAM插件也应该释放分配的IP,并且返回成功;除非这个网络命名空间对于IPAM管理器至关重要。虽然DHCP通常会给容器网络接口发送一个”释放”消息,但是由于DHCP租约存在一个生命周期,所以这个释放操作不会被认为是关键的,也不应该返回任何错误。

再例如,即使容器网络命名空间和/或容器的接口不存在了,bridge插件也应该把DEL操作委托给IPAM插件去清理自己的资源(如果有)。

CHECK操作参数

作用:检测容器的网络是否符合预期。

支持参数如下:

参数 作用
Container ID 同ADD
Network namespace path 同ADD
Network configuration 同ADD,必须包含prevResult字段,值为该容器的上一个ADD操作返回的Result
Extra arguments 同ADD
Name of the interface inside the container 同ADD

返回结果:

  • 无任何返回或者返回错误

功能规范要求:

  • 插件必须通过pervResult确定期望的网络接口和地址;

  • 插件必须允许插件链后续的插件修改网络资源,例如路由;

  • 如果CNI结果类型中包含的资源(网络接口、地址或者路由)是由插件创建的,并且在prevResult中列出,但丢失或者处于非法状态,那么插件应该返回错误;

  • 如果非CNI结果类型中的资源,丢失或者处于非法状态,如下面的资源,插件也应该返回错误:

    • 防火墙规则;
    • 流量控制;
    • IP预订;
    • 外部依赖,如链接需要的守护进程;
    • 其他。。。
  • 如果插件感知到容器处于一般无法到达的状态,应该返回错误;

  • 插件必须处理ADD之后立刻执行CHECK的调用,因此应该允许任何异步资源有合理的收敛延迟;

  • 插件应该在委托的所有插件(如IPAM)上面调用CHECK,而且返回错误给调用者;

  • runtime必须不能对未执行ADD或者执行过最后ADD之后的DEL的容器执行CHECK

  • 如果网络配置的disableCheck设置为true,那么runtime必须不能执行CHECK

  • runtime必须把前一个ADD返回的Result传递给当前JSON配置的插件(或者插件链的所有插件)的prevResult字段。runtime可能希望使用libcni提供的Result cache

  • 当一个插件返回错误,runtime可能选择停止对插件链后续插件执行CHECK

  • runtime可以在ADD之后立即执行CHECK,直到容器从network中删除;

  • runtime可能认为CHECK失败的容器,将永久处于错误状态;

VERSION操作参数

作用:报告版本信息;

参数:无;

结果:插件支持的CNI规范版本信息,详细结构如下

1
2
3
4
{
"cniVersion": "0.4.0", // the version of the CNI spec in use for this output
"supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0" ] // the list of CNI spec versions that this plugin supports
}

插件执行说明

runtime必须根据networktype字段指定的名字作为可执行文件的名字调用。runtime应该从一组预定义的目录(该规范没有指定目录列表,但库实现有:/opt/cni/bin)中搜索该可执行文件。一旦找到,runtime必须使用以下的环境变量传递参数:

  • CNI_COMMAND:表示期望执行的操作;ADDDELCHECK或者VERSION
  • CNI_CONTAINERID:容器ID;
  • CNI_NETNS:网络命名空间文件路径;
  • CNI_IFNAME:构建的接口名,如果插件无法使用该接口名,必须返回一个错误;
  • CNI_ARGS:调用时用户传入的额外参数,以’;’分割的字母数字键值对,例如:”FOO=BAR;ABC=123”
  • CNI_PATH:列出搜索CNI插件可执行文件的路径列表,路径由特定于OS的列表分割符分割,例如,Linux的’:’,Windows的’;’

JSON格式的网络配置信息,必须通过stdin流的方式传递给插件。这意味着它不需要绑定到磁盘的特定文件,而且可能包含调用过程中变化的信息。

CNI插件总结

runtime可以通过两种方式传递参数或者信息给插件:

  1. 网络配置信息,通过stdin流传递给插件;
  2. 环境变量;

Result

注意IPAM插件应该返回一个简洁的Result结构体,如[IP Allocation](#IP Allocation)章节所描述。

执行ADD操作,插件必须返回0表示成功,而且把下面的JSON结构输出到stdout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"cniVersion": "0.4.0",
"interfaces": [ (this key omitted by IPAM plugins)
{
"name": "<name>",
"mac": "<MAC address>", (required if L2 addresses are meaningful)
"sandbox": "<netns path or hypervisor identifier>" (required for container/hypervisor interfaces, empty/omitted for host interfaces)
}
],
"ips": [
{
"version": "<4-or-6>",
"address": "<ip-and-prefix-in-CIDR>",
"gateway": "<ip-address-of-the-gateway>", (optional)
"interface": <numeric index into 'interfaces' list>
},
...
],
"routes": [ (optional)
{
"dst": "<ip-and-prefix-in-cidr>",
"gw": "<ip-of-next-hop>" (optional)
},
...
],
"dns": { (optional)
"nameservers": <list-of-nameservers> (optional)
"domain": <name-of-local-domain> (optional)
"search": <list-of-additional-search-domains> (optional)
"options": <list-of-options> (optional)
}
}

ipsdns字段应该和IPAM创建返回的结果一样,除此之外插件应该合理的填充interface字段;由于IPAM插件对interfaces无感知,因此IPAM返回的输出中无interface字段。

cniVersion描述当前插件使用的CNI规范版本。插件可能支持多个CNI规范版本(通过VERSION操作获取),插件返回的cniVersion必须和网络配置中设置的cniVersion一致。如果插件不支持网络配置指定的cniVersion,插件应该返回一个错误码”1”(详情见[Error Code](#Error Codes))。

interfaces描述插件创建的网络接口。如果CNI_IFNAME环境变量存在,插件必须使用它指定的名字创建sandbox/hypervisor接口,如果不能创建则返回错误。

  • mac:字符串类型,值为接口的物理地址。如果L2地址对插件无意义,那么该字段是可选的;
  • sandbox:字符串类型,容器(基于命名空间的)环境应该返回该sandbox的网络命名空间的完整文件系统路径。hypervisor(基于虚拟机的)插件应该返回创建接口的虚拟sandbox的唯一ID。对于创建或者移动到sandbox(网络命名空间)或者hypervisor(VM)的接口,必须指定此项。

ips字段表示一组IP配置信息。详情见well-know ips

routes字段表示一组路由的配置信息。详情见well-know routes

dns字段表示常见DNS信息组成的字典。详情见well-know DNS。本规范未声明CNI消费者必须如何处理这些信息。常用的例子,生成一个/etc/resolv.conf文件并且注入容器的文件系统,或者在主机上运行一个DNS转发器。

错误

错误通过非0的返回值和如下的stdout的输出描述:

1
2
3
4
5
6
{
"cniVersion": "0.4.0",
"code": <numeric-error-code>,
"msg": <short-error-message>,
"details": <long-error-message> (optional)
}

cniVersion表示插件使用的CNI规范版本,错误码”0-99”为保留的常用的错误。”100+”的错误码插件可以自由使用。

此外,stderr可以用于输出未格式化的信息,例如日志。

Network Configuration

网络配置以JSON格式呈现。该配置可能存储在磁盘,也可能由容器runtime的其他组件生成。网络配置包含下面的通用字段:

  • cniVersion:字符串类型,当前配置符合的CNI规范版本;
  • name:字符串类型,网络名称。在主机(或者其他管理域)上的所有容器中应该是唯一的。必须以数字字符开头,后面可以跟一个或者多个字母数字、下划线_、点.或者中划线-字符的任意组合;
  • type:字符串类型,表示CNI插件可执行文件名;
  • args:字典类型(可选),容器runtime提供的额外参数。例如,可以通过设置args的一项,可以把一个标签字典传递给CNI插件;
  • ipMasq:布尔类型(可选),如果插件支持,会在主机上为该网络设置IP伪装。如果主机作为子网的网关,而子网无法路由到分配给容器的IP,那么配置ipMasq是必须的;
  • ipam:字典类型(可选),IPAM特定值的字典:
    • type:字符串类型,表示IPAM插件可执行文件名;
  • dns:字典类型(可选),DNS特定值的字典:
    • nameservers:字符串数组(可选),该网络能够识别的DNS域名服务器的优先顺序列表。数组每项包含一个IPV4或者IPV6的地址;
    • domain:字符串类型,用于短主机名查找的本地域;
    • search:字符串数组类型,为短主机名查找的优先顺序搜索域的列表。大部分解析都优先于domain执行。
    • options:字符串数组类型,传递给解析器的一组选项。查看CNI插件Result获取更多信息。

插件可以定义他们能接受的附加字段,但是如果传递未知的字段可能会报错。而args字段可以传递任意的数据,如果未知插件会忽略该字段。

示例

bridge的网络配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"cniVersion": "0.4.0",
"name": "dbnet",
"type": "bridge",
// type (plugin) specific
"bridge": "cni0",
"ipam": {
"type": "host-local",
// ipam specific
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": [ "10.1.0.1" ]
}
}

ovs网络配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"cniVersion": "0.4.0",
"name": "pci",
"type": "ovs",
// type (plugin) specific
"bridge": "ovs0",
"vxlanID": 42,
"ipam": {
"type": "dhcp",
"routes": [ { "dst": "10.3.0.0/16" }, { "dst": "10.4.0.0/16" } ]
},
// args may be ignored by plugins
"args": {
"labels" : {
"appVersion" : "1.0"
}
}
}

macvlan网络配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"cniVersion": "0.4.0",
"name": "wan",
"type": "macvlan",
// ipam specific
"ipam": {
"type": "dhcp",
"routes": [ { "dst": "10.0.0.0/8", "gw": "10.0.0.1" } ]
},
"dns": {
"nameservers": [ "10.0.0.1" ]
}
}

Network Configuration Lists

网络配置列表提供了对单容器按特定顺序执行多个插件的机制,并且把每个插件的执行结果传递给它的下一个插件。结果和网络配置一样,以JSON为格式,包含一些通用的配置项和一组[网络配置](#Network Configuration)的列表。该配置可能存储在磁盘,也可能由容器runtime的其他组件生成。下面是通用配置的描述:

  • cniVersion:字符串类型,当前配置符合的CNI规范版本;
  • name:字符串类型,网络名称。在主机(或者其他管理域)上的所有容器中应该是唯一的。必须以数字字符开头,后面可以跟一个或者多个字母数字、下划线_、点.或者中划线-字符的任意组合;
  • disableCheck:字符串类型,值为true或者false。如果disableChecktrueruntime必须不对该网络配置列表进行CHECK操作。这可以允许管理员阻止CHECK组合插件已知的假错误。
  • plugins:网络配置列表类型,一组CNI网络配置字典(详细信息见上节);

执行插件链时,runtime必须替换plugins中的每个网络配置的namecniVersion为网络配置列表本身的namecniVersion。这样可以保证执行所有插件时CNI版本一致,防止由于版本不一致导致的错误。

如果插件的网络配置文件的capabilities字段声明支持某种能力,那么runtime可能把基于该能力的配置通过插件的配置JSON的runtimeConfig字段传递过去。

对于ADD操作,runtime必须在执行第一个插件之后,把上一次插件执行结果(Result),添加到JSON配置的prevResult字段。而且插件应该把它收到的prevResult的内容输出到stdout,从而允许后续的插件(和runtime)接收到这些内容,除非是当前插件需要修改或者屏蔽到之前的结果。插件允许修改或者屏蔽prevResult的全部或者部分内容。然而,插件支持的CNI规范版本包好了prevResult字段,那么插件必须处理prevResult字段,可以是修改、透传或者屏蔽。对prevResult字段一无所知是不符合规范的。

对于CHECKDEL操作,runtime必须(除非不可用,DEL可以忽略)为每一个插件的JSON配置添加prevResult字段,其值必须是前一个ADD操作的JSON格式的结果。

runtime必须以相同的环境执行列表中的每一个插件。

DEL操作的顺序必须是和列表顺序相反。

网络配置列表错误处理

当插件列表执行操作(ADD或者DEL)出现错误时,runtime必须停止该列表的执行。

如果ADD执行失败,当runtime决定处理该错误时,它应该对列表中的所有插件执行DEL操作(相对于ADD的逆序),即使执行ADD操作时某些插件没有被调用。

网络配置列表示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"cniVersion": "0.4.0",
"name": "dbnet",
"plugins": [
{
"type": "bridge",
// type (plugin) specific
"bridge": "cni0",
// args may be ignored by plugins
"args": {
"labels" : {
"appVersion" : "1.0"
}
},
"ipam": {
"type": "host-local",
// ipam specific
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": [ "10.1.0.1" ]
}
},
{
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "500"
}
}
]
}

网络配置列表运行示例

ADD操作

以上节的网络配置列表的JSON为例,runtime运行ADD操作,会执行如下步骤。注意:runtimeplugins的所有网络配置添加cniVersionname字段,以保证所有插件运行时版本和名字一致。

  1. 首先,使用如下JSON配置,调用bridge插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "cniVersion": "0.4.0",
    "name": "dbnet",
    "type": "bridge",
    "bridge": "cni0",
    "args": {
    "labels" : {
    "appVersion" : "1.0"
    }
    },
    "ipam": {
    "type": "host-local",
    // ipam specific
    "subnet": "10.1.0.0/16",
    "gateway": "10.1.0.1"
    },
    "dns": {
    "nameservers": [ "10.1.0.1" ]
    }
    }
  2. 然后,以下面的JSON配置,调用tuning插件,prevResult字段包含了bridge插件的返回JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    {
    "cniVersion": "0.4.0",
    "name": "dbnet",
    "type": "tuning",
    "sysctl": {
    "net.core.somaxconn": "500"
    },
    "prevResult": {
    "ips": [
    {
    "version": "4",
    "address": "10.0.0.5/32",
    "interface": 2
    }
    ],
    "interfaces": [
    {
    "name": "cni0",
    "mac": "00:11:22:33:44:55"
    },
    {
    "name": "veth3243",
    "mac": "55:44:33:22:11:11"
    },
    {
    "name": "eth0",
    "mac": "99:88:77:66:55:44",
    "sandbox": "/var/run/netns/blue"
    }
    ],
    "dns": {
    "nameservers": [ "10.1.0.1" ]
    }
    }
    }

CHECK操作

以相同的网络配置列表的JSON为例,runtime运行CHECK操作,会执行如下步骤。

  1. 首先,以如下JSON,调用bridge插件,prevResult字段包含了ADD插件的返回JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    {
    "cniVersion": "0.4.0",
    "name": "dbnet",
    "type": "bridge",
    "bridge": "cni0",
    "args": {
    "labels" : {
    "appVersion" : "1.0"
    }
    },
    "ipam": {
    "type": "host-local",
    // ipam specific
    "subnet": "10.1.0.0/16",
    "gateway": "10.1.0.1"
    },
    "dns": {
    "nameservers": [ "10.1.0.1" ]
    },
    "prevResult": {
    "ips": [
    {
    "version": "4",
    "address": "10.0.0.5/32",
    "interface": 2
    }
    ],
    "interfaces": [
    {
    "name": "cni0",
    "mac": "00:11:22:33:44:55"
    },
    {
    "name": "veth3243",
    "mac": "55:44:33:22:11:11"
    },
    {
    "name": "eth0",
    "mac": "99:88:77:66:55:44",
    "sandbox": "/var/run/netns/blue"
    }
    ],
    "dns": {
    "nameservers": [ "10.1.0.1" ]
    }
    }
    }
  2. 然后,以如下JSON,调用tuning插件,prevResult字段包含了ADD插件的返回JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    {
    "cniVersion": "0.4.0",
    "name": "dbnet",
    "type": "tuning",
    "sysctl": {
    "net.core.somaxconn": "500"
    },
    "prevResult": {
    "ips": [
    {
    "version": "4",
    "address": "10.0.0.5/32",
    "interface": 2
    }
    ],
    "interfaces": [
    {
    "name": "cni0",
    "mac": "00:11:22:33:44:55"
    },
    {
    "name": "veth3243",
    "mac": "55:44:33:22:11:11"
    },
    {
    "name": "eth0",
    "mac": "99:88:77:66:55:44",
    "sandbox": "/var/run/netns/blue"
    }
    ],
    "dns": {
    "nameservers": [ "10.1.0.1" ]
    }
    }
    }

DEL操作

以相同的网络配置列表的JSON为例,runtime运行DEL操作,会执行如下步骤。注意:插件执行的顺序与ADDCHECK是相反的。

  1. 首先,以如下JSON配置,执行tuning插件,prevResult字段包含了ADD插件的返回JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    {
    "cniVersion": "0.4.0",
    "name": "dbnet",
    "type": "tuning",
    "sysctl": {
    "net.core.somaxconn": "500"
    },
    "prevResult": {
    "ips": [
    {
    "version": "4",
    "address": "10.0.0.5/32",
    "interface": 2
    }
    ],
    "interfaces": [
    {
    "name": "cni0",
    "mac": "00:11:22:33:44:55"
    },
    {
    "name": "veth3243",
    "mac": "55:44:33:22:11:11"
    },
    {
    "name": "eth0",
    "mac": "99:88:77:66:55:44",
    "sandbox": "/var/run/netns/blue"
    }
    ],
    "dns": {
    "nameservers": [ "10.1.0.1" ]
    }
    }
    }
  2. 然后,以如下JSON配置,执行bridge插件,prevResult字段包含了ADD插件的返回JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    {
    "cniVersion": "0.4.0",
    "name": "dbnet",
    "type": "bridge",
    "bridge": "cni0",
    "args": {
    "labels" : {
    "appVersion" : "1.0"
    }
    },
    "ipam": {
    "type": "host-local",
    // ipam specific
    "subnet": "10.1.0.0/16",
    "gateway": "10.1.0.1"
    },
    "dns": {
    "nameservers": [ "10.1.0.1" ]
    },
    "prevResult": {
    "ips": [
    {
    "version": "4",
    "address": "10.0.0.5/32",
    "interface": 2
    }
    ],
    "interfaces": [
    {
    "name": "cni0",
    "mac": "00:11:22:33:44:55"
    },
    {
    "name": "veth3243",
    "mac": "55:44:33:22:11:11"
    },
    {
    "name": "eth0",
    "mac": "99:88:77:66:55:44",
    "sandbox": "/var/run/netns/blue"
    }
    ],
    "dns": {
    "nameservers": [ "10.1.0.1" ]
    }
    }
    }

IP Allocation

CNI插件被期望去分配IP地址,并且安装接口相关的任何必要的路由配置。这个CNI插件带来巨大的灵活性,同时也带来巨大的负担。有些CNI插件可能需要通过相同的代码支持多种用户期望的IP管理策略(例如,dhcp或host-local)。

为了减少这种负担,并且使得IP管理策略和CNI插件类型正交,本规范定了第二种插件类型(IP Address Management Plugin = IPAM插件)。而CNI插件需要在执行过程中,选择合适的时间调用IPAM插件。IPAM插件必须决定接口的IP/子网、网关和路由并且把这些信息返回给调用它的”主“插件。

IPAM插件可以通过协议(如dhcp)、本地文件系统存储,网络配置文件的ipam字段甚至以上方式的集合获取信息。

IPAM Interface

和CNI插件类似,IPAM插件作为可执行文件被运行。可执行文件通过搜索预定义的一批路径搜索获得,通过CNI_PATH传递给CNI插件。IPAM插件必须接受CNI插件完全一样的环境变量。IPAM插件也是通过stdin接受网络配置的。

执行成功,则返回0并且输出如下JSON信息到stdout(以ADD操作为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"cniVersion": "0.4.0",
"ips": [
{
"version": "<4-or-6>",
"address": "<ip-and-prefix-in-CIDR>",
"gateway": "<ip-address-of-the-gateway>" (optional)
},
...
],
"routes": [ (optional)
{
"dst": "<ip-and-prefix-in-cidr>",
"gw": "<ip-of-next-hop>" (optional)
},
...
]
"dns": { (optional)
"nameservers": <list-of-nameservers> (optional)
"domain": <name-of-local-domain> (optional)
"search": <list-of-search-domains> (optional)
"options": <list-of-options> (optional)
}
}

注意:不像常规的CNI插件,IPAM插件应该返回一个简洁的Result结构,不包含interfaces字段,由于IPAM插件应该对interfaces无感知,而是由父插件配置,除非IPAM由特殊依赖(例如,dhcp IPAM插件)。

cniVersion描述当前插件使用的CNI规范版本。插件可能支持多个CNI规范版本(通过VERSION操作获取),插件返回的cniVersion必须和网络配置中设置的cniVersion一致。如果插件不支持网络配置指定的cniVersion,插件应该返回一个错误码”1”(详情见[Error Code](#Error Codes))。

ips字段表示一组IP配置信息。详情见well-know ips

routes字段表示一组路由的配置信息。详情见well-know routes

dns字段表示常见DNS信息组成的字典。详情见well-know DNS

错误和日志和CNI插件使用相同的方式。详情见Result错误
IPAM插件的例子:

  • host-local:选择指定区间内一个未使用的IP(相同主机上其他容器);
  • dhcp:使用DHCP协议区获取和维护租约。DHCP请求将通过创建的容器接口发送,因此,相关的网络必须支持广播。

注意

  • 默认路由可能设置为”0.0.0.0/0”。由于其他网络可能已经设置默认路由,因此CNI插件应该准备跳过默认路由设置。
  • 期望以0为指标添加路由

Well-know Structures

本章主要是说明众所周知的结构体,例如,IP、路由以及DNS,还有错误码。

ips

1
2
3
4
5
6
7
8
9
"ips": [
{
"version": "<4-or-6>",
"address": "<ip-and-prefix-in-CIDR>",
"gateway": "<ip-address-of-the-gateway>", (optional)
"interface": <numeric index into 'interfaces' list> (not required for IPAM plugins)
},
...
]

ips字段的值是由插件决定的一组IP配置信息列表。每一项表示一个网络接口的IP配置信息字典。多个网络接口或者一个接口的多个网络配置,可能通过独立ips的项表示。应该提供插件所知道的所有属性,即使不是严格要求的。

  • version:string类型,值范围为”4”或者”6”,对应当前项的IP地址的版本。所有的IP地址和网关对于给定版本必须是合法的。
  • address:string类型,CIDR格式的IP地址,例如”192.168.1.3/24”。
  • gateway:string类型,为子网设置默认的网关,如果该网关存在。CNI插件不会为该网关设置任何路由配置,路由配置是通过routes项单独设置。使用该配置的一个例子,CNI插件bridge为Linux桥配置一个IP,使得该桥成为网关。
  • interface:uint类型,CNI插件Resultinterface链表的下标,指明该IP配置将应用到那个接口上。IPAM插件不应该返回该字段,因为它们没有关于网络接口的信息。

routes

1
2
3
4
5
6
7
"routes": [
{
"dst": "<ip-and-prefix-in-cidr>",
"gw": "<ip-of-next-hop>" (optional)
},
...
]

每个条目包含如下字段。条目中,所有的IP地址必须是相同的IP版本,4或者6。

  • dst:string类型,CIDR格式的目的地址子网;
  • gw:string类型,网关的IP,如果省略,则假定为默认网关(由CNI插件决定);

每个条目必须和CNI_IFNAME指定的 sandbox的接口关联。

DNS

1
2
3
4
5
6
"dns": {
"nameservers": <list-of-nameservers> (optional)
"domain": <name-of-local-domain> (optional)
"search": <list-of-additional-search-domains> (optional)
"options": <list-of-options> (optional)
}

dns字段包含了常用DNS信息的字典。

  • nameservers:字符串数组类型,该网络能够识别的DNS域名服务器的优先顺序列表。数组每项包含一个IPV4或者IPV6的地址;
  • domain:字符串类型,用于短主机名查找的本地域;
  • search:字符串数组类型,为短主机名查找的优先顺序搜索域的列表。大部分解析都优先于domain执行。
  • options:字符串数组类型,传递给解析器的一组选项。查看CNI插件Result获取更多信息。

Error Codes

1-99为规范保留的通用错误码,必须不能被其他地方使用。

Error Code Error Description
-1 Incompatible CNI version
-2 Unsupported field in network configuration. The error message must contain the key and value of the unsupported field.
-3 Container unknown or does not exist. This error implies the runtime does not need to perform any container network cleanup (for example, calling the DEL action on the container).
-4 Invalid necessary environment variables, like CNI_COMMAND, CNI_CONTAINERID, etc. The error message must contain the names of invalid variables.
-5 I/O failure. For example, failed to read network config bytes from stdin.
-6 Failed to decode content. For example, failed to unmarshal network config from bytes or failed to decode version info from string.
-7 Invalid network config. If some validations on network configs do not pass, this error will be raised.
-11 Try again later. If the plugin detects some transient condition that should clear up, it can use this code to notify the runtime it should re-try the operation later.

Podman之网络

Podman提供了网络能力,但是不同于Docker自己实现完整的网络机制;而是通过CNI机制来实现的,类似于CRI接口的CNI使用方式。基于CNI机制,通过管理和操作CNI的网络配置文件来实现网络的管理能力。

网络子命令

网络子命令,提供了网络的管理能力,主要包括网络的创建、删除、查看等。虽然叫网络的创建、删除等,其实只是对CNI网络配置文件的管理;对应于网络配置文件的创建、删除以及查看等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ podman network --help
Manage networks

Description:
Manage networks

Usage:
podman network [command]

Available Commands:
create network create
inspect network inspect
ls network list
rm network rm

基本流程

Podman支持本地模式ABI和远端服务模式Tunnel;虽然调用方式不一样,但是最终都是通过abi.ContainerEngine的网络接口完成的。为了更好的,认识这个流程,先通过序列图理解一下。

TUNNEL模式的序列图如下:

sequenceDiagram
    autonumber
    participant Client
    participant TUNNEL
    participant libpod
    participant ABI
    Client ->> TUNNEL: call ContainerEngine interface
    TUNNEL ->> libpod: http request
    libpod ->> ABI: call ABI implement of ContainerEngine
    ABI ->> ABI: do network operator
    ABI -->> libpod: return result
    libpod -->> TUNNEL: write http response
    TUNNEL -->> Client: return result

ABI模式就是去掉了http通信的流程,直接调用ABI实现的接口;序列图如下:

sequenceDiagram
    autonumber
    participant Client
    participant ABI
    Client ->> ABI: call ContainerEngine interface
    ABI ->> ABI: do network operator
    ABI -->> Client: return result

上述流程涉及的主要类和接口如下类图所示:

classDiagram
class ContainerEngine{
    <>
    NetworkCreate()
    NetworkInspect()
    NetworkList()
    NetworkRm()
}

class abiContainerEngine{
    Libpod *libpod.Runtime
}

class tunnelContainerEngine{
    ClientCxt context.Context
}

ContainerEngine <|-- abiContainerEngine : implements
ContainerEngine <|-- tunnelContainerEngine : implements

网络创建功能

由于ABI和TUNNEL两种模式,最终使用的都是ABI的NetworkCreate,因此我们直接从该函数开始分析。

graph TD
    A(NetworkCreate) --> B{MacVLAN > 0}
    B -->|yes| C[createMacVLAN]
    B -->|no| D[createBridge]
    C --> E(create NewNcList with macvlan)
    D --> F(create NewNcList with bridge)
    E --> G[save config to json file]
    F --> G
1
2
3
4
5
6
7
8
9
10
11
// NcList describes a generic map
type NcList map[string]interface{}

// NewNcList creates a generic map of values with string
// keys and adds in version and network name
func NewNcList(name, version string) NcList {
n := NcList{}
n["cniVersion"] = version
n["name"] = name
return n
}

由于golang支持反射,所以可以通过map[string][interface]的方式,动态修改结构体,最后通过JSON库完成结构体和JSON字符串的转换。所以,网络的创建接口中,使用了NcList,把用户配置的网络参数配置到CNI网络配置中,最后生成对应的JSON字符串,并写入文件中。

注:生成的CNI网络配置均为conflist

inspect网络功能

flowchart TB
    A(NetworkInspect) --> B[foreach name]
    B --> C(read config of name)
    B --> D(save read config)

删除网络功能

flowchart TB
    A(NetworkRm) --> B[foreach name]
    B --> C(remove containers associate with network)
    B --> D(remove interface by ip command)
    B --> E(remove read config)

列举网络功能

flowchart TB
    A(NetworkList) --> B[foreach file with .conflist]
    B --> C(parse config file)
    B --> D(add parsed struct to result)

容器生命周期的网络管理

本章节主要是梳理容器生命周期中,涉及网络的内容。

容器创建

flowchart TB
    A(ContainerCreate) --> B[MakeContainer]
    B --> C(createContainerOptions)
    C --> D(namespaceOptions)
    D --> E(s.NetNS.NSMode)
    D --> F(dns, hosts, resolv)

容器启动

flowchart TB
    A(ContainerStart) --> B[Start]
    B --> C(prepareToStart)
    C --> X(prepare)
    X --> Y{c.state.NetNS == nil}
    Y -->|yes| Z(createNetNS)
    C --> D(init)
    D --> E(completeNetworkSetup)
    E --> F(setupNetNS)
    E --> G(bind mount resolv conf)
    F --> K[Create NSPath]
    F --> L[Mount NSPath]
    F --> H[configureNetNS]
    H --> I(getPodNetwork)
    H --> J(SetUpPod)

注意:

  • SetUpPod就是开始调用CNI库,执行CNI网络插件进行真正的网络资源分配了。本文主要涉及Podman的分析,CNI具体的实现不在本文分析;

容器停止

flowchart TB
    A(ContainerStop) --> B[Cleanup]
    B --> D(cleanup)
    D --> E(cleanupNetwork)
    E --> F(teardownNetNS)
    F --> G[getPodNetwork]
    F --> H[TearDownPod]
    F --> I[UnmountNS]
    F --> J[NetNS.Close]

容器删除

flowchart TB
    A(ContainerRm) --> B[RemoveContainer]
    B --> C(removeContainer)
    C --> D(cleanup)
    D --> E(cleanupNetwork)
    E --> F(teardownNetNS)
    F --> G[getPodNetwork]
    F --> H[TearDownPod]
    F --> I[UnmountNS]
    F --> J[NetNS.Close]

Podman网络之CNI模块

Podman的网络能力是基于CNI实现的,那么它是如何利用CNI网络来实现的呢?通过分析源码可以知道,Podman是直接利用了CRI-O封装的cniNetworkPlugin模块实现的。因此,本文分析的其实主要是CRI-O的cniNetworkPlugin模块的实现。

初识结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type cniNetworkPlugin struct {
// libcni功能结构体
cniConfig *libcni.CNIConfig
// loop网络配置
loNetwork *cniNetwork
sync.RWMutex
// 默认网络名
defaultNetName netName
// 管理的网络集合
networks map[string]*cniNetwork

// 网络命名空间管理模块
nsManager *nsManager
// cni配置文件存储目录
confDir string
// cni插件存储目录
binDirs []string

// 动态监控cni配置文件存储目录的机制
shutdownChan chan struct{}
watcher *fsnotify.Watcher
done *sync.WaitGroup

// The pod map provides synchronization for a given pod's network
// operations. Each pod's setup/teardown/status operations
// are synchronized against each other, but network operations of other
// pods can proceed in parallel.
podsLock sync.Mutex
// 针对每一个pod的网络平面的锁
pods map[string]*podLock

// For testcases
exec cniinvoke.Exec
cacheDir string
}

初始化CNI模块

初始化主要负责如下功能:

  • 结构体初始化;
  • 初始化netns管理模块;
  • 加载cni网络配置文件;
  • 启动监控cni网络配置目录变化的协程;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func initCNI(exec cniinvoke.Exec, cacheDir, defaultNetName string, confDir string, binDirs ...string) (CNIPlugin, error) {
// 1. 初始化 plugin := &cniNetworkPlugin
... ...
// 2. 初始化netns
sm, err := newNSManager()
plugin.nsManager = nsm
... ...
// 3. 加载cni配置文件
plugin.syncNetworkConfig()
... ...
// 4. 启动监控协程
plugin.watcher, err = newWatcher(plugin.confDir)
startWg := sync.WaitGroup{}
startWg.Add(1)
go plugin.monitorConfDir(&startWg)
// 同步协程
startWg.Wait()
... ...
}

结构体初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
plugin := &cniNetworkPlugin{
cniConfig: libcni.NewCNIConfig(binDirs, exec),
defaultNetName: netName{
name: defaultNetName,
// If defaultNetName is not assigned in initialization,
// it should be changeable
changeable: defaultNetName == "",
},
networks: make(map[string]*cniNetwork),
loNetwork: getLoNetwork(),
confDir: confDir,
binDirs: binDirs,
shutdownChan: make(chan struct{}),
done: &sync.WaitGroup{},
pods: make(map[string]*podLock),
exec: exec,
cacheDir: cacheDir,
}

需要关注的两个点:

  • cniConfig:为cniNetworkPlugin提供了CNI底层操作的能力(加入和退出网络平面);
  • defaultNetName:既支持用户设置默认网络平面名,又可以自动设置(能够兼容K8S docker-shim的实现,通过排序所有配置文件名,把第一个合法的配置文件作为默认的网络平面名);

netns模块初始化

该模块通过封装nsenter命令,实现进入网络命名空间获取对应的网络配置信息,例如,ip,mac等等相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var defaultNamespaceEnterCommandName = "nsenter"

type nsManager struct {
nsenterPath string
}

func (nsm *nsManager) init() error {
var err error
nsm.nsenterPath, err = exec.LookPath(defaultNamespaceEnterCommandName)
return err
}

// cri-o/ocicni/pkg/ocicni/util_linux.go
func getContainerDetails(nsm *nsManager, netnsPath, interfaceName, addrType string) (*net.IPNet, *net.HardwareAddr, error)

加载cni网络配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// cri-o/ocicni/pkg/ocicni/ocicni.go
// syncNetworkConfig --> loadNetworks
func loadNetworks(confDir string, cni *libcni.CNIConfig) (map[string]*cniNetwork, string, error) {
// 通过libcni库提供的API接口,
// 读取confDir目录下面所有后缀为.conf/.conflist/.json的文件
files, err := libcni.ConfFiles(confDir, []string{".conf", ".conflist", ".json"})
networks := make(map[string]*cniNetwork)
defaultNetName := ""
// 对文件名进行字典序排序
sort.Strings(files)
// 依次解析排序后的配置文件
for _, confFile := range files {
var confList *libcni.NetworkConfigList
if strings.HasSuffix(confFile, ".conflist") {
// .conflist类型的配置文件直接解析即可
confList, err = libcni.ConfListFromFile(confFile)
} else {
// .conf/.json类型的配置文件,加载之后,需要转换为.conflist类型的结构体;
// 为了统一接口使用,K8S的实现也作了相同的处理;
conf, err := libcni.ConfFromFile(confFile)

confList, err = libcni.ConfListFromConf(conf)

}
if len(confList.Plugins) == 0 {
continue
}

// Validation on CNI config should be done to pre-check presence
// of plugins which are necessary.
if _, err := cni.ValidateNetworkList(context.TODO(), confList); err != nil {
continue
}

if confList.Name == "" {
confList.Name = path.Base(confFile)
}

cniNet := &cniNetwork{
name: confList.Name,
filePath: confFile,
config: confList,
}
if _, ok := networks[confList.Name]; !ok {
// 存储未加载过的网络配置
networks[confList.Name] = cniNet
}
if defaultNetName == "" {
// 这就是上面说的:把第一个合法的网络配置,作为默认网络配置
defaultNetName = confList.Name
}
}

return networks, defaultNetName, nil
}

监控协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
func newWatcher(confDir string) (*fsnotify.Watcher, error) {                                                                                                  
// Ensure plugin directory exists, because the following monitoring logic
// relies on that.
if err := os.MkdirAll(confDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create %q: %v", confDir, err)
}
// 使用fsnotify库,创建watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, fmt.Errorf("could not create new watcher %v", err)
}
defer func() {
// Close watcher on error
if err != nil {
watcher.Close()
}
}()

// 把confDir加入到watcher中,进行监控
if err = watcher.Add(confDir); err != nil {
return nil, fmt.Errorf("failed to add watch on %q: %v", confDir, err)
}

return watcher, nil
}

// 启动协程
func (plugin *cniNetworkPlugin) monitorConfDir(start *sync.WaitGroup) {
// 通知init协程,monitor启动
start.Done()
plugin.done.Add(1)
defer plugin.done.Done()
for {
select {
// 接受watcher的通知事件
case event := <-plugin.watcher.Events:
logrus.Warningf("CNI monitoring event %v", event)

var defaultDeleted bool
// confDir目录有文件创建或者修改,就需要重新加载网络配置文件了
createWrite := (event.Op&fsnotify.Create == fsnotify.Create ||
event.Op&fsnotify.Write == fsnotify.Write)
if event.Op&fsnotify.Remove == fsnotify.Remove {
// Care about the event if the default network
// was just deleted
defNet := plugin.getDefaultNetwork()
if defNet != nil && event.Name == defNet.filePath {
// 如果有网络配置文件删除了,而且删除的是默认网络配置的文件
defaultDeleted = true
}

}
// 默认网络配置没有删除,而且无配置文件修改和新增,直接忽略事件即可;
if !createWrite && !defaultDeleted {
continue
}

// 重新加载cni网络配置文件
if err := plugin.syncNetworkConfig(); err != nil {
logrus.Errorf("CNI config loading failed, continue monitoring: %v", err)
continue
}
case err := <-plugin.watcher.Errors:
if err == nil {
continue
}
logrus.Errorf("CNI monitoring error %v", err)
return

case <-plugin.shutdownChan:
return
}
}
}

CNI模块对外接口分析

通用函数

podman支持pod加入多个网络平面,因此封装了一个forEachNetwork函数,用于依次对网络集合的所有网络进行特定操作:

  • 保障所有网络平面的interface网卡名唯一;
  • 构建runtimeconf;
  • 获取网络名对应的cni网络配置信息;
  • 执行特定操作;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
type forEachNetworkFn func(*cniNetwork, *PodNetwork, *libcni.RuntimeConf) error

func (plugin *cniNetworkPlugin) forEachNetwork(podNetwork *PodNetwork, fromCache bool, actionFn forEachNetworkFn) error {
// 获取pod需要加入的网络集合
networks := podNetwork.Networks
if len(networks) == 0 {
networks = append(networks, NetAttachment{
Name: plugin.GetDefaultNetworkName(),
})
}
// 保障网络集合的网卡名不冲突
allIfNames := make(map[string]bool)
for _, req := range networks {
if req.Ifname != "" {
// Make sure the requested name isn't already assigned
if allIfNames[req.Ifname] {
return fmt.Errorf("network %q requested interface name %q already assigned", req.Name, req.Ifname)
}
allIfNames[req.Ifname] = true
}
}
// 依次对网络集合执行actionFn
for _, network := range networks {
ifName := network.Ifname
if ifName == "" {
for i := 0; i < 10000; i++ {
candidate := fmt.Sprintf("eth%d", i)
if !allIfNames[candidate] {
allIfNames[candidate] = true
ifName = candidate
break
}
}
if ifName == "" {
return fmt.Errorf("failed to find free interface name for network %q", network.Name)
}
}
// 准备runtime config
rt, err := buildCNIRuntimeConf(plugin.cacheDir, podNetwork, ifName, podNetwork.RuntimeConfig[network.Name])
if err != nil {
logrus.Errorf("error building CNI runtime config: %v", err)
return err
}

var cniNet *cniNetwork
// 从cache获取当前网络名对应的网络配置
if fromCache {
var newRt *libcni.RuntimeConf
cniNet, newRt, err = plugin.loadNetworkFromCache(network.Name, rt)
if err != nil {
logrus.Debugf("error loading cached network config: %v", err)
logrus.Debugf("falling back to loading from existing plugins on disk")
} else {
// Use the updated RuntimeConf
rt = newRt
}
}
// 如果cache没有,从当前plugin管理的网络集合中查询
if cniNet == nil {
cniNet, err = plugin.getNetwork(network.Name)
if err != nil {
logrus.Errorf(err.Error())
return err
}
}
// 执行实际的网络操作,主要是setup,teardown或者check
if err := actionFn(cniNet, podNetwork, rt); err != nil {
return err
}
}
return nil
}

GetPodNetworkStatus

用于获取pod对应的网络命名空间的网络配置信息,该接口依赖于CNI 0.4.0版本的check接口。

graph TD
    A[GetPodNetworkStatus] --> B[forEachNetwork]
    B --> C[checkNetwork]
    C --> D{version >= 0.4.0}
    D -->|yes| E[CheckNetworkList]
    D -->|no| F[GetNetworkListCachedResult]
    E --> F
    subgraph getnetinfo
    H(getContainerDetails) --> O[run nsenter]
    O --> P[get ip info]
    O --> Q[get mac info]
    end
    F --> H
    H --> I(parse result)

Status

1
2
3
4
5
6
func (plugin *cniNetworkPlugin) Status() error {                                                                                                              
if plugin.getDefaultNetwork() == nil {
return fmt.Errorf(errMissingDefaultNetwork, plugin.confDir)
}
return nil
}

Status用于判断CNI模块是否正常,通过默认网络是否设置为判断标准。

SetUpPod

flowchart TB
    subgraph libcni
    D[addToNetwork] --> E[AddNetworkList]
    end
    subgraph loopnet
    B(setup loop) -->|2| C[LoopbackRuntimeConf]
    B -->|3| D
    end
    A(SetUpPod) -->|1| loopnet

    subgraph othernets
    F(forEachNetwork) -->|6| D
    F -->|7| G[record result]
    end
    A -->|4| H[pod lock]
    H -->|5| othernets
    othernets -->|8| I[pod unlock]

TearDownPod

flowchart TB
    subgraph libcni
    D[deleteFromNetwork] --> E[DelNetworkList]
    end
    subgraph loopnet
    B(teardown loop) -->|4| C[LoopbackRuntimeConf]
    B -->|5| D
    end
    A(TearDownPod) -->|1| loopnet

    subgraph othernets
    F(forEachNetwork) -->|6| D
    end
    A -->|2| H[pod lock]
    H -->|3| othernets
    othernets -->|7| I[pod unlock]

iSula与JSON的斗争

对于各位习惯各种高级语言的伙伴们来说,JSON的解析和生成是如呼吸般简单自然的事情。但是对于C语言,JSON的解析和生成就麻烦了。根本原因是由于C语言不支持反射,没办法对JSON作动态解析和生成。但是,容器引擎中涉及大量的JSON解析和生成。那么,我们为了更好的和JSON进行和谐相处,做了那些努力呢?

大体上,iSula经历了几个阶段,为了更好的感受这几个阶段的差距;我觉得通过武器的不同时代来感受一下。

冷兵器时代

C语言还是有一些JSON解析的库的,例如yajlcjson等等;这些库提供了把JSON字符串解析为tree结构的元素集合,然后通过遍历书可以快速的找到JSON的key/value的对应关系和值。而且也能自己构建对应的元素结合tree,然后生成JSON字符串。那么,如何通过这些库来做JSON和C结构体直接的相互转换呢?

用法

yajl为例,实现一个isula_version结构体的marshal和unmarshal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#include <yajl/yajl_tree.h>
#include <yajl/yajl_gen.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

struct isula_version {
int large;
int middle;
int small;
char *version;
};

void free_isula_version(struct isula_version *ptr)
{
if (ptr == NULL) {
return;
}
free(ptr->version);
ptr->version = NULL;
free(ptr);
}

static inline yajl_val get_val(yajl_val tree, const char *name, yajl_type type) {
const char *path[] = { name, NULL };
return yajl_tree_get(tree, path, type);
}

struct isula_version *unmarshal(const char *json_str)
{
char buf[1024];
yajl_val tree;
struct isula_version *result = NULL;

if (json_str == NULL) {
return NULL;
}
result = calloc(1, sizeof(struct isula_version));
if (result == NULL) {
return NULL;
}
tree = yajl_tree_parse(json_str, buf, sizeof(buf));
if (tree == NULL) {
printf("Invalid json string: %s\n", json_str);
goto err_out;
}
{
yajl_val val = get_val(tree, "Large", yajl_t_number);
if (val != NULL) {
result->large = YAJL_GET_INTEGER(val);
}
}
{
yajl_val val = get_val(tree, "Small", yajl_t_number);
if (val != NULL) {
result->small = YAJL_GET_INTEGER(val);
}
}
{
yajl_val val = get_val(tree, "Middle", yajl_t_number);
if (val != NULL) {
result->middle = YAJL_GET_INTEGER(val);
}
}
{
yajl_val val = get_val(tree, "Version", yajl_t_string);
if (val != NULL) {
char *str = YAJL_GET_STRING(val);
result->version = strdup(str);
}
}

goto out;
err_out:
free_isula_version(result);
result = NULL;
out:
yajl_tree_free(tree);
return result;
}

char *marshal(struct isula_version *ptr)
{
char *result = NULL;
const unsigned char *gen_buf = NULL;
size_t gen_len = 0;

if (ptr == NULL) {
return NULL;
}

yajl_gen g = yajl_gen_alloc(NULL);
yajl_gen_status stat = yajl_gen_status_ok;

stat = yajl_gen_map_open((yajl_gen)g);
if (stat != yajl_gen_status_ok) {
goto free_out;
}
/* gen struct items */
if (ptr->version != NULL) {
stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)("Version"), strlen("Version"));
if (yajl_gen_status_ok != stat) {
goto free_out;
}
stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)ptr->version, strlen(ptr->version));
if (yajl_gen_status_ok != stat) {
goto free_out;
}
}

stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)("Large"), strlen("Large"));
if (yajl_gen_status_ok != stat) {
goto free_out;
}

stat = yajl_gen_integer((yajl_gen)g, (long long int)ptr->large);
if (yajl_gen_status_ok != stat) {
goto free_out;
}

stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)("Middle"), strlen("Middle"));
if (yajl_gen_status_ok != stat) {
goto free_out;
}

stat = yajl_gen_integer((yajl_gen)g, (long long int)ptr->middle);
if (yajl_gen_status_ok != stat) {
goto free_out;
}

stat = yajl_gen_string((yajl_gen)g, (const unsigned char *)("Small"), strlen("Small"));
if (yajl_gen_status_ok != stat) {
goto free_out;
}

stat = yajl_gen_integer((yajl_gen)g, (long long int)ptr->small);
if (yajl_gen_status_ok != stat) {
goto free_out;
}

stat = yajl_gen_map_close((yajl_gen)g);
if (stat != yajl_gen_status_ok) {
goto free_out;
}

yajl_gen_get_buf(g, &gen_buf, &gen_len);
if (gen_buf == NULL) {
printf("gen buf failed\n");
goto free_out;
}

result = calloc(gen_len + 1, sizeof(char));
if (result == NULL) {
printf("out of memory\n");
goto free_out;
}
(void)memcpy(result, gen_buf, gen_len);

free_out:
yajl_gen_clear(g);
yajl_gen_free(g);
return result;
}

void show_isula_version(const struct isula_version *ptr)
{
printf("iSula version: \n");
if (ptr == NULL) {
return;
}
printf("large: %d\nmiddle: %d\nsmall: %d\n", ptr->large, ptr->middle, ptr->small);
printf("version: %s\n", ptr->version);
}

int main()
{
const char *json_str = "{\"Version\":\"1.0.0\", \"Large\": 1, \"Middle\": 0, \"Small\": 0}";
struct isula_version *ptr = NULL;
char *marshaled = NULL;

// step 1: unmarshal json string
ptr = unmarshal(json_str);
if (ptr == NULL) {
printf("unmarshal failed\n");
return -1;
}
show_isula_version(ptr);

// step 2: marshal isula version
free(ptr->version);
ptr->version = strdup("2.0.0");
ptr->large = 2;
ptr->middle = 1;
ptr->small = 1;
marshaled = marshal(ptr);
printf("marshal isula version:\n\t%s\n", marshaled);

free(marshaled);
free_isula_version(ptr);
}

执行效果如下:

1
2
3
4
5
6
7
8
$ ./a.out 
iSula version:
large: 1
middle: 0
small: 0
version: 1.0.0
marshal isula version:
{"Version":"2.0.0","Large":2,"Middle":1,"Small":1}

这种方式虽然没法和支持动态解析的语言一样高效简单,但是也算完成了任务。如果动态解析是热兵器,这个勉强能算是长矛了。

缺陷

从示例来看,完成一个结构体和JSON的映射大概需要160行左右的代码。而上面只是一个简单的结构体,而且有的项目有很多这种结构体需要做映射。这种原始的方式在大型项目中很难保证参与人员代码质量可控;而且效率低下。主要的缺陷总结如下:

  • 映射工作量较大;
  • 对每种结构体需要单独适配代码,无法实现自动化;
  • 效率低下;
  • 代码质量不可控;

伪热兵器时代

由于C不支持反射,没法做到动态解析。但是可以通过其他途径简化解析流程、提高效率、实现自动化以及实现代码质量可控。为了避免重复造论子,17年的时候发现了libocispec项目,提供了一个解决C语言JSON映射的思路:

  • 通过json schema描述JSON字符串的结构信息;
  • 通过python解析json schema信息;
  • 根据json schema信息自动生成C结构体和JSON的映射代码;

这种方式,可以解决上面的上一章节的几个缺陷:

  • 工作量大大减小,这需要写好json schema文件即可;
  • 自动化解析代码工作;
  • 效率很高;
  • 代码质量可控,取决于于生成框架的质量;

注:libocispec早期只能用于解析oci spec的json,在我们发现之后,多个开发人员参与社区,提供了大量的功能升级,才有了今天的强大能力。

iSula集成libocispec结构

iSula当前把JSON映射相关的代码,统一放到lcr项目中进行管理,通过一个动态库和头文件提供相应功能。

生成代码的开源python框架结构如下:

1
2
3
4
5
6
7
8
9
10
11
$ tree third_party/libocispec/
third_party/libocispec/
├── CMakeLists.txt
├── common_c.py
├── common_h.py
├── generate.py
├── headers.py
├── helpers.py
├── read_file.c
├── read_file.h
└── sources.py

json schema文件存放结构(由于iSula涉及的所有JSON结构都在该目录下,所以存在大量的schema文件)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ tree -d 1 src/json/schema/
src/json/schema/
├── cni
│   └── network
├── container
├── cri
├── docker
│   ├── image
│   └── types
├── embedded
├── host
├── image
├── imagetool
├── logger
├── oci
│   ├── image
│   └── runtime
├── plugin
├── registry
├── shim
│   └── client
└── storage

然后在cmake的时候,会触发python框架,根据schema目录下面所有的schema来生成对应的映射代码。会看到如下提示信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ mkdir build
$ cd build
$ cmake ../
......
Reflection: isulad-daemon-configs.json Success
Reflection: timestamp.json Success
Reflection: web-signature.json Success
Reflection: host-config.json Success
Reflection: defs.json Success
Reflection: config.json Success
Reflection: manifest.json Success
Reflection: layers.json Success
......

用法

那么,现在我们如果需要对一个新的结构体和JSON进行映射,需要做的事情就是在json schema目录下面新增一个对应的schema文件即可。这里以上一章节的isula_version为例。

新增schema文件isula_version.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat ../src/json/schema/isula_version.json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"Version": {
"type": "string"
},
"Large": {
"type": "int32"
},
"Middle": {
"type": "int32"
},
"Small": {
"type": "int32"
}
}
}

重新cmake,可以看到新生成了两个文件:

1
2
$ ls build/json/isula_version.*
build/json/isula_version.c build/json/isula_version.h

生成的代码对外的接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
$ cat build/json/isula_version.h 
// Generated from isula_version.json. Do not edit!
#ifndef ISULA_VERSION_SCHEMA_H
#define ISULA_VERSION_SCHEMA_H

#include <sys/types.h>
#include <stdint.h>
#include "json_common.h"

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
char *version;

int32_t large;

int32_t middle;

int32_t small;
}
isula_version;

void free_isula_version(isula_version *ptr);

isula_version *make_isula_version(yajl_val tree, const struct parser_context *ctx, parser_error *err);

yajl_gen_status gen_isula_version(yajl_gen g, const isula_version *ptr, const struct parser_context *ctx, parser_error *err);

isula_version *isula_version_parse_file(const char *filename, const struct parser_context *ctx, parser_error *err);

isula_version *isula_version_parse_file_stream(FILE *stream, const struct parser_context *ctx, parser_error *err);

isula_version *isula_version_parse_data(const char *jsondata, const struct parser_context *ctx, parser_error *err);

char *isula_version_generate_json(const isula_version *ptr, const struct parser_context *ctx, parser_error *err);

#ifdef __cplusplus
}
#endif

#endif

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ cat test.c 
#include "isula_version.h"
#include <stdio.h>

void show_isula_version(const isula_version *ptr)
{
printf("iSula version: \n");
if (ptr == NULL) {
return;
}
printf("large: %d\nmiddle: %d\nsmall: %d\n", ptr->large, ptr->middle, ptr->small);
printf("version: %s\n", ptr->version);
}

int main()
{
const char *json_str = "{\"Version\":\"1.0.0\", \"Large\": 1, \"Middle\": 0, \"Small\": 0}";
isula_version *ptr = NULL;
parser_error err = NULL;
char *marshaled = NULL;

// step 1: unmarshal
ptr = isula_version_parse_data(json_str, NULL, &err);
if (ptr == NULL) {
return -1;
}
show_isula_version(ptr);

// step 2: marshal
free(ptr->version);
ptr->version = strdup("2.0.0");
ptr->large = 2;
ptr->middle = 1;
ptr->small = 1;
marshaled = isula_version_generate_json(ptr, NULL, &err);
if (ptr == NULL) {
goto out;
}
printf("marshal isula version:\n\t%s\n", marshaled);

out:
free(marshaled);
free_isula_version(ptr);
return 0;
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./a.out 
iSula version:
large: 1
middle: 0
small: 0
version: 1.0.0
marshal isula version:
{
"Version": "2.0.0",
"Large": 2,
"Middle": 1,
"Small": 1
}

缺陷

通过libocispec可以实现接近于高级语言的marshalunmarshal了,只需要简单编写schema文件即可,极大的提高了效率,并且依托开源社区可以提高代码质量。但是,还是存在一些缺陷。

例如golang中,marshal之后的结构体可以通过map[string]interface保存,可以完整的记录JSON字符串中的信息。而我们当前的实现,只能根据schema来解析JSON字符串,因此,存在信息丢失的情况。有些场景,规范只规定了主体的JSON结构,并且支持拓展配置,例如CNI规范。

近乎热兵器时代

为了解决信息丢失的问题,我们通过在结构体中记录原始的元素集合tree的方案,unmarshal的时候不会丢失原始信息,marshal的时候解析记录的元素信息,从而实现原始数据完整的传递。

具体解决方案见官方PR:https://github.com/containers/libocispec/pull/56

用法

使用方式和上面的基本一致,差异主要包括以下几部分:

  1. 生成的代码有部分差异(_residual);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ cat isula_version.h 
    ... ....
    typedef struct {
    char *version;

    int32_t large;

    int32_t middle;

    int32_t small;

    yajl_val _residual;
    }
    isula_version;
    ... ....
  2. 解析是需要指定struct parser_context参数为OPT_PARSE_FULLKEY

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    $ cat test.c 
    #include "isula_version.h"
    #include <stdio.h>

    void show_isula_version(const isula_version *ptr)
    {
    printf("iSula version: \n");
    if (ptr == NULL) {
    return;
    }
    printf("large: %d\nmiddle: %d\nsmall: %d\n", ptr->large, ptr->middle, ptr->small);
    printf("version: %s\n", ptr->version);
    }

    int main()
    {
    const char *json_str = "{\"Version\":\"1.0.0\", \"Large\": 1, \"Middle\": 0, \"Small\": 0, \"resi_int\": 1, \"resi_str\": \"test\"}";
    isula_version *ptr = NULL;
    parser_error err = NULL;
    char *marshaled = NULL;
    struct parser_context ctx;
    ctx.options = OPT_PARSE_FULLKEY;

    // step 1: unmarshal
    ptr = isula_version_parse_data(json_str, &ctx, &err);
    if (ptr == NULL) {
    return -1;
    }
    show_isula_version(ptr);

    // step 2: marshal
    free(ptr->version);
    ptr->version = strdup("2.0.0");
    ptr->large = 2;
    ptr->middle = 1;
    ptr->small = 1;
    marshaled = isula_version_generate_json(ptr, &ctx, &err);
    if (ptr == NULL) {
    goto out;
    }
    printf("marshal isula version:\n\t%s\n", marshaled);

    out:
    free(marshaled);
    free_isula_version(ptr);
    return 0;
    }
  3. 效果如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ ./a.out 
    iSula version:
    large: 1
    middle: 0
    small: 0
    version: 1.0.0
    marshal isula version:
    {
    "Version": "2.0.0",
    "Large": 2,
    "Middle": 1,
    "Small": 1,
    "resi_int": 1,
    "resi_str": "test"
    }

可以看到拓展的信息,完整的传递下去了。通过这种方式完美的解决了CNI的拓展配置的支持,从而解决了iSulad动态支持多种插件的技术瓶颈。

缺陷

上面的方案已经基本和支持反射的语言实现的功能相近了,但是,还是存在部分缺陷的。例如,动态修改JSON结构的数据会比较麻烦,需要对底层的解析库比较了解,而且比较麻烦。

总结

虽然当前的框架还有一些缺陷,但是,我们的目标并不是实现一个完美的JSON和C结构体的映射框架,而是解决容器引擎使用JSON的问题。而上面的方案,已经完全满足iSula当前的需求。

因此,目前没有进一步优化的需求。如果后续使用场景或者其他用户有需求,可以到libocispec的社区进行进一步的优化。

参考文章

iSula性能测试

iSula容器引擎具有很多优点:轻、快等等。那么,如何呈现这些优点呢?这篇文章我们主要关注iSula容器引擎的“快”。为了证明“快”,那就需要有参照物进行对比。环视业内,我们发现几个能打的;容器引擎鼻祖Docker、红帽的Podman以及CRI-O。

目标确定了,我们开始明确对比范围了。

测试范围

容器引擎的使用模式主要是:

  • 客户端使用模式:多见于个人开发、测试以及部分生产场景;
  • PAAS通过CRI接口使用模式:云计算的经典场景,通过CRI接口调用容器引擎能力,管理pod集群;

为了尽量覆盖应用场景,因此我们需要覆盖上述两种场景,对客户端模式和CRI模式分别进行测试对比。

客户端模式

由于CRI-O不具备客户端功能,所以我们选择的测试对象是:

  • Docker
  • Podman
  • iSula

CRI模式

CRI接口,需要通过cri-tools工具进行测试。

为了对比的观赏性,我们在CRI模式下也选择三个测试对象:

  • Docker
  • CRI-O
  • iSula

环境准备

机器环境

X86

配置项 配置信息
OS Fedora32 X86_64
内核 linux 5.7.10-201.fc32.x86_64
CPU 48核,Intel Xeon CPU E5-2695 v2 @ 2.4GHZ
内存 132 GB

ARM

配置项 配置信息
OS Euleros
内核 linux 4.19.90
CPU 64核
内存 196 GB

安装iSulad

参考官方文档安装即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ isula version

Client:
Version: 2.0.3
Git commit: 3bb24761f07cc0ac399e1cb783053db8b33b263d
Built: 2020-08-01T09:40:06.568848951+08:00

Server:
Version: 2.0.3
Git commit: 3bb24761f07cc0ac399e1cb783053db8b33b263d
Built: 2020-08-01T09:40:06.568848951+08:00

OCI config:
Version: 1.0.1
Default file: /etc/default/isulad/config.json

安装cri-tools

CRI测试,使用统一的客户端工具进行测试,选择K8S对应的V1.15.0版本即可。

1
2
3
4
5
$ git clone https://github.com/kubernetes-sigs/cri-tools
$ cd cri-tools
$ git checkout v1.15.0
$ make
$ export PATH=$PATH:$GOPATH/bin

安装docker

根据官方文档安装即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ docker version

Client:
Version: 19.03.11
API version: 1.40
Go version: go1.14.3
Git commit: 42e35e6
Built: Sun Jun 7 21:16:58 2020
OS/Arch: linux/amd64
Experimental: false

Server: Docker Engine - Community
Engine:
Version: 19.03.11
API version: 1.40 (minimum version 1.12)
Go version: go1.14.3
Git commit: 42e35e6
Built: Sun Jun 7 00:00:00 2020
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.3.3
GitCommit:
runc:
Version: 1.0.0-rc10+dev
GitCommit: fbdbaf85ecbc0e077f336c03062710435607dbf1
docker-init:
Version: 0.18.0
GitCommit:

安装kubelet

我们选择V1.15.0版本作为测试版本,下载源码https://github.com/kubernetes/kubernetes.git

准备源码

如果下载失败或者太慢,可以配置代理:

1
2
3
4
5
6
# 设置国内代理
go env -w GOPROXY=https://goproxy.cn,direct
# 设置私有仓库地址
go env -w GOPRIVATE=.gitlab.com,.gitee.com
# 设置sum验证服务地址
go env -w GOSUMDB="sum.golang.google.cn"

开始下载源码:

1
2
3
4
5
$ cd $GOPATH/src/k8s.io
$ git clone https://github.com/kubernetes/kubernetes.git
$ cd kubernetes
$ git checkout v1.15.0
$ go mod tidy

编译

1
$ make all WHAT=cmd/kubelet

注意:

  • K8S的版本对go的版本有要求,例如V1.15.0需要go 1.12版本
  • 可以使用go mod tidy,测试依赖代码下载,如果存在鉴权失败的仓库,可以使用go get -v -insecure下载

安装

1
2
3
$ cp _output/bin/kubelet /usr/local/bin/kubelet
$ kubelet --version
Kubernetes v1.15.0

启动kubelet

1
$ kubelet --network-plugin=cni --runtime-cgroups=/systemd/system.slice --kubelet-cgroups=/systemd/system.slice --cgroup-driver="systemd"  --fail-swap-on=false  -v 5 --enable-controller-attach-detach=false --experimental-dockershim

注:cgroup由systemd管理

安装CRI-O

由于直接通过dnf安装CRI-Ov1.15.4版本有问题,所以需要源码编译安装。

1
2
3
4
5
6
$ dnf install glib-2.0 glibc-devel glibc-static container-common
$ git clone https://github.com/cri-o/cri-o.git
$ cd crio
$ make
$ make install
$ mkdir -p /etc/crio && cp crio.conf /etc/crio/

安装podman

直接使用dnf的源安装即可:

1
2
3
4
$ dnf install -y podman

$ podman --version
podman version 2.0.3

测试方案

本文档主要关注容器引擎的容器生命周期的性能,所以测试方案如下:

  • 单容器的create、start、stop、rm和run等操作的性能;
  • 100个容器并发create、start、stop、rm和run等操作的性能;
  • 单pod的runp、stopp和rmp等操作的性能;
  • 单pod包含单容器的run、stop和rm等操作的性能;
  • 100个pod并发runp、stopp和rmp等操作的性能;
  • 100个包含单容器的pod并发run、stop和rm等操作的性能;

注:pod的配置,必须指定linux,不然docker会给pod创建一个默认的网卡,导致cni插件执行失败。

1
2
3
4
5
6
7
8
9
10
11
{
"metadata": {
"name": "nginx-sandbox",
"namespace": "default",
"attempt": 1,
"uid": "hdishd83djaidwnduwk28bcsb"
},
// linux字段必须存在
"linux": {
}
}

方案详细设计

单次测试和并发测试虽然是两种测试场景,但是单次可以看成并发的特例。因此,设计测试用例的时候,通过控制并发数量来实现两种场景的区分。具体设计如下图:

graph TD
    classDef notestyle fill:#98A092,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5, 5;
    classDef parallelstyle fill:#C969A3,stroke:#666,stroke-width:1px,color:#ffa,stroke-dasharray: 5, 5;
    subgraph pretest;
    A[download images] --> B[do clean]
    end
    subgraph dotest
    C[foreach 1->10]
    D
    E(remove max and min cases)
    X(calculate avg of residual case)
    C --> D
    D --> E
    E --> X
    end
    B --> C
    subgraph runtest
    R(begin test)
    F>t1: get begin time point]
    G(parallel run all cases)
    H[wait all cases finish]
    I>t2: get end time point]
    R --> F
    F --> G
    F --> H
    H -. wait .-> G
    H --> I
    end
    R -. implements .-> D
    subgraph posttest
    Z[do clean]
    end
    X --> Z
    class R notestyle;
    class G parallelstyle;

客户端模式

X86环境测试结果

单容器操作性能对比

操作耗时 (ms) Docker (avg) Podman (avg) iSula (avg) VS Docker VS Podman
create 287 180 131 -54.36% -27.22%
start 675 916 315 -53.33% -65.61%
stop 349 513 274 -21.49% -46.59%
rm 72 187 60 -16.67% -67.91%
run 866 454 359 -58.55% -20.93%

100容器并发操作性能对比

操作耗时 (ms) Docker (avg) Podman (avg) iSula (avg) VS Docker VS Podman
100 * create 4995 3993 1911 -61.74% -52.14%
100 * start 10126 5537 3861 -61.87% -30.27%
100 * stop 8066 11100 4268 -47.09% -61.55%
100 * rm 3220 4319 1967 -38.91% -54.46%
100 * run 9822 5979 4392 -55.28% -26.54%

ARM环境测试结果

单容器操作性能对比

操作耗时 (ms) Docker (avg) Podman (avg) iSula (avg) VS Docker VS Podman
create 401 361 177 -55.86% -50.97%
start 1160 1143 523 -54.91% -54.24%
stop 634 576 395 -37.70% -31.42%
rm 105 398 89 -15.24% -77.64%
run 1261 1071 634 -49.72% -40.80%

100容器并发操作性能对比

操作耗时 (ms) Docker (avg) Podman (avg) iSula (avg) VS Docker VS Podman
100 * create 14563 12081 4172 -71.35% -65.47%
100 * start 23420 15370 5294 -77.40% -65.56%
100 * stop 22234 16973 8619 -61.24% -49.22%
100 * rm 937 10943 926 -1.17% -92.33%
100 * run 28091 16280 9015 -67.91% -44.63%

CRI模式

X86环境测试结果

单pod操作

操作耗时 (ms) Docker (avg) CRIO (avg) iSula (avg) VS Docker VS CRIO
runp 681 321 239 -64.90% -25.55%
stopp 400 356 272 -32.00% -23.60%

单pod单容器操作

操作耗时 (ms) Docker (avg) CRIO (avg) iSula (avg) VS Docker VS CRIO
run 1249 525 382 -69.42% -27.24%
stop 554 759 564 +1.81% -25.69%

100并发pod操作

操作耗时 (ms) Docker (avg) CRIO (avg) iSula (avg) VS Docker VS CRIO
100 * runp 13998 4946 3887 -72.23% -21.41%
100 * stopp 8402 4834 4631 -44.88% -4.20%
100 * rmp 2076 1388 1073 -48.31% -22.69%

100并发pod容器操作

操作耗时 (ms) Docker (avg) CRIO (avg) iSula (avg) VS Docker VS CRIO
100 * run 28158 9077 5630 -80.01% -37.98%
100 * stop 9395 8443 8196 -12.76% -2.93%
100 * rm 4415 3739 1524 -65.48% -59.24%

ARM环境测试结果

单pod操作

操作耗时 (ms) Docker (avg) CRIO (avg) iSula (avg) VS Docker VS CRIO
runp 1339 2366 536 -59.97% -77.35%
stopp 443 419 255 -42.44% -39.14%

单pod单容器操作

操作耗时 (ms) Docker (avg) CRIO (avg) iSula (avg) VS Docker VS CRIO
run 2069 3039 338 -83.66% -88.88%
stop 684 688 214 -68.71% -68.90%

100并发pod操作

操作耗时 (ms) Docker (avg) CRIO (avg) iSula (avg) VS Docker VS CRIO
100 * runp 27802 29197 9827 -64.65% -66.34%
100 * stopp 14429 11173 6394 -55.69% -42.77%
100 * rmp 771 9007 1790 +132.17% -80.13%

100并发pod容器操作

操作耗时 (ms) Docker (avg) CRIO (avg) iSula (avg) VS Docker VS CRIO
100 * run 54087 43521 5284 -90.23% -87.86%
100 * stop 18317 19108 2641 -85.58% -86.18%
100 * rm 1592 18390 2162 +35.80% -88.24%

总结分析

从测试数据来看,在容器的生命周期的操作和并发操作上面,我们iSulad都是优于其他容器引擎的。尤其是在ARM上的表现尤为出色,并发性能已经接近于X86的性能了;而其他容器引擎在ARM上面的表现不尽如人意,甚至出现性能下降1倍以上。

那么,我们iSulad为什么有这么大的优势呢?我觉得,主要是从下面几个方面来看。

  • 首先,iSulad是用C/C++语言写的,而Docker/Podman/CRI-O都是用golang写的;C/C++在速度方面本身就有优势;
  • 架构设计上面,相对于Docker,iSulad架构更加简单,调用链更短;而Podman是serverless模式,并发更加不具备优势;
  • 在容器创建流程中,减小锁粒度、消减容器的依赖(例如镜像管理模块),从而提高了并发的性能;

架构对比

iSulad架构设计如下:

arch

Docker官网给的架构图如下:

Docker架构图

但是,docker daemon里面还涉及到containerd和runc的流程没有描述,大体结构如下:

graph LR
    A(docker daemon)
    B(containerd)
    C(runc)
    A -. grpc .-> B
    B -. fork/exec .-> C

从架构来看,docker的容器生命周期流程涉及:客户端到docker daemon的restful通信;daemon到containerd的GRPC通信;然后fork执行runc。而iSulad的流程:客户端到服务端的GRPC通信,然后fork执行lxc-start。

参考文档

实现C语言的自动清理功能

GCC的变量attribute支持cleanup属性,定义如下:

1
2
3
4
5
6
7
8
9
10
11
cleanup (cleanup_function)
The cleanup attribute runs a function when the variable goes out of scope.
This attribute can only be applied to auto function scope variables; it may not
be applied to parameters or variables with static storage duration. The function
must take one parameter, a pointer to a type compatible with the variable. The
return value of the function (if any) is ignored.
If ‘-fexceptions’ is enabled, then cleanup function is run during the stack
unwinding that happens during the processing of the exception. Note that the
cleanup attribute does not allow the exception to be caught, only to perform
an action. It is undefined what happens if cleanup function does not return
normally.

如果变量设置了cleanup属性,那么在变量离开它所属作用域时会自动调用配置的cleanup函数。那么,可以通过设置变量的cleanup属性,自动回收变量相关的内存、关闭句柄以及释放锁。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void defer_func(char **p)
{
printf("this is defer, free: %s\n", *p);
free(*p);
*p = NULL;
}

int main()
{
__attribute__ ((__cleanup__(defer_func))) char *c = NULL;
__attribute__ ((__cleanup__(defer_func))) char *b = NULL;
__attribute__ ((__cleanup__(defer_func))) char *a = NULL;

a = strdup("a");
b = strdup("b");
c = strdup("c");
return 0;
}

runc的cgroup管理模块

cgroup是容器runtime最重要的基础设施之一,所以runc支持多种cgroup的多种管理方式:

  • cgroup v1直接管理:libcontainer/cgroups/fs/fs.go
  • cgroup v2直接管理:libcontainer/cgroups/fs2/fs2.go
  • cgroup v1通过systemd管理:libcontainer/cgroups/systemd/v1.go
  • cgroup v2通过sytemd管理:libcontainer/cgroups/systemd/v2.go

统一的管理接口

runc的为cgroup的管理定义了统一的对外接口Manager,定义在libcontainer/cgroups/cgroups.go,详细接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
type Manager interface {
// Applies cgroup configuration to the process with the specified pid
Apply(pid int) error

// Returns the PIDs inside the cgroup set
GetPids() ([]int, error)

// Returns the PIDs inside the cgroup set & all sub-cgroups
GetAllPids() ([]int, error)

// Returns statistics for the cgroup set
GetStats() (*Stats, error)

// Toggles the freezer cgroup according with specified state
Freeze(state configs.FreezerState) error

// Destroys the cgroup set
Destroy() error

// Path returns a cgroup path to the specified controller/subsystem.
// For cgroupv2, the argument is unused and can be empty.
Path(string) string

// Sets the cgroup as configured.
Set(container *configs.Config) error

// GetPaths returns cgroup path(s) to save in a state file in order to restore later.
//
// For cgroup v1, a key is cgroup subsystem name, and the value is the path
// to the cgroup for this subsystem.
//
// For cgroup v2 unified hierarchy, a key is "", and the value is the unified path.
GetPaths() map[string]string

// GetCgroups returns the cgroup data as configured.
GetCgroups() (*configs.Cgroup, error)

// GetFreezerState retrieves the current FreezerState of the cgroup.
GetFreezerState() (configs.FreezerState, error)

// Whether the cgroup path exists or not
Exists() bool
}

cgroup v1

v1支持直接管理和通过systemd管理两种方式,两种方式的家大体结构是一致的;

graph TB
    O(manager) --> A[subsystemSet]
    A --> CpusetGroup -.-> B(subsystem)
    A --> DevicesGroup -.-> B
    A --> MemoryGroup -.-> B
    A --> CpuGroup -.-> B
    A --> CpuacctGroup -.-> B
    A --> PidsGroup -.-> B
    A --> BlkioGroup -.-> B
    A --> HugetlbGroup -.-> B
    A --> NetClsGroup -.-> B
    A --> NetPrioGroup -.-> B
    A --> PerfEventGroup -.-> B
    A --> FreezerGroup -.-> B
    A --> NameGroup -.-> B

直接管理

manager实现统一Manager的接口,具体定义如下:

1
2
3
4
5
6
type manager struct {    
mu sync.Mutex
cgroups *configs.Cgroup // cgroup的配置
rootless bool // ignore permission-related errors
paths map[string]string // 存储cgroup各子系统的路径,以子系统名为key(Apply是初始化)
}

subsystem接口在两种方式下是不一样的,systemd方式的接口是直接管理的子集。直接管理方式的subsystem接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
type subsystem interface {    
// Name returns the name of the subsystem.
Name() string
// Returns the stats, as 'stats', corresponding to the cgroup under 'path'.
GetStats(path string, stats *cgroups.Stats) error
// Removes the cgroup represented by 'cgroupData'.
Remove(*cgroupData) error
// Creates and joins the cgroup represented by 'cgroupData'.
Apply(*cgroupData) error
// Set the cgroup represented by cgroup.
Set(path string, cgroup *configs.Cgroup) error }

Apply接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
func (m *manager) Apply(pid int) (err error) {
if m.cgroups == nil {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()

var c = m.cgroups

d, err := getCgroupData(m.cgroups, pid)
if err != nil {
return err
}

m.paths = make(map[string]string)
if c.Paths != nil {
// 容器已配置各子系统所在的路径
for name, path := range c.Paths {
_, err := d.path(name)
if err != nil {
if cgroups.IsNotFound(err) {
continue
}
return err
}
m.paths[name] = path
}
// 把pid加入到配置的cgroup的子系统
return cgroups.EnterPid(m.paths, pid)
}

// 依次把pid加入到系统支持的cgroup子系统
for _, sys := range m.getSubsystems() {
p, err := d.path(sys.Name())
if err != nil {
// The non-presence of the devices subsystem is
// considered fatal for security reasons.
if cgroups.IsNotFound(err) && sys.Name() != "devices" {
continue
}
return err
}
m.paths[sys.Name()] = p

// 调用subsystem的Apply接口,依赖各子系统的实现(cpuset为例分析)
if err := sys.Apply(d); err != nil {
// In the case of rootless (including euid=0 in userns), where an
// explicit cgroup path hasn't been set, we don't bail on error in
// case of permission problems. Cases where limits have been set
// (and we couldn't create our own cgroup) are handled by Set.
if isIgnorableError(m.rootless, err) && m.cgroups.Path == "" {
delete(m.paths, sys.Name())
continue
}
return err
}

}
return nil
}

cpuset子系统的Apply实现为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func (s *CpusetGroup) Apply(d *cgroupData) error {
// 获取子系统的完整路径
dir, err := d.path("cpuset")
if err != nil && !cgroups.IsNotFound(err) {
return err
}
return s.ApplyDir(dir, d.config, d.pid)
}

func (s *CpusetGroup) ApplyDir(dir string, cgroup *configs.Cgroup, pid int) error {
// This might happen if we have no cpuset cgroup mounted.
// Just do nothing and don't fail.
if dir == "" {
return nil
}
//获取挂载点路径
root, err := getMount(dir)
if err != nil {
return err
}
root = filepath.Dir(root)
// 'ensureParent' start with parent because we don't want to
// explicitly inherit from parent, it could conflict with
// 'cpuset.cpu_exclusive'.
if err := s.ensureParent(filepath.Dir(dir), root); err != nil {
return err
}
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
// We didn't inherit cpuset configs from parent, but we have
// to ensure cpuset configs are set before moving task into the
// cgroup.
// The logic is, if user specified cpuset configs, use these
// specified configs, otherwise, inherit from parent. This makes
// cpuset configs work correctly with 'cpuset.cpu_exclusive', and
// keep backward compatibility.
if err := s.ensureCpusAndMems(dir, cgroup); err != nil {
return err
}

// because we are not using d.join we need to place the pid into the procs file
// unlike the other subsystems
// 把pid加入cgroup的子系统
return cgroups.WriteCgroupProc(dir, pid)
}

其他子系统实现类似,都是把pid加入到对应的子系统中。

其他接口

实现逻辑基本一致,manager通过调用subsystem的接口完成对各子系统的操作。

  • Set接口:负责cgroup配置的写入;
  • GetStats接口:获取部分子系统(MemoryGroup,CpuGroup,PidsGroup,BlkioGroup,HugetlbGroup等等)的状态信息
  • Freeze接口:设置freeze子系统的状态;

还有一些接口不需要通过subsystem的实现,就能完成具体的操作。

  • Destroy接口:删除manager管理cgroup的所有子系统;
  • GetPids接口:获取属于当前cgroup的所有pid(通过读取devices子系统的cgroup.procs文件实现)
  • GetAllPids接口:获取属于当前cgroup以及子cgroup的所有pid

systemd管理

整体和直接管理差不多,区别在于通过systemd的unit任务管理容器的cgroup,它的回收由systemd负责。因此,可以看到subsystem的接口也简单一些。

manager的结构

1
2
3
4
5
type legacyManager struct {    
mu sync.Mutex
cgroups *configs.Cgroup
paths map[string]string
}

subsystem接口

1
2
3
4
5
6
7
8
type subsystem interface {    
// Name returns the name of the subsystem.
Name() string
// Returns the stats, as 'stats', corresponding to the cgroup under 'path'.
GetStats(path string, stats *cgroups.Stats) error
// Set the cgroup represented by cgroup.
Set(path string, cgroup *configs.Cgroup) error
}

管理的子系统集合,依然使用直接管理定义的结构集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var legacySubsystems = subsystemSet{
&fs.CpusetGroup{},
&fs.DevicesGroup{},
&fs.MemoryGroup{},
&fs.CpuGroup{},
&fs.CpuacctGroup{},
&fs.PidsGroup{},
&fs.BlkioGroup{},
&fs.HugetlbGroup{},
&fs.PerfEventGroup{},
&fs.FreezerGroup{},
&fs.NetPrioGroup{},
&fs.NetClsGroup{},
&fs.NameGroup{GroupName: "name=systemd"},
}

Apply接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
func (m *legacyManager) Apply(pid int) error {
var (
c = m.cgroups
unitName = getUnitName(c)
slice = "system.slice"
properties []systemdDbus.Property
)

m.mu.Lock()
defer m.mu.Unlock()
if c.Paths != nil {
// 和直接方式一样,如果配置了,直接使用用户定义的子系统路径
paths := make(map[string]string)
for name, path := range c.Paths {
_, err := getSubsystemPath(m.cgroups, name)
if err != nil {
// Don't fail if a cgroup hierarchy was not found, just skip this subsystem
if cgroups.IsNotFound(err) {
continue
}
return err
}
paths[name] = path
}
m.paths = paths
// 依次把pid加入到paths保存的cgroup子系统中
return cgroups.EnterPid(m.paths, pid)
}

if c.Parent != "" {
slice = c.Parent
}

// 配置systemd unit任务的属性。。。。
properties = append(properties, systemdDbus.PropDescription("libcontainer container "+c.Name))

// if we create a slice, the parent is defined via a Wants=
if strings.HasSuffix(unitName, ".slice") {
properties = append(properties, systemdDbus.PropWants(slice))
} else {
// otherwise, we use Slice=
properties = append(properties, systemdDbus.PropSlice(slice))
}

// only add pid if its valid, -1 is used w/ general slice creation.
if pid != -1 {
properties = append(properties, newProp("PIDs", []uint32{uint32(pid)}))
}

// Check if we can delegate. This is only supported on systemd versions 218 and above.
if !strings.HasSuffix(unitName, ".slice") {
// Assume scopes always support delegation.
properties = append(properties, newProp("Delegate", true))
}

// Always enable accounting, this gets us the same behaviour as the fs implementation,
// plus the kernel has some problems with joining the memory cgroup at a later time.
properties = append(properties,
newProp("MemoryAccounting", true),
newProp("CPUAccounting", true),
newProp("BlockIOAccounting", true))

// Assume DefaultDependencies= will always work (the check for it was previously broken.)
properties = append(properties,
newProp("DefaultDependencies", false))

dbusConnection, err := getDbusConnection(false)
if err != nil {
return err
}
resourcesProperties, err := genV1ResourcesProperties(c, dbusConnection)
if err != nil {
return err
}
properties = append(properties, resourcesProperties...)
properties = append(properties, c.SystemdProps...)

// We have to set kernel memory here, as we can't change it once
// processes have been attached to the cgroup.
if c.Resources.KernelMemory != 0 {
if err := enableKmem(c); err != nil {
return err
}
}

// 启动systemd unit任务,管理容器的cgroup
if err := startUnit(dbusConnection, unitName, properties); err != nil {
return err
}

// 依次把pid加入到paths保存的cgroup子系统中
if err := joinCgroups(c, pid); err != nil {
return err
}

// 记录cgroup子系统的路径
paths := make(map[string]string)
for _, s := range legacySubsystems {
subsystemPath, err := getSubsystemPath(m.cgroups, s.Name())
if err != nil {
// Don't fail if a cgroup hierarchy was not found, just skip this subsystem
if cgroups.IsNotFound(err) {
continue
}
return err
}
paths[s.Name()] = subsystemPath
}
m.paths = paths
return nil
}

cgroup v2

基本用法

挂载

1
$ mount -t cgroup2 nodev /mnt/cgroup2/

创建子cgroup

1
2
3
4
5
$ cd /mnt/cgroup2/
$ mkdir xxx
$ ls xxx/
cgroup.controllers cgroup.freeze cgroup.max.descendants cgroup.stat cgroup.threads cpu.pressure io.pressure
cgroup.events cgroup.max.depth cgroup.procs cgroup.subtree_control cgroup.type cpu.stat memory.pressure

删除无用的子cgroup

1
$ rmdir xxx/

查看当前cgroup支持的controller,默认不支持任何controller

1
2
$ cat cgroup.controllers
cpu io memory

使能和禁用controller,通过写文件cgroup.subtree_control实现:

1
$ echo "+cpu +memory -io" > cgroup.subtree_control

参考文档https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html

直接管理

由于v2简化了cgroup的机制,因此,管理架构变得很简单,直接通过manager即可完成。省去了subsystem的管理模块。

1
2
3
4
5
6
7
8
9
type manager struct {
config *configs.Cgroup
// dirPath is like "/sys/fs/cgroup/user.slice/user-1001.slice/session-1.scope"
dirPath string
// controllers is content of "cgroup.controllers" file.
// excludes pseudo-controllers ("devices" and "freezer").
controllers map[string]struct{}
rootless bool
}

Apply接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (m *manager) Apply(pid int) error {
// 创建目录结构,并且写入支持的controller到cgroup.subtree_control
if err := CreateCgroupPath(m.dirPath, m.config); err != nil {
// Related tests:
// - "runc create (no limits + no cgrouppath + no permission) succeeds"
// - "runc create (rootless + no limits + cgrouppath + no permission) fails with permission error"
// - "runc create (rootless + limits + no cgrouppath + no permission) fails with informative error"
if m.rootless {
if m.config.Path == "" {
if blNeed, nErr := needAnyControllers(m.config); nErr == nil && !blNeed {
return nil
}
return errors.Wrap(err, "rootless needs no limits + no cgrouppath when no permission is granted for cgroups")
}
}
return err
}
// 把pid加入到cgroup中
if err := cgroups.WriteCgroupProc(m.dirPath, pid); err != nil {
return err
}
return nil
}

Set接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func (m *manager) Set(container *configs.Config) error {
if container == nil || container.Cgroups == nil {
return nil
}
if err := m.getControllers(); err != nil {
return err
}
// pids (since kernel 4.5)
if err := setPids(m.dirPath, container.Cgroups); err != nil {
return err
}
// memory (since kernel 4.5)
if err := setMemory(m.dirPath, container.Cgroups); err != nil {
return err
}
// io (since kernel 4.5)
if err := setIo(m.dirPath, container.Cgroups); err != nil {
return err
}
// cpu (since kernel 4.15)
if err := setCpu(m.dirPath, container.Cgroups); err != nil {
return err
}
// devices (since kernel 4.15, pseudo-controller)
//
// When m.Rootless is true, errors from the device subsystem are ignored because it is really not expected to work.
// However, errors from other subsystems are not ignored.
// see @test "runc create (rootless + limits + no cgrouppath + no permission) fails with informative error"
if err := setDevices(m.dirPath, container.Cgroups); err != nil && !m.rootless {
return err
}
// cpuset (since kernel 5.0)
if err := setCpuset(m.dirPath, container.Cgroups); err != nil {
return err
}
// hugetlb (since kernel 5.6)
if err := setHugeTlb(m.dirPath, container.Cgroups); err != nil {
return err
}
// freezer (since kernel 5.2, pseudo-controller)
if err := setFreezer(m.dirPath, container.Cgroups.Freezer); err != nil {
return err
}
m.config = container.Cgroups
return nil
}

支持的controllers都是直接写入到对应的配置文件即可。

排序算法

主要关注常见的十种排序算法:

  • 快速排序
  • 冒泡排序
  • 归并排序
  • 插入排序
  • 希尔排序
  • 选择排序
  • 堆排序
  • 计数排序
  • 桶排序
  • 基数排序

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import (
"testing"
)

func TestSort(t *testing.T) {
tcs := []struct {
name string
items []int
expect []int
}{
{
"case1",
[]int{5, 2, 3, 1, 4},
[]int{1, 2, 3, 4, 5},
},
{
"case2",
[]int{1, 1, 3, 1, 4},
[]int{1, 1, 1, 3, 4},
},
{
"case3",
[]int{1, 1, 1, 1, 1},
[]int{1, 1, 1, 1, 1},
},
{
"case4",
[]int{5, 4, 3, 2, 1},
[]int{1, 2, 3, 4, 5},
},
{
"case5",
[]int{1},
[]int{1},
},
{
"case6",
[]int{1, 2, 3},
[]int{1, 2, 3},
},
}

check := func(items []int, expect []int) bool {
if len(items) != len(expect) {
return false
}
for i := 0; i < len(items); i++ {
if items[i] != expect[i] {
return false
}
}
return true
}

for _, tc := range tcs {
t.Logf("Run case: %s\n", tc.name)
QSort(tc.items)
if !check(tc.items, tc.expect) {
t.Errorf("%s Failed, expect: %v, got: %v\n", tc.name, tc.expect, tc.items)
}
}
}

快速排序

思路

首先,设定一个基准值(第一个、最后一个或者随机一个值);

然后,根据基准值,把数据集合划分为两个部分,一个部分元素都大于基准值,一个部分元素都小于等于基准值;

然后,对划分的两个部分,递归进行1,2两步;

最后,直到数据集合元素小于等于1为止。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func QSort(items []int) {
if len(items) <= 1 {
return
}
i, j := 1, len(items)-1
temp := items[0]

for i <= j {
if items[i] > temp {
items[i], items[j] = items[j], items[i]
j--
} else {
i++
}
}

// i - 1 is count of item which little and equal to temp
// so, we just move temp to right index
items[i-1], items[0] = items[0], items[i-1]

QSort(items[0 : i-1])
QSort(items[i:])
}

冒泡排序

思路

依次遍历数据集合的相邻的每对数据,如果第一个大于第二个,则交换它们的位置;

每次遍历数据集合,会确定一个最大值,因此,每次遍历会减少下一次遍历的集合数量;

遍历n次,即完成排序。

实现

1
2
3
4
5
6
7
8
9
func BubbleSort(items []int) {
for i := 0; i < len(items); i++ {
for j := 1; j < len(items)-i; j++ {
if items[j] < items[j-1] {
items[j-1], items[j] = items[j], items[j-1]
}
}
}
}

归并排序

思路

  • 把数据集合拆分为数量相当两个子集合(n/2和n-n/2)
  • 分别对拆分后的子集合进行归并排序
  • 合并已经排序好的两个子集合

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func MergeSort(items []int) {
if len(items) <= 1 {
return
}
m := len(items) / 2
MergeSort(items[0:m])
MergeSort(items[m:])

mergeHelper(items, 0, m, len(items))
}

func mergeHelper(items []int, l, m, r int) {
sorted := make([]int, (r - l))
i, j := l, m
k := 0

for i < m && j < r {
if items[i] > items[j] {
sorted[k] = items[j]
j++
} else {
sorted[k] = items[i]
i++
}
k++
}
for i < m {
sorted[k] = items[i]
k++
i++
}
for j < r {
sorted[k] = items[j]
k++
j++
}

// update items
for _, st := range sorted {
items[l] = st
l++
}
}

linux字体安装

以nerd-fonts为例;

下载字体软件

1
wget -c https://github.com/ryanoasis/nerd-fonts/releases/download/v2.1.0/SourceCodePro.zip

解压软件到/usr/share/fonts

1
2
3
mkdir /usr/share/fonts/SourceCodePro
cd /usr/share/fonts/SourceCodePro
unzip SourceCodePro.zip

安装字体

1
2
3
mkfontscale
mkfontdir
fc-cache -fv