Kubernetes 基础架构
下图是 Kubernetes 集群的架构图(引用自 Kubernetes 官方文档),从图中你可以看到,它由 Master 节点和 Node 节点组成。
控制面 Master 节点主要包含以下组件:
- kube-apiserver,负责对外提供集群各类资源的增删改查及 Watch 接口,它是 Kubernetes 集群中各组件数据交互和通信的枢纽。kube-apiserver 在设计上可水平扩展,高可用 Kubernetes 集群中一般多副本部署。当收到一个创建 Pod 写请求时,它的基本流程是对请求进行认证、限速、授权、准入机制等检查后,写入到 etcd 即可。
- kube-scheduler 是调度器组件,负责集群 Pod 的调度。基本原理是通过监听 kube-apiserver 获取待调度的 Pod,然后基于一系列筛选和评优算法,为 Pod 分配最佳的 Node 节点。
- kube-controller-manager 包含一系列的控制器组件,比如 Deployment、StatefulSet 等控制器。控制器的核心思想是监听、比较资源实际状态与期望状态是否一致,若不一致则进行协调工作使其最终一致。
- etcd 组件,Kubernetes 的元数据存储。
Node 节点主要包含以下组件:
- kubelet,部署在每个节点上的 Agent 的组件,负责 Pod 的创建运行。基本原理是通过监听 APIServer 获取分配到其节点上的 Pod,然后根据 Pod 的规格详情,调用运行时组件创建 pause 和业务容器等。
- kube-proxy,部署在每个节点上的网络代理组件。基本原理是通过监听 APIServer 获取 Service、Endpoint 等资源,基于 Iptables、IPVS 等技术实现数据包转发等功能。
从 Kubernetes 基础架构介绍中你可以看到,kube-apiserver 是唯一直接与 etcd 打交道的组件,各组件都通过 kube-apiserver 实现数据交互,它们极度依赖 kube-apiserver 提供的资源变化监听机制。而 kube-apiserver 对外提供的监听机制,也正是由我们基础篇08中介绍的 etcd Watch 特性提供的底层支持。
创建 Pod 案例
下面是创建一个 nginx 服务的 YAML 文件,Workload 是 Deployment,期望的副本数是 1。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
假设此 YAML 文件名为 nginx.yaml,首先我们通过如下的 kubectl create -f nginx.yml 命令创建 Deployment 资源。
$ kubectl create -f nginx.yml
deployment.apps/nginx-deployment created
创建之后,我们立刻通过如下命令,带标签查询 Pod,输出如下:
$ kubectl get po -l app=nginx
NAME READY STATUS RESTARTS AGE
nginx-deployment-756d9fd5f9-fkqnf 1/1 Running 0 8s
kube-apiserver 请求执行链路
下图是 kube-apiserver 的请求执行链路(引用自 sttts 分享的 PDF),当收到一个请求后,它主要经过以下处理链路来完成以上若干职责后,才能与 etcd 交互。
- 认证模块,校验发起的请求的用户身份是否合法。支持多种方式,比如 x509 客户端证书认证、静态 token 认证、webhook 认证等。
- 限速模块,对请求进行简单的限速,默认读 400/s 写 200/s,不支持根据请求类型进行分类、按优先级限速,存在较多问题。Kubernetes 1.19 后已新增 Priority and Fairness 特性取代它,它支持将请求重要程度分类进行限速,支持多租户,可有效保障 Leader 选举之类的高优先级请求得到及时响应,能防止一个异常 client 导致整个集群被限速。
- 审计模块,可记录用户对资源的详细操作行为。
- 授权模块,检查用户是否有权限对其访问的资源进行相关操作。支持多种方式,RBAC(Role-based access control)、ABAC(Attribute-based access control)、Webhhook 等。Kubernetes 1.12 版本后,默认授权机制使用的 RBAC。
- 准入控制模块,提供在访问资源前拦截请求的静态和动态扩展能力,比如要求镜像的拉取策略始终为 AlwaysPullImages。
- authorization 授权
- authentication 验证
- audit 审计
- impersonation 冒充
Kubernetes 资源存储格式
资源查询方式:
- 首先是按具体资源名称查询。它本质就是个 key-value 查询,只需要写入 etcd 的 key 名称与资源 key 一致即可。
- 其次是按 namespace 查询。因为我们知道 etcd 支持范围查询,若 key 名称前缀包含 namespace、资源类型,查询的时候指定 namespace 和资源类型的组合的最小开始区间、最大结束区间即可。
- 最后是标签名查询。这种查询方式非常灵活,业务可随时添加、删除标签,各种标签可相互组合。实现标签查询的办法主要有以下两种:
- 方案一,在 etcd 中存储标签数据,实现通过标签可快速定位(时间复杂度 O(1))到具体资源名称。然而一个标签可能容易实现,但是在 Kubernetes 集群中,它支持按各个标签组合查询,各个标签组合后的数量相当庞大。在 etcd 中维护各种标签组合对应的资源列表,会显著增加 kube-apiserver 的实现复杂度,导致更频繁的 etcd 写入。
- 方案二,在 etcd 中不存储标签数据,而是由 kube-apiserver 通过范围遍历 etcd 获取原始数据,然后基于用户指定标签,来筛选符合条件的资源返回给 client。此方案优点是实现简单,但是大量标签查询可能会导致 etcd 大流量等异常情况发生。
Kubernetes 集群选择的是哪种实现方式呢?
下面是一个 Kubernetes 集群中的 coredns 一系列资源在 etcd 中的存储格式:
/registry/clusterrolebindings/system:coredns
/registry/clusterroles/system:coredns
/registry/configmaps/kube-system/coredns
/registry/deployments/kube-system/coredns
/registry/events/kube-system/coredns-7fcc6d65dc-6njlg.1662c287aabf742b
/registry/events/kube-system/coredns-7fcc6d65dc-6njlg.1662c288232143ae
/registry/pods/kube-system/coredns-7fcc6d65dc-jvj26
/registry/pods/kube-system/coredns-7fcc6d65dc-mgvtb
/registry/pods/kube-system/coredns-7fcc6d65dc-whzq9
/registry/replicasets/kube-system/coredns-7fcc6d65dc
/registry/secrets/kube-system/coredns-token-hpqbt
/registry/serviceaccounts/kube-system/coredns
从中你可以看到,一方面 Kubernetes 资源在 etcd 中的存储格式由 prefix + “/“ + 资源类型 + “/“ + namespace + “/“ + 具体资源名组成,基于 etcd 提供的范围查询能力,非常简单地支持了按具体资源名称查询和 namespace 查询。
类似树结构.
kube-apiserver 提供了如下参数给你配置 etcd prefix,并支持将资源存储在多个 etcd 集群。
--etcd-prefix string Default: "/registry"
The prefix to prepend to all resource paths in etcd.
--etcd-servers stringSlice
List of etcd servers to connect with (scheme://ip:port), comma separated.
--etcd-servers-overrides stringSlice
Per-resource etcd servers overrides, comma separated. The individual override format: group/resource#servers, where servers are URLs,
semicolon separated.
另一方面,我们未看到任何标签相关的 key。Kubernetes 实现标签查询的方式显然是方案二,即由 kube-apiserver 通过范围遍历 etcd 获取原始数据,然后基于用户指定标签,来筛选符合条件的资源返回给 client(资源 key 的 value 中记录了资源 YAML 文件内容等,如标签)。
也就是当你执行”kubectl get po -l app=nginx”命令,按标签查询 Pod 时,它会向 etcd 发起一个范围遍历整个 default namespace 下的 Pod 操作。
$ kubectl get po -l app=nginx -v 8
I0301 23:45:25.597465 32411 loader.go:359] Config loaded from file /root/.kube/config
I0301 23:45:25.603182 32411 round_trippers.go:416] GET https://ip:port/api/v1/namespaces/default/pods?
labelSelector=app%3Dnginx&limit=500
etcd 收到的请求日志如下,由此可见当一个 namespace 存在大量 Pod 等资源时,若频繁通过 kubectl,使用标签查询 Pod 等资源,后端 etcd 将出现较大的压力。
{
"level":"debug",
"ts":"2021-03-01T23:45:25.609+0800",
"caller":"v3rpc/interceptor.go:181",
"msg":"request stats",
"start time":"2021-03-01T23:45:25.608+0800",
"time spent":"1.414135ms",
"remote":"127.0.0.1:44664",
"response type":"/etcdserverpb.KV/Range",
"request count":0,
"request size":61,
"response count":11,
"response size":81478,
"request content":"key:"/registry/pods/default/" range_end:"/registry/pods/default0" limit:500 "
}
通用存储模块
下面是 kube-apiserver 通用存储模块的创建流程图:
下面是 Deployment 资源的创建策略实现,它会进行将 deployment.Generation 设置为 1 等操作。
// PrepareForCreate clears fields that are not allowed to be set by end users on creation.
func (deploymentStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
deployment := obj.(*apps.Deployment)
deployment.Status = apps.DeploymentStatus{}
deployment.Generation = 1
pod.DropDisabledTemplateFields(&deployment.Spec.Template, nil)
}
资源安全创建及更新
我们知道 etcd 提供了 Put 和 Txn 接口给业务添加 key-value 数据,但是 Put 接口在并发场景下若收到 key 相同的资源创建,就会导致被覆盖。因此 Kubernetes 很显然无法直接通过 etcd Put 接口来写入数据。
而我们09节中介绍的 etcd 事务接口 Txn,它正是为了多 key 原子更新、并发操作安全性等而诞生的,它提供了丰富的冲突检查机制。
Kubernetes 集群使用的正是事务 Txn 接口来防止并发创建、更新被覆盖等问题。当执行完 BeforeCreate 策略后,这时 kube-apiserver 就会调用 Storage 的模块的 Create 接口写入资源。1.6 版本后的 Kubernete 集群默认使用的存储是 etcd3,它的创建接口简要实现如下:
// Create implements storage.Interface.Create.
func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64) error {
......
key = path.Join(s.pathPrefix, key)
opts, err := s.ttlOpts(ctx, int64(ttl))
if err != nil {
return err
}
newData, err := s.transformer.TransformToStorage(data, authenticatedDataString(key))
if err != nil {
return storage.NewInternalError(err.Error())
}
startTime := time.Now()
txnResp, err := s.client.KV.Txn(ctx).If(
notFound(key),
).Then(
clientv3.OpPut(key, string(newData), opts...),
).Commit
etcd 收到 kube-apiserver 的请求是长什么样子的呢?
{
"level":"debug",
"ts":"2021-02-11T09:55:45.914+0800",
"caller":"v3rpc/interceptor.go:181",
"msg":"request stats",
"start time":"2021-02-11T09:55:45.911+0800",
"time spent":"2.697925ms",
"remote":"127.0.0.1:44822",
"response type":"/etcdserverpb.KV/Txn",
"request count":1,
"request size":479,
"response count":0,
"response size":44,
"request content":"compare:<target:MOD key:"/registry/deployments/default/nginx-deployment" mod_revision:0 > success:<request_put:<key:"/registry/deployments/default/nginx-deployment" value_size:421 >> failure:<>"
}
从这个请求日志中,你可以得到以下信息:
- 请求的模块和接口,KV/Txn;
- key 路径,/registry/deployments/default/nginx-deployment,由 prefix + “/“ + 资源类型 + “/“ + namespace + “/“ + 具体资源名组成;
- 安全的并发创建检查机制,mod_revision 为 0 时,也就是此 key 不存在时,才允许执行 put 更新操作。
Watch 机制在 Kubernetes 中应用
正如我们基础架构中所介绍的,kube-controller-manager 组件中包含一系列 WorkLoad 的控制器。Deployment 资源就由其中的 Deployment 控制器来负责的,那么它又是如何感知到新建 Deployment 资源,最终驱动 ReplicaSet 控制器创建出 Pod 的呢?
获取数据变化的方案,主要有轮询和推送两种方案组成。轮询会产生大量 expensive request,并且存在高延时。而 etcd Watch 机制提供的流式推送能力,赋予了 kube-apiserver 对外提供数据监听能力。
Resource Version 与 etcd 版本号
Resource Version 是 Kubernetes API 中非常重要的一个概念,顾名思义,它是一个 Kubernetes 资源的内部版本字符串,client 可通过它来判断资源是否发生了变化。同时,你可以在 Get、List、Watch 接口中,通过指定 Resource Version 值来满足你对数据一致性、高性能等诉求。