404频道

学习笔记

术语

  • host cluster:virtual cluster 中的宿主 k8s 集群,承载了所有的计算资源。也会被叫做 super cluster。
  • virtual cluster:virtual cluster 中的租户 k8s 集群,通常简写 vc。也会被叫做 tenant cluster。
  • vCluster:k8s virtual cluster 的实现之一,即本文中要介绍的方案。

项目简介

k8s 的多租功能

image.png
k8s 自身在多租的能力上支持较差,提供了 namespace 级别的隔离,不同的租户使用不同的 namespace,但该隔离功能较弱。很多组件部署在同一个 workload 会存在诸多问题:

  1. 使用全局对象存在冲突,比如 CRD。
  2. 存在诸多安全性问题,比如多个租户之间的 pod 完全可以互访,没有任何隔离机制。
  3. 不同组件对于 k8s 的版本不统一。

为了解决多租的问题,最简单的思路就是使用多 k8s 集群,业界的 KubeFed v2、karmada、clusternet、OCM 等均为多 k8s 集群的实现。但多 k8s 集群因为存在独立的控制面和计算资源,存在资源消耗过多的问题。

还有一个中间思路为仅做 k8s 的控制面隔离,计算资源仍然共享,即 pod 也可以解决很多的多租隔离问题。k8s 的控制面隔离又存在两个主要方案:

  1. 独立的 kube-apiserver 和 etcd、kube-controller-manager, kube-scheduler 共享 host cluster。该方案中有独立的 kube-apiserver 组件,这里的 etcd 可以被 sqllite、mysql 等存储取代。该方案统称为 virtual cluster,简称为 vc。
  2. 独立的 proxy apiserver,kube-apiserver、kube-controller-manager、kube-scheduler 共享 host cluster。访问 k8s 的请求先到 proxy apiserver,proxy apiserver 转发到 host cluster 的 kube-apiserver。可以在 proxy apiserver 中提供独立的 RBAC 机制,实现一定程度的隔离。该方案在开源中未看到具体的实现。

vCluster 介绍

vCluster 为 virtual cluster 的开源实现之一,由 Loft Labs 提供,Github Star 3.7K,代码行数 4 万行。除了开源版本外,还提供了商业版本 vCluster PRO。

vCluster 设计原则:

  1. 最小化资源占用。在实现上使用了单 pod 的 k3s 作为 k8s 的控制面。
  2. 复用 host cluster 的计算、存储和网络资源。
  3. 降低对 host cluster 的请求。
  4. 简单灵活。
  5. 不需要 host cluster 的管理员权限。
  6. vCluster 多个 namespace 下的对象映射到 host cluster 的同一个 namespace 下。同时也可以支持 vCluster 的一个 namespace 对应 host cluster 的一个 namespace。
  7. 易清理。

Getting Started

k8s 集群准备

k8s 集群这里使用了 kind 方案,kind 配置 kind.conf 如下:

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
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: vcluster
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:
apiServerPort: 6443

执行 kind create cluster --config kind.conf 即可创建 k8s 集群,包含了一个 control-plane 节点,三个 worker 节点。

安装 vcluster

vCluster 提供了使用 vcluster cli、helm 和 kubectl 三种安装方式,使用 vcluster cli 最为简单,其底层同样采用 helm chart 的方式部署,下面采用 vcluster cli 的方式进行安装。
安装 vcluster cli 工具:

1
curl -L -o vcluster "https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-darwin-arm64" && sudo install -c -m 0755 vcluster /usr/local/bin && rm -f vcluster

或者执行 brew install vcluster安装 vcluster 命令行工具。

