404频道

学习笔记

本文记录常用的kubectl命令,不定期更新。

1. 统计k8s node上的污点信息

1
kubectl get nodes -o=custom-columns=NodeName:.metadata.name,TaintKey:.spec.taints[*].key,TaintValue:.spec.taints[*].value,TaintEffect:.spec.taints[*].effect

2. 查看不ready的pod

1
kubectl get pod --all-namespaces -o wide -w | grep -vE "Com|NAME|Running|1/1|2/2|3/3|4/4"

3. pod按照重启次数排序

1
2
3
kubectl get pods -A  --sort-by='.status.containerStatuses[0].restartCount' | tail

kubectl get pod -A --no-headers | sort -k5 -nr | head

4. kubectl proxy命令代理k8s apiserver

该命令经常用在开发的场景下,用来测试k8s api的结果。执行如下命令即可在本地127.0.0.1开启10999端口。

1
kubectl proxy --port=10999

在本地即可通过curl的方式来访问k8s的apiserver,而无需考虑鉴权问题。例如,curl http://127.0.0.1:10999/apis/batch/v1,即可直接返回结果。

5. --raw命令

该命令经常用在开发的场景下,用来测试k8s api的结果。执行如下的命令,即可返回json格式的结果。

1
kubectl get --raw /apis/batch/v1

6. 查看集群中的pod的request和limit情况

1
kubectl get pod -n kube-system  -o=custom-columns=NAME:.metadata.name,NAMESPACE:.metadata.namespace,PHASE:.status.phase,Request-cpu:.spec.containers\[0\].resources.requests.cpu,Request-memory:.spec.containers\[0\].resources.requests.memory,Limit-cpu:.spec.containers\[0\].resources.limits.cpu,Limit-memory:.spec.containers\[0\].resources.limits.memory

得到的效果如下:

1
2
3
4
5
NAME                                              NAMESPACE     PHASE       Request-cpu   Request-memory   Limit-cpu   Limit-memory
cleanup-for-np-processor-9pjkm kube-system Succeeded <none> <none> <none> <none>
coredns-6c6664b94-7rnm8 kube-system Running 100m 70Mi <none> 170Mi
coredns-6c6664b94-djxch kube-system Failed 100m 70Mi <none> 170Mi
coredns-6c6664b94-khvrb kube-system Running 100m 70Mi <none> 170Mi

7. 修改对象的status

因为status资源实际上为对象的subResource资源,实际上无法通过kubectl来完成,但该操作还是放到了该文档中。

下述命令的需求为修改service的status

1
2
3
4
APISERVER=https://kube-cn-nb-nbsjzxpoc-d01-a.intra.nbsjzx.ali.com:6443
TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi10b2tlbi1jazdkciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJhZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjhkZWE4MWQ4LTU2YTgtMTFlYy05MDMyLTgwNjE1ZjA4NDI0YSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTphZG1pbiJ9.O25G_MSmKRU_pIPO_9tFqDvbZm9SOM_Mix7jCJeFiZHzLiSc7n5RanP3QoEldR1IcFN4AZXzlzI1Rb0GyFQH7XmS1eLESMbKnrTR3N5s3wlRp-D5QT0c_RAVAiLKrP7KsSKNcLjQkIO8-Csf_kmizIh6fP0-b7Mp90cw0J8oSlM-ScEeUEksQyXvyisVN9qvvTIkmbsgh7pX5IFcB4mGbvV9loOUC3-LdiiaCkMJzMisEeSJRaajmLlwpWXtb2BSi9HmBMktUE9IVB8ryZ06wbPRianbjoBwtAhcXuRyj1LaEog3aJHsyMA_DOZJtvjYis60AIRZ1iBnc-gZtEFCxw
obj=nginx-ingress-lb
curl -XPATCH -H "Accept: application/json" -H "Content-Type: application/merge-patch+json" --header "Authorization: Bearer $TOKEN" --insecure -d '{"status": {"loadBalancer": {"ingress": [{"ip": "10.209.97.170"}]}}}' $APISERVER/api/v1/namespaces/acs-system/services/nginx-ingress-lb/status

APISERVER变量可以通过kubeconfig文件获取到。

TOKEN变量可以直接使用kube-system下的admin账号。

  1. 执行 kubectl get secrets -n kube-system | grep admin 获取到sa对应的secret
  2. 执行 kubectl get secrets -n kube-system xxxx -o yaml 获取到base64之后的token
  3. 执行 echo "xxxx" | base64 -d 即可获取到对应的token

obj变量为要修改的service对象名称。

8. 查看 secret 内容

1
kubectl get secret -n ark-system ark.cmdb.https.origin.tls -o jsonpath='{.data.ca\.pem}' | base64 -d

9. 修改 secret 或 cm 的内容

很多场景下使用 kubectl edit 修改不能完全满足需求,比如某个 key 对应的 value 非常长且包含空格,很难直接编辑。可以通过导出 key 对应的 value 到文件,然后再重新 apply 的方式合入。

导出 configmap 中特定的 key:

1
kubectl get cm -n kube-system  networkpolicy-config -o jsonpath='{.data.config\.yaml}'

修改完成后,将文件重新 apply cm

1
kubectl create --save-config cm  networkpolicy-config -n kube-system --from-file /tmp/config.yaml -o yaml --dry-run | kubectl apply -f -

9. 删除所有 pod(或特定状态 pod)

1
kubectl get pods --all-namespaces -o wide --no-headers | grep -v Running | awk '{print $1 " " $2}' | while read AA BB; do kubectl get pod -n $AA $BB --no-headers; done

10. kubectl debug

常用于网络问题排查,其中镜像 netshoot 中包含了丰富的网络命令。

1
kubectl exec -it nginx-statefulset-0 bash

进入到 k8s node 网络,其中 ${node} 为 k8s 的 node 名字。

1
kubectl debug node/${node} -it --image=nicolaka/netshoot

11. kubectl patch

1
kubectl patch -p '{"spec":{"filed1": "value1"}}' --type=merge xxx -n yyy

12. 查询对象的 labels

1
2
3
4
5
6
7
# 查看所有 k8s 节点及其 label
kubectl get nodes -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}{end}'
# 查看单个 k8s 节点的 label
kubectl get node c25e04016.cloud.e04.amtest130 -o jsonpath="{.metadata.labels['topology\.kubernetes\.io/zone']}"

# 获取没有污点的节点对应的 label machine-type
kubectl get nodes -o json | jq -r '.items[] | select(.spec.taints | not) | [.metadata.name, .metadata.labels["machine-type"]] | @tsv'

ipv6在k8s中的支持情况

ipv6特性在k8s中作为一个特定的feature IPv6DualStack来管理。在1.15版本到1.20版本该feature为alpha版本,默认不开启。从1.21版本开始,该feature为beta版本,默认开启,支持pod和service网络的双栈。1.23版本变为稳定版本。

配置双栈

alpha版本需要通过kube-apiserver、kube-controller-manager、kubelet和kube-proxy的–feature-gates=”IPv6DualStack=true”命令来开启该特性,beta版本该特性默认开启。

kube-apiserver:

  • --service-cluster-ip-range=<IPv4 CIDR>,<IPv6 CIDR>

kube-controller-manager:

  • --cluster-cidr=<IPv4 CIDR>,<IPv6 CIDR>
  • --service-cluster-ip-range=<IPv4 CIDR>,<IPv6 CIDR>
  • --node-cidr-mask-size-ipv4|--node-cidr-mask-size-ipv6 对于 IPv4 默认为 /24,对于 IPv6 默认为 /64

kube-proxy:

  • --cluster-cidr=<IPv4 CIDR>,<IPv6 CIDR>

使用kind创建k8s集群

由于ipv6的环境并不容易找到,可以使用kind快速在本地拉起一个k8s的单集群环境,同时kind支持ipv6的配置。

检查当前系统是否支持ipv6,如果输出结果为0,说明当前系统支持ipv6.

1
2
$ sysctl -a | grep net.ipv6.conf.all.disable_ipv6
net.ipv6.conf.all.disable_ipv6 = 0

创建ipv6单栈k8s集群执行如下命令:

1
2
3
4
5
6
7
8
9
10
11
cat > kind-ipv6.conf <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: ipv6-only
networking:
ipFamily: ipv6
# networking:
# apiServerAddress: "127.0.0.1"
# apiServerPort: 6443
EOF
kind create cluster --config kind-ipv6.conf

创建ipv4/ipv6双栈集群执行如下命令:

1
2
3
4
5
6
7
8
9
10
11
cat > kind-dual-stack.conf <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: dual-stack
networking:
ipFamily: dual
# networking:
# apiServerAddress: "127.0.0.1"
# apiServerPort: 6443
EOF
kind create cluster --config kind-dual-stack.conf

service网络

spec定义

k8s的service网络为了支持双栈,新增加了几个字段。

.spec.ipFamilies用来设置地址族,service一旦场景后该值不可认为修改,但可以通过修改.spec.ipFamilyPolicy来简介修改该字段的值。为数组格式,支持如下值:

  • [“IPv4”]
  • [“IPv6”]
  • [“IPv4”,”IPv6”] (双栈)
  • [“IPv6”,”IPv4”] (双栈)

上述数组中的第一个元素会决定.spec.ClusterIP中的字段。

.spec.ClusterIPs:由于.spec.ClusterIP的value为字符串,不支持同时设置ipv4和ipv6两个ip地址。因此又扩展出来了一个.spec.ClusterIPs字段,该字段的value为宿主元祖。在Headless类型的Service情况下,该字段的值为None。

.spec.ClusterIP:该字段的值跟.spec.ClusterIPs中的第一个元素的值保持一致。

.spec.ipFamilyPolicy支持如下值:

  • SingleStack:默认值。会使用.spec.ipFamilies数组中配置的第一个协议来分配cluster ip。如果没有指定.spec.ipFamilies,会使用service-cluster-ip-range配置中第一个cidr中来配置地址。
  • PreferDualStack:如果 .spec.ipFamilies 没有设置,使用 k8s 集群默认的 ipFamily。
  • RequireDualStack:同时分配ipv4和ipv6地址。.spec.ClusterIP的值会从.spec.ClusterIPs选择第一个元素。

service配置场景

先创建如下的deployment,以便于后面试验的service可以关联到pod。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: MyApp
spec:
replicas: 3
selector:
matchLabels:
app: MyApp
template:
metadata:
labels:
app: MyApp
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

未指定任何协议栈信息

在没有指定协议栈信息的Service,创建出来的service .spec.ipFamilyPolicy为SingleStack。同时会使用service-cluster-ip-range配置中第一个cidr中来配置地址。如果第一个cidr为ipv6,则service分配的clusterip为ipv6地址。创建如下的service

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: my-service
labels:
app: MyApp
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80

会生成如下的service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Service
metadata:
labels:
app: MyApp
name: my-service
namespace: default
spec:
clusterIP: 10.96.80.114
clusterIPs:
- 10.96.80.114
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: MyApp
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}

指定.spec.ipFamilyPolicy为PreferDualStack

创建如下的service

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: my-service-2
labels:
app: MyApp
spec:
ipFamilyPolicy: PreferDualStack
selector:
app: MyApp
ports:
- protocol: TCP
port: 80

