404频道

学习笔记

CSI:Container Storage Interface (CSI)

Volume 的三个阶段:

  1. Provision and Delete:负责卷的创建以及销毁。
  2. Attaching and Detaching:将卷设备挂载到本地或者从本地卸载。
  3. Mount and Umount:将 Attaching 的块设备以目录形式挂载到 pod中,或者从 pod 中卸载块设备。

开发

CSI 插件分为三个部分:

  1. CSI Identity:用来获取 CSI 的身份信息
  2. CSI Controller
  3. CSI Node

参考 k8s 官方的 hostpath 项目:https://github.com/kubernetes-csi/csi-driver-host-path

为了方便开发,在每个阶段 k8s 官方均实现了对应的 SideCarSet 容器。要想研发,仅需要实现 grpc server,又 SideCarSet 容器调用自研的容器。

自研的容器需要实现如下的接口即可。

CSI Identity

1
2
3
4
5
6
7
8
9
10
service Identity {
// 返回插件名字以及版本号
rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {}

// 返回插件的包含的功能
rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {}

rpc Probe (ProbeRequest)
returns (ProbeResponse) {}
}

External provisioner 会调用该接口。

CSI Controller

实现 Volume 中的 Provisioning and Deleting 和 Attaching and Detaching 两个阶段的功能。只有块存储 CSI 插件才需要 Attach 功能。
该部分以中心化组件的方式部署,比如 Deployment。Provision 功能对应的 SidecarSet 为 external-provisioner,Attach 对应的 SidecarSet 为 external-attacher

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
service Controller {
// Provisioning, External provisioner调用
// hostPath 实现中会调用 fallocate 实现
rpc CreateVolume (CreateVolumeRequest)
returns (CreateVolumeResponse) {}

// Deleting, External provisioner调用
rpc DeleteVolume (DeleteVolumeRequest)
returns (DeleteVolumeResponse) {}

// Attaching, 只有块设备才需要实现,比如云盘,由External attach 调用
rpc ControllerPublishVolume (ControllerPublishVolumeRequest)
returns (ControllerPublishVolumeResponse) {}

// Detaching,External attach 调用
rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)
returns (ControllerUnpublishVolumeResponse) {}

rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)
returns (ValidateVolumeCapabilitiesResponse) {}

rpc ListVolumes (ListVolumesRequest)
returns (ListVolumesResponse) {}

rpc GetCapacity (GetCapacityRequest)
returns (GetCapacityResponse) {}

rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
returns (ControllerGetCapabilitiesResponse) {}

// 创建快照功能
rpc CreateSnapshot (CreateSnapshotRequest)
returns (CreateSnapshotResponse) {}

// 删除快照功能
rpc DeleteSnapshot (DeleteSnapshotRequest)
returns (DeleteSnapshotResponse) {}

rpc ListSnapshots (ListSnapshotsRequest)
returns (ListSnapshotsResponse) {}

rpc ControllerExpandVolume (ControllerExpandVolumeRequest)
returns (ControllerExpandVolumeResponse) {}

rpc ControllerGetVolume (ControllerGetVolumeRequest)
returns (ControllerGetVolumeResponse) {
option (alpha_method) = true;
}

rpc ControllerModifyVolume (ControllerModifyVolumeRequest)
returns (ControllerModifyVolumeResponse) {
option (alpha_method) = true;
}
}

CSI Node

实现 Volume 中的Mount 和 Umount 阶段,由 kubelet 负责调用。
该部分以 DaemonSet 的形式部署在每个 k8s node 上,对应的 SidecarSet 容器为 node-driver-registrer

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
service Node {
// 针对块存储类型,将块设备格式化后先挂载到一个临时全局目录
rpc NodeStageVolume (NodeStageVolumeRequest)
returns (NodeStageVolumeResponse) {}

rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
returns (NodeUnstageVolumeResponse) {}

// 如果是块存储设备,在执行完 NodeStageVolume 后,使用 linux 的 bind mount 技术将全局目录挂载到pod 中的对应目录
rpc NodePublishVolume (NodePublishVolumeRequest)
returns (NodePublishVolumeResponse) {}

rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
returns (NodeUnpublishVolumeResponse) {}

rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
returns (NodeGetVolumeStatsResponse) {}


rpc NodeExpandVolume(NodeExpandVolumeRequest)
returns (NodeExpandVolumeResponse) {}


rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
returns (NodeGetCapabilitiesResponse) {}

rpc NodeGetInfo (NodeGetInfoRequest)
returns (NodeGetInfoResponse) {}
}

资料

协议:https://github.com/container-storage-interface/spec/blob/master/spec.md

重装过电脑操作系统的同学大概知道操作系统的安装流程如下:

  1. 在 BIOS 中将系统设置为光驱/USB开机优先模式
  2. 以DVD或者 U 盘中的操作系统开机,进入到装机界面
  3. 完成一系列的装机初始化,比如磁盘分区、语言选择等
  4. 重启进入新安装的操作系统

以上过程必须要手工才能完成,安装一台电脑还可以,但如果要大批量安装一批机器就不适用了。为此,Intel 公司研发了 PXE(Pre-boot Execution Environment) 技术,可以通过网络的方式批量安装操作系统。

PXE 基于 C/S 架构,分为PXE client 和PXE server,其中 PXE client 为要安装操作系统的机器,PXE server 用来提供安装操作系统必须的镜像等信息。要想实现从网络上安装操作系统,必须要解决如下几个问题:

  1. 因为还没有安装操作系统,此时并不存在 ip 地址,在装机之前必须要获取到一个 ip 地址。
  2. 安装操作系统需要的 boot loader 和操作系统镜像如何获取。

为了解决 PXE client 的 ip 地址问题,PXE 中采用了 DHCP 协议来给 client 分配 ip 地址,这就要求 PXE server 必须要运行 dhcp server。为了解决 PXE server 可以提供 boot loader 和操作系统基线,PXE server 通过 tftp 协议的方式对 client 提供服务。

client 端需要 DHCP client 和 tftp client 的功能,为此 PXE 协议中将该功能以硬件的方式内置在网卡 ROM 中。当启动时,BIOS 会加载内置在网卡中的 ROM,从而该机器具备了 DHCP client 和 tftp client 的功能。