执行命令 vcluster create my-vcluster 创建 virtual cluster。会在 host cluster 上创建 namespace vcluster-my-vcluster,该 namespace 下创建如下对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ kubectl get all -n vcluster-my-vcluster
NAME READY STATUS RESTARTS AGE
pod/coredns-68559449b6-l5whx-x-kube-system-x-my-vcluster 1/1 Running 2 (5m45s ago) 3d
pod/my-vcluster-0 1/1 Running 2 (5m45s ago) 3d

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kube-dns-x-kube-system-x-my-vcluster ClusterIP 10.96.67.119 <none> 53/UDP,53/TCP,9153/TCP 3d
service/my-vcluster NodePort 10.96.214.58 <none> 443:30540/TCP,10250:31621/TCP 3d
service/my-vcluster-headless ClusterIP None <none> 443/TCP 3d
service/my-vcluster-node-vcluster-control-plane ClusterIP 10.96.69.115 <none> 10250/TCP 3d

NAME READY AGE
statefulset.apps/my-vcluster 1/1 3d

可以看到在该 namespace 下创建了 coredns 和 StatefulSet my-vcluster。每个租户有独立的 coredns 组件,用来做域名解析。my-vcluster 为 vCluster 的管控面组件,包括了 k8s controller plane 和 syncer 组件。

执行 vcluster connect my-vcluster 后会在本地启动代理,并自动切换本地的 kubeconfig context,将 context 切换到 virtual cluster。执行 kubectl 命令即可连接到对应的 k8s 集群。virtual cluster 集群中的信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ kubectl get all -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system pod/coredns-68559449b6-jg2bs 1/1 Running 1 (20m ago) 51m

NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-system service/kube-dns ClusterIP 10.96.120.59 <none> 53/UDP,53/TCP,9153/TCP 51m
default service/kubernetes ClusterIP 10.96.35.53 <none> 443/TCP 51m

NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
kube-system deployment.apps/coredns 1/1 1 1 51m

NAMESPACE NAME DESIRED CURRENT READY AGE
kube-system replicaset.apps/coredns-68559449b6 1 1 1 51m

在 virtual cluster 可以看到仅包含了 coredns 组件。

在 virtual cluster 和在 host cluster 中的 node 信息:

1
2
3
4
5
6
7
8
9
10
$ kubectl get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
vcluster-worker3 Ready <none> 51m v1.27.3 10.96.118.228 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1

$ kubectl get node -o wide --context kind-vcluster
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
vcluster-control-plane Ready control-plane 86m v1.27.3 172.19.0.3 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1
vcluster-worker Ready <none> 85m v1.27.3 172.19.0.2 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1
vcluster-worker2 Ready <none> 85m v1.27.3 172.19.0.5 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1
vcluster-worker3 Ready <none> 85m v1.27.3 172.19.0.4 <none> Debian GNU/Linux 11 (bullseye) 5.10.76-linuxkit containerd://1.7.1

可以看到在 virtual cluster 和 host cluster 中的 node 名字相同,这是因为 node 在 vCluster 中并没有做隔离,而是从 host cluster 中做了同步。但 virtual cluster 中的 node 节点仅包含 pod 在 host cluster 中已经使用的 node 节点,未使用的节点并不会在 virtual cluster 上。
同时可以看到 vc 和 host cluster 中的 node ip 地址并不相同,vc 中的 node ip 地址跟 host cluster 中的对应 service clusterip 相同,在 host cluster 中的对应 service 名字为 $vClusterName-node-$hostClusterNodeName

1
2
3
$ kubectl get svc --context kind-vcluster -n vcluster-my-vcluster my-vcluster-node-vcluster-worker3
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-vcluster-node-vcluster-worker3 ClusterIP 10.96.118.228 <none> 10250/TCP 3h54m

在 virtual cluster 中创建 k8s 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在 virtual cluster 创建 namespace
$ kubectl create ns demo-nginx
namespace/demo-nginx created

# 创建 Deployment
$ kubectl create deployment nginx-deployment -n demo-nginx --image=nginx

# 在 virtual cluster 上创建出了 pod
$ kubectl get pod -n demo-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-66fb7f764c-dn59g 1/1 Running 0 11m 10.244.0.7 vcluster-control-plane <none> <none>

# 由于 pod 调度了 host cluster 新节点,在 virtual cluster 中可以看到新的 k8s node,k8s node 为刚刚创建
$ kubectl get node
NAME STATUS ROLES AGE VERSION
vcluster-worker2 Ready <none> 2m45s v1.27.3
vcluster-worker3 Ready <none> 57m v1.27.3