提交到环境后会生成如下的service,可以看到.spec.clusterIPs中的ip地址跟ipFamilies中的顺序一致。

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
apiVersion: v1
kind: Service
metadata:
labels:
app: MyApp
name: my-service-2
namespace: default
spec:
clusterIP: 10.96.221.70
clusterIPs:
- 10.96.221.70
- fd00:10:96::7d1
ipFamilies:
- IPv4
- IPv6
ipFamilyPolicy: PreferDualStack
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: MyApp
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}

查看Endpoints,可以看到subsets中的地址为pod的ipv4协议地址。Endpoints中的地址跟service的.spec.ipFamilies数组中的第一个协议的值保持一致。

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
apiVersion: v1
kind: Endpoints
metadata:
labels:
app: MyApp
name: my-service-2
namespace: default
subsets:
- addresses:
- ip: 10.244.0.5
nodeName: dual-stack-control-plane
targetRef:
kind: Pod
name: nginx-deployment-65b5dd4c68-vrfps
namespace: default
resourceVersion: "16875"
uid: 30a1d787-f799-4250-8c56-c96564ca9239
- ip: 10.244.0.6
nodeName: dual-stack-control-plane
targetRef:
kind: Pod
name: nginx-deployment-65b5dd4c68-wgz72
namespace: default
resourceVersion: "16917"
uid: 8166d43e-2702-45c6-839e-b3005f44f647
- ip: 10.244.0.7
nodeName: dual-stack-control-plane
targetRef:
kind: Pod
name: nginx-deployment-65b5dd4c68-x4lt5
namespace: default
resourceVersion: "16896"
uid: f9c2968f-ca59-4ba9-a69f-358c202a964b
ports:
- port: 80
protocol: TCP

接下来指定.spec.ipFamilies的顺序再看一下执行的结果,创建如下的service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: my-service-3
labels:
app: MyApp
spec:
ipFamilyPolicy: PreferDualStack
ipFamilies:
- IPv6
- IPv4
selector:
app: MyApp
ports:
- protocol: TCP
port: 80

在环境中生成的service如下,可以看到.spec.clusterIPs中的顺序第一个为ipv6地址,.spec.clusterIP同样为ipv6地址。

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
apiVersion: v1
kind: Service
metadata:
labels:
app: MyApp
name: my-service-3
namespace: default
spec:
clusterIP: fd00:10:96::c306
clusterIPs:
- fd00:10:96::c306
- 10.96.147.82
ipFamilies:
- IPv6
- IPv4
ipFamilyPolicy: PreferDualStack
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: MyApp
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}

查看Endpoints,可以看到subsets中的地址为pod的ipv6协议地址。Endpoints中的地址跟service的.spec.ipFamilies数组中的第一个协议的值保持一致。

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
apiVersion: v1
kind: Endpoints
metadata:
labels:
app: MyApp
name: my-service-3
namespace: default
subsets:
- addresses:
- ip: fd00:10:244::5
nodeName: dual-stack-control-plane
targetRef:
kind: Pod
name: nginx-deployment-65b5dd4c68-vrfps
namespace: default
resourceVersion: "16875"
uid: 30a1d787-f799-4250-8c56-c96564ca9239
- ip: fd00:10:244::6
nodeName: dual-stack-control-plane
targetRef:
kind: Pod
name: nginx-deployment-65b5dd4c68-wgz72
namespace: default
resourceVersion: "16917"
uid: 8166d43e-2702-45c6-839e-b3005f44f647
- ip: fd00:10:244::7
nodeName: dual-stack-control-plane
targetRef:
kind: Pod
name: nginx-deployment-65b5dd4c68-x4lt5
namespace: default
resourceVersion: "16896"
uid: f9c2968f-ca59-4ba9-a69f-358c202a964b
ports:
- port: 80
protocol: TCP

单栈和双栈之间的切换

虽然.spec.ipFamilies字段不允许直接修改,但.spec.ipFamilyPolicy字段允许修改,但并不影响单栈和双栈之间的切换。
单栈变双栈只需要修改.spec.ipFamilyPolicy从SingleStack变为PreferDualStack或者RequireDualStack即可。
双栈同样可以变为单栈,只需要修改.spec.ipFamilyPolicy从PreferDualStack或者RequireDualStack变为SingleStack即可。此时.spec.ipFamilies会自动变更为一个元素,.spec.clusterIPs同样会变更为一个元素。

LoadBalancer类型的Service

对于LoadBalancer类型的Service,单栈的情况下,会在.status.loadBalancer.ingress设置vip地址。如果是ingress中的ip地址为双栈,此时应该是将双栈的vip地址同时写到.status.loadBalancer.ingress中,并且要保证其顺序跟serivce的.spec.ipFamilies中的顺序一致。

pod网络

pod网络要支持ipv6,需要容器网络插件的支持。为了支持ipv6特性,新增加了.status.podIPs字段,用来展示pod上分配的ipv4和ipv6的信息。.status.podIP字段的值跟.status.podIPs数组的第一个元素的值保持一致。

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
apiVersion: v1
kind: Pod
metadata:
labels:
k8s-app: kube-dns
pod-template-hash: 558bd4d5db
name: coredns-558bd4d5db-b2zbj
namespace: kube-system
spec:
containers:
- args:
- -conf
- /etc/coredns/Corefile
image: k8s.gcr.io/coredns/coredns:v1.8.0
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 5
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
name: coredns
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
- containerPort: 9153
name: metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /ready
port: 8181
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
resources:
limits:
memory: 170Mi
requests:
cpu: 100m
memory: 70Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- NET_BIND_SERVICE
drop:
- all
readOnlyRootFilesystem: true
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /etc/coredns
name: config-volume
readOnly: true
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-hgxnc
readOnly: true
dnsPolicy: Default
enableServiceLinks: true
nodeName: dual-stack-control-plane
nodeSelector:
kubernetes.io/os: linux
preemptionPolicy: PreemptLowerPriority
priority: 2000000000
priorityClassName: system-cluster-critical
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: coredns
serviceAccountName: coredns
terminationGracePeriodSeconds: 30
tolerations:
- key: CriticalAddonsOnly
operator: Exists
...
volumes:
- configMap:
defaultMode: 420
items:
- key: Corefile
path: Corefile
name: coredns
name: config-volume
- name: kube-api-access-hgxnc
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2022-01-16T12:51:24Z"
status: "True"
type: Initialized
...
containerStatuses:
- containerID: containerd://6da36ab908291ca1b4141a86d70f8c2bb150a933336d852bcabe2118aa1a3439
image: k8s.gcr.io/coredns/coredns:v1.8.0
imageID: sha256:296a6d5035e2d6919249e02709a488d680ddca91357602bd65e605eac967b899
lastState: {}
name: coredns
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2022-01-16T12:51:27Z"
hostIP: 172.18.0.2
phase: Running
podIP: 10.244.0.3
podIPs:
- ip: 10.244.0.3
- ip: fd00:10:244::3
qosClass: Burstable
startTime: "2022-01-16T12:51:24Z"

pod中要想获取到ipv4、ipv6地址,可以通过downward api的形式将.status.podIPs以环境变量的形式传递到容器中,在pod中通过环境变量获取到的格式为: 10.244.1.4,a00:100::4

k8s node

在宿主机上可以通过ip addr show eth0的方式来查看网卡上的ip地址。

1
2
3
4
5
6
7
8
9
# ip addr show eth0
23: eth0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fc00:f853:ccd:e793::2/64 scope global nodad
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever

宿主机的网卡上有ipv6地址,k8s node上的.status.addresses中有所体现,type为InternalIP即包含了ipv4地址,又包含了ipv6地址,但此处并没有字段标识当前地址为ipv4,还是ipv6。

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
apiVersion: v1
kind: Node
metadata:
annotations:
kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock
node.alpha.kubernetes.io/ttl: "0"
volumes.kubernetes.io/controller-managed-attach-detach: "true"
labels:
beta.kubernetes.io/arch: amd64
beta.kubernetes.io/os: linux
kubernetes.io/arch: amd64
kubernetes.io/hostname: dual-stack-control-plane
kubernetes.io/os: linux
node-role.kubernetes.io/control-plane: ""
node-role.kubernetes.io/master: ""
node.kubernetes.io/exclude-from-external-load-balancers: ""
name: dual-stack-control-plane
spec:
podCIDR: 10.244.0.0/24
podCIDRs:
- 10.244.0.0/24
- fd00:10:244::/64
providerID: kind://docker/dual-stack/dual-stack-control-plane
status:
addresses:
- address: 172.18.0.2
type: InternalIP
- address: fc00:f853:ccd:e793::2
type: InternalIP
- address: dual-stack-control-plane
type: Hostname
allocatable:
cpu: "4"
ephemeral-storage: 41152812Ki
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 15936568Ki
pods: "110"
capacity:
cpu: "4"
ephemeral-storage: 41152812Ki
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 15936568Ki
pods: "110"
conditions:
- lastHeartbeatTime: "2022-01-16T15:01:48Z"
lastTransitionTime: "2022-01-16T12:50:54Z"
message: kubelet has sufficient memory available
reason: KubeletHasSufficientMemory
status: "False"
type: MemoryPressure
...
daemonEndpoints:
kubeletEndpoint:
Port: 10250
images:
- names:
- k8s.gcr.io/kube-proxy:v1.21.1
sizeBytes: 132714699
...
nodeInfo:
architecture: amd64
bootID: 7f95abb9-7731-4a8c-9258-4a91cdcfb2ca
containerRuntimeVersion: containerd://1.5.2
kernelVersion: 4.18.0-305.an8.x86_64
kubeProxyVersion: v1.21.1
kubeletVersion: v1.21.1
machineID: 8f6a98bffc184893ab6bc260e705421b
operatingSystem: linux
osImage: Ubuntu 21.04
systemUUID: f7928fdb-32be-4b6e-8dfd-260b6820f067

Ingress

Ingress 可以通过开关 disable-ipv6 来控制是否开启 ipv6,默认 ipv6 开启。

在开启 ipv6 的情况下,如果 nginx ingress 的 pod 本身没有ipv6 的 ip地址,则在 nginx 的配置文件中并不会监听 ipv6 的端口号。

1
2
listen 80  ;
listen 443 ssl http2 ;

如果 nginx ingress 的 pod 本身包含 ipv6 地址,则 nginx 的配置文件如下:

1
2
3
4
listen 80  ;
listen [::]:80 ;
listen 443 ssl http2 ;
listen [::]:443 ssl http2 ;

参考资料:

引用


背景

Federation V1

Federation v1 在 K8s v1.3 左右就已经着手设计(Design Proposal),并在后面几个版本中发布了相关的组件与命令行工具(kubefed),用于帮助使用者快速建立联邦集群,并在 v1.6 时,进入了 Beta 阶段;但 Federation v1 在进入 Beta 后,就没有更进一步的发展,由于灵活性和 API 成熟度的问题,在 K8s v1.11 左右正式被弃用。

Federation V2架构

v2 版本利用 CRD 实现了整体功能,通过定义多种自定义资源(CR),从而省掉了 v1 中的 API Server;v2 版本由两个组件构成:

  • admission-webhook 提供了准入控制
  • controller-manager 处理自定义资源以及协调不同集群间的状态

image
image

代码行数在2万行左右。

术语

  • Host Cluster:主集群/父集群
  • Member Cluster:子集群/成员集群

