Kubelet

每个节点上都运行一个kubelet服务进程,默认监听10250端口

  • 接收并执行master发来的指令
  • 管理Pod及Pod中的容器
  • 每个kubelet会在APIServer上注册节点自身信息,定期向master节点汇报节点状态及资源使用情况,监控容器状态

获取Pod清单的方式:

  • 本地文件。启动参数–config指定的配置目录下的文件(默认/etc/kubernetes/manifests),每20秒重新检查一次
  • URL。启动参数–manifest-url设置,每20秒检查一次
  • APIServer。通过APIServer监听etcd,同步Pod清单
  • HTTP Server。监听HTTP请求,获取新提交的Pod清单

Sandbox

k8s启动Pod时,会先启动一个Sandbox容器即 /pause 容器。

  • 镜像非常小,执行的命令是无限sleep,不消耗资源,及其稳定
  • 把这个sandbox容器作为Pod的底座,先启动这个容器,配置好网络,再启动业务容器
    • 网络等各种Namespace就配置在sandbox容器上
    • 保证网络等配置稳定,业务容器启动时需要网络

kubelet启动Pod流程

kubelet创建Pod时,会依次调用CSI->CRI->CNI接口进行配置。

CRI

容器运行时运行在k8s的每个节点上,负责容器的整个生命周期。为了解决容器运行时和k8s集成的问题,推出CRI容器运行时接口,以支持更多的容器运行时。

CRI是k8s定义的一组gRPC服务,kubelet作为客户端,基于gRPC框架,通过socket和容器运行时通信。包括两类服务:

  • 镜像服务,提供下载、检查和删除镜像的rpc接口
  • 运行时服务,提供容器生命周期管理、容器交互(exec/attach)的rpc接口

容器运行时细分:

  • 高层运行时,如Docker/containerd
    • containerd不需要Dockerd/Docker-shim,在容器创建删除和启停上有性能优势,适用于生产
    • docker支持镜像推送操作,适用与开发(标准的CRI接口没有定义镜像推送)
  • 低层运行时,主要是runc,实现容器程序的运行,Namespace/cgroups的配置,挂在根文件系统等。

Docker早期架构中,daemon是主进程,其它容器进程是由daemon fork出来的。

  • 带来的问题是,Docker升级重启时,daemon这个父进程需要销毁,容器进程也要销毁。
  • Docker内部也是依赖containerd,层层封装,不好维护,出问题只能重启。

containerd的处理方式是,使用container shim作为容器进程的父进程,shim的父进程是systemd。这样在containerd重启时,容器不受影响。

  • shim和容器是一对一的,重启一个shim也只会影响一个容器。
  • shim是轻量级的,基本不需要升级。

CNI

k8s网络模型设计原则是:

  • 所有的Pod能够不通过NAT就能互相访问
  • 所有的节点能够不通过NAT就能互相访问
  • 容器内看见的IP地址和外部组件看到的容器IP是一样的

IP地址是以Pod为单位分配的,一个Pod内部所有容器共享一个网络栈,即宿主机上的一个网络命名空间。

在k8s中,提供了一个轻量的通用容器网络接口CNI,专门用于设置和删除容器的网络连通性。容器运行时通过CNI调用网络插件来完成容器的网络设置。

容器运行时一般需要配置两个参数:

  • –cni-bin-dir 网络插件的可执行文件所在目录,默认是 /opt/cni/bin
  • –cni-conf-dir 网络插件的配置文件所在目录,默认是 /etc/cni/net.d

容器运行时在启动时会从CNI的配置目录中读取JSON格式的配置文件,如果目录下有多个文件,按字母顺序选择第一个文件作为默认配置,并加载其中指定的CNI插件名称和配置参数。

  • kubelet调CRI是gRPC,CRI调CNI是二进制调用。

runtime把操作目标和参数告诉CNI插件,然后由CNI插件做网络配置。

  • 如,ipam插件给pod分配ip,calico把ip绑定给容器的namespce,bandwidth限制带宽

配置完成后将结果返回给 container runtime,由runtime把ip等信息返回给kubelet,kubelet上报给APIServer,APIServer更新Pod状态。

Flannel

简单。封包解包有额外开销,重效率的系统不会用。只是做CNI插件,没有完备生态,没有network policy的防火墙插件,这点不如Calico。

