404频道

学习笔记

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

今年六月份入职阿里正好三周年,在阿里入职三年被称为”三年醇”,三年才称之为真正的阿里人。我个人在这三年的时间里也变化了许多,值得反思总结一下。

阿里缘起

三年前,从上家公司离开。当时可选择的机会还是比较多的,基本上职位比较match的面试都通过了,甚至有公司经过了七面。之所以最终选择了阿里,主要如下两个原因:

  1. 阿里的技术在国内的知名度是响当当的。从技术深度和广度而言,阿里的技术都可圈可点,进入阿里我想多接触一下牛人,技术更进一层。
  2. 过往工作经历没有一线大厂,需要丰富下阅历。大厂的经历还是比较关键,不光是为了给自己的职业生涯中增加一份光鲜的经历,更多的是想看大厂是如何运作的,同样一件事情,在不同规模的公司有着不同的处理思路。

主要是基于上述原因,即使不是最好选择,即使做出了部分牺牲,毅然决定加入阿里云,加入了国内最领先的云计算厂商之一。

工作内容及感受

上家公司为互联网公司,处理的业务为高并发的在线业务,在互联网公司摸爬滚打积累了大量经验。来到阿里云后,加入了混合云,场景变为了专有云,虽然工作内容同样为云原生领域,但本质上为离线业务,跟互联网的在线业务在工作内容上有了较大的区别。互联网业务在线化,仅需要几套在线环境即可满足需求,专有云场景却需要维护大量的测试环境和客户的离线环境,从而导致业务复杂度急剧上升。主要体现在如下方面:

  1. 专有云场景强化对版本的概念。由于大量环境的存在,要想做到统一的管理和运维,必须用版本来强管理,否则维护大量环境将成为灾难。
  2. 专有云对自动化部署的要求变高。几套环境的情况下,考虑到ROI,并不需要很高的自动化程度,相反引入自动化会给变更引入额外的复杂性。但如果成百上千套环境,自动化部署成为了强需求,混合云的很多能力都是围绕着自动化部署展开的。
  3. 专有云场景对自动化运维的要求变高。运维场景包括了升级、扩缩容、故障处理等,由于是离线的场景,运维的流程变成了:客户 -> 驻场运维 -> 远程运维支持 -> 研发,相比在线业务的支持流程会更长,更难找到bug的真正owner。一方面需要对支持链路上的人员进行相关的培训,更为重要的是需要通过增强平台的自动化运维能力来降低运维成本。
  4. bug在客户环境的问题收敛非常慢。由于有大量的客户环境存在,一旦发现bug,需要在客户现场打hotfix,由于绝大多数环境无法远程,需要人工在客户现场操作,受限于人力、客户变更窗口等限制,hotfix patch的速度很难快起来。从而导致一个bug,会在多个客户现场环境爆发。
  5. 墨菲定律凸显,只要是 bug 暴露出来的概率明显增加。由于大量环境的存在,且客户现场运行着多个历史版本,非常难以拉齐版本,哪怕是多年前的旧 bug,在客户现场也极有可能会被发现。
  6. 专有云场景下忌架构变更大,架构复杂。专有云场景下经常出现的问题是,一个组件在版本一的架构,还没等一线的运维熟悉架构,在版本二下更换为了另外一套方案,导致运维性特别差。在引入“高大上”的高级特性时也一定要慎重,避免将架构搞复杂,将学习成本变高,专有云的试错成本太高。专有云场景下做好的架构解决方案是,使用尽可能通用简洁的方案来解决复杂的业务问题。

虽然两份工作内容都是云原生领域,但因为业务场景的不同,却对工作的重心产生了质的差异,量变产生了质变。正是因为质变,导致用互联网的“快糙猛”、“短平快”的思路来做需要精心打磨的专有云业务,路子非常容易走偏。

从工作量上来看,虽然上一份工作也不怎么轻松,但跟当前工作相比,却是小巫见大巫。尤其是前两年的时间,正处在业务从0到1的阶段,基本上一直处于高度运转的状态,每天都会有大量的钉钉消息和大量的会议,不是在救火就是在救火的路上,钉钉消息泛滥到惨不忍睹。能休息的周末也非常少,基本上周末都用来加班干一周攒下来的工作了。下面为相对卷的轻一些的2021年度的钉钉报告,一年光钉钉的会议就有三万多分钟,平均一天有两个小时,还不包含阿里内部的阿里郎会议和会议室中参加的线下会议。