CRD扩展

KubeFedCluster

子集群抽象,通过kubefedctl join命令创建。后续controller会使用该信息来访问子k8s集群。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kind: KubeFedCluster
metadata:
name: kind-cluster1
namespace: kube-federation-system
spec:
apiEndpoint: https://172.21.115.165:6443
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1ekNDQWMrZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeE1USXlOREUyTlRNME9Wb1hEVE14TVRJeU1qRTJOVE0wT1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTmxjCnF3UHd5cHNFc2M0aXB3QnFuRndDN044RXo2Slhqajh3ZzRybnlXTDNSdlZ0cmVua1Nha1VyYlRjZWVIck9lQTUKWGlNUVo2T1FBY25tUGU0Q2NWSkFoL2ZLQzBkeU9uL0ZZeXgyQXppRjBCK1ZNaUFhK2dvME1VMmhMZ1R5eVFGdQpDbWFmUGtsNmJxZUFJNCtCajZJUWRqY3dVMHBjY3lrNGhSTUxnQmhnTUh4NWkzVkpQckQ2Y284dHcwVnllWncyCkdDUlh2ZzlSd0QweUs5eitOVS9LVS83QjBiMTBvekpNRlVJMktPZmI4N1RkQ0h2NmlBdlVRYVdKc1BqQ0M3MzQKcnBma1ZGZXB2S2liKy9lUVBFaDE4VE5qaitveEVGMnl0Vmo2ZWVKeFc3VVZrbit0T3BxdGpEeUtKVDllNnlwdAp6U1VDTnRZWTAzckdhNTlLczBVQ0F3RUFBYU5DTUVBd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZOTnRQU3hsMlMxUldOb1BCeXNIcHFoRXJONlVNQTBHQ1NxR1NJYjMKRFFFQkN3VUFBNElCQVFDME0vaktCL2dqcWRSNlRLNXJQVktSSFU2cy8xTTh2eTY2WFBiVEx4M0srazdicUFWdAoxVUFzcUZDZmNrVk91WHY3eFQ0ZHIzVzdtMHE1bDYzRGg3ZDJRZDNiQ00zY2FuclpNd01OM0lSMlpmYzR0VlBGCnRTMFFONElTa0hsYnBudXQxb0F3cy9CaXNwaXNRQ0VtbHF3Zy9xbmdPMStlWWRoWm5vRW40SEFDamF4Slc5MS8KNXlOR1pKRXdia2dYQTVCbSs3VEZRL2FiYnp5a1JvOWtTMnl5c29pMnVzcUg0ZnlVS0ZWK2RETnp3Ujh0ck16cgpjWkRBNHpaVFB1WGlYRkVsWlNRa2NJSGIyV0VsYmZNRGpKNjlyVG5wakJCOWNPQ25jaHVmK0xiOXQwN1lJQ01wCmNlK0prNVp2RElRUFlKK3hTeGdRaVJxMTFraWlKcFE4Wm1FWgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
proxyURL: ""
secretRef:
name: kind-cluster1-dbxns
status:
conditions:
- lastProbeTime: "2021-12-24T17:00:47Z"
lastTransitionTime: "2021-12-24T17:00:07Z"
message: /healthz responded with ok
reason: ClusterReady
status: "True"
type: Ready

Fedrated

发布应用到子集群时手工创建。每一个要被联邦管理的资源都会对应一个Fedrated类型的资源,比如ConfigMap对应的是FederatedConfigMap。FederatedConfigMap包含了三部分的信息:

  • template:联邦的资源详细spec信息,需要特别注意的是并没有包含metadata部分的信息。
  • placement:template中的spec信息要部署的k8s子集群信息
  • overrides:允许对部分k8s的部分资源进行自定义修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    apiVersion: types.kubefed.io/v1beta1
    kind: FederatedConfigMap
    metadata:
    name: test-configmap
    namespace: test-namespace
    spec:
    template:
    data:
    A: ala ma kota
    placement:
    clusters:
    - name: cluster2
    - name: cluster1
    overrides:
    - clusterName: cluster2
    clusterOverrides:
    - path: /data
    value:
    foo: bar

FederatedTypeConfig

通过kubefedctl enable 创建。定义了哪些k8s api资源需要联邦,下面的例子描述了k8s ConfigMap要被联邦资源FederatedConfigMap所管理。

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
apiVersion: core.kubefed.io/v1beta1
kind: FederatedTypeConfig
metadata:
annotations:
meta.helm.sh/release-name: kubefed
meta.helm.sh/release-namespace: kube-federation-system
finalizers:
- core.kubefed.io/federated-type-config
labels:
app.kubernetes.io/managed-by: Helm
name: configmaps
namespace: kube-federation-system
spec:
federatedType:
group: types.kubefed.io
kind: FederatedConfigMap
pluralName: federatedconfigmaps
scope: Namespaced
version: v1beta1
propagation: Enabled # 是否启用该联邦对象
targetType:
kind: ConfigMap
pluralName: configmaps
scope: Namespaced
version: v1
status:
observedGeneration: 1
propagationController: Running
statusController: NotRunning

ReplicaSchedulingPreference

用来管理相同名字的FederatedDeployment或FederatedReplicaset资源,保证所有子集群的副本数为spec.totalReplicas。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: scheduling.kubefed.io/v1alpha1
kind: ReplicaSchedulingPreference
metadata:
name: test-deployment
namespace: test-ns
spec:
targetKind: FederatedDeployment
totalReplicas: 9
clusters:
A:
minReplicas: 4
maxReplicas: 6
weight: 1
B:
minReplicas: 4
maxReplicas: 8
weight: 2

安装

使用kind来在本机创建多个k8s集群的方式测试。通过kind创建两个k8s集群 kind-cluster1和kind-cluster2。

安装kubefedctl

1
2
3
4
wget https://github.com/kubernetes-sigs/kubefed/releases/download/v0.9.0/kubefedctl-0.9.0-linux-amd64.tgz
tar zvxf kubefedctl-0.9.0-linux-amd64.tgz
chmod u+x kubefedctl
mv kubefedctl /usr/local/bin/

在第一个集群安装KubeFed,在第一个集群执行如下的命令

1
2
3
4
5
6
7
8
9
$ helm repo add kubefed-charts https://raw.githubusercontent.com/kubernetes-sigs/kubefed/master/charts
$ helm repo list
$ helm --namespace kube-federation-system upgrade -i kubefed kubefed-charts/kubefed --version=0.9.0 --create-namespace

# 会安装如下两个deployment,其中一个是controller,另外一个是webhook
$ kubectl get deploy -n kube-federation-system
NAME READY UP-TO-DATE AVAILABLE AGE
kubefed-admission-webhook 1/1 1 1 7m40s
kubefed-controller-manager 2/2 2 2 7m40s

子集群注册

将cluster1和cluster2集群加入到cluster1中

1
2
kubefedctl join kind-cluster1 --cluster-context kind-cluster1 --host-cluster-context kind-cluster1 --v=2
kubefedctl join kind-cluster2 --cluster-context kind-cluster2 --host-cluster-context kind-cluster1 --v=2

在第一个集群可以看到在kube-federation-system中创建出了两个新的kubefedcluster对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ kubectl -n kube-federation-system get kubefedcluster
NAME AGE READY
kind-cluster1 17s True
kind-cluster2 16s True

$ apiVersion: core.kubefed.io/v1beta1
kind: KubeFedCluster
metadata:
name: kind-cluster1
namespace: kube-federation-system
spec:
apiEndpoint: https://172.21.115.165:6443
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1ekNDQWMrZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeE1USXlOREUyTlRNME9Wb1hEVE14TVRJeU1qRTJOVE0wT1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTmxjCnF3UHd5cHNFc2M0aXB3QnFuRndDN044RXo2Slhqajh3ZzRybnlXTDNSdlZ0cmVua1Nha1VyYlRjZWVIck9lQTUKWGlNUVo2T1FBY25tUGU0Q2NWSkFoL2ZLQzBkeU9uL0ZZeXgyQXppRjBCK1ZNaUFhK2dvME1VMmhMZ1R5eVFGdQpDbWFmUGtsNmJxZUFJNCtCajZJUWRqY3dVMHBjY3lrNGhSTUxnQmhnTUh4NWkzVkpQckQ2Y284dHcwVnllWncyCkdDUlh2ZzlSd0QweUs5eitOVS9LVS83QjBiMTBvekpNRlVJMktPZmI4N1RkQ0h2NmlBdlVRYVdKc1BqQ0M3MzQKcnBma1ZGZXB2S2liKy9lUVBFaDE4VE5qaitveEVGMnl0Vmo2ZWVKeFc3VVZrbit0T3BxdGpEeUtKVDllNnlwdAp6U1VDTnRZWTAzckdhNTlLczBVQ0F3RUFBYU5DTUVBd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZOTnRQU3hsMlMxUldOb1BCeXNIcHFoRXJONlVNQTBHQ1NxR1NJYjMKRFFFQkN3VUFBNElCQVFDME0vaktCL2dqcWRSNlRLNXJQVktSSFU2cy8xTTh2eTY2WFBiVEx4M0srazdicUFWdAoxVUFzcUZDZmNrVk91WHY3eFQ0ZHIzVzdtMHE1bDYzRGg3ZDJRZDNiQ00zY2FuclpNd01OM0lSMlpmYzR0VlBGCnRTMFFONElTa0hsYnBudXQxb0F3cy9CaXNwaXNRQ0VtbHF3Zy9xbmdPMStlWWRoWm5vRW40SEFDamF4Slc5MS8KNXlOR1pKRXdia2dYQTVCbSs3VEZRL2FiYnp5a1JvOWtTMnl5c29pMnVzcUg0ZnlVS0ZWK2RETnp3Ujh0ck16cgpjWkRBNHpaVFB1WGlYRkVsWlNRa2NJSGIyV0VsYmZNRGpKNjlyVG5wakJCOWNPQ25jaHVmK0xiOXQwN1lJQ01wCmNlK0prNVp2RElRUFlKK3hTeGdRaVJxMTFraWlKcFE4Wm1FWgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
proxyURL: ""
secretRef:
name: kind-cluster1-dbxns
status:
conditions:
- lastProbeTime: "2021-12-24T17:00:47Z"
lastTransitionTime: "2021-12-24T17:00:07Z"
message: /healthz responded with ok
reason: ClusterReady
status: "True"
type: Ready

要想将cluster2从主集群中移除,可以执行 kubefedctl unjoin kind-cluster2 –cluster-context kind-cluster2 –host-cluster-context kind-cluster1 –v=2

应用发布

集群联邦API

kubefed将资源分为普通k8s资源类型和联邦的资源类型。在默认的场景下,kubefed已经内置将很多资源类型做了集群联邦。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ k get FederatedTypeConfig -n kube-federation-system
NAME AGE
clusterrolebindings.rbac.authorization.k8s.io 19h
clusterroles.rbac.authorization.k8s.io 19h
configmaps 19h
deployments.apps 19h
ingresses.extensions 19h
jobs.batch 19h
namespaces 19h
replicasets.apps 19h
secrets 19h
serviceaccounts 19h
services 19h

将crd资源类型集群联邦,执行 kubefedctl enable 命令