有三种模式:

  • overlay网络
    • UDP,IP In UDP,依赖tun设备和用户态进程flanneld,多次用户态/内核态切换开销大
    • VxLan,Mac In UDP,依赖内核态的VxLan虚拟设备,涉及ARP表,FDB表,路由表。
  • Underlay网络
    • hostGW,Node节点IP作为下一跳,要求Node节点二层互通

Calico

提供CNI插件及网络管理功能

三种组网模式:

  • VxLan, MAC on UDP
  • IPIP,内核tun设备封包
  • BGP,基于BGP协议交换路由,Node网卡做网管,要求二层互通

CSI

容器运行时存储,存的是镜像里的层,本身也是一个文件系统(Docker和containerd都使用overlayFS作为运行时存储驱动)。写到这里面也可以,但是性能很低,不建议写到这里。写文件建议使用运行时之外的存储插件。

kubelet和插件怎么交互,命令怎么发到插件里?kubelet不支持动态链接,要么像CNI那样执行本地文件方式,要么像CSI这样通过unix socket调用。

kubelet通过RPC(unix socket)调用存储驱动。kubelet是个framework,它只调接口。至于接口具体怎么实现,是每种插件去定义的。kubelet和插件就解耦了。

External Components/Custom Components

Rook/Ceph

存储相关API对象

主要分为:

  • 临时存储 emptyDir
  • 半持久化存储 hostPath
  • 持久化存储 StorageClass, PersistentVolume, PV, PVC

临时存储 emptyDir

emptyDir卷和Pod的存在是绑定的,Pod删除后emptyDir也会丢失。Pod重启不会丢失emptyDir数据。

  • 默认情况下,emptyDir卷存储在该节点所使用的存储介质上,可以是本地磁盘或网络存储
  • 也可设置 emptyDir.medium为 Memory 使k8s为容器安装tmpf,此时数据存储在内存。
  • 在节点重启时,内存数据会被清除,如果存在磁盘上,则不会被清除
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    spec:
      containers:
        - name: nginx
          image: nginx
          volumeMounts:
          - mountPath: /cache
            name: cache-volume
      volumes:
      - name: cache-volume
        emptyDir: {}
      #volumes:
      #- name: cache-volume
      #emptyDir:
      #  medium: Memory 

半持久化存储 hostPath

hostPath能将主机节点文件系统上的文件或目录挂在到指定Pod,Pod删除后数据还在。

  • 普通用户一般不需要这样的卷
  • 对需要获取节点系统信息的Pod而言是有必要的
    • 如把节点日志目录mount到pod
    • 通过hostPath访问节点的/proc目录
    • DaemonSet向主机拷贝文件

Kubernetes ⽀持 hostPath 类型的 PersistentVolume 使⽤节点上的⽂件或⽬录来模拟附带⽹络的存储。

  • 需要注意的是在⽣产集群中,我们不会使⽤ hostPath,集群管理员会提供⽹络存储资源,⽐如 NFS 共享卷或 Ceph 存储卷,集群管理员 还可以使⽤ StorageClasses 来设置动态提供存储。
  • 因为 Pod 并不是始终固定在某个节点上⾯的,所以要使⽤ hostPath 的话我们就需要将 Pod 固定在某个节点上,这样显然就⼤⼤降低了应⽤ 的容错性。

使⽤ hostPath 也有⼀些好处的,因为 PV 直接使⽤的是本地磁盘,尤其是 SSD 盘,它的读写性能相⽐于⼤多数远程存储来说, 要好得 多,所以对 于⼀些对磁盘 IO 要求⽐较⾼的应⽤⽐如 etcd 就⾮常实⽤了。不过相⽐于正常的 PV 来说,使⽤了 hostPath 的这些节点⼀旦宕机数据就可能丢 失,所以这就要求使⽤ hostPath 的应⽤必须具备数据备份和恢复的能⼒,允许你把这些数据定时 备份在其他位置。

 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
apiVersion: v1
kind: Pod
metadata:
  name: task-pv-pod
spec:
  volumes:
    - name: task-pv-storage
      persistentVolumeClaim:
        claimName: task-pv-claim
  containers:
    - name: task-pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: task-pv-storage
------
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pv-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
------
apiVersion: v1
kind: PersistentVolume
metadata:
  name: task-pv-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 100Mi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data" # Pod删除后,hostPath路径还在

Pod调度与Volume挂载的顺序