成长

最大的成长是在工作方法上有了比较大的进步。阿里有一套工作处事的方法论,“搞不定就上升问题”,“责任边界”,“总结思考”等基本上是行走江湖必备技能,在网络上大家也可以看到大量的带有“阿里味”的文章,虽然处理起工作我还是比较喜欢按照自己的方式,但实在搞不定的时候可作为兜底处理思路的,在阿里的江湖里要想风生水起除了靠技术实力,做事方法也极为关键。

文档能力有了很大提升。在过往的工作经历中,很多事情想好了直接就开干了,反正使用方也不会很多,导致写的文档比较少,毕竟写一篇文档有可能花费时间比写代码的时间还要长。但目前我基本上已经养成了写文档的好习惯,写文档是一个整理思路的好方法,同时也可以将方案分享出去。在阿里的这几年深切体会到文档的重要性,因为业务方特别多,哪怕是一个技术点也会被多个人频繁提问,这时候扔一篇文档是比较好的减负方法。

另一个比较大的转变是很少失眠了。上一份工作经历大概是处理在线业务的原因,或者是工作不够饱和,经常出现失眠的情况。来阿里之前还比较担心失眠的问题发生,顶不住阿里高强度的工作压力。结果后来发现失眠根本就不存在,当每天都处理N件事情后,超卖非常严重,已经从原来的脑力劳动变成了体力加脑力劳动,根本不存在失眠的情况。到现在为止,即使工作没有刚加入阿里时那么饱和,失眠的情况也比较少了。

不足

面向钉钉的工作方式没有改掉。刚加入阿里的时候,看到同事的钉钉怎么那么多群,而且都把消息清一色设置为了屏蔽状态,我还特别的不理解。入职没多长时间就渐渐习惯了,每天总会加入几个群,刚开始的时候还会关注群消息,后来发现大部分群都是跟自己无关的。由于会随时被@,所以就把消息提醒给关掉了,否则会特别影响注意力。关掉消息提醒后,消息的查看方式就从push模式变了定期pull模式,基本上每隔几分钟就会打开一次钉钉。虽然大部分情况下都能看到新的消息要处理,但也养成了一个坏习惯,一旦不打开钉钉,总感觉心里不舒服。我不知道这个坏习惯算不算一种医学上的疾病,至少我扣手指的坏习惯在医学上找到了对应的名称“强迫性皮肤剥离症”。这个坏习惯一度尝试修正,比如采用“番茄工作法”的方式,每隔十五分钟再打开一次钉钉,但一直没有强有力的推动下去,总是忍不住打开钉钉看一眼消息。钉钉俨然成为了我工作效率的最大杀手,这句话也不完全对,因为钉钉本来就是我工作的一部分。如果这个问题可以解掉,我的工作效率会上一个大大的台阶。

能力陷阱问题突出。工作中有大量时间是作为客服来答疑解惑各类问题,前期阶段不管我是不是owner,只要是我力所能及的都尽可能提供帮助,直到我实在扛不住。虽然一方面服务了大量的客户,但另一方面我自己可支配的时间变的非常少。后面渐渐将不是我的问题路由到其他同学,自己才释放出一些时间。但路由器其实不是那么好当的,平台有大量组件,而且不同的组件往往归属不同的owner,有很多问题其实并不能直观看出具体是哪个组件,很多情况下都是多个组件配合的问题,因此还需要充当问题初步定位的工作以及没有owner问题的默认路由。

自身的学习投入比重偏少。工作时间多的最突出问题就是没有太多的精力来投入到新技术新知识的学习,虽然最近一年有很大的改善,但相比上家公司学习的技术的投入是完全不能比的。更别提,刚工作那几年曾经看过满满一书架的技术书籍了,现在想来真是一种奢侈。

前瞻性不够。每天都有很多问题在排队的后果就是规划能力不够。阿里还是特别强调抬头看路,但现实中,我却大部分的时间消耗在了低头解问题,思考的时间偏少。长此以往,渐渐发现自己的创造性是不够的。多花时间独立思考,这条路永远是正确的。

写在最后