优点:

  1. 规模化:可以批量实现多台服务器的安装
  2. 自动化:可以自动化安装
  3. 远程实现:不用本地的光盘来安装 OS

客户机的前提条件:

  1. 网络必须要支持 PXE 协议
  2. 主板支持网络引导,一般在 BIOS 中可以配置

服务端:

  1. DHCP 服务,用来给客户机分配 ip 地址
  2. TFTP 服务:用来提供操作系统文件的下载

TCP Fast Open(TFO)是一种TCP协议的扩展,旨在加快建立TCP连接的速度和降低延迟。传统的TCP连接需要进行三次握手(SYN-SYN/ACK-ACK)才能建立连接,而TFO允许在第一个数据包中携带连接建立的请求。

TFO的工作原理如下:

  1. 客户端在首次建立TCP连接时,在发送的SYN包中插入一个加密的Cookie。这个Cookie由服务器生成并发送给客户端。
  2. 当客户端发送带有TFO Cookie的SYN包到服务器时,服务器会验证Cookie的有效性。
  3. 如果Cookie有效,服务器会立即发送带有SYN+ACK标志的数据包,这样客户端就可以立即发送数据而无需等待ACK响应。
  4. 客户端收到带有SYN+ACK标志的数据包后,发送带有ACK标志的数据包,建立完整的TCP连接。

相关内核参数

net.ipv4.tcp_fastopen

支持如下值:

  • 0:关闭
  • 1: 作为客户端可以使用 TFO 功能
  • 2: 作为服务端可以使用 TFO 功能
  • 3: 作为客户端和服务端均可使用 TFO

net.ipv4.tcp_fastopen_key

用来产生 TFO 的 Cookie。

zone 设置

DNS 记录的 zone 信息为全局配置,配置地方包括 kubelet 和 coredns 两部分。

kubelet 的启动参数

  1. 通过 kubelet 的 yaml 配置文件的 clusterDomain 字段。
  2. 通过 kubelet 的参数 --cluster-domain

设置了 kubelet 的启动参数后,会设置容器的 /etc/resolv.conf 中的 search 域为如下格式:

1
2
3
search default.svc.cluster.local svc.cluster.local cluster.local tbsite.net
nameserver 10.181.48.10
options ndots:5 single-request-reopen

其中 search 域中的 cluster.local 为 kubelet 的配置。

coredns 的配置文件

coredns controller 需要 watch k8s 集群中的 pod 和 service,将其进行注册,因此 coredns 需要知道集群的 zone 配置。该配置信息位于 coredns 的配置文件 ConfigMap kube-system/coredns 中,默认的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}

其中 cluster.local 为对应的 k8s zone。

域名注册

在 k8s 中,Service 和 Pod 对象会创建 DNS 记录,用于 k8s 集群内部的域名解析。

Pod 域名注册

规则一:

每个 k8s pod 都会创建 DNS 记录: <pod_ip>.<namespace>.pod.<cluster-domain>。其中 为 pod ip 地址,但需要将 ip 地址中的 . 转换为 -

比如 pod nginx-deployment-57d84f57dc-cpgkc 会创建 A 记录 10-244-3-8.default.pod.cluster.local

1
2
3
$ kubectl get pod -o wide nginx-deployment-57d84f57dc-cpgkc -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-57d84f57dc-cpgkc 1/1 Running 0 2m59s 10.244.3.8 vc-worker2 <none> <none>

规则二:

pod 如何同时指定了 spec.hostnamespec.subdomain,则会创建 A 记录:<hostname>.<subdomain>.<namespace>.svc.cluster.local,而不是 <pod_ip>.<namespace>.pod.<cluster-domain>。对于 Statefulset 类型的 pod 会自动设置 spec.hostname 为 pod 的名字,spec.subdomain 为 StatefulSet 的 spec.serviceName

比如 pod nginx-statefulset-0 会创建 A 记录 nginx-statefulset-0.nginx.default.svc.cluster.local

1
2
3
$ kubectl get pod nginx-statefulset-0 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-statefulset-0 1/1 Running 0 62m 10.244.3.7 vc-worker2 <none> <none>

Deployment/DaemonSet 管理的 pod

使用 Deployment/DaemonSet 拉起的 pod,k8s 会创建额外的 DNS 记录:<pod_ip>.<deployment-name/daemonset-name>.<namespace>.svc.<cluster-domain>

Service 域名注册

普通 Service

除了 headless service 之外的其他 service 会在 DNS 中生成 my-svc.my-namespace.svc.cluster-domain.example 的 A 或者 AAAA 记录,A 记录指向 ClusterIP。

headless service 会在 DNS 中生成 my-svc.my-namespace.svc.cluster-domain.example 的 A 或者 AAAA 记录,但指向的为 pod ip 地址集合。

k8s 在 pod 的 /etc/resolv.conf 配置如下:

1
2
3
nameserver 10.32.0.10
search <namespace>.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

对于跟 pod 同一个 namespace 下的 service,要访问可以直接使用 service 名字接口。跟 pod 不在同一个 namespace 下的 service,访问 service 必须为 service name.service namespace

ExternalName Service

service 的 spec.typeExternalName,该种类型的服务会向 dns 中注册 CNAME 记录,CNAME 记录指向 externalName 字段。例子如下:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com

当访问 my-service.prod.svc.cluster.local 时,DNS 服务会返回 CNAME 记录,指向地址为 my.database.example.com

externalIPs 字段

可以针对所有类型的 Service 生效,用来配置多个外部的 ip 地址(该 ip 地址不是 k8s 分配),kube-proxy 会设置该 ip 地址的规则,确保在 k8s 集群内部访问该 ip 地址时,可以路由到后端的 pod。效果就跟访问普通的 ClusterIP 类型 Service 没有区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 49152
externalIPs:
- 198.51.100.32

当在集群内部访问 198.51.100.32:80 时,流量会被 kube-proxy 路由到当前 Service 的 Endpoint。

该字段存在中间人攻击的风险,不推荐使用。Detect CVE-2020-8554 – Unpatched Man-In-The-Middle (MITM) Attack in Kubernetes

Headless Service

翻译成中文又叫无头 Service,显式的将 Service spec.clusterIP 设置为 "None",表示该 Service 为 Headless Service。此时,该 Service 不会分配 clusterIP。因为没有 clusterIP,因此 kube-proxy 并不会处理该 service。