从卷的⽣命周期来讲,卷被Pod使⽤或者卷被回收,会依赖顺序严格的⼏个阶段

卷被Pod使⽤

  • provision,卷分配成功
  • attach,卷挂载在对应workernode
  • mount,卷挂载为⽂件系统并且映射给对应Pod

卷被回收

  • umount,卷已经和对应workernode解除映射,且已经从⽂件系统umount
  • detach,卷已经从workernode卸载
  • recycle,卷被回收

从Kubernetes存储系统来讲,卷⽣命周期管理的职责,⼜分散于不同的控制器中

  • pv controller,负责创建和回收卷
  • attach/detach controller,负责挂载和卸载卷
  • volume manager,负责mount和umount卷

假设scheduler已经完成worker node选择,确定了调度的节点。此时创建Pod前,需要先完成卷映射到Pod路径中,整个过程如下:

  1. 卷分配,pvc绑定pv,由pv controller负责完成
  2. attach/detach controlle,如果pod分配到worker node,并且对应卷已经创建,则将卷attach到对应worker node,并且给worker node资源增加volume已经attach对应卷的状态信息
  3. volume manager在worker node中负责将卷mount到对应路径
  • pod分配到本workernode后,获取Pod需要的volume,通过对⽐node状态中的volumesAttached,确认volume是否已经attach到 node节点
  • 如果attach到node节点则将⾃身actualStateOfWorld中的volume状态设置成attached
  • 如果已经attached成功,则进⼊到⽂件系统挂载流程
    • 先挂载到node中全局路径,⽐如/var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-3ecd68c7b7d211e8/globalmount
    • 映射到Pod对应路径,⽐如/var/lib/kubelet/pods/49a5fede-b811-11e8-844f-fa7378845e00/volumes/kubernetes.io~csi/pvc-3ecd68c7b7d211e8/mount
    • actualStateOfWorld中设置volume为挂载成功状态
  1. pod controller确认卷已经映射成功,继续启动Pod

PV/PVC

PV和PVC的容量不匹配会怎样?pvc的容量可以小于pv,仍能正常绑定;pvc的容量大于pv,就会pending。

PV 的全称是:PersistentVolume(持久化卷),是对底层共享存储的⼀种抽象,PV 由管理员进⾏创建和配置(或根据PVC的申请需求动态创建,需要CSI Driver支持)。 和具体的底层的共享存储技术的实现⽅式有关,⽐如 Ceph、GlusterFS、NFS、hostPath 等,通过插件机制完成与共享存储的对接。

集群管理员定义完StorageClass,会安装好对应的插件。当用户创建PVC时,指定使用的StorageClass,此时这个SC对应的插件就要去工作:关联由管理员创建好的PV,或者CSI Driver动态创建PV。

PV 的状态,实际上描述的是 PV 的⽣命周期的某个阶段,⼀个 PV 的⽣命周期中,可能会处于4种不同的阶段:

  • Available(可⽤):表示可⽤状态,还未被任何 PVC 绑定
  • Bound(已绑定):表示 PVC 已经被 PVC 绑定
  • Released(已释放):PVC 被删除,但是资源还未被集群重新声明
  • Failed(失败): 表示该 PV 的⾃动回收失败

PVC 的全称是:PersistentVolumeClaim(持久化卷声明),PVC 是⽤户存储的⼀种声明,PVC 和 Pod ⽐较类似,Pod 消耗的是节点,PVC 消耗的是 PV 资源,Pod 可以请求 CPU 和内存,⽽ PVC 可以请求特定的存储空间和访问模式。对于真正使⽤存储的⽤户不需要关⼼底层的存 储实现细节,只需要直接使⽤ PVC 即可。

创建 PVC 之后,Kubernetes 就会去查找满⾜我们声明要求的 PV, ⽐如 storageClassName、accessModes 以及容量这些是否满⾜要求, 如果满⾜要求就会将 PV 和 PVC 绑定在⼀起。

PVC中声明的accessMode:

  • RWO, ReadWriteOnce 该卷只能在一个节点上被mount,属性为可读可写
  • ROX, ReadOnlyMany 可以在不同节点上被mount,属性为只读
  • RWX, ReadWriteMany 可以在不同节点上被mount,属性为可读可写