1
2
3
4
$ kubefedctl enable customresourcedefinitions
I1224 20:32:54.537112 687543 util.go:141] Api resource found.
customresourcedefinition.apiextensions.k8s.io/federatedcustomresourcedefinitions.types.kubefed.io created
federatedtypeconfig.core.kubefed.io/customresourcedefinitions.apiextensions.k8s.io created in namespace kube-federation-system

可以看到会多出一个名字为federatedcustomresourcedefinitions.types.kubefed.io 的CRD 资源,同时会新创建一个FederatedTypeConfig类型的资源。当创建了FederatedTypeConfig后,就可以通过创建federatedcustomresourcedefinition类型的实例来向各个子集群发布CRD资源了。

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
$ k get crd federatedcustomresourcedefinitions.types.kubefed.io
NAME CREATED AT
federatedcustomresourcedefinitions.types.kubefed.io 2021-12-24T12:32:54Z

$ k get FederatedTypeConfig -n kube-federation-system customresourcedefinitions.apiextensions.k8s.io -o yaml
apiVersion: core.kubefed.io/v1beta1
kind: FederatedTypeConfig
metadata:
finalizers:
- core.kubefed.io/federated-type-config
name: customresourcedefinitions.apiextensions.k8s.io
namespace: kube-federation-system
spec:
federatedType:
group: types.kubefed.io
kind: FederatedCustomResourceDefinition
pluralName: federatedcustomresourcedefinitions
scope: Cluster
version: v1beta1
propagation: Enabled
targetType:
group: apiextensions.k8s.io
kind: CustomResourceDefinition
pluralName: customresourcedefinitions
scope: Cluster
version: v1
status:
observedGeneration: 1
propagationController: Running
statusController: NotRunning

在example/sample1下包含了很多例子,可以直接参考。接下来使用sample1中的例子进行试验。

在父集群创建namespace test-namespace

1
2
$ kubectl create -f namespace.yaml
namespace/test-namespace created

在主集群中创建federatednamespace资源

1
2
$ k apply -f federatednamespace.yaml
federatednamespace.types.kubefed.io/test-namespace created

在子集群中即可查询到新创建的namespace资源

1
2
3
$ k get ns test-namespace
NAME STATUS AGE
test-namespace Active 4m49s

其他的对象均可以按照跟上述namespace同样的方式来创建,比较特殊的对象为clusterrulebinding,该对象在默认情况下没有联邦api FederatedClusterRoleBinding,因此需要手工先创建FederatedClusterRoleBinding联邦api类型。

1
2
3
4
$ kubefedctl enable clusterrolebinding
I1225 01:46:42.779166 818254 util.go:141] Api resource found.
customresourcedefinition.apiextensions.k8s.io/federatedclusterrolebindings.types.kubefed.io created
federatedtypeconfig.core.kubefed.io/clusterrolebindings.rbac.authorization.k8s.io created in namespace kube-federation-system

在创建完成FederatedClusterRoleBinding联邦api类型后,即可以创建出FederatedClusterRoleBinding类型的对象。

子集群的个性化配置

通过Fedrated中的spec.overrides来完成,可以覆盖spec.template中的内容

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
apiVersion: types.kubefed.io/v1beta1
kind: FederatedDeployment
metadata:
name: test-deployment
namespace: test-namespace
spec:
template:
metadata:
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
placement:
clusters:
- name: cluster2
- name: cluster1
overrides:
- clusterName: cluster2
clusterOverrides:
- path: "/spec/replicas"
value: 5
- path: "/spec/template/spec/containers/0/image"
value: "nginx:1.17.0-alpine"
- path: "/metadata/annotations"
op: "add"
value:
foo: bar
- path: "/metadata/annotations/foo"
op: "remove"

多集群应用调度

发布应用到特定的子集群

在Fedrated的spec.placement.clusters中,定义了要发布到哪个子集群的信息。

1
2
3
4
placement:
clusters:
- name: cluster2
- name: cluster1

不过上述方法需要指定特定的集群,为了有更丰富的灵活性,kubefed还提供了label selector的机制,可以提供spec.placement.clusterSelector来指定一组集群。

1
2
3
4
5
6
spec:
placement:
clusters: []
clusterSelector:
matchLabels:
foo: bar

标签选择器通过跟kubefedclusters对象的label来进行匹配,可以执行下述命令给kubefedclusters来打标签。

1
kubectl label kubefedclusters -n kube-federation-system kind-cluster1 foo=bar

子集群差异化调度

上述调度功能被选择的子集群为平等关系,如果要想子集群能够有所差异,可以使用ReplicaSchedulingPreference来完成,目前仅支持deployment和replicaset两种类型的资源,还不支持statefulset。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: scheduling.kubefed.io/v1alpha1
kind: ReplicaSchedulingPreference
metadata:
name: test-deployment
namespace: test-ns
spec:
targetKind: FederatedDeployment
totalReplicas: 9
clusters:
A:
minReplicas: 4
maxReplicas: 6
weight: 1
B:
minReplicas: 4
maxReplicas: 8
weight: 2

spec.targetKind用来指定管理的类型,仅支持FederatedDeployment和FederatedReplicaset。
spec.totalReplicas用来指定所有子集群的副本数总和。优先级要高于FederatedDeployment和FederatedReplicaset的spec.template中指定的副本数。
spec.clusters用来指定每个子集群的最大、最小副本数和权重。
spec.rebalance自动调整整个集群中的副本数,比如一个集群中的pod挂掉后,可以将pod迁移到另外的集群中。

子集群之间的交互

缺点

  1. 父集群充当了所有集群的单一控制面
  2. 通过联邦CRD来管理资源,无法直接使用k8s原生的资源,集群间维护CRD版本和API版本一致性导致升级比较复杂。

参考资料

使用openssl创建自签名证书

创建CA

1
2
3
openssl genrsa -out root.key 4096
openssl req -new -x509 -days 1000 -key root.key -out root.crt
openssl x509 -text -in root.crt -noout

创建私钥和公钥文件

1
2
3
4
# 用来产生私钥文件server.key
openssl genrsa -out server.key 2048
# 产生公钥文件
openssl rsa -in server.key -pubout -out server.pem

创建签名请求

1
2
3
4
openssl req -new -key server.key -out server.csr

# 查看创建的签名请求
openssl req -noout -text -in server.csr

创建自签名证书
新建自签名证书的附加信息server.ext,内容如下

1
2
3
4
5
6
7
8
9
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
IP.2 = 127.0.0.1
IP.3 = 10.66.3.6

使用ca签发ssl证书,此时会产生server.crt文件,即为证书文件

1
2
3
openssl x509 -req -in server.csr -out server.crt -days 3650 \
-CAcreateserial -CA root.crt -CAkey root.key \
-CAserial serial -extfile server.ext

查看证书文件内容

1
openssl x509 -in server.crt   -noout -text

使用ca校验证书是否通过

1
openssl verify -CAfile root.crt server.crt

使用cfssl签发证书

cfssl是CloudFlare开源的一款tls工具,使用go语言编写,地址:https://github.com/cloudflare/cfssl。

安装

mac用户:brew install cfssl

linux用户可以直接下载二进制文件:https://github.com/cloudflare/cfssl/releases

签发证书

待补充

证书格式

证书按照格式可以分为二进制和文本文件两种格式。

二进制格式分为:

  1. .der或者.cer:用来存放证书信息,不包含私钥。

文本格式分为:

  1. .pem:存放证书或者私钥。一般是.key文件存放私钥信息。对于pem或者key文件,如果存在——BEGIN CERTIFICATE——,则说明这是一个证书文件。如果存在—–BEGIN RSA PRIVATE KEY—–,则说明这是一个私钥文件。
  2. *.key:用来存放私钥文件。
  3. *.crt:证书请求文件,格式的开头为:—–BEGIN CERTIFICATE REQUEST—–

证书格式的转换

将cert证书转换为pem格式

1
2
openssl rsa -in server.key -text > server-key.pem
openssl x509 -in server.crt -out server.pem

将pem格式转换为cert格式

证书查看

查看特定域名的证书内容:echo | openssl s_client -connect www.baidu.com:443 -servername www.baidu.com 2>/dev/null | openssl x509 -noout -text

获取特定域名的证书内容:openssl s_client -servername www.baidu.com -connect www.baidu.com:443 </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'

在 k8s 中,证书通常存放在 secret 中,可以使用 kubeclt + openssl 命令查看证书内容:kubectl get secret -n test-cluster sharer -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout

证书的使用

curl命令关于证书的用法:

  • –cacert:指定ca来校验server端的证书合法性
  • –cert:指定客户端的证书文件,用在双向认证mTLS中
  • –key:私钥文件名,用在双向认证mTLS中

参考链接

简介

腾讯云开源的k8s多集群管理方案,可发布应用到多个k8s集群。
https://github.com/clusternet/clusternet
GitHub Star:891

架构

image.png
image.png
包含clusternet-hub和clusternet-agent两个组件。

clusternet-hub部署在父集群,用途:

  • 接收子集群的注册请求,并为每个自己群创建namespace、serviceaccount等资源
  • 提供父集群跟各个子集群的长连接
  • 提供restful api来访问各个子集群,同时支持子集群的互访

clusternet-agent部署在子集群,用途:

  • 自动注册子集群到父集群
  • 心跳报告到中心集群,包括k8s版本、运行平台、健康状态等
  • 通过websocket协议跟父集群通讯

CRD抽象

ClusterRegistrationRequest