Headless Service 按照是否配置了 spec.selector 在实现上又有不同的区分。

未配置 spec.selector 的 Service,不会创建 EndpointSlice 对象,但是会注册如下的记录:

  • 对于 ExternalName Service,配置 CNAME 记录。
  • 对于非 ExternalName Service,配置 A/AAAA 记录,指向 EndPoint 的所有 ip 地址。如果未配置 Endpoint,但配置了 externalIPs 字段,则指向 externalIPs。

配置 spec.selector 的 Service,会创建 EndpointSlice 对象,并修改 DNS 配置返回 A 或者 AAAA 记录,指向 pod 的集合。

域名查询

待补充

资料

json patch

该规范定义在 RFC 6902,定义了修改 json 格式的规范,同时还可以配合 http patch 请求一起使用,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PATCH /my/data HTTP/1.1
Host: example.org
Content-Length: 326
Content-Type: application/json-patch+json
If-Match: "abc123"

[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]

支持add、remove、replace、move、copy和 test 六个patch动作。

协议规范

add

格式如下:

1
{ "op": "add", "path": "/hello", "value": [ "foo" ] }

规范:

  1. 如果原始 json 中不存在 key “/hello”,则会全新创建 key。
  2. 如果原始 json 存在 key “/hello”,则会直接覆盖;即使”/hello”为数组,也不会在原先的基础上追加,而是直接强制覆盖;

原始 json 如下:

1
2
3
{
"hello": ["123"]
}

执行后结果如下:

1
2
3
4
5
{
"hello": [
"world"
]
}

remove

用来删除某个 key,格式如下:

1
[{ "op": "remove", "path": "/hello" }]

replace

用来替换某个 key,跟 add 动作的差异是,如果 key 不存在,则不会创建 key。

1
{ "op": "replace", "path": "/hello", "value": 42 }

如果原始 json 格式为: {},执行完成后,输出 json 格式仍然为:{}

move

用来修改 key 的名称,格式如下:

1
{ "op": "move", "from": "/hello", "path": "/hello2" }

如果 key 不存在,则不做任何修改。

copy

用来复制某个 key,格式如下:

1
{ "op": "copy", "from": "/hello", "path": "/hello2" }

如果原始 key 不存在,则不复制;如果目标 key 已经存在,则仍然会复制。

原始 json 如下:

1
2
3
4
{
"hello": "world",
"hello2": "world2"
}

执行完成后的 json 如下:

1
2
3
4
{
"hello": "world",
"hello2": "world"
}

test

用来测试 key 对应的 value 是否相等,该操作并不常用

1
{ "op": "test", "path": "/a/b/c", "value": "foo" }

工具

  • JSON Patch Builder Online 在线工具,可根据原始 json 和 patch 完成后的 json,产生 json patch
  • jsonpatch.me 在线工具,可根据原始 json 和 json patch,产生 patch 完成后的 json

总结

通过上述协议可以发现如下缺点:

  1. 对于数组的处理不是太理想,如果要删除数组中的某个元素,或者在数组中追加某个元素,则无法表达。
  2. 该协议对于人类并不友好。

json merge patch

定义在 RFC 7386,由于patch 能力比较有限,使用场景较少。

同样可以配合 http patch 方法一起使用,http 请求如下:

1
2
3
4
5
6
7
8
9
10
PATCH /target HTTP/1.1
Host: example.org
Content-Type: application/merge-patch+json

{
"a":"z",
"c": {
"f": null
}
}

下面结合具体的实例来说明 json merge patch 的功能。
原始 json 格式如下:

1
2
3
4
5
6
7
8
9
{
"title": "Goodbye!",
"author" : {
"givenName" : "John",
"familyName" : "Doe"
},
"tags":[ "example", "sample" ],
"content": "This will be unchanged"
}

patch json 格式如下:

1
2
3
4
5
6
7
8
{
"title": "Hello!",
"phoneNumber": "+01-123-456-7890",
"author": {
"familyName": null
},
"tags": [ "example" ]
}

其中 null 用来表示该 key 需要删除。对于数组类型,则直接覆盖数组中的值。
patch 完成后的 json 如下:

1
2
3
4
5
6
7
8
9
{
"title": "Hello!",
"author" : {
"givenName" : "John"
},
"tags": [ "example" ],
"content": "This will be unchanged",
"phoneNumber": "+01-123-456-7890"
}

通过上述实例可以发现如下的功能缺陷:

  1. 如果某个 json 的 key 对应的值为 null,则无法表达,即不可以将某个 key 对应的value 设置为 null。
  2. 对于数组的处理非常弱,是直接对数组中所有元素的替换。

k8s strategic merge patch

该协议的资料较少,官方参考资料只有两篇文章,最好结合着 k8s 的代码才能完全理解:

背景

无论是 json patch,还是 json merge patch 协议,对于数组元素的支持都不够友好。
比如对于如下的 json:

1
2
3
4
spec:
containers:
- name: nginx
image: nginx-1.0

期望能够 patch 如下的内容

1
2
3
4
spec:
containers:
- name: log-tailer
image: log-tailer-1.0

从而可以实现 containers中包含两个元素的情况,无论是 json patch 还是 json merge patch,其行为是对数组元素的直接替换,不能实现追加的功能。

协议规范

为了解决 json merge patch 的功能缺陷,strategic merge patch 通过如下两种方式来扩展功能:

  1. json merge patch 的 json 语法增强,增加一些额外的指令
  2. 通过增强原始 json 的 struct 结构实现,跟 golang 语言强绑定,通过 golang 中的 struct tag 机制实现。这样的好处是不用再扩充 json merge patch 的 json 格式了。支持如下 struct tag:
    1. patchStrategy: 指定策略指令,支持:replace、merge 和 delete。默认的行为为 replace,保持跟 json merge patch 的兼容性。
    2. patchMergeKey: 数组一个子 map 元素的主键,类似于关系型数据库中一行记录的主键。

支持如下指令:

  1. replace
  2. merge
  3. delete

replace

支持 go struct tag 和 在 json patch 中增加指令两种方式。
replace 是默认的指令模式,对于数组而言会直接全部替换数组内容。

如下指令用来表示,

1
2
3
4
$patch: replace  # recursive and applies to all fields of the map it's in
containers:
- name: nginx
image: nginx-1.0

delete