在持久化容器数据的时候使⽤ PV/PVC 有什么好处呢? ⽐如我们之前直接在 Pod 下⾯也可以使⽤ hostPath 来持久化数据,为什么还要费劲去创建 PV、PVC 对象来引⽤呢?

  • PVC 和 PV 的设计,其实跟“⾯向对象”的思想完全⼀致,PVC 可以理解为持久化存储的“接⼝”,它提供了对某种持久化存储的描述,但不提 供具体的实现;⽽这个持久化存储的实现部分则由 PV 负责完成。
  • 这样做的好处是,作为应⽤开发者,我们只需要跟 PVC 这个“接⼝”打交道,⽽不必关⼼具体的实现是 hostPath、NFS 还是 Ceph。毕竟这 些存储相关的知识太专业了,应该交给专业的⼈去做,这样对于我们的 Pod 来说就不⽤管具体的细节了,你只需要给我⼀个可⽤的 PVC 即 可了,这样就完全屏蔽了细节实现解耦。

PV和PVC过早绑定

PV和PVC过早绑定可能会带来调度问题

  • 如PV和PVC创建完成后,PVC会和第⼀个创建的位于master节点的PV绑定;
  • 等创建Pod时,Pod的亲和性只允许运⾏在node0节点导致冲突,Pod状态Pending。

可以利用StorageClass中的volumeBindingMode: WaitForFirstConsumer配置,将PV和PVC的StorageClass改为这个新建的local-storage,⾥⾯声明了延迟绑定。 这样PVC不会⽴即和PV绑定,⽽是延迟到对应的Pod进⾏调度时。

1
2
3
4
5
6
apiVersion: storage.k8s.io/v1 
kind: StorageClass 
metadata:
  name: local-storage 
provisioner: kubernetes.io/no-provisioner  # static provisioner,手动创建PV
volumeBindingMode: WaitForFirstConsumer

StorageClass

仅仅通过 PVC 请求到⼀定的存储空间也很有可能不 ⾜以满⾜应⽤对于存储设备的各种需求,⽽且不同的 应⽤程序对于存储性能的要求可能也不尽相同,⽐如 读写速度、并发性能等。

为了解决这⼀问题,Kubernetes ⼜为我们引⼊了⼀ 个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义 为某种类型的资源,⽐如快速存储、慢速存储等,⽤ 户根据 StorageClass 的描述就可以⾮常直观的知道 各种存储资源的具体特性了,这样就可以根据应⽤的 特性去申请合适的存储资源了。

此外 StorageClass 还可以为我们⾃动⽣成 PV,免 去了每次⼿动创建的麻烦(当然也支持手动创建PV)。

  • static provisioner 手动创建PV,通过StorageClass关联,绑定PVC
  • dynamic provisioner 自动创建PV,绑定PVC,需要插件支持

在StorageClass的定义中,重要的是指定provisioner插件是哪一个,自动生成PV的能力是由插件实现的。

1
2
3
4
kind: StorageClass # 部署完rook-ceph存储插件后,就可以定义了
metadata:
  name: rook-ceph-blok
provisioner: rook-ceph.rbd.csi.ceph.com
 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
apiVersion: v1
kind: Pod
metadata:
  name: task-pv-pod
spec:
  volumes:
    - name: task-pv-storage
      persistentVolumeClaim: # 指定PVC
        claimName: rook-ceph
  containers:
    - name: task-pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/mnt/ceph"
          name: task-pv-storage
------
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rook-ceph
spec:
  storageClassName: rook-ceph-block # PVC指定使用的StorageClass
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi

Local PV

hostPath volume并不适合在⽣产环境中使⽤。由于集群中每个节点的差异化,使⽤hostPath,必须通过节点亲和性精确调度,特别繁琐。

Local PV⽤来解决hostPath的各种问题。 PV Controller 和 Scheduler 会对 Local PV做特殊的逻辑处理,实现Pod使⽤本地存储时在发⽣ re-schedule的情况下能再次调度到 local volume 所在的Node。

但是,毕竟本质上还是节点上的本地存储。如果没有存储副本机制,⼀旦节点或者磁盘异常,使⽤该volume的Pod也会异常,甚⾄出现数据丢失。

其它

  • Local Volume 用于高IOPS应用,创建独占的Volume,Pod删除后数据也会删除
  • Dynamic Local Volume 用于需要大容量存储空间,一块盘不够的场景,利用Linux LVM将多块盘组合成一个Volume Group。