在 host cluster 中看到如下对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在 host cluster 上并没有对应的 namespace demo-nginx
$ kubectl get ns --context kind-vcluster demo-nginx
Error from server (NotFound): namespaces "demo-nginx" not found

# 在 host cluster 上并没有对应的 deployment nginx-deployment
$ kubectl get deploy -A --context kind-vcluster
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
ingress-nginx ingress-nginx-controller 1/1 1 1 90m
kube-system coredns 2/2 2 2 91m
local-path-storage local-path-provisioner 1/1 1 1 91m

# 但在 host cluster 上却看到了对应的 pod,位于 vcluster 统一的 namespace vcluster-my-vcluster 之下
$ kubectl get pod -n vcluster-my-vcluster --context kind-vcluster
NAME READY STATUS RESTARTS AGE
coredns-68559449b6-jg2bs-x-kube-system-x-my-vcluster 1/1 Running 1 (26m ago) 57m
my-vcluster-0 1/1 Running 1 (26m ago) 86m
nginx-deployment-66fb7f764c-sffqt-x-demo-nginx-x-my-vcluster 1/1 Running 0 2m22s

可以看到仅 pod 在 host cluster 中同步存在,而 namespace、deployment 这些对象仅存在于 virtual cluster 中。在 virtual cluster 中创建的多个不同 namespace pod 仅会存在于 host cluster 的同一个 namespace 下。

验证 service

在 virtual cluster 中创建 service 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx
kubernetes.io/cluster-service: "true"
name: nginx
namespace: demo-nginx
spec:
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx-deployment
type: ClusterIP

在 virtual cluster 中包含如下的 Service 对象:

1
2
3
$ kubectl get svc -n demo-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP 10.96.239.53 <none> 80/TCP 2m11s

在 host cluster 中会同步创建如下的 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
apiVersion: v1
kind: Service
metadata:
annotations:
vcluster.loft.sh/object-name: nginx
vcluster.loft.sh/object-namespace: demo-nginx
vcluster.loft.sh/object-uid: 5ab7aa9c-90b6-46f9-a162-9ea9ca9826f3
creationTimestamp: "2023-11-28T09:32:22Z"
labels:
vcluster.loft.sh/label-my-vcluster-x-a172cedcae: nginx
vcluster.loft.sh/label-my-vcluster-x-d9125f8911: "true"
vcluster.loft.sh/managed-by: my-vcluster
vcluster.loft.sh/namespace: demo-nginx
name: nginx-x-demo-nginx-x-my-vcluster
namespace: vcluster-my-vcluster
ownerReferences:
- apiVersion: v1
controller: false
kind: Service
name: my-vcluster
uid: 463a503e-d889-49a7-94e0-0cba5299dd47
resourceVersion: "12344"
uid: fc9ea383-996e-471c-9c27-ee1c22fec7a3
spec:
clusterIP: 10.96.239.53
clusterIPs:
- 10.96.239.53
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
vcluster.loft.sh/label-my-vcluster-x-a172cedcae: nginx-deployment
vcluster.loft.sh/managed-by: my-vcluster
vcluster.loft.sh/namespace: demo-nginx
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}

可以看到 host cluster 中的 service 的 ClusterIP 跟 virutal cluster 一致,但 spec.selector 字段已经被 syncer 修改,以便可以匹配到正确的 pod。

验证 Ingress

默认情况下 Ingress 不会同步到 host cluster,需要通过开关的方式启动。创建文件 values.yaml,内容如下:

1
2
3
sync:
ingresses:
enabled: true

执行 vcluster create my-vcluster --upgrade -f values.yaml 即可修改现在 vcluster 集群配置。

在 virtual cluster 中创建如下的 Ingress 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
labels:
app: nginx
name: nginx
namespace: demo-nginx
spec:
rules:
- host: nginx.aa.com
http:
paths:
- backend:
service:
name: nginx
port:
number: 80
path: /
pathType: ImplementationSpecific