删除数组中的特定元素,下面例子可以删除数组中包含 name: log-tailer 的元素。

1
2
3
4
5
containers:
- name: nginx
image: nginx-1.0
- $patch: delete
name: log-tailer # merge key and value goes here

删除 map 的特定 key,如下实例可以删除 map 中的 key rollingUpdate。

1
2
rollingUpdate:
$patch: delete

merge

该指令仅支持 go struct tag 模式,格式为:$deleteFromPrimitiveList/<keyOfPrimitiveList>: [a primitive list]

deleteFromPrimitiveList

删除数组中的某个元素
go struct 定义如下:

1
Finalizers []string `json:"finalizers,omitempty" patchStrategy:"merge" protobuf:"bytes,14,rep,name=finalizers"`

原始 yaml 如下:

1
2
3
4
5
finalizers:
- a
- b
- c
- b

patch yaml 如下,用来表示删除finalizers中的所有元素 b 和 c

1
2
3
4
5
6
# The directive includes the prefix $deleteFromPrimitiveList and
# followed by a '/' and the name of the list.
# The values in this list will be deleted after applying the patch.
$deleteFromPrimitiveList/finalizers:
- b
- c

最终得到结果:

1
2
finalizers:
- a

setElementOrder

用于数组中的元素排序

简单数组排序例子

原始内容如下:

1
2
3
4
finalizers:
- a
- b
- c

设置排序顺序:

1
2
3
4
5
6
# The directive includes the prefix $setElementOrder and
# followed by a '/' and the name of the list.
$setElementOrder/finalizers:
- b
- c
- a

最终得到排序顺序:

1
2
3
4
finalizers:
- b
- c
- a
map 类型数组排序例子

其中 patchMergeKey 为 name 字段

1
2
3
4
5
6
7
containers:
- name: a
...
- name: b
...
- name: c
...

patch 指令的格式:

1
2
3
4
5
# each map in the list should only include the mergeKey
$setElementOrder/containers:
- name: b
- name: c
- name: a

最终获得结果:

1
2
3
4
5
6
7
containers:
- name: b
...
- name: c
...
- name: a
...

retainKeys

用来清理 map 结构中的 key,并指定保留的 key
原始内容:

1
2
3
union:
foo: a
other: b

patch 内容:

1
2
3
4
5
6
union:
retainKeys:
- another
- bar
another: d
bar: c

最终结果,可以看到 foo 和 other 因为不在保留列表中已经被清楚了。同时新增加了字段 another 和 bar,新增加的是字段是直接 patch 的结果,同时这两个字段也在保留的列表内。

1
2
3
4
union:
# Field foo and other have been cleared w/o explicitly set them to null.
another: d
bar: c

strategic merge patch 在 k8s 中应用

kubectl patch 命令通过–type 参数提供了几种 patch 方法。

1
--type='strategic': The type of patch being provided; one of [json merge strategic]
  1. json:即支持 json patch 协议,例子:kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"newimage"}]
  2. merge:对应的为 json merge patch 协议。
  3. stategic:k8s 特有的 patch 协议,在 json merge patch 协议基础上的扩展,可以解决 json merge patch 的缺点。

对于 k8s 的 CRD 对象,由于不存在 go struct tag,因此无法使用 stategic merge patch。

TODO:待补充具体例子

kubectl patch、replace、apply之间的区别

patch

kubectl patch 命令的实现比较简单,直接调用 kube-apiserver 的接口在 server 端实现 patch 操作。

replace

如果该命令使用 –force=true 参数,则会先删除对象,然后再提交,相当于是全新创建。

apply

apply 的实现相对比较复杂,逻辑也比较绕,可以实现 map 结构中的字段增删改操作,数组中数据的增删改操作。实现上会将多份数据进行merge 后提交,数据包含:

  1. 要重新 apply 的 yaml
  2. 对象的annotation kubectl.kubernetes.io/last-applied-configuration 包含的内容
  3. 运行时的 k8s 对象

具体的操作步骤:

  1. 要重新 apply 的 yaml 跟annotation kubectl.kubernetes.io/last-applied-configuration 包含的内容比较,获取到要删除的字段。
  2. 要重新 apply 的 yaml 跟运行时的 k8s 对象进行比较,获取到要增加的字段。
  3. 上述两个结果再进行一次 merge 操作,最终调用 kube-apiserver 的接口实现 patch 操作。

为什么一定需要用到kubectl.kubernetes.io/last-applied-configuration的数据呢?
在 yaml 对象提交到 k8s 后,k8s 会自动增加一些字段,也可以通过额外的方式修改对象增加一些字段。如果 patch 内容仅跟运行时结果比较,会导致一些运行时的k8s 自动增加的字段或者手工更新的字段被删除掉。

试验 上次提交 last-applied-configuration 运行时 patch 内容 结果 结果分析
试验一 label1: first label1: first label2: second label2: second 1. patch 内容跟上次内容比较,发现要删除字段 label1
2. patch 内容跟运行时比较,发现新增加了字段 label2
3. 最终 label1 被删除,仅保留 label2
试验二 label1: first label1: first
label2: second
label1: first label1: first
label2: second
1. patch 内容跟上次内容比较,发现结果无变化
2. patch 内容跟运行时比较,发现要新增加字段 label2
3. 最终新增加字段 label2

引用

本文将通过 kubeadm 实现单 master 节点模式和集群模式两种部署方式。

所有节点均需初始化操作

所有节点均需做的操作。

主机准备

1
2
3
4
5
6
7
8
cat > /etc/sysctl.d/kubernets.conf <<EOF
net.ipv4.ip_forward=1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
vm.swappiness=0
EOF
sysctl --system
modprobe br_netfilter

安装containerd

由于 dockerd 从 k8s 1.24 版本开始不再支持,这里选择 containerd。

手工安装

安装 containerd,containerd 的版本可以从这里获取 https://github.com/containerd/containerd/releases

1
2
3
4
5
wget https://github.com/containerd/containerd/releases/download/v1.6.11/containerd-1.6.11-linux-amd64.tar.gz
tar Cxzvf /usr/local containerd-1.6.11-linux-amd64.tar.gz
wget https://raw.githubusercontent.com/containerd/containerd/main/containerd.service -P /usr/local/lib/systemd/system/
systemctl daemon-reload
systemctl enable --now containerd