自己对未来有很多的期待,这里最想提升的点为:打造个人品牌影响力。个人影响力方面过去不够重视,期望将来能够行动起来。要想做到这一点并非易事,需要很多基础能力,比如需要大量的知识积累,需要大量的工作实践等。期望借着阿里的平台,能够最大化个人影响力,未来可期!

OCI(Open Container Initiative)为Linux基金会下的子项目,成立于2015年,由docker、coreos等公司发起,用来制定开放的容器和容器镜像标准,同时docker将其镜像格式和容器运行时runc捐献给了OCI,因此OCI中包含了容器镜像标准和容器运行时标准两部分。

容器镜像的发展历史

2013年,docker横空出世,docker的核心能力之一为将应用封装为镜像,便于打包、发布。

2014年,docker将镜像格式定位为docker镜像规范v1版本。

2016年,docker制定了新的镜像格式规范v2,解决v1版本的部分设计缺陷。

2017年,OCI发布了OCI镜像规范1.0版本,整个规范以docker镜像规范v2为基础,两者的规范比较类似。

容器镜像标准

官方定义:OCI Image Format Specification

容器镜像标准包含了镜像索引(可选)、manifest、层文件和配置文件四部分内容。包含了容器的构建、分发和准备可以运行的镜像。

https://github.com/opencontainers/image-spec/blob/main/img/media-types.png

skopeo为一个容器镜像和镜像仓库的命令行操作工具,可以使用该工具来学习OCI容器镜像规范,可以直接使用 yum install skopeo -y 进行安装。

使用如下命令可以将docker的镜像转换为oci格式,并将其保存到/tmp/local_nginx目录下。

1
skopeo copy docker://nginx oci:/tmp/local_nginx

/tmp/local_nginx的目录包含如下结构,参考链接:OCI Image Layout Specification

  • blobs 目录:包含了镜像Manifest、镜像层和镜像配置信息,均已sha256值命名的文件形式存储,文件可以为文本文件,也可以为gzip压缩的二进制文件。
  • oci-layout 文件:定义了当前目录结构的版本信息
  • index.json 文件:定义了镜像索引信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── blobs
│   └── sha256
│   ├── 4a7307612456a7f65365e1da5c3811df49cefa5a2fd68d8e04e093d26a395d60
│   ├── 67e9751bc5aab75bba375f0a24702d70848e5b2bea70de55e50f21ed11feed14
│   ├── 8f46223e4234ce76b244c074e79940b9ee0a01b42050012c8555ebc7ac59469e
│   ├── 935cecace2a02d2545e0c19bd52fe9c8c728fbab2323fc274e029f5357cda689
│   ├── b85a868b505ffd0342a37e6a3b1c49f7c71878afe569a807e6238ef08252fcb7
│   ├── efb56228dbd26a7f02dafc74a2ca8f63d5e3bb6df9046a921f7d8174e5318387
│   ├── f4407ba1f103abb9ae05a4b2891c7ebebaecab0c262535fc6659a628db25df44
│   └── fe0ef4c895f5ea450aca17342e481fada37bf2a1ee85d127a4473216c3f672ea
├── index.json
└── oci-layout

2 directories, 10 files

镜像索引

非必须部分。如果包含镜像索引,用来解决多架构问题。不同的平台上,可以使用同一个镜像tag,即可以获取到对应平台的镜像。

镜像索引为 json 格式的文件,查看 index.json 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:15beb598b14fca498f13a46923de0614a17012abf675ba06e364904d642d8a61",
"size": 1183
}
]
}

镜像索引文件可以包含多种架构。其中digest对应的sha256值指向 blobs/sha256 下的文件,其文件为Manifest文件。

需要注意的是:docker 可以使用 docker manifest命令来查看镜像的manifest信息,但格式并非为OCI Manifest格式,而更类似于OCI index的信息,下面的命令中,可以看到rancher镜像为多镜像。

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
$ docker manifest inspect rancher/rancher
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 4732,
"digest": "sha256:b8f1fdb8228d32ae5fc6f240503cd8e22b214fcfd4ad2a8a0b03274f3ead4e95",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 4731,
"digest": "sha256:ae0fa74e8dce9b72bdc6611815deb16bbddc8fe0a555052ccc8127fdc1b76980",
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 4519,
"digest": "sha256:94c03afba43e81885c3cd2f5065032d1b7f8f540860fcc1fce1bbd7f1068d3db",
"platform": {
"architecture": "s390x",
"os": "linux"
}
}
]
}