查看 host cluster 中的 Ingress 信息如下:

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
$ kubectl get ingress --context kind-vcluster -n vcluster-my-vcluster -o yaml nginx-x-demo-nginx-x-my-vcluster
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
vcluster.loft.sh/object-name: nginx
vcluster.loft.sh/object-namespace: demo-nginx
vcluster.loft.sh/object-uid: 4b8034a6-1513-4ccd-b80a-66807d862b4e
creationTimestamp: "2023-11-28T10:07:52Z"
generation: 1
labels:
vcluster.loft.sh/label-my-vcluster-x-a172cedcae: nginx
vcluster.loft.sh/managed-by: my-vcluster
vcluster.loft.sh/namespace: demo-nginx
name: nginx-x-demo-nginx-x-my-vcluster
namespace: vcluster-my-vcluster
ownerReferences:
- apiVersion: v1
controller: false
kind: Service
name: my-vcluster
uid: 463a503e-d889-49a7-94e0-0cba5299dd47
resourceVersion: "16292"
uid: 1d0de02b-5aad-4858-a8cf-2caa345ca85b
spec:
rules:
- host: nginx.aa.com
http:
paths:
- backend:
service:
name: nginx-x-demo-nginx-x-my-vcluster
port:
number: 80
path: /
pathType: ImplementationSpecific
status:
loadBalancer:
ingress:
- hostname: localhost

可以看到 Ingress 中对应的 Service 名字已经修改了 host cluster 中对应的 Service 名字。

销毁

在使用完成后执行如下命令即可销毁 virtual cluster:

1
2
3
4
# 切换本地的 context
vcluster disconnect
# 删除 vcluster
vcluster delete my-vcluster

架构

image.png

组件

整个架构中,有两大核心组件:k8s Control Plane 和 syncer。其中 StatefulSet my-vcluster 中容器 syncer,默认情况下在该容器中同时启动了 k3s 容器作为 vCluster 控制平面和 vCluster 的 syncer 进程。

1
2
3
4
5
6
$ kubectl exec -it --context kind-vcluster -n vcluster-my-vcluster my-vcluster-0 -- ps -ef
Defaulted container "syncer" out of: syncer, vcluster (init)
PID USER TIME COMMAND
1 root 2:57 /vcluster start --name=my-vcluster --kube-config=/data/k3s
17 root 18:35 /k3s-binary/k3s server
46 root 0:00 ps -ef

controller plane

控制平面默认使用 k3s,存储使用 sqllite,也可以使用 etcd、mysql、postgresql。k8s 发行版也可以使用 k0s、Vanilla(标准 k8s)、第三方镜像等。控制平面由如下几个组件组成:

  1. k8s apiserver。
  2. 数据存储,比如 sqllite、etcd 等。
  3. kube-controller-manager
  4. kube-scheduler:可选组件,默认使用 host cluster 调度器。

在 Pro 版本中,允许控制面跟 pod 部署在不同的 host cluster。

syncer

virtual cluster 中并不包含实际的计算、存储和网络资源,syncer 的职责为将对象从 virtual cluster 同步到 host cluster,也有少部分对象需要从 host cluster 同步到 virtual cluster。
vCluster 将 k8s 对象划分为 low level 和 high level,其中 high level 的对象仅存在于 virtual cluster 中,比如 Deployment、CRD 等对象。low level 的对象会通过 syncer 模块同步到 host cluster 上,包括 Pod、ConfigMap、Secret 等。low level 的对象在 virutal cluster 为多个 namespace,但均会映射到 host cluster 的一个 namespace 下。另外,vCluster 也支持将 virtual cluster 的多个 namespace 映射到 host cluster 的多个 namespace,该特性目前处于 alpha 状态。
vCluster 可以通过配置的方式来定制资源的同步,更复杂的同步规则提供了插件机制实现。
vCluster 默认支持的同步资源列表:https://www.vcluster.com/docs/syncer/core_resources

已经创建完成的 syncer 配置,可以通过 vcluster create my-vcluster --upgrade -f values.yaml 的方式修改,该命令会调用 helm update,helm update 命令最终会修改 StatefulSet syncer 的配置,并触发 pod 的重启。

k8s node 同步