安装 runc

1
2
wget https://github.com/opencontainers/runc/releases/download/v1.1.4/runc.amd64
install -m 755 runc.amd64 /usr/local/sbin/runc

yum 源安装

1
2
3
4
5
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install containerd.io -y
systemctl daemon-reload
systemctl enable --now containerd

通过 yum 安装的containerd 没有启用 cri,在其配置文件 /etc/containerd/config.toml 中包含了 disabled_plugins = ["cri"] 配置,需要将配置信息注释后并重启 containerd。

1
2
sed -i 's/disabled_plugins/#disabled_plugins/'  /etc/containerd/config.toml
systemctl restart containerd

安装 kubeadm/kubelet/kubectl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
enabled=1
gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kubelet kubeadm kubectl
EOF

# Set SELinux in permissive mode (effectively disabling it)
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

sudo systemctl enable --now kubelet

单 master 节点模式

节点 角色
172.21.115.190
master 节点

kubeadm 初始化

创建文件 kubeadm-config.yaml,文件内容如下:

1
2
3
4
5
6
7
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.25.4
---
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
cgroupDriver: systemd

执行命令:

1
2
3
kubeadm init --config kubeadm-config.yaml
kubeadm config print init-defaults --component-configs KubeletConfiguration > cluster.yaml
kubeadm init --config cluster.yaml

接下来初始化 kubeconfig 文件,这样即可通过 kubectl 命令来访问 k8s 了。

1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

安装网络插件

刚部署完成的节点处于NotReady 的状态,原因是因为还没有安装网络插件。

cilim 网络插件

cilim 网络插件比较火爆,下面介绍其安装步骤:

1
2
3
4
5
6
7
8
9
10
11
# 安装 cilium 客户端
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/master/stable.txt)
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
rm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}

# 网络插件初始化
cilium install

在安装完网络插件后,node 节点即可变为 ready 状态。

查看环境中包含如下的 pod:

1
2
3
4
5
6
7
8
9
10
11
$ kubectl  get pod  -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system cilium-7zj7t 1/1 Running 0 82s
kube-system cilium-operator-bc4d5b54-kvqqx 1/1 Running 0 82s
kube-system coredns-565d847f94-hrm9b 1/1 Running 0 14m
kube-system coredns-565d847f94-z5kwr 1/1 Running 0 14m
kube-system etcd-k8s002 1/1 Running 0 14m
kube-system kube-apiserver-k8s002 1/1 Running 0 14m
kube-system kube-controller-manager-k8s002 1/1 Running 0 14m
kube-system kube-proxy-bhpqr 1/1 Running 0 14m
kube-system kube-scheduler-k8s002 1/1 Running 0 14m

k8s 自带的 bridge 插件