clusternet-agent在父集群中创建的CR,一个k8s集群一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: clusters.clusternet.io/v1beta1
kind: ClusterRegistrationRequest
metadata:
labels:
clusters.clusternet.io/cluster-id: 2bcd48d2-0ead-45f7-938b-5af6af7da5fd
clusters.clusternet.io/cluster-name: clusternet-cluster-pmlbp
clusters.clusternet.io/registered-by: clusternet-agent
name: clusternet-2bcd48d2-0ead-45f7-938b-5af6af7da5fd
spec:
clusterId: 2bcd48d2-0ead-45f7-938b-5af6af7da5fd
clusterName: clusternet-cluster-pmlbp # 集群名称
clusterType: EdgeCluster # 集群类型
syncMode: Dual
status:
caCertificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1ekNDQWMrZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeE1USXhNekV3TWpjeE5Wb1hEVE14TVRJeE1URXdNamN4TlZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTHVCCmJwdXNaZXM5RXFXaEw1WUVGb2c4eHRJai9jbFh2S1lTMnZYbGR4cUt1MUR3VHV3aUcxZUN4VGlHa2dLQXVXd1IKVFFheEh5aGY3U29lc0hqMG43NnFBKzhQT05lS1VGdERJOWVzejF2WTF5bXFoUHR0QVo0cGhkWmhmbXJjZTJLRQpuMS84MzNWbWlXd0pSZmNWcEJtTU52MjFYMVVwNWp6RGtncS9tY0JOOGN0ZU5PMEpKNkVVeTE2RXZLbjhyWG90ClErTW5PUHE4anFNMzJjRFFqYWVEdGxvM2kveXlRd1NMa2wzNFo4aElwZDZNWWVSTWpXcmhrazF4L0RYZjNJK3IKOGs5S1FBbEsvNWZRMXk5NHYwT25TK0FKZTliczZMT2srYVFWYm5SbExab2E2aVZWbUJNam03UjBjQ2w0Y1hpRwpyekRnN1ZLc3lsY1o3aGFRRTNFQ0F3RUFBYU5DTUVBd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZPaGwxMHUwNnJvenZJUm9XVmpNYlVPMzFDbERNQTBHQ1NxR1NJYjMKRFFFQkN3VUFBNElCQVFBQytqMXpPQVZYVXpNUFJ0U29Kd3JhMU1FSkJOUTNMWitmWnZRYjdIbk53b00zaCthMgoyc25yUitSWTYrOFFDbXVKeis1eU5yYStEZDlnNzQ1Q0hDaFpVZzlsY3RjQTRzZVR5OXZqcUVQNVBNSzEvbi9PCnFEcDQyMUpqTjYvUXJmamIvbVM0elUvZXNGZGowQXRYQ0FLMWJsNmF0ai9jZXVBbzh1bTRPaUVlNnJhanBYTHUKWFlmQ1FVZFV5TWpQdEZHTDMwNytna0RpdlVsdXk3RkQ4aUpURTh2QWpxOVBXOW40SmxFMjdQWXR5QnNocy9XSApCZ2czQjZpTG10SjZlNzJiWnA3ZmptdmJWTC9VdkxzYXZqRXltZDhrMnN1bFFwQUpzeVJrMkEzM1g3NWJpS0RGCk1CU29DaHc3U2JMSkJrN0FNckRzTjd1Q2U3WWVIWGdZemdhRAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
dedicatedNamespace: clusternet-sdqrh
managedClusterName: clusternet-cluster-pmlbp
result: Approved
token: ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNkluZFpWMUV6UmxCbFdUUldWa1Y0UmxBeWMzaDJOV3RuUzBabVJsaG9jWG94TkhKM2JtUkxiRTlrYUZVaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUpqYkhWemRHVnlibVYwTFhOa2NYSm9JaXdpYTNWaVpYSnVaWFJsY3k1cGJ5OXpaWEoyYVdObFlXTmpiM1Z1ZEM5elpXTnlaWFF1Ym1GdFpTSTZJbU5zZFhOMFpYSnVaWFF0YmpneWFHc3RkRzlyWlc0dGN6SnpjRFVpTENKcmRXSmxjbTVsZEdWekxtbHZMM05sY25acFkyVmhZMk52ZFc1MEwzTmxjblpwWTJVdFlXTmpiM1Z1ZEM1dVlXMWxJam9pWTJ4MWMzUmxjbTVsZEMxdU9ESm9heUlzSW10MVltVnlibVYwWlhNdWFXOHZjMlZ5ZG1salpXRmpZMjkxYm5RdmMyVnlkbWxqWlMxaFkyTnZkVzUwTG5WcFpDSTZJamN3TWpRMk1tTTBMV0prTlRFdE5HVXpNeTA1WXpnekxUWmpPV1F6TUdGall6bG1aU0lzSW5OMVlpSTZJbk41YzNSbGJUcHpaWEoyYVdObFlXTmpiM1Z1ZERwamJIVnpkR1Z5Ym1WMExYTmtjWEpvT21Oc2RYTjBaWEp1WlhRdGJqZ3lhR3NpZlEuUjJCdEQ1YzQxRm1seC1hTEFKSWQzbGxvOThSY25TakVUak5pSnVuTXg1US1jYXhwc3VkamUxekVtUF9mTHR6TjU4d21Dd3RXcHpoaXhSMWFTUHE5LXJTRlBIYzk3aDlTT3daZGdicC10alFBSjA4dllfYWdiVFRKLU1WN1dpN0xQVzRIcmt1U3RlemUzVHh2RW11NWwySlpzbG5UTXdkR3NGRVlVZzVfeWFoLUQwQ2pnTkZlR1ljUjJ1TlJBWjdkNW00Q1c5VmRkdkNsanNqRE5WX1k0RkFEbGo2cHgzSlh0SDQ4U1ZUd254TS1sNHl0eXBENjZFbG1PYXpUMmRwSWd4eWNSZ0tJSUlacWNQNnZVc0ZOM1Zka21ZZ29ydy1NSUcwc25YdzFaZ1lwRk9SUDJRa3hjd2NOUVVOTjh6VUZqSUZSSmdSSFdjNlo4aXd2eURnZTVn

ClusterRole和Role

ClusterRegistrationRequest被接收后会创建ClusterRole

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
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
annotations:
clusternet.io/autoupdate: "true"
labels:
clusternet.io/created-by: clusternet-hub
clusters.clusternet.io/bootstrapping: rbac-defaults
clusters.clusternet.io/cluster-id: 2bcd48d2-0ead-45f7-938b-5af6af7da5fd
name: clusternet-2bcd48d2-0ead-45f7-938b-5af6af7da5fd
rules:
- apiGroups:
- clusters.clusternet.io
resources:
- clusterregistrationrequests
verbs:
- create
- get
- apiGroups:
- proxies.clusternet.io
resourceNames:
- 2bcd48d2-0ead-45f7-938b-5af6af7da5fd
resources:
- sockets
verbs:
- '*'

并会在cluster对应的namespace下创建Role

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
annotations:
clusternet.io/autoupdate: "true"
labels:
clusternet.io/created-by: clusternet-hub
clusters.clusternet.io/bootstrapping: rbac-defaults
name: clusternet-managedcluster-role
namespace: clusternet-sdqrh
rules:
- apiGroups:
- '*'
resources:
- '*'
verbs:
- '*'

ManagedCluster

clusternet-hub在接受ClusterRegistrationRequest后,会创建一个ManagedCluster。一个k8s集群一个namespace

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
apiVersion: clusters.clusternet.io/v1beta1
kind: ManagedCluster
metadata:
labels:
clusternet.io/created-by: clusternet-agent
clusters.clusternet.io/cluster-id: 2bcd48d2-0ead-45f7-938b-5af6af7da5fd
clusters.clusternet.io/cluster-name: clusternet-cluster-pmlbp
name: clusternet-cluster-pmlbp
namespace: clusternet-sdqrh
spec:
clusterId: 2bcd48d2-0ead-45f7-938b-5af6af7da5fd
clusterType: EdgeCluster
syncMode: Dual
status:
allocatable:
cpu: 7600m
memory: "30792789992"
apiserverURL: https://10.233.0.1:443 # default/kubernetes的service clusterip
appPusher: true
capacity:
cpu: "8"
memory: 32192720Ki
clusterCIDR: 10.233.0.0/18
conditions:
- lastTransitionTime: "2021-12-15T07:47:10Z"
message: managed cluster is ready.
reason: ManagedClusterReady
status: "True"
type: Ready
healthz: true
heartbeatFrequencySeconds: 180
k8sVersion: v1.21.5
lastObservedTime: "2021-12-15T07:47:10Z"
livez: true
nodeStatistics:
readyNodes: 1
parentAPIServerURL: https://172.21.115.160:6443 # 父集群地址
platform: linux/amd64
readyz: true
serviceCIDR: 10.233.64.0/18
useSocket: true

Subscription

应用发布的抽象,人工提交到环境中。要发布的资源即可以是helm chart,也可以是原生的k8s对象。clusternet在部署的时候会查看部署的优先级,并且支持weight,优先部署cluster级别的资源,这点跟helm部署的逻辑一致。

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
apiVersion: apps.clusternet.io/v1alpha1
kind: Subscription
metadata:
name: app-demo
namespace: default
spec:
# 定义应用要发布到哪个k8s集群
subscribers: # defines the clusters to be distributed to
- clusterAffinity:
matchLabels:
clusters.clusternet.io/cluster-id: dc91021d-2361-4f6d-a404-7c33b9e01118 # PLEASE UPDATE THIS CLUSTER-ID TO YOURS!!!
# 定义了要发布哪些资源
feeds: # defines all the resources to be deployed with
- apiVersion: apps.clusternet.io/v1alpha1
kind: HelmChart
name: mysql
namespace: default
- apiVersion: v1
kind: Namespace
name: foo
- apiVersion: v1
kind: Service
name: my-nginx-svc
namespace: foo
- apiVersion: apps/v1
kind: Deployment
name: my-nginx
namespace: foo

HelmChart

Subscriber发布的一个子资源之一,对应一个helm chart的完整描述

1
2
3
4
5
6
7
8
9
10
apiVersion: apps.clusternet.io/v1alpha1
kind: HelmChart
metadata:
name: mysql
namespace: default
spec:
repo: https://charts.bitnami.com/bitnami
chart: mysql
version: 8.6.2
targetNamespace: abc

Localization

差异化不同于其他集群的配置,用来描述namespace级别的差异化配置。

Globalization

用来描述不同集群的cluster级别的差异化配置。

Base

Description

跟Base对象根据Localization和Globalization渲染得到的最终要发布到集群中的最终对象。

安装

本文使用kind进行测试,使用kind创建两个k8s集群host和member1,两个集群的apiserver均监听在宿主机的端口,确保从一个集群可以访问到另外一个集群的apiserver。

在父集群执行如下命令安装clusternet管控组件:

1
2
3
helm repo add clusternet https://clusternet.github.io/charts
helm install clusternet-hub -n clusternet-system --create-namespace clusternet/clusternet-hub
kubectl apply -f https://raw.githubusercontent.com/clusternet/clusternet/main/manifests/samples/cluster_bootstrap_token.yaml

会在clusternet-system namespace下创建deployment clusternet-hub。

最后一条命令,会创建一个secret,其中的token信息为07401b.f395accd246ae52d,在子集群注册的时候需要用到。

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
apiVersion: v1
kind: Secret
metadata:
# Name MUST be of form "bootstrap-token-<token id>"
name: bootstrap-token-07401b
namespace: kube-system

# Type MUST be 'bootstrap.kubernetes.io/token'
type: bootstrap.kubernetes.io/token
stringData:
# Human readable description. Optional.
description: "The bootstrap token used by clusternet cluster registration."

# Token ID and secret. Required.
token-id: 07401b
token-secret: f395accd246ae52d

# Expiration. Optional.
expiration: 2025-05-10T03:22:11Z

# Allowed usages.
usage-bootstrap-authentication: "true"
usage-bootstrap-signing: "true"

# Extra groups to authenticate the token as. Must start with "system:bootstrappers:"
auth-extra-groups: system:bootstrappers:clusternet:register-cluster-token

在子集群执行如下命令,其中parentURL要修改为父集群的apiserver地址

1
2
3
4
5
helm repo add clusternet https://clusternet.github.io/charts
helm install clusternet-agent -n clusternet-system --create-namespace \
--set parentURL=https://10.0.248.96:8443 \
--set registrationToken=07401b.f395accd246ae52d \
clusternet/clusternet-agent

其中parentURL为父集群的apiserver地址,registrationToken为父集群注册的token信息。

访问子集群

kubectl-clusternet命令行方式访问

安装kubectl krew插件,参考 https://krew.sigs.k8s.io/docs/user-guide/setup/install/,执行如下命令:

1
2
3
4
5
6
7
8
9
10
11
12
yum install git -y
(
set -x; cd "$(mktemp -d)" &&
OS="$(uname | tr '[:upper:]' '[:lower:]')" &&
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" &&
KREW="krew-${OS}_${ARCH}" &&
curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" &&
tar zxvf "${KREW}.tar.gz" &&
./"${KREW}" install krew
)
echo 'export PATH="${PATH}:${HOME}/.krew/bin"' >> ~/.bashrc
source ~/.bashrc