支持多种 node 的同步行为,通过修改 syncer 的启动参数:

  1. Fake Node:默认行为。根据 pod 中的 spec.nodeName 创建 Fake Node。Fake Node 为 syncer 服务自动创建。如果没有 pod 调度到 Fake Node 上,则 Fake Node 会自动删除。
  2. Real Node:根据 pod 中的 spec.nodeName 创建 Real Node,Real Node 的信息从 host cluster 同步。如果没有 pod 调度到 Real Node 上,则 Real Node 会自动删除。
  3. Real Node All:同步 host cluster 的所有 node 到 virtual cluster。如果要使用 DaemonSet,需要使用该模式。
  4. Real Nodes Label Selector:仅同步 label selector 匹配的 host node 到 virtual cluster 中。
  5. Real Nodes + Label Selector:仅同步包含在 pod spec.nodeName 且 Label selector 可以选中的 host cluster node 到 virtual cluster 中。

pod 调度

默认情况下,virtual cluster 中的 pod 调度会使用 host cluster 的调度,但存在如下的问题:

  1. 在 virtual cluster node 上的 label 对于 pod 调度不会生效。
  2. drait、trait 命令对于 virtual cluster 上的 pod 没有影响。
  3. virtual cluster 中使用自定义调度器不生效。

基于上述限制,vCluster 支持如下两种方案:

  1. 支持在 virtual cluster 中使用独立的调度器。可以给 virtual cluster 上的 node 增加标签、污点等信息,pod 的调度在 virtual cluster 中的调度器实现,syncer 组件仅将已经调度完成的 pod 同步到 host cluster。
  2. 仍然复用 host cluster 调度器,但做了部分功能的增强:在 syncer 服务中指定仅同步部分 host node 到 virtual cluster 中,这样 pod 就仅会调度到 host cluster 的特定 node 上。

网络

virutal cluster 中无独立的 pod 网络和 service 网络,完全复用 host cluster 的网络。

Service 网络

  1. 会从 virtual cluster 同步到 host cluster,两者的 clusterip 一致。
  2. 允许将一些 host cluster 中的 service 同步到 virtual cluster 中,同时指定service 的名字。
  3. 允许将 virtual cluster 中的 service 同步到 host cluster 中,同时指定service 的名字。

Ingress 网络

允许将 virtual cluster 中的 Ingress 同步到 host cluster,以便复用 host cluster 中的 Ingress Controller。

DNS 解析

在 virtual cluster 中部署了单独的 coredns 组件,默认情况下,在 vritual cluster 中的域名仅能解析内部的域名,不能解析 host cluster 上的域名。可以通过开关的方式,将 virtual cluster 中的域名解析转发到 host cluster 的 coredns。

在 PRO 版本中,coredns 组件可以集成到 syncer 组件内部,以便节省资源。

NetworkPolicy

默认情况下,vcluster 中会忽略 virtual cluster 中的 NetworkPolicy 资源。可以通过开关的方式打开该配置,即可将 NetworkPolicy 规则同步到 host cluster。

存储

image.png
默认情况下,host StorageClass 不会同步到 vc,可以通过开关的方式打开同步。
默认情况下,pv 不会从 vc 同步到 host cluster,可以通过开关的方式打开。

可观测性

monitoring

metrics-server 用来监控 k8s 的 Deployment、StatefulSet 等对象,metrics-server 可以复用 host cluster 中的,但需要启用 metrics server proxy 功能。也可以在 vc 中单独部署一套 metrics server。
在 vc 集群中,由于每个 k8s node 的 ip 地址为 host cluster 中的 service clusterip,在 vc 中网络是可达的,可以获取到对应的监控信息。

logging

需要用到Hostpath Mapper组件,该组件为 DaemonSet 的形式。后续即可以部署 loki 等组件。

安全

隔离模式

在启动的时候指定--isolate,在该模式下对 workload 做了多种限制。

  1. 对 vcluster pod 的 Pod Security 做限制,不符合规范的 pod 不会同步到 host cluster。
  2. 可以对 vc 中 pod 的总资源量做限制。
  3. 在 host cluster 上通过 NetworkPolicy 做隔离。