在单节点的场景下,pod 不需要跨节点通讯,k8s 自带的 bridge 插件也可以满足单节点内的 pod 相互通讯,类似于 docker 的 bridge 网络模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mkdir -p /etc/cni/net.d
cat > /etc/cni/net.d/10-mynet.conf <<EOF
{
"cniVersion": "0.2.0",
"name": "mynet",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "172.19.0.0/24",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
EOF

如果 k8s 节点已经部署完成,需要重启下 kubelet 进程该配置即可生效。

添加其他节点

1
2
kubeadm join 172.21.115.189:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:457fba2c4181a5b02d2a4f202dfe20f9ce5b9f2274bf40b6d25a8a8d4a7ce440

此时即可以将节点添加到 k8s 集群中

1
2
3
4
$ kubectl  get node 
NAME STATUS ROLES AGE VERSION
k8s002 Ready control-plane 79m v1.25.4
k8s003 Ready <none> 35s v1.25.4

节点清理

清理普通节点

1
2
3
4
5
kubectl drain <node name> --delete-emptydir-data --force --ignore-daemonsets
kubeadm reset
# 清理 iptabels 规则
iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X
kubectl delete node <node name>

清理 control plan 节点

1
kubeadm reset

集群模式部署

待补充,参考文档:Creating Highly Available Clusters with kubeadm | Kubernetes

资料

题图为望京傍晚的天气,夕阳尽情散发着落山前的最后余光,层次分明的云朵映射在建筑物的上熠熠生辉。上班族结束了一天紧张的工作,朝着地铁站的方向奔向自己的家,这才是城市生活该有的模样。不过可惜的是,对于很多打工族而言,一天的工作还远未结束,晚饭后仍要坐在灯火通明的写字楼内或为生活或为梦想挥霍着自己的时光。

资源

kaniko

Google开源的一款可以在容器内部通过Dockerfile构建docker镜像的工具。

我们知道docker build命令可以根据Dockerfile构建出docker镜像,但该操作实际上是由docker daemon进程完成。如果docker build命令在docker容器中执行,由于容器中并没有docker daemon进程,因此直接执行docker build肯定会失败。

kaniko则重新实现了Dockerfile构建镜像的功能,使得构建镜像不再依赖docker daemon。随着gitops的技术普及,CI工具也正逐渐on k8s部署,kaniko正好可以在k8s的环境中根据Dockerfile完成镜像的打包过程,并将镜像推送到镜像仓库中。

arc42

技术人员在写架构文档的时候,遇到最多的问题是该如何组织技术文档的结构,arc42 提供了架构文档的模板,将架构文档分为了 12 个章节,每个章节又包含了多个子章节,用来帮助技术人员更好的编写架构文档。

相关链接:https://topic.atatech.org/articles/205083?spm=ata.21736010.0.0.18c23b50NAifwr#tF1lZkHm

Carina

国内云厂商博云发起的一款基于 Kubernetes CSI 标准实现的存储插件,用来管理本地的存储资源,支持本地磁盘的整盘或者LVM方案来管理存储。同时,还包含了Raid管理、磁盘限速、容灾转移等高级特性。

相关链接:一篇看懂 Carina 全貌

kube-capacity

k8s的命令行工具kubectl用来查看集群的整体资源情况往往操作会比较复杂,可能需要多条命令配合在一起才能拿得到想要的结果。kube-capacity命令行工具用来快速查看集群中的资源使用情况,包括node、pod维度。

相关链接:Check Kubernetes Resource Requests, Limits, and Utilization with Kube-capacity CLI

Kubeprober

在k8s集群运维的过程中,诊断能力非常重要,可用来快速的定位发现问题。Kubeprober为一款定位为k8s多集群的诊断框架,提供了非常好的扩展性来接入诊断项,诊断结果可以通过grafana来统一展示。

社区里类似的解决方案还有Kubehealthy和Kubeeye。

相关链接:用更云原生的方式做诊断|大规模 K8s 集群诊断利器深度解析

Open Policy Agent

OPA为一款开源的基于Rego语言的通用策略引擎,CNCF的毕业项目,可以用来实现一些基于策略的安全防护。比如在k8s中,要求pod的镜像必须为某个特定的registry,用户可以编写策略,一旦pod创建,OPA的gatekeeper组件通过webhook的方式来执行策略校验,一旦校验失败从而会导致pod创建失败。

比如 阿里云的ACK的gatekeeper 就是基于OPA的实现。

docker-squash

docker-squash为一款docker镜像压缩工具。在使用Dockerfile来构建镜像时,会产生很多的docker镜像层,当Dockerfile中的命令过多时,会产生大量的docker镜像层,从而导致docker镜像过大。该工具可以将镜像进行按照层合并压缩,从而减小镜像的体积。

FlowUs

FlowUs为国内研发的一款在线编辑器,支持文档、表格和网盘功能,该软件可以实现笔记、项目管理、共享文件等功能,跟蚂蚁集团的产品《语雀》功能比较类似。但相比语雀做的好的地方在于,FlowUs通过”块编辑器“的方式,在FlowUs看来所有的文档形式都是”块“,作者可以在文档中随意放置各种类型的”块“,在同一个文档中即可以有功能完善的表格,也可以有网盘。而语雀要实现一个相对完整的表格,需要新建一种表格类型的文档,类似于Word和Excel。

k8tz

k8s中的pod默认的时区跟pod的镜像有关,跟pod宿主机所在的时区没有关系。很多情况下,用户都期望pod里看到的时区能够跟宿主机的保持一致。用户的一种实现方式是将宿主机的时区文件挂载到pod中,但需要修改pod的yaml文件。本工具可以通过webhook的方式自动化将宿主机的时区文件挂载到pod中。

文章

  1. 中美云巨头歧路,中国云未来增长点在哪?

文章结合全球的云计算行业,对国内的云计算行业做了非常透彻的分析。”全球云,看中美;中美云,看六大云“,推荐阅读。

  1. 程序员必备的思维能力:结构化思维

结构化思维不仅对于程序员,对于职场中的很多职业都非常重要,无论是沟通、汇报、晋升,还是写代码结构化思维都非常重要。本文深度剖析了金字塔原理以及如何应用,非常值得一读。文章的作者将公众号的文章整理为了《程序员底层思维》一书,推荐大家阅读。

  1. 中文技术文档的写作规范

阮一峰老师的中文技术文档写作规范,写技术文档的同学可以参考。

书籍

  1. 《程序员的底层思维》

通过书名中的“程序员”来看有点初级,但实际上书中的内容适合所有软件行业的从业者,甚至同样适合于其他行业的从业者,因为底层思维本来就是共性的东西,万变不离其宗。作者曾在阿里巴巴有过很长一段工作经历,书中结合着工作中的实践经验介绍了16种思维能力,讲解浅显易懂,部分内容上升到了哲学的角度来讲解。

作为软件行业从业者的我,实际上书中的大部分思维能力在工作中都有应用,但却没有形成理论来总结。阅读本书,有助于对工作的内容进行总结,找到工作的理论基础。另一方面,有了书中的理论总结,也可以更好的指导工作。

ipv6的优势

  1. 拥有更大的地址空间。
  2. 点对点通讯更方便。由于ipv6地址足够多,可以不再使用NAT功能,避免NAT场景下的各种坑。
  3. ip配置方便。每台机器都有一个唯一48位的mac地址,如果再增加一个80位的网段前缀即可组成ipv6地址。因此,在分配ip地址的时候,只需要获取到网段前缀即可获取到完整的ipv6地址了。
  4. 局域网内更安全。去掉了arp协议,而是采用Neighbor Discovery协议。

ipv6的数据包格式

分为报头、扩展报头和上层协议数据单元(PDU)三部分组成。

报头

固定为40个字节,相比于 ipv4 的可变长度报文更简洁。

  • 版本:固定为4bit,固定值6。

  • 业务流类别:8bit,用来表明数据流的通讯类别或者优先级。

  • 流标签:20bit,标记ipv6路由器需要特殊处理的数据流,目的是为了让路由器对于同一批数据报文能够按照同样的逻辑来处理。目前该字段的应用场景较少。

  • 净荷长度:16bit,扩展头和上次协议数据单元的字节数,不包含报头的固定 40 字节。

  • 下一个头:8bit,每个字段值有固定含义,用来表示上层协议。如果上层协议为 tcp,那么该字段的值为 4。

  • 跳限制:8bit,即跳数限制,等同于ipv4中的ttl值。

  • 源ip地址:128bit

  • 目的ip地址:128bit

扩展报头

扩展报头的长度任意

ipv6地址

地址表示方法

  1. 采用16进制表示法,共128位,分为8组,每组16位,每组用4个16进制表示。各组之间使用:分割。例如:1080:0:0:0:8:800:200C:417A。
  2. 地址中出现连续的0,可以使用::来代替连续的0,一个地址中只能出现一次连续的0。例如上述地址可以表示为:1080::8:800:200C:417A。本地的回环地址可使用 ::1 表示。
  3. 如果ipv6的前面地址全部为0,可能存在包含ipv4地址的场景,可以使用ipv4的十进制表示方法。例如:0:0:0:0:0:0:61.1.133.1或者::61.1.133.1。

ip地址结构包含了64位的网络地址和64位的主机地址,其中64位的网络地址又分为了48位的全球网络标识符和16位的本地子网标识符。

网段表示方法

在ipv6中同样有网段的概念,如 2001:0:0:CD30::/60 ,其中前60位为前缀长度,后面的所有位表示接口 ID,使用 :: 表示,但前面的两个0不能使用 :: 表示。

地址分类

包括了如下地址类型,但跟 ipv4 不同的地方在于没有广播地址。

单播地址

单播地址又可以分为如下类型:

链路本地地址(LLA)

类似于 ipv4 的私网地址段,但比 ipv4 的私网地址段范围更小,仅可以用于本地子网内通讯,不可被路由。以fe80::/10开头的地址段。设备要想支持 ipv6,必须要有链路本地地址,且只能设置一个。

在设备启动时,通常该地址会自动设置,也可以手动设置。自动生成的地址通常会根据 mac 地址有关,因为每个设备的 mac 地址都是唯一的。

公网单播地址(GUA)

类似于 ipv4 的公网地址。

地址范围:2000::3 - 3fff::/16,即以 2 或者 3 开头,用总 ipv6 地址空间的1/8。

通常情况下,公网路由前缀为 48 位,子网 id 为 16 位,接口 id 为 64 位。

本地唯一单播地址(ULA)

范围:fc00::/7,当前唯一有效的前缀为fd00::/8。只能在私网内部使用,不能在公网上路由。

其全网 id 的部分采用伪随机算法,可以尽最大可可能确保全局的唯一性,从而在两个网络进行并网的时候减少地址冲突的概率。

loopback地址

::1,等同于 ipv4 的 127.0.0.1/8

未指定单播地址

::

多播地址(Multicast)

又叫组播地址,标识一组节点,目的为组播地址的流量会转发到组内的所有节点,类似于 ipv4 的广播地址。地址范围:FF00::/8

任意播地址(Anycast)

标识一组节点,所有节点的接口分配相同的 ip 地址,目的为组播地址的流量会转发到组内的就近节点。任意播地址没有固定的前缀。

DNS服务和ipv6

双栈请求域名请求顺序

在开启ipv4/ipv6双栈的情况下,域名解析会同时发出A/AAAA请求,发出请求的先后顺序由/etc/resolv.conf的option中的inet6选项决定。

ipv6与 /etc/hosts文件

通常在/etc/hosts文件中包含了如下的回环地址

1
::1             localhost6.localdomain6 localhost6

检测域名是否支持ipv6

dig aaaa方法

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
dig aaaa  ipv6.google.com.hk

; <<>> DiG 9.10.6 <<>> aaaa ipv6.google.com.hk
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50248
;; flags: qr rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;ipv6.google.com.hk. IN AAAA

;; ANSWER SECTION:
ipv6.google.com.hk. 21600 IN CNAME ipv6.google.com.
ipv6.google.com. 21600 IN CNAME ipv6.l.google.com.
ipv6.l.google.com. 300 IN AAAA 2607:f8b0:4003:c0b::71
ipv6.l.google.com. 300 IN AAAA 2607:f8b0:4003:c0b::64
ipv6.l.google.com. 300 IN AAAA 2607:f8b0:4003:c0b::65
ipv6.l.google.com. 300 IN AAAA 2607:f8b0:4003:c0b::8b

;; Query time: 83 msec
;; SERVER: 30.30.30.30#53(30.30.30.30)
;; WHEN: Mon Jun 20 10:45:37 CST 2022
;; MSG SIZE rcvd: 209

使用网站测试

使用该网址,将域名更换为对应的域名:http://ipv6-test.com/validate.php?url=http://www.microsoft.com

启用ipv6

ipv6特性可以设置在整个系统级别或者单个网卡上,默认启用。

系统级别

系统级别可以通过内核参数 net.ipv6.conf.all.disable_ipv6 来查看是否启用,如果输出结果为0,说明启用。可以修改该内核参数的值来开启或者关闭系统级别的ipv6特性。

也可以通过修改grub的内核参数来选择开启或者关闭,在 /etc/default/grub 中GRUB_CMDLINE_LINUX追加如下内容,其中xxxxx代表当前已经有的参数:

1
GRUB_CMDLINE_LINUX="xxxxx ipv6.disable=1"

网卡级别

可以通过 ifconfig ethx 命令来查看网卡信息,如果其中包含了inet6,则说明该网卡启用了ipv6特性。

也可以通过 sysctl net.ipv6.conf.ethx.disable_ipv6 来查看网卡是否启用。其中ethx为对应的网卡名称。

nginx支持ipv6

默认情况下,nginx不支持ipv6,要支持ipv6,需要在编译的时候指定 –with-ipv6 选项。

在编译完成后,通过nginx -V 查看需要包含 –with-ipv6 选项。

配置 ipv6

ifconfig eth0 inet6 add 2607:f8b0:4003:c0b::71

参考资料

Linux有问必答:如何在Linux下禁用IPv6

2012 年,Heroku 创始人 Adam Wiggins 发布十二要素应用宣言,又被称为微服务十二要素。

内容

基准代码

  1. 一个应用一个代码仓库,不要出现一个代码仓库对应多个应用的情况
  2. 如果多个应用共享一份代码,那么需要将该代码拆分为独立的类库

依赖

规范:

  1. 应用必须通过配置显式声明出所有的依赖项,即使依赖的工具在所有的操作系统上都存在。比如 python 可以使用 pip 的 requirements.txt 文件声明出所有的依赖包及其版本信息。
  2. 在运行时需要通过隔离手段确保应用不会调用未显式声明的依赖项。比如 python 应用可以通过virtualenv 技术来确保隔离性。

优点:

  1. 简化开发者的环境配置,通过构建命令即可安装所有的依赖项。

配置

规范:

  1. 代码和配置单独管理,不要将配置放到代码仓库中管理。
  2. 配置传递给应用的方式之一为配置文件,另外一种为环境变量。

后端服务

规范:

  1. 后端服务分为本地服务和第三方服务,对应用而言都是后端服务,不应该区别对待。

构建、发布和运行

规范:

  1. 严格区分从代码到部署的三个阶段:构建编译打包代码;将构建结果和部署需要的配置放到环境中;指定发布版本,在环境中启动应用。
  2. 每个发布版本必须对应一个唯一的 id 标识。

反模式:

  1. 不可以直接修改环境中的代码,可能会导致非常难同步会构建步骤。

进程

规范:

  1. 应用的进程需要持久化的数据一定要保存到后端服务中,保持应用进程的无状态。
  2. 本地的内存和磁盘可以作为应用的缓存数据。
  3. 反对使用基于 session 的粘滞技术(某一个用户的请求通过一致性 hash 等技术请求到同一个后端服务,而后端服务将用户数据缓存在内存中),推荐将用户数据缓存到Redis 等分布式缓存中。很多互联网公司都会采用 session 粘滞技术,都违背了这一原则。

端口绑定

要求应用自己通过监听端口的方式来对外提供服务。

并发

规范:

  1. 允许通过多进程或者多线程的方式来处理高并发
  2. 应用不需要守护进程或者写入 PID 文件,可以借助如 systemd 等工具来实现。

易处理

规范:

  1. 进程启动速度尽可能快,以便于更好的支持弹性伸缩、部署应用。
  2. 进程在接收到SIGTERM 信号后需要优雅停止。比如对于 nginx 而言,要等到处理完所有的连接后才可以退出。
  3. 进程要能够处理各种异常情况。

开发环境与生产环境等价

核心就只有一点,尽可能保证开发环境与生产环境的一致性。

日志

规范:

  1. 应用程序将日志直接输出到标准输出,标准输出的内容由日志收集程序消费。

反模式:

  1. 现实中应用程序将日志直接写到了日志文件,并且自己来管理日志文件。

管理进程

规范:

  1. 仅需要执行一次的进程,跟普通的进程一样的管理思路,比如版本、执行环境等。

参考

CRI为k8s提供的kubelet扩展接口,用来支持多种容器运行时。CRI协议为protobuf格式,kubelet作为客户端,容器运行时作为服务端,两者通过gRpc协议通讯。下面主要解读 CRI的协议定义

spec解读

RuntimeService

https://github.com/kata-containers/kata-containers/blob/main/docs/design/arch-images/api-to-construct.png

包括了Pod和容器相关的操作。

Pod相关的操作包括:

  1. 启:RunPodSandbox
  2. 停:StopPodSandbox
  3. 删:RemovePodSandbox
  4. 查:PodSandboxStatus、ListPodSandbox、ListPodSandboxStats、PodSandboxStats

容器相关的操作:

  1. 增、启:CreateContainer、StartContainer
  2. 停:StopContainer
  3. 删:RemoveContainer
  4. 查:ListContainers、ContainerStatus、
  5. 改:UpdateContainerResources
  6. 控:ReopenContainerLog、ExecSync、Exec、Attach、PortForward
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
// Runtime service defines the public APIs for remote container runtimes
service RuntimeService {
// Version returns the runtime name, runtime version, and runtime API version.
rpc Version(VersionRequest) returns (VersionResponse) {}

// RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
// the sandbox is in the ready state on success.
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}

// StopPodSandbox stops any running process that is part of the sandbox and
// reclaims network resources (e.g., IP addresses) allocated to the sandbox.
// If there are any running containers in the sandbox, they must be forcibly
// terminated.
// This call is idempotent, and must not return an error if all relevant
// resources have already been reclaimed. kubelet will call StopPodSandbox
// at least once before calling RemovePodSandbox. It will also attempt to
// reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
// multiple StopPodSandbox calls are expected.

rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
// RemovePodSandbox removes the sandbox. If there are any running containers
// in the sandbox, they must be forcibly terminated and removed.
// This call is idempotent, and must not return an error if the sandbox has
// already been removed.

rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
// PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
// present, returns an error.

rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}

