深入剖析Kubernetes笔记
背景
张磊的深入剖析Kubernetes,最近学了下。做下笔记用于备忘。由于这个课程时间是18年的,我们现在得以辩证眼光看待里面的内容。读书笔记内容不一定都来自书上,有些是自己的思考或者搜集的信息,请留意。
课前必读
主要讲了一些历史,主要是k8s和docker的标准之争。google坚持创新,并且通过打容器编排和管理的生态的差异化特点来弯道超车,这点值得我们学习。另外,想要占领标准,足够开放是很重要的。k8s是民主化的架构,从API到运行时都暴露了足够的扩展点。
容器技术基本概念
Croups和Namespace
容器实现的基础是cgroups和namespace技术,这个技术由于是Linux系统本身提供的,控制的粒度很细、控制的内容很薄,所以十分轻量。这也是相比早期虚拟机技术的重要优势。
- namespace: 主要用于资源隔离。Linux提供多种资源的namespace,包括文件系统、pid、网络、IPC、hostname和用户等。Linux底层的系统调用clone、unshare、setns可以用于namespace相关操作。查看ns信息可以在ls -l /proc/${PID}/ns查看
- cgroups: 全称,Linux control group。主要用于**资源控制。**这些资源主要指的是硬件资源,例如cpu、memory、网络等。Linux系统重挂载cgroups后可以在/sys/fs/cgroup/下看到相关控制点包括cpu、blkio、memory等等。
TIPS:
- 容器是单进程模型。一个容器本质是个Linux层面应用namespace的应用进程。容器内其他进程都是容器内1号进程fork出来的子进程。
- 由于依赖Linux一些关键的系统调用,所以docker是需要root权限来执行的
根文件系统rootfs
容器创建的步骤实际上就三步:
- 启用 Linux Namespace 配置;
- 设置指定的 Cgroups 参数;
- 切换进程的根目录(Change Root),本质采用的是mount namespace,来改变根目录视图。具体的系统调用包括privot_root和chroot。这步执行时,容器进程可以看到宿主机上的整个文件系统
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
rootfs与容器镜像
docker提出的容器镜像实际上是一种增量制作rootfs的技术。引入layer的概念,配合union file system来达成。Union FS实际上就是将mnt到同一个挂载点的目录进行合并的技术。如下的多个Layer实际上就是五个增量 rootfs,每一层都是 Ubuntu 操作系统文件与目录的一部分;而在使用镜像时,Docker 会把这些增量联合挂载在一个统一的挂载点上
1 |
|
例如Ubuntu容器镜像,可以在/var/lib/docker/aufs/mnt/下查看挂载的union fs。这里是已经合并的fs,如果要查看具体合并了哪些fs,也可以配合/proc/mounts下的信息和/sys/fs/aufs下的信息来查看。一个制作好的Union FS容器镜像,每个layer底下都有个合并前的mnt id:
这里了解下挂载的权限:
- ro: readonly只读
- rw: 可读写
- wh: whiteout,可以称之为白障,用于支持只读层的删除。实际上不进行物理删除,只是创建一个对应的.wh.${fileName}文件,然后在联合挂载的时候感知到后遮挡掉原文件达到类似delete的效果。
可以看到以上layer分了多个部分:
- 只读层:主要是操作系统核心的文件
- init层:docker项目单独生成的内部层,存放/etc/hosts和/etc/resolv.conf等信息
- 可读写层:容器内新的写入内容会在这一层
Volume
卷主要是为了支持宿主机上的目录和文件映射到容器中,方便读取和修改。使用的是修改inode指针实现的bind mount。
绑定挂载是在容器运行时指定的,一般在docker run的时候可以指定。
Google Borg与K8s
k8s基本的结构如下,请求都是先由api server承接,配合另外2个核心组件controller manager和scheduler完成service、deployment、schedule相关的重要能力。
borg[2] 是google内部集群管理和任务调度基础设施。K8s中的很多理念都来自于borg并进行了改良。观察k8s的设计,我们可以学到的是:
- 以任务为中心:区别于很多人以docker为中心设计,k8s参考borg以任务为中心,将docker只是作为一个运行时。这样可以充分利用borg上任务调度、编排的经验。
- 统一抽象任务的关系并且重点关注任务之间关系的处理:相比docker能够在更加高层的角度抽象任务的关系也是其相比docker swarm的优势。
- 标准化与丰富的扩展:基于各种标准化的规范设计的API,例如CNI、CRI、CSI。这些为K8S后续的快速发展提供了重要基础
- 声明式API与资源定义:可以看到k8s基本上是follow google的api design guide设计的。以资源为核心通过声明式API将他们串联起来。这也倒推设计者在设计之初需要做好资源抽象。
- 设计最小调度单位的抽象pod: 灵感来自于borg,实际上是个任务组。对于pod来说,内部可以包含1个或多个容器,这些容器共享网络和存储资源,是一组密切相关的应用程序实例。[3] POD底层共享namespace。
pod是具体的执行单元,通常为一个docker运行时。围绕编排、调度与管理可以拓展出k8s功能全景图。像configMap保存配置、Deplyment和StatefulSet都是很常用的对象,后面文章会介绍。
Kubernetes集群搭建与实践
我本地电脑是mac,直接用docker desktop配合IDEA的k8s插件部署本地k8s是比较容易的。可以参考kubernetes IDEA插件的教程视频在本地管理k8s集群。
拓展:serverless k8s
[11] 中提到了几种servless kk8s的流派来解决K8S的遗留问题值得关注。k8s的缺点主要是:
- 运维复杂性:强大能力和扩展性也使得运维和使用复杂性大幅度提升
- 资源共享:基于cgroup的隔离有时候不够彻底
因此也衍生了一些serveless k8s的流派来解决以上痛点:
- Nodeless k8s: 主要是google GKE Autopilot。相当于云厂商进一步托管,然后在资源基础上像用户额外进行收费。没有改变K8S架构,本质是Node的下沉与托管,算是一种商业上的模式。
- Serveless container: 代表是AWS EKS on Fargate和阿里云ACK on ECI和Azure AKS with ACI。摒弃Node概念,Pod运行在独立的安全沙箱中,利用虚拟化技术实现资源隔离和安全隔离。
容器编排与作业管理
POD
- 为什么需要设计pod的概念:docker本质是进程,k8s把自己当做一个类似OS的系统来设计,需要考虑进程和进程组。这个也是来自于borg的实践知识。没有组的概念,具备密切关系的一组进程将没法很好的被进行管理。
- **pod保持对等关系和资源共享需要依赖infra容器(pause容器): **一个很轻量的容器(一直处于pending只用来hold住网络等资源),使得pod内其他容器都是对等的容器,可以共享network、volume等资源
- pod与sidecar设计模式:有了pod这个概念后,在一个pod内可以通过组合不同功能的容器来达到各种目的,因为一个pod内下的存储、网络都是共享的。举个例子,比如war包容器和tomcat容器配合工作;isto微服务治理 都是基于这种sidecar容器的理念。
- 熟悉常用pod yaml字段:projected volume可以支持Secret、downloadAPI(获取pod API对象)、探针等
- 使用PodPreset装饰pod,增加扩展性
控制器模型与负反馈系统
k8s定义了很多控制器来完成编排能力。k8s采用的通用编排模式如下。比较实际和期望状态,可以称为reconcile(调和)。
1 |
|
控制器模型的完整实现主要就是deployment。deployment的一个非常重要功能就是:
- 对pod进行水平扩展和收缩:基于应用控制器模式对pod个数做控制,因此要求容器的restartPolicy只能为always
- 支持金丝雀发布和蓝绿发布:kube-proxy能力不是最强,而且其是基于副本的。现在主流可以配合isto等proxy来做流量控制和发布管理。
控制器模型在往上抽象来理解的话,本质是个负反馈系统。Kubernetes架构的核心就是就是控制循环 (control loops),是一个典型的"负反馈"控制系统。当控制器观察到期望状态与当前状态存在不一致,就会持续调整资源,让当前状态趋近于期望状态。基本都是按照如下模式进行的:
常用资源对象
StatefulSet
状态抽象
核心是抽象了两种状态:
- 拓扑状态:有拓扑顺序
- **存储状态:**pod重建也能关联有状态的存储
headless service
service的访问有两种方式:
- normal service: 通过LB的VIP地址访问下挂的pod
- headless service: 通过DNS直接访问POD。在yaml文件中,clusterIp设置为None就表示是headless service了。绑定的DNS记录格式如下:
1 | <pod-name>.<svc-name>.<namespace>.svc.cluster.local |
statefulset和deployment
statefulset可以理解是deployment的改良,写法上相差很小,仅仅是把引用的service改成引用headless service。
Persistent Volume Claim(PVC)和Persistent Volume(PV)
直接使用Volume的话耦合太高,如下例子中volume包含很多存储本身自带的领域信息:
1 |
|
k8s为了简化使用和降低耦合,提供了PVC对象,本质是在存储和POD之间加了一层额外的抽象层,用来统一描述下挂的存储。PVC是开发者对所需要的PV的一个声明,k8s会从预定义好的pv中获取符合要求的pv。以下是pvc和pv的yaml文件:
1 |
|
1 |
|
PVC与pod绑定
支持有状态应用除了拓扑以外另外个关键点就是状态存储。pvc可以绑定pod,这样pod重建的时候pvc和pv不会被删除,等pod重启后直接关联即可。如果使用普通volume的话,k8s会重新创建新的volume,虽然可以用远程存储保存持久化数据,但是每次重新初始化持久化数据效率就比较低了。
绑定通过volumeClaimTemplates来完成,效果如下:
1 |
|
核心设计
StatefulSet 这个控制器来支持有状态pod的核心设计是:
- 稳定的网络标识符:每个pod的网络标识符固定(hostanme,一般后面接序号)
- 单个pod按序执行:避免并发产生脑裂问题,1次启动一个,成功后再执行下一个
限制
statefulset要求管理的多个有状态应用实例是通过同一份pod模板创建出来的,用的同一个docker镜像。如果不同节点镜像不一样需要用Operator
DaemonSet
k8s中damonpod会运行在集群里的每个节点上,常见的应用比如监控。daaemonset的设计中有如下关键点:
- 通过tolerations来容忍一些原本不允许被调度的情况,例如下面网络插件未安装的情况也允许部署daemonset pod:
1 |
|
- 通过nodeAffinity确保daemonSet pod绑定node
一些实践注意点:
- daemonset需要设置resources字段避免宿主机资源过多占用
Job
deploy、ds和statefulset都是面向Long running task这种长作业的场景。job与crontab则面向离线业务、一次性任务。这个也是来自于borg的理念,将任务分成了LRS(long running service)和Batch jobs。一个基本的job可以按照如下定义:
1 |
|
job是一次性执行,pod最后会进入completed状态。restartPolicy只允许设置为Always何OnFailure。
使用job常用的三种方式:
- 外部管理器+JOB模板:通过设置变量的方式
1 |
|
- 固定任务数目并行:设置parallelism和completion来并行
- 只设置并行度:把k8s当做一个工作队列,我们自己往里面填充任务
CronJob
写法如下。cronjob本质是个controller来控制job。通过配置job template
1 |
|
定时任务需要处理的问题就是上个任务还没执行完毕,下个任务将要开始,k8s提供了几种策略:
- concurrencyPolicy=Allow,这也是默认情况,这意味着这些 Job 可以同时存在
- concurrencyPolicy=Forbid,这意味着不会创建新的 Pod,该创建周期被跳过
- concurrencyPolicy=Replace,这意味着新产生的 Job 会替换旧的、没有执行完的 Job
声明式API与k8s编程范式
基本理念
理解k8s的声明式API可以和过去使用docker swarm的命令式的命令行风格进行比较。k8s本身也提供命令行,但是它本质是创建资源对象然后对资源对象进行处理的过程。资源对象是k8s的核心。这个可以参考google api design guide来了解如何设计声明式API。
课程中提到的是一个isito使用K8S声明式API PATCH的能力来将envoy的配置字段自动添加到用户提交的Pod的API对象里。采用的是k8s的dynamic admission controller能力,底层是依靠了API PATCH的能力,对API对象进行更新。可见k8s依托于面向资源的API提供了非常强大的资源管理以及拓展能力。envoy能够击败nginx、haproxy等竞品和他基于声明式API的配置能力是分不开的,换句话说也就是说envoy更加的云原生
API对象实现原理
k8s中所有API对象可以采用如下树形结构表示:
例如声明一个cronjob对象,在这个 YAML 文件中,“CronJob”就是这个 API 对象的资源类型(Resource),“batch”就是它的组(Group),v2alpha1 就是它的版本(Version)。
1 |
|
创建一个cronjob的流程可以参考下图,图上可以看到group、version的层层递进查找。
tips: 对于 Kubernetes 里的核心 API 对象,比如:Pod、Node 等,是不需要 Group 的(即:它们的 Group 是“”)。所以,对于这些 API 对象来说,Kubernetes 会直接在 /api 这个层级进行下一步的匹配过程。
过去K8S ApiServer大量基于go代码生成,复杂度较高,不太容易添加一个新的K8S风格的API资源类型。1.7以后提供了新的API插件机制CRD(custom resource definition)后才变得比较容易。通过一个资源定义yaml和go代码可以自己实现一个API资源类型。
自定义资源类型步骤(利用CRD)
- 利用CRD(custom resource definition)声明新资源类型
- 为API对象编写自定义控制器,支持API对象的增、删、改操作
自定义控制器的工作原理参考下图:
- informer:其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 API server的client。通过Reflector包中的ListAndWatch监听APIServer端资源对象上的动作。
- **control loop: **informer监听的事件在workqueue被control loop轮询处理。control loop实现的核心就是我们说的控制器模式,拿实际状态和期望状态进行比较,然后进行处理,也就是reconcile(协调)。
基于角色的权限控制:RBAC
基本概念
api server接受kubectl请求,访问控制会经历如下步骤:认证、权限、合规。
权限控制主要是authorization,这块主要是基于RBAC的理念来做的。在k8s中:
- user:k8s提供了一个轻量化的抽象subjects字段来描述用户主体。subjects可以指定外部认证对象,也可以指定ServiceAccount。配合accountService(主要用于内部认证)以及外部用户认证系统和凭证可以达成用户认证的目的。采用这种轻量化的设计,将用户认证能力委托出去的方式非常好,可以使得k8s非常好的整合业界已经成熟的认证系统。
- role: k8s提供了role和clusterRole两个资源对象来定义角色。role主要用在namespace,clusterRole作用于集群。
- 权限:k8s中的权限就是vers,可以理解为可以针对资源对象具体做什么操作,例如get、watch、list
1 | apiVersion: rbac.authorization.k8s.io/v1 |
1 | apiVersion: rbac.authorization.k8s.io/v1 |
binding授权
定义好了角色和对应的权限,还有非常重要的步骤就是授权。
- RoleBinding: 将role授权给主体。授权的role可以是普通的role或者是ClusterRole。只能绑定到一个命名空间内的用户或者组
- ClusterRoleBinding: 可以绑定到整个集群范围内的用户或组,也就是说绑定到集群的所有命名空间。创建了绑定后,不能修改绑定对象所引用的role或者clusterrole。如果要修改只能删除再重新绑定roleRef。
1 | apiVersion: rbac.authorization.k8s.io/v1 |
Operator
Operator是现在有状态应用接入k8s的主要手段。operator是基于CRD能力的拓展(CRD相当于提供了机制),提供更强的自定义资源对象的管理。
K8S容器持久化存储
PV与PVC的更多内容
- 大规模环境可以用自动创建PV的机制:dynamic provisioning。利用StorageClass API对象来定义PV,避免管理大量PV yaml。在整体体系中位置如下图所示:
容器网络
容器通信基础
不同容器网络之间通过两端容器内的Veth网卡(容器内名字是eth0,宿主机上显示未vethxxxx)和网桥docker0相连来实现通信。大体上就是通过宿主机docker0当网桥配合veth虚拟网卡协同工作的过程。当你遇到容器连不通“外网”的时候,你都应该先试试 docker0 网桥能不能 ping 通,然后查看一下跟 docker0 和 Veth Pair 设备相关的 iptables 规则是不是有异常,往往就能够找到问题的答案了。
Flannel容器跨主网络实现
UDP
数据放在UDP包发送,配合虚拟网络设备TUN和管理的子网meta信息将UDP包发送到指定对端。Flannel UDP实现本质是一个三层的overlay网络。
UDP实现的主要问题是,用户态的UDP包处理,性能不太好,后续被弃用。
VXLAN
利用Linux内核的vxlan能力,利用VTEP虚拟设备直接在内核态构建overlay网络。通信的两端节点可以看成是一个VTEP设备。宿主机不感知inner vxlan的信息,只是作为UDP包发送,包内部包含完整overlay网络的信息,例如inner ip、vtep mac地址等。流程如下,可以看到包头有额外的vxlan信息和inner信息。这里注意路由的元信息是在node初始化的时候就确定好了(放在一个转发数据库FDB中),不需要ARP协议来确定IP地址对应的MAC地址了。
host-gw
即host gateway,直接将本机作为网关,直接设置下一跳地址为目标宿主机的IP地址。使用flannel的host-gw模式,flanneld进程会直接在宿主机上创建规则。同样的,这些网络路由拓扑信息都是在etcd元数据中的。采用这种点对点的方式性能相对来说也会比vxlan好。当然直接指定的话要求宿主机在一个vlan内,否则这样通信时不可达的。
1 | 10.244.1.0/24 via 10.168.0.3 dev eth0 |
Calico与host-gw
类似采用host-gw模式实现的还有Calico,只不过路由信息是依靠Linux内核原声的BGP(border gateway protocl边际网关协议)来实现的,利用BGP共享网络节点的路由信息。calico也提供IPIP模式来支持跨VLAN通信,不过性能就要变差了,也要依赖etcd元数据引入额外的隧道。
K8S网络
CNI网络
k8s的网络也是基于这样的方式构建一个overlay网络,只不过采用的CNI接口,宿主机上设备从docker0变成了cni0。基于CNI标准实现的网络插件在K8S当中都可以使用,包括flannel、calico等。关于网络插件选型可以参考[8]。像flannel在K8S中使用的效果就变成如下的方式,k8s给flannel.1 vtep设备分配了个子网。k8s不使用docker0而使用CNI是为了提供一个标准化的网络实现扩展点,可以快速和已有的网络实现生态融合,而不是自己陷入网络实现细节。
TIPS: CNI网络插件由kubelet管理
NetworkPolicy与k8s网络隔离
k8s提供NetworkPolicy资源来管理网络,如果是支持NetworkPolicy的网络插件则可以配合使用。networkpolicy主要用于做网络隔离,本质是基于iptables实现的。
内部服务发现与负载均衡
k8s内部的服务发现和负载均衡,主要指的是serviice controller之间和pod之间的关系,发现pod,对一个service下的pod代理和负载均衡。
基本步骤
service是由kube-proxy(宿主机上的进程,不是容器)和iptables一起共同实现的。步骤是:
- 创建service对象提交给k8s
- kube-proxy通过service controller的Informer感知到service对象添加,然后针对该事件创建一个iptables规则链(iptable规则的集合),最终通过iptable指向代理的pod
IPVS负载均衡
基于大量iptable规则,在control loop中刷新并且判断iptable规则的效率会比较低,因此kube-proxy引入了IPVS模式(ip virtual server)来解决这个问题。IPVS是Linux内核态提供的高性能负载均衡机制,通过一个hash表维护ipset,相比单纯的iptable规则性能会好很多。IPVS模式通过维护一个虚拟 IP 地址和端口的映射表,以及一个后端 Pod 列表,来实现请求的分发。请求首先到达虚拟 IP 地址和端口,然后根据负载均衡算法将请求分配给后端 Pod 进行处理。
外部服务发现与负载均衡
外部需要感知k8s中的service也需要设计服务发现以及负载均衡,一般有如下方式
通过NodePort做服务发现
NodePort实际上就是内部serivce的代理,用于对外暴露端口和服务。下图我们可以看到service中spec中可以指定NodePort
1 |
|
NodePort模式下,宿主机IP包发往POD的时候会做一次SNAT操作将IP包中的源端IP地址换成宿主机CNI网桥地址。对包做SNAT转换,主要是为了避免POD直接根据IP包中源端IP地址回包给client,引发client报错。因为client实际期望的回包是来自于自己期望的目标端。下面拓扑可以参考:
1 |
|
上图中如果需要pod直接感知client的地址,可以设置spec.externalTracfficPolicy为local,不过这样拓扑就变成如下,node2不能访问到pod和service了,相当于必须直连。
1 |
|
通过LoadBalancer做服务发现和负载均衡
可以指定loadbalancer类型依赖外部的负载均衡器。
1 |
|
ExternalName指定DNS做服务发现
externalname可以直接通过service的dns访问service,实际上就是将service的dns映射成一个外部DNS,就是在kube-dns中添加一条CNAME记录。
1 |
|
ExternalIp指定可访问service的外部ip
指定后ipvs网桥只允许特定外部ip访问service。注意这种方式没法做负载均衡。
ingress负载均衡
每个service去配置对应的负载均衡器效率很低,因此产生了全局的为代理不同后端service而设置的负载均衡服务Ingress服务。 Ingress可以理解成service的 service,相当于对k8s service做反向代理。配置起来效果如下,下面通过一个统一的ingress代理了tea和coffee两个service。ingress有很多实现,但是他们都符合k8s ingress对象的标准,例如nginx ingress controller。
1 |
|
TIPS: 配置TLS等HTTP相关配置的时候要在更高层的Ingress层面配置,而不是service层面
k8s资源与调度
资源模型与资源管理
资源定义
资源是在pod层面定义的,因为pod是调度的基本单位。资源主要是指:
- 可压缩资源CPU: 只会有资源饥饿,影响服务性能,不会导致服务不可用
- 不可压缩资源MEM:资源不足会直接退出进程
TIPS: cgroup可以管理很多资源,k8s/kubelet中主要用到了cpu/memory/pid/hugetlb等几种类型
资源配额——动态资源边界
K8S的资源限额采用了borg论文中动态资源边界的定义,这个很值得我们学习。资源配额如果只设定一个值 ,那么只能进行静态的资源分配,资源就没有弹性的空间。k8s设置request和limit,实际上就是给了pod资源弹性的空间,同时limit也保障了调度时整体阈值的控制,避免pod资源scale up导致的系统性资源短缺问题。
我们自己在设计调度系统的时候也可以多借鉴这种思路,资源配额采用动态资源边界。
资源分配与QoS: POD的QoS与Eviction
基于动态资源边界的设置,k8s针对不同的request、limit组合定义了POD的QoS
- Guranteed: request==limit,不会被抢占,优先级最高,k8s会优先evict其他QoS类型pod
- Burstable: 可突发的,意味着pod资源可以有弹性,guranteed的pod剩余未使用的资源,burstable的pod有需要的时候可以进行弹性。
- BestEffort: 优先级最低,集群有资源的时候可以运行,否则则会被优先evict出集群
k8s根据pod的QoS类型在资源不足时针对性的进行evict操作。eviction阈值可以设置,例如:
1 |
|
cpuset和cpushare
可以设置cpuset让pod绑定cpu,避免不同CPU之间的上下文切换。
调度器
基本工作原理
调度器实现是两个独立的控制循环:
- informer path:监听调度相关的API对象的变化
- scheduling path:负责POD调度的主循环,利用predicates过滤Node,然后利用priorities对Node打分,然后通过Bind将Pod对象的nodeName字段修改为选出来的Node的名字。bind只更新scheduler cache,后续是异步和API server发起真正的POD更新。k8s核心组件都是通过异步和cache来配合ApiServer工作的,这样异步化后可以方便做并行,避免进程阻塞等问题,提高整个系统的效率。
调度算法
调度主循环提供的两组调度算法是:
- Predicate: 本质是个filter,可以过滤符合条件的node,可以针对pod、volume等进行过滤。
- Priority: 本质是排序,针对节点打分。打分规则可以按照空闲资源、资源均衡性等来分配。
调度时的POD抢占
这个注意和QoS eviction的区别。QoS eviction是运行的pod如果资源占用变大时如何进行eviction,强调运行时。调度时的POD抢占主要针对的是调度时,如果发现集群资源不足,如何抢占资源,并且驱逐pod。驱逐pod也是优先考虑QoS,然后再看priorityClass的。是的,当集群资源不足时,即使一个 Pod 的 PriorityClass 设置为低优先级,但其 QoS 为 guaranteed,该 Pod 也可能会被优先保留而驱逐 PriorityClass 为高优先级、但 QoS 不为 guaranteed 的 Pod。
民主化的scheduler
scheduler也支持扩展,例如可以在节点过滤、排序等维度上都进行自定义。下图参考k8s调度框架的官方文档,可以看到k8s调度框架提供的扩展点。
k8s调度器扩展
k8s的调度器的特色是足够开放与民主,同时良好的抽象与设计也保证了其强大的能力。业内我们可以看到很多的调度系统设计。一般而言设计一个调度系统从能力上需要考虑:
- 工作负载类型:这里有多种划分方式,例如按照long-running、batch或者按照应用特点,微服务、AI、数据库等等。这里我觉得可以考虑通用抽象+label的方式可能比较合理。
- 物理资源类型:cpu、内存、存储、GPU等等
- 调度效率与性能
- 弹性效率
- 资源利用率
- 安全性
GPU和Device Plugin
调度系统除了经典的CPU、内存,也需要考虑异构资源的管理,例如GPU和其他设备。像AI的应用,就涉及GPU比较多。k8s通过device plugin来引入新设备的管理。像这种设备管理,在容器运行前就需要初始化好的,都基本是通过kubelet来管理(类似CNI网络插件)。
容器运行时
Kubelet与容器运行时
kubelet像ApiServer一样,是K8S中最核心的组件之一。主要负责的能力包括.
- POD生命周期管理:作为独立容器之外运行的进程,用于管理POD
- 资源管理:管理节点资源使用,避免超限使用
- 设备管理:网络、存储、其他设备(例如GPU)
- 监控与日志
- 安全
实现上也是一个负反馈系统,采用一个syncLoop和多个子循环来不断监听事件、比较期望值和调整。
kubelet与容器之间通过CRI接口提供抽象,屏蔽底层容器运行时的差异。底下的容器运行时可以是docker,也可以是其他的(像2023年比较时髦的大概就是K8S中的WASM运行时了)。
CRI与容器运行时
CRI是K8S提供的容器运行时接入标准。具体运行时的实现,一般可以按照高层运行时和底层运行时来理解。
- 高层容器运行时(CNCF项目):像container-d和CRI-O,主要负责容器生命周期管理(包括镜像的上传下载等),为容器生命周期管理提供统一接口
- 低层容器运行时(或者称之为运行时引擎,OCI项目):像mysql存储可以下挂各种存储引擎一样,但是他们都遵循mysql存储接口标准。底层容器运行时主要是具体的运行时引擎实现,比如runC、Kata-container还有gVisor。
容器引擎现在可以分为两大类
- runC: runC是基于Linux的cgroup、namespace和seccomp等特性实现的,可以在裸机或者虚拟机上运行。相比runV更加轻量化,缺点是共享内核态,安全和隔离不彻底。
- runV: 一般是基于硬件上虚拟化的KVM技术或者OS上虚拟化的运行时,容器有独立的内核态,相比runC重一些,但是有更好的安全和隔离性。这块近些年发展比较快,各大云厂商和开源生态在runV运行时引擎上项目比较多。追求更加轻量、高性能的runV引擎是现在主流的方向。现在主流的是kata-container和gVisor
gVisor
gVisor的runV比较特别点。采用的是一种轻量虚拟化技术叫做用户态OS内核。提供的sentry进程需要实现一个用户态的OS内核,会借用KVM进行syscall拦截。利用syscall拦截和用户态OS也达到了runV运行时的安全和隔离性。
TIPS:
- CRI是直接和容器交互的,不耦合pod,避免因为耦合带来额外的副作用。
- OCI主要提供了容器镜像标准和容器运行时标准
监控与日志
k8s与prometheus
prometheus是对k8s进行监控的主要部件。
搜集的监控指标包括:
- 宿主机监控数据:通过Node Exporter
- 核心组件的metric: 通过K8S核心组件的metric api来获取metric信息
- k8s core metric: 主要涉及pod、node、容器、service等核心metric。通过metric server暴露
监控指标规划USE原则和RED原则
其中,USE 原则指的是,按照如下三个维度来规划资源监控指标,主要关注资源:
- 利用率(Utilization),资源被有效利用起来提供服务的平均时间占比;
- 饱和度(Saturation),资源拥挤的程度,比如工作队列的长度;
- 错误率(Errors),错误的数量。
而 RED 原则指的是,按照如下三个维度来规划服务监控指标,主要关注运行时的服务质量:
- 每秒请求数量(Rate)
- 每秒错误数量(Errors)
- 服务响应时间(Duration)
custom metrics
利用custom metrics配合k8s的HPA(horizontal pod autoscaler)可以来制定扩展策略。利用Aggregator APIServer扩展一个custom metrics apiServer的实现可以使用KubeBuilder的工具库
容器日志搜集与管理
k8s采用cluster-level-logging确保可以搜集到所有的日志。日志搜集一般可以部署个sidecar容器来负责日志搜集:
参考资料
- [1] [公众号] 彻底搞懂容器技术的基石: namespace (上)
- [2] Malte Schwarzkopf. “Operating system support for warehouse-scale computing”. PhD thesis. University of Cambridge Computer Laboratory (to appear), 2015, Chapter 2.
- [3] [公众号] k8s主要概念大梳理!
- [4] [公众号] K8S架构原理详解
- [5] [公众号] 从零开始入门 K8s | K8s 安全之访问控制
- [6] [公众号] K8S API访问控制之RBAC利用
- [7] [公众号] 十分钟弄懂 k8s Operator 应用的制作流程
- [8] Kubernetes CNI 插件选型和应用场景探讨
- [9] [公众号] 揭开阿里巴巴复杂任务资源混合调度技术面纱
- [10] 资源调度泛弹
- [11] [公众号] 没有银弹,只有取舍 - Serverless Kubernetes 的思考与征程(一)
- [12] 云原生钻石课程 | 第1课:容器运行时技术深度剖析
- [13] 容器运行时概览
- [14] 云原生之容器安全实践