virtual cluster 集群的创建

目前仅能通过 vcluster cli、helm 的方式来创建,底层均为 helm chart 的方式来管理,缺少服务化功能。

virtual cluster 集群对外暴露方法

获取 kubeconfig

vcluster connect 命令

该命令可以修改本地的 kubeconfig 文件,并将 context 切换为 virtual cluster context。默认为 virutual cluster 的管理员权限,可以指定使用特定的 ServiceAccount。

host cluster secret 中获取到 kubeconfig

在 host cluster 中,在 vc 的 namespace 下,存在一个以 vc- 开头的 Secret,该 Secret 中保存了 kubeconfig 完整信息。

vc 集群中的 apiserver 的暴露地址

可以在 syncer 启动的时候指定获取的 kubeconfig 中的 endpoint 地址。endpoint 地址即为 vc 集群中的 kube-apiserver 的地址,该 kube-apiserver 的地址可以通过 host cluster 中的 Ingress、LoadBalancer Service、NodePort Service 等方式对外暴露。

高可用设计

control plane 高可用

k3s 可以支持高可用架构,在创建 vc 的时候通过指定的副本的方式来设置高可用。其他的 k8s 发行版同样类似的实现。

备份与恢复

vCluster 本身并没有提供对于 vc 集群的数据备份与恢复功能,可以通过通用的 velero 方式实现备份与恢复功能。

总结

未做网络隔离,容器网络和 service 网络仍然在同一个平面,要想相互隔离,必须使用 NetworkPolicy。

其他

  1. 获取 helm chart 到本地
helm repo add lofts https://charts.loft.sh/
helm fetch lofts/vcluster
``

CSI:Container Storage Interface (CSI)

Volume 的三个阶段:

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

开发

CSI 插件分为三个部分:

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

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

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

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

CSI Identity

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

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

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

External provisioner 会调用该接口。

CSI Controller

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
service Controller {
// Provisioning, External provisioner调用
// hostPath 实现中会调用 fallocate 实现
rpc CreateVolume (CreateVolumeRequest)
returns (CreateVolumeResponse) {}

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

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

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

rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)
returns (ValidateVolumeCapabilitiesResponse) {}

rpc ListVolumes (ListVolumesRequest)
returns (ListVolumesResponse) {}

rpc GetCapacity (GetCapacityRequest)
returns (GetCapacityResponse) {}

rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
returns (ControllerGetCapabilitiesResponse) {}

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

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

rpc ListSnapshots (ListSnapshotsRequest)
returns (ListSnapshotsResponse) {}

rpc ControllerExpandVolume (ControllerExpandVolumeRequest)
returns (ControllerExpandVolumeResponse) {}

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

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