安装kubectl插件clusternet:kubectl krew install clusternet

使用kubectl clusternet get可以看到发布的应用,非发布的应用看不到。

代码层面访问子集群

可以参照例子:https://github.com/clusternet/clusternet/blob/main/examples/clientgo/demo.go#L42-L45

改动代码非常小,仅需要获取到对应集群的config信息即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
config, err := rest.InClusterConfig()
if err != nil {
klog.Error(err)
os.Exit(1)
}

// This is the ONLY place you need to wrap for Clusternet
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return clientgo.NewClusternetTransport(config.Host, rt)
})

// now we could create and visit all the resources
client := kubernetes.NewForConfigOrDie(config)
_, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "demo",
},
}, metav1.CreateOptions{})

应用发布

image.png
在主集群提交如下的yaml文件

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
apiVersion: apps.clusternet.io/v1alpha1
kind: Subscription
metadata:
name: app-demo
namespace: default
spec:
subscribers: # defines the clusters to be distributed to
- clusterAffinity:
matchLabels:
clusters.clusternet.io/cluster-id: dc91021d-2361-4f6d-a404-7c33b9e01118 # PLEASE UPDATE THIS CLUSTER-ID TO YOURS!!!
feeds: # defines all the resources to be deployed with
- apiVersion: apps.clusternet.io/v1alpha1
kind: HelmChart
name: mysql
namespace: default
- apiVersion: v1
kind: Namespace
name: foo
- apiVersion: apps/v1
kind: Service
name: my-nginx-svc
namespace: foo
- apiVersion: apps/v1
kind: Deployment
name: my-nginx
namespace: foo

实现分析

在主集群实现了两个aggregated apiservice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
annotations:
meta.helm.sh/release-name: clusternet-hub
meta.helm.sh/release-namespace: clusternet-system
labels:
app.kubernetes.io/instance: clusternet-hub
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: clusternet-hub
helm.sh/chart: clusternet-hub-0.2.1
name: v1alpha1.proxies.clusternet.io
spec:
group: proxies.clusternet.io
groupPriorityMinimum: 1000
insecureSkipTLSVerify: true
service:
name: clusternet-hub
namespace: clusternet-system
port: 443
version: v1alpha1
versionPriority: 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
annotations:
meta.helm.sh/release-name: clusternet-hub
meta.helm.sh/release-namespace: clusternet-system
labels:
app.kubernetes.io/instance: clusternet-hub
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: clusternet-hub
helm.sh/chart: clusternet-hub-0.2.1
name: v1alpha1.shadow
spec:
group: shadow
groupPriorityMinimum: 1
insecureSkipTLSVerify: true
service:
name: clusternet-hub
namespace: clusternet-system
port: 443
version: v1alpha1
versionPriority: 1

相关链接

经常有快速创建一个测试k8s集群的场景,为了能够快速完成,整理了如下的命令,即可在主机上快速启动一个k8s集群。部分命令需要外网访问,推荐直接使用海外的主机。

常用工具安装

1
2
3
4
5
6
7
8
9
yum install vim git make -y

# neovim
curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux64.tar.gz
sudo rm -rf /opt/nvim
sudo tar -C /opt -xzf nvim-linux64.tar.gz
echo 'export PATH="$PATH:/opt/nvim-linux64/bin"' >>~/.bash_profile
source ~/.bash_profile
git clone https://github.com/LazyVim/starter ~/.config/nvim

安装docker

下面命令可以安装最新版本的docker-ce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
yum install -y yum-utils
yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
yum install docker-ce docker-ce-cli containerd.io -y
systemctl enable docker && systemctl start docker

安装特定版本的docker

如果要安装特定版本的docker-ce,可以使用如下方法。

使用如下命令查询yum源中的docker-ce版本

1
2
3
4
5
6
yum list docker-ce --showduplicates | sort -r
Last metadata expiration check: 0:00:27 ago on Sat 09 Apr 2022 12:39:09 AM CST.
docker-ce.x86_64 3:20.10.9-3.el8 docker-ce-stable
docker-ce.x86_64 3:20.10.8-3.el8 docker-ce-stable
docker-ce.x86_64 3:20.10.7-3.el8 docker-ce-stable
docker-ce.x86_64 3:20.10.6-3.el8 docker-ce-stable

选择特定版本的docker-ce和docker-ce-cli,执行如下命令

1
yum install docker-ce-<VERSION_STRING> docker-ce-cli-<VERSION_STRING> containerd.io

安装kubectl kind helm

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
# 安装kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/bin/
# 如果找不到rpm包,可以通过网络安全 rpm -ivh http://mirror.centos.org/centos/7/os/x86_64/Packages/bash-completion-2.1-8.el7.noarch.rpm
yum install -y bash-completion
echo -e '\n# kubectl' >> ~/.bash_profile
echo 'source <(kubectl completion bash)' >>~/.bash_profile
echo 'alias k=kubectl' >>~/.bash_profile
echo 'complete -F __start_kubectl k' >>~/.bash_profile
source ~/.bash_profile

# 安装helm
wget https://get.helm.sh/helm-v3.14.2-linux-amd64.tar.gz
tar zvxf helm-v3.14.2-linux-amd64.tar.gz
mv linux-amd64/helm /usr/bin/
rm -rf linux-amd64
rm -f helm-v3.14.2-linux-amd64.tar.gz

# 安装kubectx kubens
git clone https://github.com/ahmetb/kubectx /tmp/kubectx
cp /tmp/kubectx/kubens /usr/bin/kns
cp /tmp/kubectx/kubectx /usr/bin/kctx
wget https://github.com/junegunn/fzf/releases/download/0.29.0/fzf-0.29.0-linux_amd64.tar.gz -P /tmp
tar zvxf /tmp/fzf-0.29.0-llinux_amd64.tar.gz -C /tmp/
mv /tmp/fzf /usr/local/bin/

# 安装kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.18.0/kind-linux-amd64
chmod +x ./kind
mv ./kind /usr/local/bin/
echo -e "\n# kind" >> ~/.bash_profile
echo 'source <(kind completion bash)' >>~/.bash_profile

创建集群

其中将apiServerAddress指定为了本机,即创建出来的k8s集群仅允许本集群内访问。如果要是需要多个k8s集群之间的互访场景,由于kind拉起的k8s运行在docker容器中,而docker容器使用的是容器网络,此时如果设置apiserver地址为127.0.0.1,那么集群之间就没法直接通讯了,此时需要指定一个可以在docker容器中访问的宿主机ip地址。

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
if=eth0
ip=`ifconfig $if|grep inet|grep -v 127.0.0.1|grep -v inet6|awk '{print $2}'|tr -d "addr:"`
cat > kind.conf <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: kind
nodes:
- role: control-plane
# 如果需要 ingress,则需要指定该参数
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
# 指定 k8s 版本,默认不指定
# image: kindest/node:v1.23.17
- role: worker
- role: worker
- role: worker
networking:
apiServerAddress: "$ip"
apiServerPort: 6443
EOF
kind create cluster --config kind.conf

如果使用 nginx ingress,额外执行命令 kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml

其他周边工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 安装kustomize
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
mv kustomize /usr/local/bin/

# 安装golang
wget https://go.dev/dl/go1.21.8.linux-amd64.tar.gz -P /opt
tar zvxf /opt/go1.21.8.linux-amd64.tar.gz -C /opt/
mkdir /opt/gopath
echo -e '\n# golang' >> ~/.bash_profile
echo 'export GOROOT=/opt/go' >> ~/.bash_profile
echo 'export GOPATH=/opt/gopath' >> ~/.bash_profile
echo 'export PATH=$PATH:$GOPATH/bin:$GOROOT/bin' >> ~/.bash_profile
source ~/.bash_profile

# 安装controller-gen,会将controller-gen命令安装到GOPATH/bin目录下
go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest

# 安装dlv工具
go install github.com/go-delve/delve/cmd/dlv@latest

安装krew

1
2
3
4
5
6
7
8
9
10
11
12
13
(
set -x; cd "$(mktemp -d)" &&
OS="$(uname | tr '[:upper:]' '[:lower:]')" &&
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" &&
KREW="krew-${OS}_${ARCH}" &&
curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" &&
tar zxvf "${KREW}.tar.gz" &&
./"${KREW}" install krew
)

echo -e '\n# kubectl krew' >> ~/.bash_profile
echo 'export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile

资料

概念

  1. 物理卷PV(Physical Volume):可以是整块物理磁盘或者物理磁盘上的分区
  2. 卷组VG(Volume Group):由一个或多个物理卷PV组成,可在卷组上创建一个或者多个LV,可以动态增加pv到卷组
  3. 逻辑卷LV(Logical Volume):类似于磁盘分区,建立在VG之上,在LV上可以创建文件系统 逻辑卷建立后可以动态的增加或缩小空间
  4. PE(Physical Extent): PV可被划分为PE的基本单元,具有唯一编号的PE是可以被LVM寻址的最小单元。PE的大小是可以配置的,默认为4MB。
  5. LE(Logical Extent): LV可被划分为LE的基本单元,LE跟PE是一对一的关系。

基本操作

lvm相关的目录如果没有按照,在centos下使用yum install lvm2进行安装。

初始磁盘状态如下,/dev/sda上有40g磁盘空间未分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 40G 2.8G 38G 7% /
devtmpfs 236M 0 236M 0% /dev
tmpfs 244M 0 244M 0% /dev/shm
tmpfs 244M 4.5M 240M 2% /run
tmpfs 244M 0 244M 0% /sys/fs/cgroup
tmpfs 49M 0 49M 0% /run/user/1000

# fdisk -l

Disk /dev/sda: 85.9 GB, 85899345920 bytes, 167772160 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000a05f8

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 83886079 41942016 83 Linux

对磁盘的操作

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
# fdisk /dev/sda
Welcome to fdisk (util-linux 2.23.2).

Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

# 创建主分区2,空间为1g,磁盘格式为lvm
Command (m for help): n
Partition type:
p primary (1 primary, 0 extended, 3 free)
e extended
Select (default p): p
Partition number (2-4, default 2): 2
First sector (83886080-167772159, default 83886080):
Using default value 83886080
Last sector, +sectors or +size{K,M,G} (83886080-167772159, default 167772159): +1G
Partition 2 of type Linux and of size 1 GiB is set

Command (m for help): t
Partition number (1,2, default 2): 2
Hex code (type L to list all codes): 8e
Changed type of partition 'Linux' to 'Linux LVM'

# 创建主分区3,空间为5g,磁盘格式为lvm
Command (m for help): n
Partition type:
p primary (2 primary, 0 extended, 2 free)
e extended
Select (default p): p
Partition number (3,4, default 3): 3
First sector (85983232-167772159, default 85983232):
Using default value 85983232
Last sector, +sectors or +size{K,M,G} (85983232-167772159, default 167772159): +5G
Partition 3 of type Linux and of size 5 GiB is set

Command (m for help): t
Partition number (1-3, default 3): 3
Hex code (type L to list all codes): 8e
Changed type of partition 'Linux' to 'Linux LVM'

# 保存上述操作
Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.

