[(img-aGo2OYHI-1598930364142)(https://img.kubernetes.org.cn/2016/10/20161028141542.jpg)]
容器基础
对 Docker 项目来说,它最核心的原理实际上就是
- 为待创建的用户进程:
- 启用 Linux Namespace 配置;(隔离)
- 设置指定的 Cgroups 参数;切换进程的根目录(Change Root)(限制).
docker分层
隔离与限制
Namespace 和 Cgroups。
Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace 内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈看不见的墙。这么一折腾,进程就真的被“装”在了一个与世隔绝的房间里,而这些房间就是 PaaS 项目赖以生存的应用“沙盒”。
Linux Cgroups
Linux Cgroups 是 Linux 内核中用来为进程设置资源限制的一个重要功能。
Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。在 Ubuntu 16.04 机器里,我可以用 mount 指令把它们展示出来,这条命令是:
比如在/sys/fs/cgroup/cpu
下创建文件,文件的属性是cpu资源限制文件,这样可以在这个文件里设定cpu的占用率。
使用下面的命令可以设置特定的cgroup的使用比例,总的是100ms,下面的命令是设置本cgroup的最高占用为20ms,也就是1/5
$ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
docker镜像
https://time.geekbang.org/column/article/17921
再创建新的进程的时候,需要指定再进程(容器)中挂载的文件和盘,否则和宿主机是一样的。
作为一个普通用户,我们希望的是一个更友好的情况:每当创建一个新容器时,我希望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。怎么才能做到这一点呢?
不难想到,我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。
在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工作。顾名思义,它的作用就是帮你“change root file system”,即改变进程的根目录到你指定的位置。它的用法也非常简单。
假设,我们现在有一个 $HOME/test 目录,想要把它作为一个 /bin/bash 进程的根目录。
首先,创建一个 test 目录和几个 lib 文件夹:
$ mkdir -p $HOME/test
$ mkdir -p $HOME/test/{bin,lib64,lib}
$ cd $T
然后,把 bash 命令拷贝到 test 目录对应的 bin 路径下:
$ cp -v /bin/{bash,ls}
$HOME/test/bin
接下来,把 bash 命令需要的所有 so 文件,也拷贝到 test 目录对应的 lib 路径下。找到 so 文件可以用 ldd 命令:
$ T=$HOME/test
$ list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
$ for i in $list; do cp -v "$i" "${T}${i}"; done
最后,执行 chroot 命令,告诉操作系统,我们将使用 $HOME/test 目录作为 /bin/bash 进程的根目录:
$ chroot $HOME/test /bin/bash
这时,你如果执行 “ls /”,就会看到,它返回的都是 $HOME/test 目录下面的内容,而不是宿主机的内容。更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 $HOME/test 了。这种视图被修改的原理,是不是跟我之前介绍的 Linux Namespace 很类似呢?没错!实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
需要配置:
下载docker和k8s镜像
创建master和node
ip和hos的t配置
节点间配置子网
基本
- 一个容器一个进程
- Namespace 做隔离,Cgroups 做限制,rootfs 做文件系统
- 有几个空间牢牢记住:集群,service, node, pod
构造
架构
- master
- node
master
Kubernetes 架构是一个比较典型的二层架构和 server-client 架构。Master 作为中央的管控节点,会去与 Node 进行一个连接。所有 UI 的、clients、这些 user 侧的组件,只会和 Master 进行连接,把希望的状态或者想执行的命令下发给 Master,Master 会把这些命令或者状态下发给相应的节点,进行最终的执行。
Kubernetes 的 Master 包含四个主要的组件:API Server、Controller、Scheduler 以及 etcd。如下图所示:
node
Kubernetes 的 Node 是真正运行业务负载的,每个业务负载会以 Pod 的形式运行。等一下我会介绍一下 Pod 的概念。一个 Pod 中运行的一个或者多个容器,真正去运行这些 Pod 的组件的是叫做 kubelet,也就是 Node 上最为关键的组件,它通过 API Server 接收到所需要 Pod 运行的状态,然后提交到我们下面画的这个 Container Runtime 组件中。
kubelet 是一个节点上的主要服务,它周期性地从 API Server 接受新的或者修改的 Pod 规范并且保证节点上的 Pod 和其中容器的正常运行,还会保证节点会向目标状态迁移,该节点仍然会向 Master 节点发送宿主机的健康状况。
另一个运行在各个节点上的代理服务 kube-proxy 负责宿主机的子网管理,同时也能将服务暴露给外部,其原理就是在多个隔离的网络中把请求转发给正确的 Pod 或者容器。
https://blog.youkuaiyun.com/yjk13703623757/article/details/79819415
-
一个Service
Service是Kubernetes为为屏蔽这些后端实例(Pod)的动态变化和对多实例的负载均衡而引入的资源对象。Service通常与deployment绑定,定义了服务的访问入口地址,应用(Pod)可以通过这个入口地址访问其背后的一组由Pod副本组成的集群实例。Service与其后端Pod副本集群之间则是通过Label Selector来实现映射。(即service会使用sekector来选择pod)Service的类型(Type)决定了Service如何对外提供服务,根据类型不同,服务可以只在Kubernetes cluster中可见,也可以暴露到集群外部。**Service有三种类型,ClusterIP,NodePort和LoadBalancer。**具体的使用场景会在下文中进行阐述。
-
三个IP
Kubernetes为描述其网络模型的IP对象,抽象出Cluster IP和Pod IP的概念。
PodIP是Kubernetes集群中每个Pod的IP地址。它是Docker Engine 根据docker0网桥的IP地址段进行分配的,是一个虚拟的二层网络。Kubernetes中Pod间能够彼此直接通讯,Pod里的容器访问另外一个Pod里的容器,是通过Pod IP所在进行通信。
Cluster IP仅作用于Service,其没有实体对象所对应,因此Cluster IP无法被ping通。它的作用是为Service后端的实例提供统一的访问入口。当访问ClusterIP时,请求将被转发到后端的实例上,默认是轮询方式。Cluster IP和Service一样由kube-proxy组件维护,其实现方式主要有两种,iptables和IPVS。在1.8版本后kubeproxy开始支持IPVS方式。在上例中,SVC的信息中包含了Cluster IP。
nodeip就是物理机IP。
-
四个Port
在Kubernetes中,涉及容器,Pod,Service,集群各等多个层级的对象间的通信,为在网络模型中区分各层级的通信端口,这里对Port进行了抽象。
- nodePort
nodePort为外部机器提供了访问集群内服务的方式。比如一个Web应用需要被其他用户访问,那么需要配置type=NodePort,而且配置nodePort=30001,那么其他机器就可以通过浏览器访问scheme://node:30001访问到该服务,例如http://node:30001。
- Port
该Port非一般意义上的TCP/IP中的Port概念,它是特指Kubernetes中Service的port,是Service间的访问端口,例如Mysql的Service默认3306端口。它仅对集群内容器提供访问权限,而无法从集群外部通过该端口访问服务。
-
containerPort : containerPort是在pod控制器中定义的、pod中的容器需要暴露的端口。
-
targetPort
targetPort是pod的端口(最根本的端口入口),从port/nodePort上来的数据,经过kube-proxy流入到后端pod的targetPort上,最后进入容器。与制作容器时暴露的端口一致(DockerFile中EXPOSE),例如http://docker.io官方的nginx暴露的是80端口。
总结
- Node IP:Node节点的IP地址,即物理网卡的IP地址。(也是外部访问的ip)
- Pod IP:Pod的IP地址,即docker容器的IP地址,此为虚拟IP地址。(为了同server下的容器通信)
- Cluster IP:Service的IP地址,此为虚拟IP地址。在不同Service下的pod节点在集群间相互访问可以通过Cluster IP
port和nodePort都是service的端口,前者暴露给k8s集群内部服务访问,后者暴露给k8s集群外部流量访问。从这两个端口到来的数据都需要经过反向代理kube-proxy,流入后端pod的targetPort上,最后到达pod内容器的containerPort。
外部IP
为什么当service作为服务时候需要nodeip
Service对象在Cluster IP range池中分配到的IP只能在内部访问,如果服务作为一个应用程序内部的层次,还是很合适的。如果这个Service作为前端服务,准备为集群外的客户提供业务,我们就需要给这个服务提供公共IP了。
外部访问者是访问集群代理节点的访问者。为这些访问者提供服务,我们可以在定义Service时指定其spec.publicIPs,一般情况下publicIP 是代理节点的物理IP地址。和先前的Cluster IP range上分配到的虚拟的IP一样,kube-proxy同样会为这些publicIP提供Iptables 重定向规则,把流量转发到后端的Pod上。有了publicIP,我们就可以使用load balancer等常用的互联网技术来组织外部对服务的访问了。
spec.publicIPs在新的版本中标记为过时了,代替它的是spec.type=NodePort,这个类型的service,系统会给它在集群的各个代理节点上分配一个节点级别的端口,能访问到代理节点的客户端都能访问这个端口,从而访问到服务。
service
Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。比如,一个 Deployment 有 3 个 Pod,那么我就可以定义一个 Service。然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod。
service是Kubernetes入口
Kubernetes服务被认为是在集群中运行的。但是您希望能够从外部访问这些服务。Kubernetes有几个组件可以以不同程度的简化和健壮性来实现这一需求,包括NodePort和LoadBalancer,但是最灵活的组件是Ingress。Ingress是一个API,用于管理对集群服务的外部访问,通常通过HTTP方式进行。
kind: Service
apiVersion: v1
metadata:
name: mallh5-service
namespace: abcdocker
spec:
selector:
app: mallh5web
type: NodePort
ports:
- protocol: TCP
port: 3017
targetPort: 5003
nodePort: 31122
这里举出了一个service的yaml,其部署在abcdocker的namespace中。
- 这里配置了nodePort,因此其类型Type就是NodePort,注意大小写。(可以由外部机器访问到)
- 若没有配置nodePort,那这里需要填写ClusterIP,即表示只支持集群内部服务访问。
Headless Service
Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。比如,一个 Deployment 有 3 个 Pod,那么我就可以定义一个 Service。然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod。
那么,这个 Service 又是如何被访问的呢?第一种方式,是以 Service 的 VIP(Virtual IP,即:虚拟 IP)方式。比如:当我访问 10.0.23.1 这个 Service 的 IP 地址时,10.0.23.1 其实就是一个 VIP,它会把请求转发到该 Service 所代理的某一个 Pod 上。这里的具体原理,我会在后续的 Service 章节中进行详细介绍。
第二种方式,就是以 Service 的 DNS 方式。比如:这时候,只要我访问“my-svc.my-namespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod。
而在第二种 Service DNS 的方式下,具体还可以分为两种处理方法:
- 第一种处理方法,是 Normal Service。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致了。
- 而第二种处理方法,正是 Headless Service。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。可以看到,这里的区别在于,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。
下面是一个标准的 Headless Service 对应的 YAML 文件:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
可以这样理解:一个service具有一定的访问的ip,这个ip可以看成是一个作为“头”的pod的ip。Headless Service,没有一个 VIP 作为“头”。这也就是 Headless 的含义。所以,这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。
而它所代理的 Pod,是 Label Selector 机制选择出来的,即:所有携带了 app=nginx 标签的 Pod,都会被这个 Service 代理起来。然后关键来了。当你按照这样的方式创建了一个 Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。有了这个“可解析身份”,只要你知道了一个 Pod 的名字,以及它对应的 Service 的名字,你就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。
怎样做的
StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是:-。而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。
通过这种方法,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。
StatefulSet 其实可以认为是对 Deployment 的改良。
与此同时,通过 Headless Service 的方式,StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录,来作为它的访问入口。
集群搭建
使用kubeadm搭建
# 创建一个Master节点
$ kubeadm init
# 将一个Node节点加入到当前集群中
$ kubeadm join <Master节点的IP和端口>
POD
同一个pod中的容器哪些空间是共享的
https://www.cnblogs.com/rexcheny/p/11017146.html
-
MNT名称空间隔离
Mount名称空间,提供对磁盘挂载点和文件系统的隔离能力。同一主机上的不同进程访问相同的路径会得到相同的内容,因为它们共享本地主机的磁盘和文件系统。在同一POD内容器之间挂载点名称空间是隔离的,如果该POD的多个容器挂载一个POD级别的Volume,那么它们就可以实现挂载点的共享,但共享的也仅仅是这一个Volume并不是整个文件系统。
-
NET名称空间共享
网络名称空间,同一主机上的不同进程可以进行localhost或者本地unix socket通信。在单独启动容器的时候不同容器是隔离的,但是在POD中不同容器通过一个Infra容器来进行共享网络名称空间,其原理是其他用户自己定义的容器都Join这个Infra容器的网络。这里我启动的就是一个Cetnos镜像,无法做本地通信验证。不过它的确是通过Infra容器来共享的。
Deployment、ReplicaSet (版本控制)
如上所示,Deployment 的控制器,实际上控制的是 ReplicaSet 的数目,以及每个 ReplicaSet 的属性。而一个应用的版本,对应的正是一个 ReplicaSet;这个版本应用的 Pod 数量,则由 ReplicaSet 通过它自己的控制器(ReplicaSet Controller)来保证。通过这样的多个 ReplicaSet 对象,Kubernetes 项目就实现了对多个“应用版本”的描述。
Deployment 对应用进行版本控制的具体原理
- ReplicaSet 是一个具体的版本,在滚动升级时,每个版本(ReplicaSet )可能存在多个pod
以下简单介绍几个指令
- kubectl rollout history 指令,看到每个版本对应的 Deployment 的 API 对象的细节
- kubectl rollout undo 命令,就能把整个 Deployment 回滚到上一个版本
- kubectl rollout pause,是让这个 Deployment 进入了一个“暂停”状态
- kubectl rollout resume 指令,把这个 Deployment“恢复”回来
Deployment 对象有一个字段,叫作 spec.revisionHistoryLimit,就是 Kubernetes 为 Deployment 保留的“历史版本”个数。所以,如果把它设置为 0,你就再也不能做回滚操作了。
StatefulSet:拓扑状态
StatefulSet 的设计其实非常容易理解。它把真实世界里的应用状态,抽象为了两种情况:
拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。
StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。
卷
- PVC和PV相当于面向对象的接口和实现
- 用户创建的Pod声明了PVC,K8S会找一个PV配对,如果没有PV,就去找对应的StorageClass,帮它创建一个PV,然后和PVC完成绑定
- 新创建的PV,要经过Master节点Attach为宿主机创建远程磁盘,再经过每个节点kubelet组件把Attach的远程磁盘Mount到宿主机目录
PV、PVC、StorageClass
- PVC 描述的,是 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等。
- PV 描述的,则是一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储服务器地址等。而
- StorageClass 的作用,则是充当 PV 的模板。并且,只有同属于一个 StorageClass 的 PV 和 PVC,才可以绑定在一起。当然,StorageClass 的另一个重要作用,是指定 PV 的 Provisioner(存储插件)。这时候,如果你的存储插件支持 Dynamic Provisioning 的话,Kubernetes 就可以自动为你创建 PV 了。
- PV 描述的是持久化存储数据卷,通常情况下,PV由运维人员负责创建;
PVC 和 PV
PVC 和 PV 的设计,其实跟“面向对象”的思想完全一致。PVC 可以理解为持久化存储的“接口”,它提供了对某种持久化存储的描述,但不提供具体的实现;而这个持久化存储的实现部分则由 PV 负责完成。这样做的好处是,作为应用开发者,我们只需要跟 PVC 这个“接口”打交道,而不必关心具体的实现是 NFS 还是 Ceph。毕竟这些存储相关的知识太专业了,应该交给专业的人去做。
Volume Controller
在 Kubernetes 中,实际上存在着一个专门处理持久化存储的控制器,叫作 Volume Controller。这个 Volume Controller 维护着多个控制循环,其中有一个循环,扮演的就是撮合 PV 和 PVC 的“红娘”的角色。它的名字叫作 PersistentVolumeController。
StorageClass
PV 这个对象的创建,是由运维人员完成的。但是,在大规模的生产环境里,这其实是一个非常麻烦的工作。这是因为,一个大规模的 Kubernetes 集群里很可能有成千上万个 PVC,这就意味着运维人员必须得事先创建出成千上万个 PV。更麻烦的是,随着新的 PVC 不断被提交,运维人员就不得不继续添加新的、能满足条件的 PV,否则新的 Pod 就会因为 PVC 绑定不到 PV 而失败。在实际操作中,这几乎没办法靠人工做到。所以,Kubernetes 为我们提供了一套可以自动创建 PV 的机制,即:Dynamic Provisioning。
相比之下,前面人工管理 PV 的方式就叫作 Static Provisioning。Dynamic Provisioning 机制工作的核心,在于一个名叫 StorageClass 的 API 对象。而 StorageClass 对象的作用,其实就是创建 PV 的模板。具体地说,StorageClass 对象会定义如下两个部分内容:
- 第一,PV 的属性。比如,存储类型、Volume 的大小等等。
- 第二,创建这种 PV 需要用到的存储插件。比如,Ceph 等等。
有了这样两个信息之后,Kubernetes 就能够根据用户提交的 PVC,找到一个对应的 StorageClass 了。然后,Kubernetes 就会调用该 StorageClass 声明的存储插件,创建出需要的 PV。
在 PVC 里指定要使用的 StorageClass 名字
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claim1
spec:
accessModes:
- ReadWriteOnce
storageClassName: block-service
resources:
requests:
storage: 30Gi
怎样在pod中使用静态绑定的pv,pvc
-
为node挂载磁盘,创建PersistentVolume,yaml中有
local: path: /mnt/disks/vol1
这种挂载的语句;需要nodeAffinity字段指示挂载在哪个 机器中。 -
建立一个 StorageClass 来描述这个 PV,如果是Local Persistent Volume不支持动态绑定,并且一定要设置为延迟绑定
StorageClass 里的 volumeBindingMode=WaitForFirstConsumer 的含义,就是告诉 Kubernetes 里的 Volume 控制循环(“红娘”):虽然你已经发现这个 StorageClass 关联的 PVC 与 PV 可以绑定在一起,但请不要现在就执行绑定操作(即:设置 PVC 的 VolumeName 字段)。而要等到第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定。
-
接下来,只需要定义一个非常普通的 PVC,就可以让 Pod 使用到上面定义好的 Local Persistent Volume 了。**它声明的 storageClassName 是 local-storage。**所以,将来 Kubernetes 的 Volume Controller 看到这个 PVC 的时候,不会为它进行绑定操作。
-
然后,编写一个 Pod 来声明使用这个 PVC
kind: Pod apiVersion: v1 metadata: name: example-pv-pod spec: volumes: - name: example-pv-storage persistentVolumeClaim: claimName: example-local-claim containers: - name: example-pv-container image: nginx ports: - containerPort: 80 name: "http-server" volumeMounts: - mountPath: "/usr/share/nginx/html" name: example-pv-storage
一旦使用 kubectl create 创建这个 Pod,就会发现,我们前面定义的 PVC,会立刻变成 Bound 状态,与前面定义的 PV 绑定在了一起。也就是说,在我们创建的 Pod 进入调度器之后,“绑定”操作才开始进行。
在删除时的步骤
-
删除使用这个 PV 的 Pod;
-
从宿主机移除本地磁盘(比如,umount 它);
-
删除 PVC;
-
删除 PV。
当然,由于上面这些创建 PV 和删除 PV 的操作比较繁琐,Kubernetes 其实提供了一个 Static Provisioner 来帮助你管理这些 PV。比如,我们现在的所有磁盘,都挂载在宿主机的 /mnt/disks 目录下。那么,当 Static Provisioner 启动后,它就会通过 DaemonSet,自动检查每个宿主机的 /mnt/disks 目录。然后,调用 Kubernetes API,为这些目录下面的每一个挂载,创建一个对应的 PV 对象出来。
CSI 插件体系的设计原理
https://mritd.me/2020/06/28/how-to-write-a-csi-driver-for-kubernetes/
CSI的目的是定义行业标准“容器存储接口”,使存储供应商(SP)能够开发一个符合CSI标准的插件并使其可以在多个容器编排(CO)系统中工作。CO包括Cloud Foundry, Kubernetes, Mesos等
在 Kubernetes 以前的版本中,其所有受官方支持的存储驱动全部在 Kubernetes 的主干代码中,其他第三方开发的自定义插件通过 FlexVolume 插件的形势提供服务;**相对于 kubernetes 的源码树来说,内置的存储我们称之为 “树内存储”,外部第三方实现我们称之为 “树外存储”;**在很长一段时间里树内存储和树外存储并行开发和使用,但是随着时间推移渐渐的就出现了很严重的问题:
- 想要添加官方支持的存储必须在树内修改,这意味着需要 Kubernetes 发版
- 如果树内存储出现问题则也必须等待 Kubernetes 发版才能修复
为了解决这种尴尬的问题,Kubernetes 必须抽象出一个合适的存储接口,并将所有存储驱动全部适配到这个接口上,存储驱动最好与 Kubernetes 之间进行 RPC 调用完成解耦,这样就造就了 CSI(Container Storage Interface)。
所谓的 CSI 插件开发事实上并非面向 Kubernetes API 开发,而是面向 Sidecar Containers 的 gRPC 开发,Sidecar Containers 一般会和我们自己开发的 CSI 驱动程序在同一个 Pod 中启动,然后 Sidecar Containers Watch API 中 CSI 相关 Object 的变动,接着通过本地 unix 套接字调用我们编写的 CSI 驱动:
CSI 插件体系的设计思想,就是把这个 Provision 阶段,以及 Kubernetes 里的一部分存储管理功能,从主干代码里剥离出来,做成了几个单独的组件,这些组件会通过 Watch API 监听 Kubernetes 里与存储相关的事件变化,比如 PVC 的创建,来执行具体的存储管理动作。
而这些管理动作,比如“Attach 阶段”和“Mount 阶段”的具体操作,实际上就是通过调用 CSI 插件来完成的。
CSI 整个流程实际上大致分为以下三大阶段
Provisioning and Deleting
Provisioning and Deleting 阶段实现**与外部存储供应商协调卷的创建/删除处理,简单地说就是需要实现 CreateVolume 和 DeleteVolume;**假设外部存储供应商为阿里云存储那么此阶段应该完成在阿里云存储商创建一个指定大小的块设备,或者在用户删除 volume 时完成在阿里云存储上删除这个块设备;除此之外此阶段还应当响应存储拓扑分布从而保证 volume 分布在正确的集群拓扑上。
Attaching and Detaching
Attaching and Detaching 阶段实现将外部存储供应商提供好的卷设备挂载到本地或者从本地卸载,简单地说就是实现 ControllerPublishVolume 和 ControllerUnpublishVolume;同样以外部存储供应商为阿里云存储为例,在 Provisioning 阶段创建好的卷的块设备,在此阶段应该实现将其挂载到服务器本地或从本地卸载,在必要的情况下还需要进行格式化等操作。
Mount and Umount
这个阶段在 CSI 设计文档中没有做详细描述,在前两个阶段完成后,当一个目标 Pod 在某个 Node 节点上调度时,kubelet 会根据前两个阶段返回的结果来创建这个 Pod;同样以外部存储供应商为阿里云存储为例,此阶段将会把已经 Attaching 的本地块设备以目录形式挂载到 Pod 中或者从 Pod 中卸载这个块设备。
Hostpath CSI 源码分析
针对官方给出的 CSI 样例,首先把源码弄到本地,然后通过 IDE 打开;这里默认为读者熟悉 Go 语言相关语法以及 go mod 等依赖配置,开发 IDE 默认为 GoLand
docker中的卷
- Kubernetes 卷具有明确的生命周期——与包裹它的 Pod 相同。 因此,卷比 Pod 中运行的任何容器的存活期都长,在容器重新启动时数据也会得到保留。 当然,当一个 Pod 不再存在时,卷也将不再存在。 也许更重要的是,Kubernetes 可以支持许多类型的卷,Pod 也能同时使用任意数量的卷。
- 卷的核心是包含一些数据的目录,Pod 中的容器可以访问该目录。 特定的卷类型可以决定这个目录如何形成的,并能决定它支持何种介质,以及目录中存放什么内容。
- 使用卷时, Pod 声明中需要提供卷的类型 (
.spec.volumes
字段)和卷挂载的位置 (.spec.containers.volumeMounts
字段). - 容器中的进程能看到由它们的 Docker 镜像和卷组成的文件系统视图。 Docker 镜像 位于文件系统层次结构的根部,并且任何 Volume 都挂载在镜像内的指定路径上。 卷不能挂载到其他卷,也不能与其他卷有硬链接。 Pod 中的每个容器必须独立地指定每个卷的挂载位置。
volumes 的6种
-
emptyDir
-
gitRepo
-
hostpath
-
PersistentVolumeClaim
-
configMap,secret
-
各种云平台的存储磁盘卷如google的gce,aws的ebs,azure的azureDisk
其实4只是一个概括,nfs,chef 这些网络存储通通可以单独来使用。但我觉得实际使用中还是讲这些网络存储转化成pv,pvc
emptyDir
emptyDir 两种应用场景:
- 同一个pod中,各个容器间共享文件。
- 当程序对大数据处理时内存空间不够时临时写入文件(当然也可以使用宿主主机的内存)
例子:
apiVersion: v1
kind: Pod
metadata:
name: fortune
spec:
containers:
- image: luksa/fortune
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPaht: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
emptyDir: {} (为{}表示使用节点服务器的文件系统)
- name: html-2
emptyDir:
medium: Memory (使用节点服务器的内存)
当 Pod 指定到某个节点上时,首先创建的是一个
emptyDir
卷,并且只要 Pod 在该节点上运行,卷就一直存在。 就像它的名称表示的那样,卷最初是空的。 尽管 Pod 中的容器挂载emptyDir
卷的路径可能相同也可能不同,但是这些容器都可以读写emptyDir
卷中相同的文件。 当 Pod 因为某些原因被从节点上删除时,emptyDir
卷中的数据也会永久删除。容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃时emptyDir
卷中的数据是安全的。
kubernetes关于pod挂载卷的知识
首先要知道卷是pod资源的属性,pv,pvc是单独的资源。pod 资源的volumes属性有多种type,其中就包含有挂载pvc的类型。
pv一般是系统管理员做的
pvc 是一般k8s用户声明的,大概意思就是说我需要一个 什么权限的,多少存储空间的卷,然后k8s api收到这个请求就去找pv资源对象,如果两者相匹配,那么pv就和pvc绑定了。
从这里我们也想到了,pv如果是手动创建的话,那就麻烦大了。几个,几十个还好说,上万个,管理员该如何创建这么多。所以才有了动态创建pv的需求。这就引出另外一个资源 storageClass ,这个资源声明后端挂载什么样的存储服务,比如nfs,chef等,有了这个一般用户在定义pvc的时候,在提出存储空间和读写权限的同时声明用那个storageClass了,
如下:
通信
https://zhuanlan.zhihu.com/p/81667781
-
同一个机器里的node通信:
Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥,凡是连接在 docker0 网桥上的容器,就可以通过它来进行通信。而且可以跨namespace。使用的是Veth Pair。
容器网络
一个 Linux 容器能看见的“网络栈”,实际上是被隔离在它自己的 Network Namespace 当中的。
而所谓“网络栈”,就包括了:网卡(Network Interface)、回环设备(Loopback Device)、路由表(Routing Table)和 iptables 规则。对于一个进程来说,这些要素,其实就构成了它发起和响应网络请求的基本环境。
bridge模式和docker0详解
集群内部通信
单节点
(一台物理机的通信)
集群单节点内的通信,主要包括两种情况,同一个pod内的多容器间通信以及同一节点不同pod间的通信。由于不涉及跨节点访问,因此流量不会经过物理网卡进行转发。
1.pod内部
- 子网共享外部的网络命名空间但是有自己的子网地址用于内部通信
2. Pod间通信
由于Pod内共享网络命名空间(由pause容器创建),所以**本质上也是同节点容器间的通信。**同时,同一Node中Pod的默认路由都是docker0的地址,由于它们关联在同一个docker0网桥上,地址网段相同,所有它们之间应当是能直接通信的。来看看实际上这一过程如何实现。如上图,Pod1中容器1和容器2共享网络命名空间,因此对pod外的请求通过pod1和Docker0网桥的veth对(图中挂在eth0和ethx上)实现。
跨节点通信
flannel
- 三成overlayed
- flannel相当于每个小区的(node)“传达室”。
- vxlan模式的是直接转发给其他传达室
- udp模式的是先给上一层(邮局)再由邮局给传达室。
CNI:容器网络接口
集群内跨节点通信涉及到不同的子网间通信,仅靠docker0无法实现,这里需要借助CNI网络插件来实现。图中展示了使用flannel实现跨节点通信的方式。
简单说来,flannel的用户态进程flanneld会为每个node节点创建一个flannel.1的网桥,根据etcd或apiserver的全局统一的集群信息为每个node分配全局唯一的网段,避免地址冲突。同时会为docker0和flannel.1创建veth对,docker0将报文丢给flannel.1,。
外部访问集群
从集群外访问集群有多种方式,比如loadbalancer,Ingress,nodeport,nodeport和loadbalancer是service的两个基本类型,是将service直接对外暴露的方式,ingress则是提供了七层负载均衡,其基本原理将外部流量转发到内部的service,再转发到后端endpoints,在平时的使用中,我们可以依据具体的业务需求选用不同的方式。这里主要介绍nodeport和ingress方式。
- Nodeport
(直接找后端的service ip)
通过将Service的类型设置为NodePort,就可以在Cluster中的主机上通过一个指定端口暴露服务。注意通过Cluster中每台主机上的该指定端口都可以访问到该服务,发送到该主机端口的请求会被kubernetes路由到提供服务的Pod上。采用这种服务类型,可以在kubernetes cluster网络外通过主机IP:端口的方式访问到服务。
- ingress
Ingress是推荐在生产环境使用的方式,它起到了七层负载均衡器和Http方向代理的作用,可以根据不同的url把入口流量分发到不同的后端Service。外部客户端只看到http://foo.bar.com这个服务器,屏蔽了内部多个Service的实现方式。采用这种方式,简化了客户端的访问,并增加了后端实现和部署的灵活性,可以在不影响客户端的情况下对后端的服务部署进行调整。
K8s易混点辨析:nodePort、port、targetPort、containerPort
同一个pod
Pod中的容器们运行在一个逻辑“主机”上。他们使用同一个网络命名空间(network namespace,换句话讲,就是同样的IP地址和端口空间),以及同样的IPC(inter-process communication,进程间通信)命名空间,他们还使用共享卷(shared volume)。这些特征使得Pod内的容器能互相高效地通信。同时,Pod使得你可以将多个紧耦合的应用容器当做一个实体来管理。
- nodePort
nodePort提供了集群外部客户端访问service的一种方式,:nodePort提供了集群外部客户端访问service的端口,即nodeIP:nodePort提供了外部流量访问k8s集群中service的入口。
比如外部用户要访问k8s集群中的一个Web应用,那么我们可以配置对应service的type=NodePort,nodePort=30001。其他用户就可以通过浏览器http://node:30001访问到该web服务。
而数据库等服务可能不需要被外界访问,只需被内部服务访问即可,那么我们就不必设置service的NodePort。
- port
port是暴露在cluster ip上的端口,:port提供了集群内部客户端访问service的入口,即clusterIP:port。
mysql容器暴露了3306端口(参考DockerFile),集群内其他容器通过33306端口访问mysql服务,但是外部流量不能访问mysql服务,因为mysql服务没有配置NodePort。对应的service.yaml如下:
apiVersion: v1
kind: Service
metadata:
name: mysql-service
spec:
ports:
- port: 33306
targetPort: 3306
selector:
name: mysql-pod
- targetPort
targetPort是pod上的端口,从port/nodePort上来的数据,经过kube-proxy流入到后端pod的targetPort上,最后进入容器。
与制作容器时暴露的端口一致(使用DockerFile中的EXPOSE),例如官方的nginx(参考DockerFile)暴露80端口。 对应的service.yaml如下:
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort // 配置NodePort,外部流量可访问k8s中的服务
ports:
- port: 30080 // 服务访问端口
targetPort: 80 // pod控制器中定义的端口
nodePort: 30001 // NodePort
selector:
name: nginx-pod
-
containerPort
containerPort是在pod控制器中定义的、pod中的容器需要暴露的端口。 -
总结
总的来说,port和nodePort都是service的端口,前者暴露给k8s集群内部服务访问,后者暴露给k8s集群外部流量访问。从这两个端口到来的数据都需要经过反向代理kube-proxy,流入后端pod的targetPort上,最后到达pod内容器的containerPort。 -
参考文章
Kubernetes中的nodePort,targetPort,port的区别和意义
kubernetes中port、target port、node port的对比分析,以及kube-proxy代理
作业调度与源管理
Pod 是最小的原子调度单位
在 Kubernetes 中,像 CPU 这样的资源被称作“可压缩资源”(compressible resources)。它的典型特点是,当可压缩资源不足时,Pod 只会“饥饿”,但不会退出。而像内存这样的资源,则被称作“不可压缩资源(incompressible resources)。当不可压缩资源不足时,Pod 就会因为 OOM(Out-Of-Memory)被内核杀掉。