CSI Node

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
service Node {
// 针对块存储类型,将块设备格式化后先挂载到一个临时全局目录
rpc NodeStageVolume (NodeStageVolumeRequest)
returns (NodeStageVolumeResponse) {}

rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
returns (NodeUnstageVolumeResponse) {}

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

rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
returns (NodeUnpublishVolumeResponse) {}

rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
returns (NodeGetVolumeStatsResponse) {}


rpc NodeExpandVolume(NodeExpandVolumeRequest)
returns (NodeExpandVolumeResponse) {}


rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
returns (NodeGetCapabilitiesResponse) {}

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

资料

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

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

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

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

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

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

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

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

优点:

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

客户机的前提条件:

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

服务端:

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

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

TFO的工作原理如下:

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

相关内核参数

net.ipv4.tcp_fastopen

支持如下值:

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

net.ipv4.tcp_fastopen_key

用来产生 TFO 的 Cookie。

zone 设置

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

kubelet 的启动参数

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

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

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

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

coredns 的配置文件

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

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

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

域名注册

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

Pod 域名注册

规则一:

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

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

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

规则二:

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

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

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

Deployment/DaemonSet 管理的 pod

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

Service 域名注册

普通 Service

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

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

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

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

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

ExternalName Service

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

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

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

externalIPs 字段

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

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

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

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

Headless Service

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

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

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

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

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

域名查询

待补充

资料

json patch

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

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

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

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

协议规范

add

格式如下:

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

规范:

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

原始 json 如下:

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

执行后结果如下:

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

remove

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

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

replace

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

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

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

move

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

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

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

copy

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

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

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

原始 json 如下:

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

执行完成后的 json 如下:

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

test

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

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

工具

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

总结

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

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

json merge patch

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

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

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

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

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

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

patch json 格式如下:

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

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

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

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

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

k8s strategic merge patch

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

背景

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

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

期望能够 patch 如下的内容

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

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

协议规范

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

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

支持如下指令:

  1. replace
  2. merge
  3. delete

replace

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

如下指令用来表示,

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

delete

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

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

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

1
2
rollingUpdate:
$patch: delete

merge

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

deleteFromPrimitiveList

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

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

原始 yaml 如下:

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

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

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

最终得到结果:

1
2
finalizers:
- a

setElementOrder

用于数组中的元素排序

简单数组排序例子

原始内容如下:

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

设置排序顺序:

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

最终得到排序顺序:

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

其中 patchMergeKey 为 name 字段

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

patch 指令的格式:

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

最终获得结果:

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

retainKeys

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

1
2
3
union:
foo: a
other: b

patch 内容:

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

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

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

strategic merge patch 在 k8s 中应用

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

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

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

TODO:待补充具体例子

kubectl patch、replace、apply之间的区别

patch

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

replace

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

apply

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

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

具体的操作步骤:

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

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

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

引用

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

所有节点均需初始化操作

所有节点均需做的操作。

主机准备

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

安装containerd

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

手工安装

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

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

安装 runc

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

yum 源安装

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

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

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

安装 kubeadm/kubelet/kubectl

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

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

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

sudo systemctl enable --now kubelet

单 master 节点模式

节点 角色
172.21.115.190
master 节点

kubeadm 初始化

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

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

执行命令:

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

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

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

安装网络插件

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

cilim 网络插件

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

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

# 网络插件初始化
cilium install

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

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

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

k8s 自带的 bridge 插件

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

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

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

添加其他节点

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

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

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

节点清理

清理普通节点

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

清理 control plan 节点

1
kubeadm reset

集群模式部署

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

资料

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

资源

kaniko

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

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

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

arc42

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

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

Carina

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

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

kube-capacity

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

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

Kubeprober

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

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

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

Open Policy Agent

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

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

docker-squash

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

FlowUs

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

k8tz

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

文章

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

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

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

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

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

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

书籍

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

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

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

ipv6的优势

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

ipv6的数据包格式

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

报头

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

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

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

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

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

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

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

  • 源ip地址:128bit

  • 目的ip地址:128bit

扩展报头

扩展报头的长度任意

ipv6地址

地址表示方法

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

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

网段表示方法

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

地址分类

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

单播地址

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

链路本地地址(LLA)

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

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

公网单播地址(GUA)

类似于 ipv4 的公网地址。

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

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

本地唯一单播地址(ULA)

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

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

loopback地址

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

未指定单播地址

::

多播地址(Multicast)

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

任意播地址(Anycast)

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

DNS服务和ipv6

双栈请求域名请求顺序

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

ipv6与 /etc/hosts文件

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

1
::1             localhost6.localdomain6 localhost6

检测域名是否支持ipv6

dig aaaa方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dig aaaa  ipv6.google.com.hk

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

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

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

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

使用网站测试

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

启用ipv6

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

系统级别

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

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

1
GRUB_CMDLINE_LINUX="xxxxx ipv6.disable=1"

网卡级别

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

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

nginx支持ipv6

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

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

配置 ipv6

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

参考资料

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

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

内容

基准代码

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

依赖

规范:

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

优点:

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

配置

规范:

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

后端服务

规范:

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

构建、发布和运行

规范:

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

反模式:

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

进程

规范:

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

端口绑定

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

并发

规范:

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

易处理

规范:

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

开发环境与生产环境等价

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

日志

规范:

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

反模式:

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

管理进程

规范:

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

参考

0%