WARNING: Re-reading the partition table failed with error 16: Device or resource busy.
The kernel still uses the old table. The new table will be used at
the next reboot or after you run partprobe(8) or kpartx(8)
Syncing disks.

当前磁盘空间状态如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# fdisk -l

Disk /dev/sda: 85.9 GB, 85899345920 bytes, 167772160 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000a05f8

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 83886079 41942016 83 Linux
/dev/sda2 83886080 85983231 1048576 8e Linux LVM
/dev/sda3 85983232 96468991 5242880 8e Linux LVM

将上述两个lvm磁盘分区创建pv,使用pvremove /dev/sda2可以删除pv

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
# pvcreate /dev/sda2 /dev/sda3
Physical volume "/dev/sda2" successfully created.
Physical volume "/dev/sda3" successfully created.
[root@localhost vagrant]# pvdisplay
"/dev/sda2" is a new physical volume of "1.00 GiB"
--- NEW Physical volume ---
PV Name /dev/sda2
VG Name
PV Size 1.00 GiB
Allocatable NO
PE Size 0
Total PE 0
Free PE 0
Allocated PE 0
PV UUID MgarF2-Ka6D-blDi-8ecd-1SEU-y2GD-JtiK2c

"/dev/sda3" is a new physical volume of "5.00 GiB"
--- NEW Physical volume ---
PV Name /dev/sda3
VG Name
PV Size 5.00 GiB
Allocatable NO
PE Size 0
Total PE 0
Free PE 0
Allocated PE 0
PV UUID ToKp2T-30mS-0P8c-ZrcB-2lO4-ayo9-62fuDx

接下来创建vg,vg的名字可以随便定义,并将创建的两个pv都添加到vg中,可以看到vg的空间为两个pv之和

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
# vgcreate vg1 /dev/sda1
# vgdisplay -v
--- Volume group ---
VG Name vg1
System ID
Format lvm2
Metadata Areas 2
Metadata Sequence No 1
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 0
Open LV 0
Max PV 0
Cur PV 2
Act PV 2
VG Size 5.99 GiB
PE Size 4.00 MiB
Total PE 1534
Alloc PE / Size 0 / 0
Free PE / Size 1534 / 5.99 GiB
VG UUID XI8Biv-JtUv-tsur-wuvm-IJQz-HLZu-6a2u5G

--- Physical volumes ---
PV Name /dev/sda2
PV UUID MgarF2-Ka6D-blDi-8ecd-1SEU-y2GD-JtiK2c
PV Status allocatable
Total PE / Free PE 255 / 255

PV Name /dev/sda3
PV UUID ToKp2T-30mS-0P8c-ZrcB-2lO4-ayo9-62fuDx
PV Status allocatable
Total PE / Free PE 1279 / 1279

接下来创建lv,并从vg1中分配空间2g

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# lvcreate -L 2G -n lv1 vg1
Logical volume "lv1" created.

# lvdisplay
--- Logical volume ---
LV Path /dev/vg1/lv1
LV Name lv1
VG Name vg1
LV UUID EesY4i-lSqY-ef1R-599C-XTrZ-IcVL-P7W46Q
LV Write Access read/write
LV Creation host, time localhost.localdomain, 2019-06-16 07:29:38 +0000
LV Status available
# open 0
LV Size 2.00 GiB
Current LE 512
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 8192
Block device 253:0

接下来给lv1格式化磁盘格式为ext4,并将磁盘挂载到/tmp/lvm目录下

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
# mkfs.ext4 /dev/vg1/lv1
mke2fs 1.42.9 (28-Dec-2013)
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=0 blocks
131072 inodes, 524288 blocks
26214 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=536870912
16 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912

Allocating group tables: done
Writing inode tables: done
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done

# mkdir /tmp/lvm
[root@localhost vagrant]# mount /dev/vg1/lv1 /tmp/lvm
[root@localhost vagrant]# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 40G 2.9G 38G 8% /
devtmpfs 236M 0 236M 0% /dev
tmpfs 244M 0 244M 0% /dev/shm
tmpfs 244M 4.5M 240M 2% /run
tmpfs 244M 0 244M 0% /sys/fs/cgroup
tmpfs 49M 0 49M 0% /run/user/1000
/dev/mapper/vg1-lv1 2.0G 6.0M 1.8G 1% /tmp/lvm

接下来对lv的空间从2G扩展到3G,此时通过df查看分区空间大小仍然为2g,需要执行resize2fs命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# lvextend -L +1G /dev/vg1/lv1
Size of logical volume vg1/lv1 changed from 2.00 GiB (512 extents) to 3.00 GiB (768 extents).
Logical volume vg1/lv1 successfully resized.

# resize2fs /dev/vg1/lv1
resize2fs 1.42.9 (28-Dec-2013)
Filesystem at /dev/vg1/lv1 is mounted on /tmp/lvm; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 1
The filesystem on /dev/vg1/lv1 is now 786432 blocks long.

# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 40G 2.9G 38G 8% /
devtmpfs 236M 0 236M 0% /dev
tmpfs 244M 0 244M 0% /dev/shm
tmpfs 244M 4.5M 240M 2% /run
tmpfs 244M 0 244M 0% /sys/fs/cgroup
tmpfs 49M 0 49M 0% /run/user/1000
/dev/mapper/vg1-lv1 2.9G 6.0M 2.8G 1% /tmp/lvm

问题

试验时操作失误,出现了先格式化磁盘,后发现pv找不到对应设备。vg删除不成功。

正常删除vg的方式,此时lv会自动消失

1
2
vgreduce --removemissing vg1
vgremove vg1