Manifest

参考:OCI Image Manifest Specification

Manifest为json格式的描述文件,包含了如下三个用途:

  1. 每一个镜像都有一个唯一的 id 标识
  2. 对于同一个镜像tag,可以支持多架构镜像
  3. 可以直接转换为OCI的运行时规范

查看 blobs/sha256/15beb598b14fca498f13a46923de0614a17012abf675ba06e364904d642d8a61 内容如下:

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
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:67e9751bc5aab75bba375f0a24702d70848e5b2bea70de55e50f21ed11feed14",
"size": 6567
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:b85a868b505ffd0342a37e6a3b1c49f7c71878afe569a807e6238ef08252fcb7",
"size": 31379408
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:f4407ba1f103abb9ae05a4b2891c7ebebaecab0c262535fc6659a628db25df44",
"size": 25354178
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:4a7307612456a7f65365e1da5c3811df49cefa5a2fd68d8e04e093d26a395d60",
"size": 603
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:935cecace2a02d2545e0c19bd52fe9c8c728fbab2323fc274e029f5357cda689",
"size": 893
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:8f46223e4234ce76b244c074e79940b9ee0a01b42050012c8555ebc7ac59469e",
"size": 666
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:fe0ef4c895f5ea450aca17342e481fada37bf2a1ee85d127a4473216c3f672ea",
"size": 1394
}
]
}

包含了config 和 layers 两部分信息,其中 config 信息为运行镜像的配置,layers 为镜像中的层信息,其中gzip说明镜像的层为gzip压缩格式,每个层一个压缩文件。

镜像配置

参考:OCI Image Configuration