// ListPodSandbox returns a list of PodSandboxes.
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}

// CreateContainer creates a new container in specified PodSandbox
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}

// StartContainer starts the container.
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}

// StopContainer stops a running container with a grace period (i.e., timeout).
// This call is idempotent, and must not return an error if the container has
// already been stopped.
// The runtime must forcibly kill the container after the grace period is
// reached.
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}

// RemoveContainer removes the container. If the container is running, the
// container must be forcibly removed.
// This call is idempotent, and must not return an error if the container has
// already been removed.

rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}

// ListContainers lists all containers by filters.
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}

// ContainerStatus returns status of the container. If the container is not
// present, returns an error.
rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}

// UpdateContainerResources updates ContainerConfig of the container.
rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}

// ReopenContainerLog asks runtime to reopen the stdout/stderr log file
// for the container. This is often called after the log file has been
// rotated. If the container is not running, container runtime can choose
// to either create a new log file and return nil, or return an error.
// Once it returns error, new container log file MUST NOT be created.
rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}

// ExecSync runs a command in a container synchronously.
rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}

// Exec prepares a streaming endpoint to execute a command in the container.
rpc Exec(ExecRequest) returns (ExecResponse) {}

// Attach prepares a streaming endpoint to attach to a running container.
rpc Attach(AttachRequest) returns (AttachResponse) {}

// PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}

// ContainerStats returns stats of the container. If the container does not
// exist, the call returns an error.
rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {}

// ListContainerStats returns stats of all running containers.
rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {}

// PodSandboxStats returns stats of the pod sandbox. If the pod sandbox does not
// exist, the call returns an error.
rpc PodSandboxStats(PodSandboxStatsRequest) returns (PodSandboxStatsResponse) {}

// ListPodSandboxStats returns stats of the pod sandboxes matching a filter.
rpc ListPodSandboxStats(ListPodSandboxStatsRequest) returns (ListPodSandboxStatsResponse) {}

// UpdateRuntimeConfig updates the runtime configuration based on the given request.
rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {}

// Status returns the status of the runtime.
rpc Status(StatusRequest) returns (StatusResponse) {}
}

ImageService

跟镜像相关的操作,包括了

  1. 增:PullImage
  2. 查:ListImages、ImageStatus和ImageFsInfo
  3. 删:RemoveImage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ImageService defines the public APIs for managing images.
service ImageService {
// ListImages lists existing images.
rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}

// ImageStatus returns the status of the image. If the image is not
// present, returns a response with ImageStatusResponse.Image set to
// nil.
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}

// PullImage pulls an image with authentication config.
rpc PullImage(PullImageRequest) returns (PullImageResponse) {}

// RemoveImage removes the image.
// This call is idempotent, and must not return an error if the image has
// already been removed.
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}

// ImageFSInfo returns information of the filesystem that is used to store images.
rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
}

CRI CLI

CRI 自带了命令行工具 crictl。

Linux 下安装脚本:

1
2
3
4
VERSION="v1.26.0" # check latest version in /releases page
wget https://github.com/kubernetes-sigs/cri-tools/releases/download/$VERSION/crictl-$VERSION-linux-amd64.tar.gz
sudo tar zxvf crictl-$VERSION-linux-amd64.tar.gz -C /usr/local/bin
rm -f crictl-$VERSION-linux-amd64.tar.gz
0%