lvremove操作执行的时候经常会出现提示“Logical volume xx contains a filesystem in use.”的情况,该问题一般是由于有其他进程在使用该文件系统导致的。网络上经常看到的是通过fuser或者lsof命令来查找使用方,但偶尔该命令会失效,尤其在本机上有容器的场景下。另外一个可行的办法是通过 grep -nr "/data" /proc/*/mount 命令,可以找到挂载该目录的所有进程,简单有效。

三种Logic Volume

LVM的机制可以类比于RAID,RAID一个核心的机制是性能和数据冗余,并提供了多种数据的冗余模块可供配置。lvm在性能和数据冗余方面支持如下三种Logic Volume: 线性逻辑卷、条带化逻辑卷和镜像逻辑卷。上述几种模式是在vg已经创建完成后创建lv的时候指定的模式,会影响到lv中的pe分配。

下面使用如下的机器配置来进行各个模式的介绍和功能测试。

1
2
3
4
5
6
7
8
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
vda 253:0 0 40G 0 disk
└─vda1 253:1 0 40G 0 part /
vdb 253:16 0 100G 0 disk
vdc 253:32 0 200G 0 disk
vdd 253:48 0 300G 0 disk
vde 253:64 0 400G 0 disk

线性逻辑卷 Linear Logic Volume

当一个VG中有两个或者多个磁盘的时候,LV分配磁盘容量的时候是按照VG中的PV安装顺序分配的,即一个PV用完后才会分配下一块PV。也可以在创建LV的通过指定PV中的PE段来将数据分散到多个PV上。该模式也是LVM的默认模式。

使用/dev/vdb和/dev/vdc两块磁盘来进行测试,先创建对应的PV。

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
$ pvcreate /dev/vdb /dev/vdc
Physical volume "/dev/vdb" successfully created.
Physical volume "/dev/vdc" successfully created.

$ pvdisplay
"/dev/vdb" is a new physical volume of "100.00 GiB"
--- NEW Physical volume ---
PV Name /dev/vdb
VG Name
PV Size 100.00 GiB
Allocatable NO
PE Size 0
Total PE 0
Free PE 0
Allocated PE 0
PV UUID 8NnlYc-4f3f-fkeW-a3l3-LXoC-9UEH-fvpb5V

"/dev/vdc" is a new physical volume of "200.00 GiB"
--- NEW Physical volume ---
PV Name /dev/vdc
VG Name
PV Size 200.00 GiB
Allocatable NO
PE Size 0
Total PE 0
Free PE 0
Allocated PE 0
PV UUID I9ffpN-c1vc-PQOB-yKyd-MdzO-Ngff-6e116t

创建VG vg1,该容量为上面两个磁盘空间之和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ vgcreate vg1 /dev/vdb /dev/vdc
Volume group "vg1" successfully created

$ vgdisplay
--- Volume group ---
VG Name vg1
System ID
Format lvm2
Metadata Areas 2
Metadata Sequence No 1
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 0
Open LV 0
Max PV 0
Cur PV 2
Act PV 2
VG Size 299.99 GiB
PE Size 4.00 MiB
Total PE 76798
Alloc PE / Size 0 / 0
Free PE / Size 76798 / 299.99 GiB
VG UUID Gi0bJx-jqY8-YpSo-kB0l-9wdk-ZfCT-GpgFZY

从vg1中创建LV lv1,其大小为vg的全部大小

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
$ lvcreate --type linear -l 100%VG -n lv1 vg1
Logical volume "lv1" created.

$ lvdisplay
--- Logical volume ---
LV Path /dev/vg1/lv1
LV Name lv1
VG Name vg1
LV UUID JQZ193-dz6A-I0Ue-rTKC-6XrQ-gb1F-Qy9kDl
LV Write Access read/write
LV Creation host, time iZt4nd6wiprf8foracovwqZ, 2022-01-08 23:28:45 +0800
LV Status available
# open 0
LV Size 299.99 GiB
Current LE 76798
Segments 2
Allocation inherit
Read ahead sectors auto
- currently set to 256
Block device 252:0

$ lvs -o lv_name,lv_attr,lv_size,seg_pe_ranges
LV Attr LSize PE Ranges
lv1 -wi-a----- 299.99g /dev/vdc:0-51198
lv1 -wi-a----- 299.99g /dev/vdb:0-25598

条带化逻辑卷 Striped Logic Volume

类似于raid0模式,在该模式下,多块磁盘均会分配PE给LV。可以通过-i参数来指定可以使用VG中多少个PV。

优点:将数据的读写压力分散到了多个磁盘,可以提升读写性能。
缺点:一个磁盘损坏后会导致数据丢失。

但在使用striped模式时,需要注意:

  1. 如果PV来自于同一个磁盘的不同分区,会导致更多的随机读写,不仅不能提升磁盘性能,反而会导致性能下降。
  2. 如果VG中某一个PV过小,则无法将所有的PV平均使用起来,存在木桶效应。

使用--type striped来指定为striped模式,--stripes来指定需要使用的pv数量,--stripesize指定写足够数量的数据后再更换为另外一个pv。在下面的创建命令中,可以看到创建出来了12800个LE,PE是平均分配到了两个pv上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ lvcreate --type striped --stripes 2 --stripesize 32k -L 50G -n lv1 vg1
Logical volume "lv1" created.

$ lvs -o lv_name,lv_attr,lv_size,seg_pe_ranges
LV Attr LSize PE Ranges
lv1 -wi-a----- 50.00g /dev/vdb:0-6399 /dev/vdc:0-6399

$ lvdisplay
--- Logical volume ---
LV Path /dev/vg1/lv1
LV Name lv1
VG Name vg1
LV UUID FnOmCM-IkuE-3Rvv-fQqk-Cv1Q-lb8S-l5X7O3
LV Write Access read/write
LV Creation host, time iZt4nd6wiprf8foracovwqZ, 2022-01-09 00:07:08 +0800
LV Status available
# open 0
LV Size 50.00 GiB
Current LE 12800
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 256
Block device 252:0

如果指定的lv大小为250G,由于分配到两块磁盘上,由于最小的磁盘只有100G,100G * 2无法满足250G的磁盘大小需求,此时会报错。

1
2
lvcreate --type striped --stripes 2 --stripesize 32k -L 250G -n lv1 vg1
Insufficient suitable allocatable extents for logical volume lv1: 12802 more required

镜像逻辑卷 Mirror Logic Volume

类似于raid1。可以解决磁盘的单点问题,一块磁盘挂掉后不至于丢失数据。

通过使用--type mirror来指定为镜像模式,-m参数来指定冗余的数量。

常见问题

报错 “Device /dev/sdd excluded by a filter.”

当执行 pvcreate /dev/sdd 命令是报错信息如下:

1
Device /dev/sdd excluded by a filter.

检查下 /etc/lvm/lvm.conf 文件中的 filter 字段是否将磁盘过滤掉了。

执行 pvcreate /dev/sdd -vvv可以查看更详细的报错信息。

可以通过执行wipefs -a /dev/sdd 命令后再执行 pvcreate。

ref

《金字塔原理》是一本教你如何清晰的思考问题,表达观点的畅销书籍,但其并非万能的,其层层架构建立在因果关系基础之上。但现实世界中的很多问题并非简单的因果关系,此时金字塔原理就用不上了。

什么是金字塔原理

定义:任何一件事情都可以归纳出一个中心论点,而这个中心论点由3到7个论据进行支撑。每一个论据它本身又可以拆分成一个论点,该论点同样也可以拆分成3到7个论据,如此重复形状就像金字塔一样。

三个注意点:

  • 结论先行
  • 每层结论下面的论据不要超过7个
  • 每一个论点要言之有物,有明确的思想

组织思想的方法

在有了论点之后,论据该怎么拆分呢?可以按照下面四个原则来组织思想:

  • 时间顺序
  • 空间顺序
  • 重要性顺序,比如按照老弱优先原则
  • 逻辑演绎顺序。所以的逻辑演绎,就是三大段:大前提、小前提和结论。比如,凡人皆有一死,苏格拉底是人,所以苏格拉底也会死。但不推荐用此方法,因为对于听众的要求比较高,必须能够集中注意力才能听明白。

在思考的过程中,如果还没有论点,也可以使用上述方法先得出论据,然后推理出论点。
梳理出的论据必须符合MECE法则:每一个论点下的论据,都应该相互独立,但又可以完全穷尽,即论据要做到不遗漏不重叠。

如何让别人对自己的观点感兴趣

SCQ法则:Situation(设置背景)、Complication(冲突)和Question(疑问)。背景即先介绍大家都认同的背景信息从而引入话题。在SCQ都讲完后,就可以引入自己的论点了。

参考

  • 得到 - 《金字塔原理》成甲解读

最近因为《致阿里》的缘故,读了不少阿里内网的热帖,很多都是洋洋洒洒几千字,而且说的有理有据。如果换做是我,很多帖子哪怕我憋上一天都是写不出来的。我一直在思考,我到底比别人差在了哪里。思来想去,其中一个原因是因为平时的思考总结不够,缺少积累。

恰巧看了张一鸣在字节跳动9周年演讲,三观跟我特别合,想以贴为例,一来分享一下我个人的内心想法加深对演讲内容的认识,二来可以依次来锻炼自己的逻辑归纳能力。后续如有特别值得学习的演讲,也会分享一下自己的学习和思考,比如2020年张小龙的微信公开课的演讲就特别值得学习。

文中提到最多的词莫过于“平常心”了,“平常心”是一个佛源词,看来张一鸣没少研究佛学,整篇演讲显得也比较佛系,对于平常心的最直白的解释就是:

| 吃饭的时候好好吃饭,睡觉的时候好好睡觉

要想做到“好好吃饭,好好睡觉”对我来说是挺难的,我举一个简单的例子。平常周末来说,特别想睡一个懒觉,如果在睡觉前自己明确知道因为工作没有完成,第二天早上会有人找我,那么第二天早上一定会醒的比较早,想睡个懒觉都很难,即使第二天上午并不一定有人在我醒之前找到我。说明因为有事情的缘故,已经在无形中影响了睡眠质量。再举个例子,春节假期的睡眠质量明显会高于平常周末的睡眠质量,原因是心里总有各种工作的事情不能完全放下。如果将春节假期的心态为平常心,那么一年中的其他时间对我而言都不是平常心。

每家公司在年初的时候总会定义一些目标,比如全年营收目标为1个亿。一旦有了营收目标后,那么大家的工作重心一定会围绕的着目标展开。因为毕竟公司的资源是有限的,但是为了达成营收的目标,并不能保持一颗“平常心”来工作,焦虑的员工很难打磨出最好的产品,往往其他的地方就不会做的太好,比如用户体验、代码质量等等,一些本来很好的点子因为有营收目标的缘故,也很难展开实施,从而导致一些创新项目的流失。

接下来就是一些平常心的工作原则,总结下来有如下几点:

  • 平常心对待自己,平常人做非常事
  • 平常心对待预期,没有预期和标签的束缚会发挥的更好
  • 平常心对待过去和未来,关注当下
  • 平常心对待竞争对手
  • 平常心对待业务
  • 平常心对待成功和失败

文中特意提到了互联网八股文的一段话,这里就不再摘出来,我平时也没少见到类似的话术,看完后的感觉就是真牛逼,打死我都写不出来,但转头就忘记讲啥了,也许就是当时压根就看不懂,因为这些话太抽象了。互联网行业本身是一个特别务实接地气的行业,现在也渐渐在内卷严重,抽象的词汇也越来越多,期望下一个风口的到来,或许会将这股邪气吹走一些。

引用

张一鸣演讲全文:外部波澜起伏,内心平静如常

异常处理分为error和defer和recover两类,其中error用来处理可预期的异常,recover用来处理意外的异常。

error

支持多个返回值,可以将业务的返回值和错误的返回值分开,很多都会返回两个值。如果不使用error返回值,可以用_变量来忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// parseConfig returns a parsed configuration for an Azure cloudprovider config file
func parseConfig(configReader io.Reader) (*Config, error) {
var config Config

if configReader == nil {
return &config, nil
}

configContents, err := ioutil.ReadAll(configReader)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(configContents, &config)
if err != nil {
return nil, err
}

// The resource group name may be in different cases from different Azure APIs, hence it is converted to lower here.
// See more context at https://github.com/kubernetes/kubernetes/issues/71994.
config.ResourceGroup = strings.ToLower(config.ResourceGroup)
return &config, nil
}


error的几种使用方式:

使用error的方式 说明 举例
errors.New 简单静态字符串的错误,没有额外的信息 errors.New(“shell not specified”)
fmt.Errorf 用于格式化的错误字符串 fmt.Errorf(“failed to start kubernetes.io/kube-apiserver-client-kubelet certificate controller: %v”, err)
实现Error()方法的自定义类型 客户段需要检测并处理该错误时使用该方式 见下文自定义error
Error wrapping Go 1.13支持的特性

errors.New

原则:

  • 不要在客户端判断error中的包含字符串信息。
BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package foo

func Open() error {
return errors.New("could not open")
}

// package bar

func use() {
if err := foo.Open(); err != nil {
if err.Error() == "could not open" {
// handle
} else {
panic("unknown error")
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// handle
} else {
panic("unknown error")
}
}

当然也可以使用自定义error类型,但此时由于要实现自定义error类型,代码量会增加。

自定义error

error是个接口,可以用来扩展自定义的错误处理。

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
// file: k8s.io/kubernetes/pkg/volume/util/nestedpendingoperations/nestedpendingoperations.go

// NewAlreadyExistsError returns a new instance of AlreadyExists error.
func NewAlreadyExistsError(operationName string) error {
return alreadyExistsError{operationName}
}

// IsAlreadyExists returns true if an error returned from
// NestedPendingOperations indicates a new operation can not be started because
// an operation with the same operation name is already executing.
func IsAlreadyExists(err error) bool {
switch err.(type) {
case alreadyExistsError:
return true
default:
return false
}
}

type alreadyExistsError struct {
operationName string
}

var _ error = alreadyExistsError{}

func (err alreadyExistsError) Error() string {
return fmt.Sprintf(
"Failed to create operation with name %q. An operation with that name is already executing.",
err.operationName)
}

还可以延伸出更复杂一些的树形error体系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package net

type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}



type UnknownNetworkError string

func (e UnknownNetworkError) Error() string

func (e UnknownNetworkError) Temporary() bool

func (e UnknownNetworkError) Timeout() bool

Error Wrapping

error类型仅包含一个字符串类型的信息,如果函数的调用栈信息为A -> B -> C,如果函数C返回err,在函数A处打印err信息,那么很难判断出err的真正出错位置,不利于快速定位问题。我们期望的效果是在函数A出打印err,能够精确的找到err的源头。


为了解决上述问题,需要error类型在函数调用栈之间传递,有如下解决方法:


使用fmt.Errorf()来封装error信息,基于已经存在的error再产生一个新的error类型,需要避免error中包含冗余信息。

BadGood
1
2
3
4
5
6
// err: failed to call api: connection refused
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %s", err)
}
1
2
3
4
5
6
// err: call api: connection refused
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %s", err)
}
1
2
failed to create new store: failed to call api: connection refused
error中会有很多的冗余信息
1
2
new store: call api: connection refused
error中没有冗余信息,同时包含了调用栈信息

但使用fmt.Errorf()来全新封装的error信息的缺点也非常明显,丢失了最初的err信息,已经在中间转换为了全新的err。

类型断言

类型转换如果类型不正确,会导致程序crash,必须使用类型判断来判断类型的正确性。

BadGood
1
t := i.(string)
1
2
3
4
t, ok := i.(string)
if !ok {
// handle the error gracefully
}

panic

用于处理运行时的异常情况。
image.png
使用原则

  • 不要使用panic,在kubernetes项目中几乎没有使用panic的场景
  • 即使使用panic后,一定要使用recover会捕获异常
  • 在测试用例中可以使用panic
BadGood
1
2
3
4
5
6
7
8
9
10
func run(args []string) {
if len(args) == 0 {
panic("an argument is required")
}
// ...
}

func main() {
run(os.Args[1:])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func run(args []string) error {
if len(args) == 0 {
return errors.New("an argument is required")
}
// ...
return nil
}

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

client-go

client-go利用队列来进行重试


https://github.com/kubernetes/client-go/blob/master/examples/workqueue/main.go#L93

kube-builder

kube-builder为client-go的更上次封装,本质上跟client-go利用队列来进行重试的机制完全一致。

发生了错误后该如何处理

  • 打印错误日志
  • 根据业务场景选择忽略或者自动重试
  • 程序自己crash

如何避免

  • 在编写代码时增加防御式编程意识,不能靠契约式编程。一个比较简单的判断错误处理情况的方法,看下代码中if语句占用的比例。https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_volumes.go
  • 需求的评估周期中,不仅要考虑到软件开发完成的时间,同时要考虑到单元测试(单元测试用例的编写需要较长的时间)和集成测试的时间
  • 单元测试覆盖率提升,测试场景要考虑到各种异常场景
0%