通过 manifest 中的config信息,可以找到镜像的配置信息 blobs/sha256/67e9751bc5aab75bba375f0a24702d70848e5b2bea70de55e50f21ed11feed14:

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
{
"created": "2022-06-23T04:13:24.820503805Z",
"architecture": "amd64",
"os": "linux",
"config": {
"ExposedPorts": {
"80/tcp": {}
},
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.23.0",
"NJS_VERSION=0.7.5",
"PKG_RELEASE=1~bullseye"
],
"Entrypoint": [
"/docker-entrypoint.sh"
],
"Cmd": [
"nginx",
"-g",
"daemon off;"
],
"Labels": {
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
},
"StopSignal": "SIGQUIT"
},
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:08249ce7456a1c0613eafe868aed936a284ed9f1d6144f7d2d08c514974a2af9",
"sha256:d5b40e80384bb94d01a8d2d8fb2db1328990e7088640132c33d3f691dd8a88ee",
"sha256:b2f82de68e0d9246de01fa8283876427af5d6f3fe21c4bb04785892d5d071aef",
"sha256:41451f050aa883f9102df03821485fc2e27611da05689c0ba25f69dcda308988",
"sha256:44193d3f4ea2bae7a5ae5983f2562f551618b787751a6abfb732b6d17393bb88",
"sha256:e7344f8a29a34b4861faf6adcf072afb26fadf6096756f0e3fc4c289cdefb7c2"
]
},
"history": [
{
"created": "2022-06-23T00:20:27.020952309Z",
"created_by": "/bin/sh -c #(nop) ADD file:8adbbab04d6f84cd83b5f4205b89b0acb7ecbf27a1bb2dc181d0a629479039fe in / "
},
{
"created": "2022-06-23T00:20:27.337378745Z",
"created_by": "/bin/sh -c #(nop) CMD [\"bash\"]",
"empty_layer": true
},
{
"created": "2022-06-23T04:13:05.737870066Z",
"created_by": "/bin/sh -c #(nop) LABEL maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>",
"empty_layer": true
},
{
"created": "2022-06-23T04:13:05.834940798Z",
"created_by": "/bin/sh -c #(nop) ENV NGINX_VERSION=1.23.0",
"empty_layer": true
},
{
"created": "2022-06-23T04:13:05.931909571Z",
"created_by": "/bin/sh -c #(nop) ENV NJS_VERSION=0.7.5",
"empty_layer": true
},
{
"created": "2022-06-23T04:13:06.026686816Z",
"created_by": "/bin/sh -c #(nop) ENV PKG_RELEASE=1~bullseye",
"empty_layer": true
},
{
"created": "2022-06-23T04:13:23.901038357Z",
"created_by": "/bin/sh -c set -x && addgroup --system --gid 101 nginx && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos \"nginx user\" --shell /bin/false --uid 101 nginx && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates && NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; found=''; for server in hkp://keyserver.ubuntu.com:80 pgp.mit.edu ; do echo \"Fetching GPG key $NGINX_GPGKEY from $server\"; apt-key adv --keyserver \"$server\" --keyserver-options timeout=10 --recv-keys \"$NGINX_GPGKEY\" && found=yes && break; done; test -z \"$found\" && echo >&2 \"error: failed to fetch GPG key $NGINX_GPGKEY\" && exit 1; apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* && dpkgArch=\"$(dpkg --print-architecture)\" && nginxPackages=\" nginx=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} \" && case \"$dpkgArch\" in amd64|arm64) echo \"deb https://nginx.org/packages/mainline/debian/ bullseye nginx\" >> /etc/apt/sources.list.d/nginx.list && apt-get update ;; *) echo \"deb-src https://nginx.org/packages/mainline/debian/ bullseye nginx\" >> /etc/apt/sources.list.d/nginx.list && tempDir=\"$(mktemp -d)\" && chmod 777 \"$tempDir\" && savedAptMark=\"$(apt-mark showmanual)\" && apt-get update && apt-get build-dep -y $nginxPackages && ( cd \"$tempDir\" && DEB_BUILD_OPTIONS=\"nocheck parallel=$(nproc)\" apt-get source --compile $nginxPackages ) && apt-mark showmanual | xargs apt-mark auto > /dev/null && { [ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark; } && ls -lAFh \"$tempDir\" && ( cd \"$tempDir\" && dpkg-scanpackages . > Packages ) && grep '^Package: ' \"$tempDir/Packages\" && echo \"deb [ trusted=yes ] file://$tempDir ./\" > /etc/apt/sources.list.d/temp.list && apt-get -o Acquire::GzipIndexes=false update ;; esac && apt-get install --no-install-recommends --no-install-suggests -y $nginxPackages gettext-base curl && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list && if [ -n \"$tempDir\" ]; then apt-get purge -y --auto-remove && rm -rf \"$tempDir\" /etc/apt/sources.list.d/temp.list; fi && ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log && mkdir /docker-entrypoint.d"
},
{
"created": "2022-06-23T04:13:24.128160562Z",
"created_by": "/bin/sh -c #(nop) COPY file:65504f71f5855ca017fb64d502ce873a31b2e0decd75297a8fb0a287f97acf92 in / "
},
{
"created": "2022-06-23T04:13:24.233980553Z",
"created_by": "/bin/sh -c #(nop) COPY file:0b866ff3fc1ef5b03c4e6c8c513ae014f691fb05d530257dfffd07035c1b75da in /docker-entrypoint.d "
},
{
"created": "2022-06-23T04:13:24.337299368Z",
"created_by": "/bin/sh -c #(nop) COPY file:0fd5fca330dcd6a7de297435e32af634f29f7132ed0550d342cad9fd20158258 in /docker-entrypoint.d "
},
{
"created": "2022-06-23T04:13:24.441125652Z",
"created_by": "/bin/sh -c #(nop) COPY file:09a214a3e07c919af2fb2d7c749ccbc446b8c10eb217366e5a65640ee9edcc25 in /docker-entrypoint.d "
},
{
"created": "2022-06-23T04:13:24.534829205Z",
"created_by": "/bin/sh -c #(nop) ENTRYPOINT [\"/docker-entrypoint.sh\"]",
"empty_layer": true
},
{
"created": "2022-06-23T04:13:24.627520512Z",
"created_by": "/bin/sh -c #(nop) EXPOSE 80",
"empty_layer": true
},
{
"created": "2022-06-23T04:13:24.724935944Z",
"created_by": "/bin/sh -c #(nop) STOPSIGNAL SIGQUIT",
"empty_layer": true
},
{
"created": "2022-06-23T04:13:24.820503805Z",
"created_by": "/bin/sh -c #(nop) CMD [\"nginx\" \"-g\" \"daemon off;\"]",
"empty_layer": true
}
]
}

其中包含了如下几个关键信息:

  1. config:运行镜像的参数,比如entrypoint、labels等,跟通过 docker inspect 命令看到的信息比较类似。
  2. rootfs:镜像的层信息
  3. history:镜像的历史构建信息,如果empty_layer的值为true,说明未产生新的层

镜像层

镜像层同样存在于 blobs/sha256 目录下,且以压缩格式存储,一个层一个压缩文件。manifests文件中的 application/vnd.oci.image.layer.v1.tar+gzip 说明镜像层的压缩格式为gzip。

容器运行时标准

用来定义容器的配置、运行环境和声明周期。runc为容器运行时的官方实现,其主要代码来源为docker的容器运行时,kara-containers也有对应的OCI实现。

参考文档:opencontainers/runtime-spec

容器配置

定义在config.json文件中,定义了创建容器的字段。由于runc更具体的操作系统环境有关,其中部分的规范是跟具体操作系统有关的。执行runc spec可以获取到默认的config.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
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
{
"ociVersion": "1.0.2-dev",
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/",
"capabilities": {
"bounding": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"effective": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"permitted": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"ambient": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
]
},
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"noNewPrivileges": true
},
"root": {
"path": "rootfs",
"readonly": true
},
"hostname": "runc",
"mounts": [
{
"destination": "/proc",
"type": "proc",
"source": "proc"
},
{
"destination": "/dev",
"type": "tmpfs",
"source": "tmpfs",
"options": [
"nosuid",
"strictatime",
"mode=755",
"size=65536k"
]
},
{
"destination": "/dev/pts",
"type": "devpts",
"source": "devpts",
"options": [
"nosuid",
"noexec",
"newinstance",
"ptmxmode=0666",
"mode=0620",
"gid=5"
]
},
{
"destination": "/dev/shm",
"type": "tmpfs",
"source": "shm",
"options": [
"nosuid",
"noexec",
"nodev",
"mode=1777",
"size=65536k"
]
},
{
"destination": "/dev/mqueue",
"type": "mqueue",
"source": "mqueue",
"options": [
"nosuid",
"noexec",
"nodev"
]
},
{
"destination": "/sys",
"type": "sysfs",
"source": "sysfs",
"options": [
"nosuid",
"noexec",
"nodev",
"ro"
]
},
{
"destination": "/sys/fs/cgroup",
"type": "cgroup",
"source": "cgroup",
"options": [
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
]
}
],
"linux": {
"resources": {
"devices": [
{
"allow": false,
"access": "rwm"
}
]
},
"namespaces": [
{
"type": "pid"
},
{
"type": "network"
},
{
"type": "ipc"
},
{
"type": "uts"
},
{
"type": "mount"
}
],
"maskedPaths": [
"/proc/acpi",
"/proc/asound",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/proc/scsi"
],
"readonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
}
}

运行时和生命周期

在config.json文件中可以声明跟容器生命周期相关的部分,比如prestart、poststop等。

定义了很多子命令,比如状态查询的state <container-id>,删除容器的delete <container-id>等,这些子命令runc部分均有实现。通过 runc state mycontainerid 来查看的输出结果如下:

1
2
3
4
5
6
7
8
9
10
{
"ociVersion": "1.0.2-dev",
"id": "mycontainerid",
"pid": 40805,
"status": "running",
"bundle": "/mycontainer",
"rootfs": "/mycontainer/rootfs",
"created": "2022-06-29T13:51:54.795617419Z",
"owner": ""
}

镜像分发规范

pull

pull manifest

接口定义:GET /v2/<name>/manifests/<reference>

<name>: 镜像的 namespace。
<reference>:镜像的 tag 或者摘要信息。

pull bolb

接口定义:GET /v2/<name>/blobs/<digest>

push

POST /v2/<name>/blobs/uploads/?digest=<digest>
PUT /v2/<name>/manifests/<reference>

list tag

接口定义:GET /v2/<name>/tags/list

返回格式如下:

1
2
3
4
5
6
7
8
{
"name": "<name>",
"tags": [
"<tag1>",
"<tag2>",
"<tag3>"
]
}

list references

接口定义: GET /v2/<name>/referrers/<digest>

delete tag

接口定义: DELETE /v2/<name>/manifests/<tag>

delete manifest

接口定义: DELETE /v2/<name>/manifests/<digest>

delete blobs

接口定义: DELETE /v2/<name>/blobs/<digest>

k8s支持情况

K8s可以通过 pod 的 spec.runtimeClassName 来指定 oci runtime 的实现方式。

参考

0%