深入浅出k8s
部署k8s
概念
Service
所谓 Service,其实就是 Kubernetes 为 Pod 分配的、固定的、基于 iptables(或者 IPVS)的访问入口。而这些访问入口代理的 Pod 信息,则来自于 Etcd,由 kube-proxy 通过控制循环来维护。
Service 原理
- 首先给 Service 分配一个VIP,然后增加 iptables 规则将访问该 IP 的请求转发到后续的 iptables 链。
- KUBE-SERVICES 或者 KUBE-NODEPORTS 规则对应的 Service 的入口链,这个规则应该与 VIP 和 Service 端口一一对应;
- iptables 链实际是一个集合,包含了各个 Pod 的IP(这些称为 Service 的 Endpoints),使用 Round Robin 方式的负载均衡。
- KUBE-SEP-(hash) 规则对应的 DNAT 链,这些规则应该与 Endpoints 一一对应;
- KUBE-SVC-(hash) 规则对应的负载均衡链,这些规则的数目应该与 Endpoints 数目一致;
- 然后,这些 Endpoints 对应的 iptables 规则,正是 kube-proxy 通过监听 Pod 的变化事件,在宿主机上生成并维护的。
上述模式需要维护大量 iptables,在大量Pod的情况下,性能不佳,于是出现了 IPVS 模式。以创建虚拟网卡,并分配虚拟IP的形式,直接使用Linux 的 IPVS 模块,由于将转发逻辑放到了 Linux 内核中执行,性能上有所提升。
DNS
在 Kubernetes 中,Service 和 Pod 都会被分配对应的 DNS A 记录(从域名解析 IP 的记录)。
- ClusterIP:
<serviceName>.<namespace>.svc.cluster.local - Headless Service:
<podName>.<serviceName>.<namesapce>.svc.cluster.local
Pod
- Pod是k8s中调度的最小单元
- Pod 这个看似复杂的 API 对象,实际上就是对容器的进一步抽象和封装
调度
- NodeSelector:是一个供用户将 Pod 与 Node 进行绑定的字段。
- NodeName:Pod具体调度到的节点。
一旦 Pod 的这个字段被赋值,Kubernetes 项目就会被认为这个 Pod 已经经过了调度,调度的结果就是赋值的节点名字。所以,这个字段一般由调度器负责设置,但用户也可以设置它来“骗过”调度器,当然这个做法一般是在测试或者调试的时候才会用到。
常用定义
- HostAliases:定义了 Pod 的 hosts 文件(比如 /etc/hosts)里的内容
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
这样,这个 Pod 启动后,/etc/hosts 文件的内容将如下所示:
cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3 foo.remote
10.1.2.3 bar.remote
- 凡是跟容器的 Linux Namespace 相关的属性和Pod 中的容器要共享宿主机的 Namespace,也一定是 Pod 级别的。
比如,Pod 中的容器共享 PID Namespace,而且会共享宿主机的 Network、IPC 和 PID Namespace。
spec:
# 共享PID Namespace
shareProcessNamespace: true
# 共享宿主机的 Network、IPC 和 PID Namespace
hostNetwork: true
hostIPC: true
hostPID: true
containers:
- name: nginx
image: nginx
生命周期
Pod 遵循一个预定义的生命周期,起始于 Pending 阶段,如果至少其中有一个主要容器正常启动,则进入 Running,之后取决于 Pod 中是否有容器以失败状态结束而进入 Succeeded 或者 Failed阶段。
在 Pod 运行期间,kubelet 能够重启容器以处理一些失效场景。在 Pod 内部,Kubernetes 跟踪不同容器的状态并确定使 Pod 重新变得健康所需要采取的动作。
在 Kubernetes API 中,Pod 包含规约部分和实际状态部分。 Pod 对象的状态包含了一组 Pod 状况(Conditions)。 如果应用需要的话,你也可以向其中注入自定义的就绪性信息。
Pod 在其生命周期中只会被调度一次。 一旦 Pod 被调度(分派)到某个节点,Pod 会一直在该节点运行,直到 Pod 停止或者 被终止。
阶段
Pod 生命周期的变化,主要体现在 Pod API 对象的 Status 部分,这是它除了 Metadata 和 Spec 之外的第三个重要字段。其中,pod.status.phase,就是 Pod 的当前状态,它有如下几种可能的情况:
- Pending。这个状态意味着,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功。
- Running。这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
- Succeeded。这个状态意味着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
- Failed。这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志。
- Unknown。这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。
更进一步地,Pod 对象的 Status 字段,还可以再细分出一组 Conditions。这些细分状态的值包括:PodScheduled、Ready、Initialized,以及Unschedulable。它们主要用于描述造成当前 Status 的具体原因是什么。
比如,Pod 当前的 Status 是 Pending,对应的 Condition 是 Unschedulable,这就意味着它的调度出现了问题。
容器Containers
常用定义
- ImagePullPolicy: Boolean,它定义了镜像拉取的策略,默认是 Always即每次创建 Pod 都重新拉取一次镜像。改为False每次就使用本地存在的镜像
- Lifecycle 字段,它定义的是 Container Lifecycle Hooks,在容器状态发生变化时触发一系列“钩子”。
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
postStart 容器启动后通过 echo 命令写入一段欢迎信息。
执行在 Docker 容器 ENTRYPOINT 执行之后,但不保证严格顺序,也就是说,在 postStart 启动时,ENTRYPOINT 有可能还没有结束。
preStop 容器被杀死之前(比如,收到了 SIGKILL 信号)调用 nginx 退出命令,优雅停止。
而需要明确的是,preStop 操作的执行,是同步的。所以,它会阻塞当前的容器杀死流程,直到这个 Hook 定义操作完成之后,才允许容器被杀死,这跟 postStart 不一样。
节点上的kubelet将等待最多宽限期(在 Pod 上指定,或从命令行传递;默认为 30 秒)以关闭容器,然后强行终止进程(使用SIGKILL)。请注意,此宽限期包括执行preStop勾子的时间。
容器探针
健康检查
在 Kubernetes 中,你可以为 Pod 里的容器定义一个健康检查“探针”(Probe)。这样,kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器镜像是否运行(来自 Docker 返回的信息)作为依据。
这种机制,是生产环境中保证应用健康存活的重要手段。
这是一个livenessProbe探针的使用示例
spec:
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
initialDelaySeconds:从启动后5s执行检查
periodSeconds:每5s执行一次检查
成功返回0,Pod就认为此容器启动成功
除了在容器中执行命令外,livenessProbe 也可以定义为发起 HTTP 或者 TCP 请求的方式
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: X-Custom-Header
value: Awesome
initialDelaySeconds: 3
periodSeconds: 3
---
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
所以,你的 Pod 其实可以暴露一个健康检查 URL(比如 /healthz),或者直接让健康检查去检测应用的监听端口。这两种配置方法,在 Web 服务类的应用中非常常用。
恢复机制
Kubernetes 里的 Pod 恢复机制,也叫 restartPolicy。它是 Pod 的 Spec 部分的一个标准字段(pod.spec.restartPolicy),默认值是 Always,即:任何时候这个容器发生了异常,它一定会被重新创建。
但一定要强调的是,Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。
事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点。
这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。
而如果你想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理 Pod,哪怕你只需要一个 Pod 副本。
除了 Always,它还有 OnFailure 和 Never 两种情况:
- Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
- OnFailure: 只在容器 异常时才自动重启容器;
- Never: 从来不重启容器。
在实际使用时,我们需要根据应用运行的特性,合理设置这三种恢复策略。
而如果你要关心这个容器退出后的上下文环境,比如容器退出后的日志、文件和目录,就需要将 restartPolicy 设置为 Never。
PodPrese
PodPreset 是一种 K8s API 资源,用于在创建 Pod 时注入其他运行时需要的信息,这些信息包括 secrets、volume mounts、environment variables 等。
可以看做是 Pod 模板。
首先定义一个 PodPreset 对象,把想要的字段都加进去:
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
name: allow-database
spec:
selector:
matchLabels:
role: frontend
env:
- name: DB_PORT
value: "6379"
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
通过matchLabels:role: frontend匹配到对应的Pod,然后k8s会自动把PodPreset对象里的预定义的字段添加进去,这里就是env、volumeMounts、volumes3个字段。
然后我们写一个简单的Pod
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
spec:
containers:
- name: website
image: nginx
ports:
- containerPort: 80
其中的 Label role: frontend和PodPreset allow-database 匹配,所以会在创建Pod之前自动把预定义字段添加进去。
需要说明的是,PodPreset 里定义的内容,只会在 Pod API 对象被创建之前追加在这个对象本身上,而不会影响任何 Pod 的控制器的定义。
Pod 的终止
由于 Pod 所代表的是在集群中节点上运行的进程,当不再需要这些进程时允许其体面地 终止是很重要的。一般不应武断地使用 KILL 信号终止它们,导致这些进程没有机会 完成清理操作。
具体停止过程大致如下:
- 用户删除 Pod
- Pod 进入 Terminating 状态;
- 与此同时,k8s 会从对应的 service 上移除该 Pod 对应的 endpoint
- 与此同时,针对有 preStop hook 的容器,kubelet 会调用每个容器的 preStop hook,假如 preStop hook 的运行时间超出了 grace period(默认30秒),kubelet 会发送 SIGTERM 并再等 2 秒;
- 与此同时,针对没有 preStop hook 的容器,kubelet 发送 SIGTERM
- grace period 超出之后,kubelet 发送 SIGKILL 干掉尚未退出的容器
kubelet 向runtime发送信号,最终runtime会将信号发送给容器中的主进程。
所以在程序中监听该信号可以实现优雅关闭。
Volume
概念
k8s 中的 Volume 属于 Pod 内部共享资源存储,生命周期和 Pod 相同,与 Container 无关,即使 Pod 上的容器停止或者重启,Volume 不会受到影响,但是如果 Pod 终止,那么这个 Volume 的生命周期也将结束。
这样的存储无法满足有状态服务的需求,于是推出了 Persistent Volume,故名思义,持久卷是能将数据进行持久化存储的一种资源对象。它是独立于Pod的一种资源,是一种网络存储,它的生命周期和 Pod 无关。云原生的时代,持久卷的种类也包括很多,iSCSI,RBD,NFS,以及CSI, CephFS, OpenSDS, Glusterfs, Cinder 等网络存储。可以看出,在kubernetes 中支持的持久卷基本上都是网络存储,只是支持的类型不同。
k8s 中支持多种类型的卷(持久卷),比如:
- 本地的普通卷
- emptyDir
- hostpath
- 本地持久化卷
- local
- 远程文件系统或者块存储
- nfs
- rbd
- 一些特殊卷
- configMap
- secret
挂载流程
Kubernetes 的 Volume 挂载流程分为两大部分:
- Volume 准备阶段:包括将远程存储设备挂载到宿主机。
- Volume 挂载到 Pod 容器阶段:将宿主机上的目录绑定到容器中。
这两部分由kube-controller-manager、kubelet和Container Runtime Interface (CRI)共同完成。
Volume 挂载至宿主机(远程存储 Volume)
只有远程存储(使用 PV)才需要通过此阶段,将存储设备挂载到宿主机。过程包括两个主要步骤:
Attach 阶段
负责将远程磁盘通过网络挂载到宿主机,以下是详细流程:
- 负责组件:
AttachDetachController(Kubernetes 核心组件之一)。- 存储系统的 Sidecar
external-attacher。
- 处理逻辑:
AttachDetachController是VolumeController的一部分,运行在kube-controller-manager中。它会持续检查宿主机与 PV 的关系是否符合期望,如果发现要 Attach 则会创建一个 VolumeAttachment 对象。external-attacher通过监听 VolumeAttachment 的状态,调用存储系统的 CSIControllerPublishVolume接口,将存储设备绑定到宿主机。以 Google Cloud Persistent Disk 为例,行为类似执行以下命令:
$ gcloud compute instances attach-disk <虚拟机名字> --disk <磁盘名字>
这实际上调用了 Google Cloud 的 API 为虚拟机挂载磁盘。
Mount 阶段
负责将挂载到宿主机的磁盘设备格式化并挂载到 Volume 对应的宿主机目录。
- 负责组件:
VolumeManagerReconciler(运行在kubelet内部)。
- 处理逻辑:
- Kubelet 通过
VolumeManager启动一个循环机制,周期性检查 PV 和宿主机的挂载情况。如果发现有新挂载需求,会执行 Mount 操作。 - 对于块存储(如 iSCSI),其行为如下:
# 获取磁盘设备 ID $ lsblk
# 格式化为 ext4 文件系统 $ mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/<磁盘设备ID>
# 挂载到 Volume 对应目录 $ mkdir -p /var/lib/kubelet/pods/<PodID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>
$ mount /dev/<磁盘设备ID> /var/lib/kubelet/pods/<PodID>/volumes/kubernetes.io~<Volume类型>/<Volume名字> - 对于文件系统存储(如 NFS),可以直接挂载:
$ mount -t nfs <NFS服务器地址>:/ /var/lib/kubelet/pods/<PodID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>
最终,准备完成的 Volume 会存储在宿主机的目录中:
/var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>
- Kubelet 通过
Volume 挂载到 Pod 容器
这一步由 CRI 负责,将宿主机目录通过 Bind Mount 挂载到 Pod 中。
- 实现过程:
- 创建容器进程,并开启
Mount namespace,以隔离容器的文件系统与宿主机。 - 将宿主机目录挂载到容器目录。例如:
$ mount /var/lib/kubelet/pods/<PodID>/volumes/kubernetes.io~<Volume类型>/<Volume名字> <容器中的目录> - 调用
pivot_root或chroot,重新设置容器的根文件系统,确保容器只能够访问挂载到其内部的目录
至此,Pod 内的容器可以访问 Volume。
这是一张volume挂载流程示意图

小结:
- 创建容器进程,并开启
- 用户创建 Pod,指定使用某个 PVC
- AttachDetachController watch 到有 Pod 创建,且 Pod 指定的 PVC 对应的 PV 还没有执行 Attach,那么就执行 Attach 操作,实际为创建一个 VolumeAttachment 对象
- external-attacher sidecar watch 到有 VolumeAttachment 创建,就调用 CSI Plugin 的ControllerPublish 接口完成 Attach 步骤
- kubelet 通过 VolumeManager 启动 reconcile loop,当观察到有新的使用 PersistentVolumeSource 为CSI 的 PV 的 Pod 调度到本节点上,于是调用 reconcile 函数进行 Mount/Unmount 相关逻辑处理。
- volume 准备完成后,调用 CRI 接口创建 Pod,将 volume 通过 Mounts 参数传递过去,最终由 CRI 将 volume 对应的宿主机目录挂载到 Pod 中
PV (持久化存储数据卷)
我提供什么,不关心谁在用我
PV 是集群中一块已经配置好的存储,是集群级别的资源。它如同节点(Node)一样,是构成集群基础设施的一部分。
- 角色:对底层实际存储(如云盘、NFS、本地磁盘)的抽象封装。
- 供给方式:
- 静态供给 (Static):集群管理员预先手动创建一系列 PV。这些 PV 包含了底层存储的详细信息,等待被符合条件的 PVC 声明和绑定。
- 动态供给 (Dynamic):当没有合适的静态 PV 可用时,如果 PVC 指定了
StorageClass,系统会根据StorageClass的定义自动创建一个新的 PV,并与该 PVC 绑定。这是云原生环境中最常用、最灵活的方式。
- 状态 (Status):
Available:可用,未被任何 PVC 绑定。Bound:已绑定到某个 PVC。Released:绑定的 PVC 已被删除,但资源尚未被集群回收。Failed:因某种原因回收失败。
- 回收策略 (Reclaim Policy):定义了当 PV 被释放(其绑定的 PVC 被删除)后,如何处置底层存储卷。
Retain(保留):保留底层存储卷和数据。PV 状态变为Released,需要管理员手动清理和回收。常用于生产环境,防止误删数据。Delete(删除):删除底层存储卷和数据(例如,云服务商的磁盘会被删除)。常用于开发测试环境。Recycle(回收):(已废弃) 会擦除卷上数据,使其可被新 PVC 复用。
PVC (PV 使用请求/持久卷声明)
我需要什么,不关心存储的实现
PVC 是用户对存储资源的“申请”。它将应用对存储的需求与底层存储的实现细节解耦。
- 角色:作为应用(Pod)与存储(PV)之间的中间层。应用开发者只需声明所需的存储大小和访问模式,而无需关心存储来自何处。
- 声明内容:
- 存储容量 (
spec.resources.requests.storage):需要多大的空间。 - 访问模式 (
spec.accessModes):描述了卷应如何被节点挂载。ReadWriteOnce(RWO):卷可以被单个节点以读写方式挂载。ReadOnlyMany(ROX):卷可以被多个节点以只读方式挂载。ReadWriteMany(RWX):卷可以被多个节点以读写方式挂载。ReadWriteOncePod(RWOP): 卷只能被单个 Pod 以读写方式挂载 (v1.22+)。
- 存储类 (
spec.storageClassName):指定使用哪个StorageClass来动态创建 PV。
- 存储容量 (
- 生命周期:PVC 的生命周期独立于 Pod。Pod 被删除后,PVC 及其绑定的数据卷依然存在,直到用户手动删除 PVC。
StorageClass(存储类/PV 的创建模板)
Volume Binding Mode(卷绑定模式)
关于k8s中StorageClass的VOLUMEBINDINGMODE属性的两种模式的差别
Immediate
- 绑定时机:PVC 一创建就会立即去匹配合适的 PV。
- 特征:不管 Pod 是否存在,PVC 都会先绑定上存储。
- 场景:适合共享存储(NFS、CephFS、云盘等支持跨节点挂载),因为调度时无需考虑存储与节点的拓扑。
- 问题:如果底层存储是有拓扑限制的(比如只能挂在某些 zone/节点上),那么 PVC 可能被绑定到一个 Pod 永远无法调度到的存储卷 → 调度失败。
WaitForFirstConsumer
- 绑定时机:只有当有 Pod 真正引用 PVC 且要调度时,才会进行 PV 选择和绑定。
- 特征:调度器会把存储拓扑(zone、节点可用性等)作为考虑因素,保证存储和 Pod 部署位置匹配。
- 场景:适合有拓扑限制的存储系统(本地盘、分区化云盘、AZ 限定的存储卷等)。
- 优点:避免卷先随便绑定,导致 Pod 卡住。
PV & PVC & StorageClass 下的存储体系具体运作流程如下:
创建 StorageClass & provisione
首先由运维人员创建对应 StorageClass 并在 K8S 集群中运行配套的 provisioner 组件。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-sc
annotations:
storageclass.kubernetes.io/is-default-class: "true"
parameters:
archiveOnDelete: "false"
reclaimPolicy: "Delete"
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner-nfs-sc
StorageClass 中的 provisioner 指明了要使用的 provisioner,然后以 deployment 方式部署对应的 provisioner:
当然这里还需要 RBAC 权限等配置,比较多先省略了,完整配置见 nfs-subdir-external-provisioner
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner-nfs-sc
labels:
app: nfs-client-provisioner-nfs-sc
namespace: kube-system
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner-nfs-sc
template:
metadata:
labels:
app: nfs-client-provisioner-nfs-sc
spec:
serviceAccountName: nfs-client-provisioner
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/master
operator: In
values:
- ""
containers:
- name: nfs-client-provisioner
image: caas4/nfs-subdir-external-provisioner:v4.0.2
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner-nfs-sc
- name: NFS_SERVER
value: 172.20.151.105
- name: NFS_PATH
value: /tmp/nfs/data
volumes:
- name: nfs-client-root
nfs:
server: 172.20.151.105
path: /tmp/nfs/data
其中以环境变量的方式指明了 PROVISIONER_NAME 为 k8s-sigs.io/nfs-subdir-external-provisioner-nfs-sc,以及 NFS 的相关参数。
StorageClass 中的 provisioner: k8s-sigs.io/nfs-subdir-external-provisioner-nfs-sc 和这里的 PROVISIONER_NAME 是对应的,因此可以找到对应的 provisioner。
创建 PVC
然后开发人员创建需要的 PVC,比如
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-claim
spec:
storageClassName: nfs-sc
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi
需要注意的一点是 storageClassName 必须和前面创建的 StorageClass 对应。
PVC 创建之后就轮到 PersistentVolumeController 登场了。
PersistentVolumeController 会根据 PVC 寻找对应的 PV 来进行绑定,没有的话自然无法绑定。
这时候我们前面创建的 provisioner 就起作用了,他会 watch pvc 对象,比如这里我们创建了 PVC,provisioner 就会收到相应事件,然后根据 PVC 中的 storageClassName 找到对应 StorageClass,然后根据 StorageClass中的 provisioner 字段找到对应 provisioner,发现是自己处理的,就 调用 CSI Plugin 的接口 CreateVolume 创建出 volume,然后在 k8s 里创建对应的 PV 来指代这个 volume。
CSI Plugin CreateVolume 接口则由具体厂商实现,比如 阿里云实现的 CreateVolume 可能就是在阿里云上创建了一块云盘。
最后 PV 创建之后,PersistentVolumeController 就将二者进行绑定。
创建 Pod
PVC 和 PV 绑定之后就可以使用了,创建一个 Pod 来使用这个 PVC:
kind: Pod
apiVersion: v1
metadata:
name: test-pod
spec:
containers:
- name: test-pod
image: busybox:stable
command:
- "/bin/sh"
args:
- "-c"
- "touch /mnt/SUCCESS && exit 0 | exit 1"
volumeMounts:
- name: nfs-pvc
mountPath: "/mnt"
restartPolicy: "Never"
volumes:
- name: nfs-pvc
persistentVolumeClaim:
claimName: test-claim
这里就是通过 claimName 来指定要使用的 PVC。
Pod 创建之后 k8s 就可以根据 claimName 找到对应 PVC,然后 PVC 绑定的时候会把 PV 的名字填到 spec.volumeName 字段上,因此这里又可以找到对应的 PV,然后就进入到第二节中的挂载流程了。
小结
至此,k8s 的这套持久化存储体系运作流程就算是完成了。流程如下图所示:

- 管理员预先创建好 StorageClass 和 Provisioner
- 用户创建 PVC
- 根据 PVC 中的 StorageClass 最终找到 Provisioner
- Provisioner 创建 PV
- PersistentVolumeController 绑定 PVC 和 PV
- 创建 Pod 时指定 PVC,随后进入第二节的持久卷挂载流程
Projected Volume
它们存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。
这些特殊 Volume 的作用,是为容器提供预先定义好的数据。
所以,从容器的角度来看,这些 Volume 里的信息就是仿佛是被 Kubernetes“投射”(Project)进入容器当中的。这正是 Projected Volume 的含义。
到目前为止,Kubernetes 支持的 Projected Volume 一共有四种:
- ConfigMap
- Secret
- Downward API
- ServiceAccountToken
ConfigMap
创建
ConfigMap主要提供配置文件的键值对,主要用法如下
# 创建
$ kubectl create configmap
# 删除
$ kubectl delete configmap ConfigMap名称
# 编辑
$ kubectl edit configmap ConfigMap名称
# 查看-列表
$ kubectl get configmap
# 查看-详情
$ kubectl describe configmap ConfigMap名称
可以使用 kubectl create configmap 从以下多种方式创建 ConfigMap。
- yaml 描述文件:事先写好标准的configmap的yaml文件,然后kubectl create -f 创建
- –from-file:通过指定目录或文件创建,将一个或多个配置文件创建为一个ConfigMap
- –from-literal:通过直接在命令行中通过 key-value 字符串方式指定configmap参数创建
- –from-env-file:从 env 文件读取配置信息并创建为一个ConfigMap
使用
可以通过环境变量或者文件的形式挂载到Pod中(每一个键对应一个文件)
环境变量
envFrom:
- configMapRef:
name: env-cm
文件
volumes:
- name: foo
configMap:
name: cm1
Secret
Secret主要用于为Pod提供密码、token等敏感数据,内部数据都是加密之后的
ServiceAccountToken 一种特殊的 Secret,是 Kubernetes 系统内置的一种“服务账户”,它是 Kubernetes 进行权限分配的对象。
为了方便使用,Kubernetes 已经为你提供了一个默认“服务账户”(default Service Account)。并且,任何一个运行在 Kubernetes 里的 Pod,都可以直接使用这个默认的 Service Account,而无需显示地声明挂载它(k8s 默认会为每一个Pod 都挂载该Volume)。
Secret有三种类型
- Opaque:base64 编码格式的 Secret,用来存储密码、密钥等;但数据也可以通过base64 –decode解码得到原始数据,所以加密性很弱。
- Service Account:用来访问Kubernetes API,由Kubernetes自动创建,并且会自动挂载到Pod的 /run/secrets/kubernetes.io/serviceaccount 目录中。
- kubernetes.io/dockerconfigjson : 用来存储私有docker registry的认证信息。
创建
Secret 同样有多种创建方式
- yaml 描述文件:事先写好标准的secret的yaml文件,然后kubectl create -f 创建
- –from-file:通过指定目录或文件创建,将一个或多个配置文件创建为一个Secret
- –from-literal:通过直接在命令行中通过 key-value 字符串方式指定configmap参数创建
一般通过如下命令加密数据
$ echo -n 'world'|base64
d29ybGQ=
然后填入这样的yaml中
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
hello: d29ybGQ=
使用
用环境变量加载Secret
env:
- name: CUSTOM_HELLO
valueFrom:
secretKeyRef:
name: s2
key: hello
通过卷挂载方式,同样一个键对应一个文件
volumeMounts:
- name: config
mountPath: "/etc/foo"
readOnly: true
volumes:
- name: config
projected:
sources:
- secret:
name: mysecret
Downward API
我也不清楚怎么用,以后补完
Controller (控制器)
Kubernetes 中的 Controller(控制器)是其自动化和自愈能力的核心。所有控制器都遵循一个通用的编排模式,即:控制循环(control loop)。
这个循环的核心思想是不断地将系统的 “实际状态” (Actual State)与用户定义的 “期望状态” (Desired State)进行比较,并通过一系列操作来消除它们之间的差异。
这个过程可以用以下伪代码来描述:
for {
期望状态 := 获取对象 X 的期望状态 (Desired State)
实际状态 := 获取集群中对象 X 的实际状态 (Actual State)
if 实际状态 == 期望状态 {
// 状态一致,什么都不做
} else {
// 状态不一致,执行编排动作,将实际状态调整为期望状态
执行调整(期望状态, 实际状态)
}
}
Kubernetes 内置了多种控制器(如 Deployment, ReplicaSet, StatefulSet, DaemonSet 等),每种控制器负责管理特定类型的资源,共同构成了 Kubernetes 强大的集群管理能力。
Deployment (部署)
Deployment 是 Kubernetes 中最常用、也是最具代表性的控制器之一。它为 Pod 和 ReplicaSet 提供了一种声明式的管理方式。实际上,它是一个两层控制器,其设计精妙地将 “版本管理” 和 “副本数量保证” 这两个职责分离开来。
- 首先,它通过 管理 ReplicaSet 来描述应用的不同版本。每次应用更新,Deployment 都会创建一个新的 ReplicaSet。
- 然后,每个 ReplicaSet 再通过其
replicas属性来保证其所管理的 Pod 副本数量。
注:Deployment 控制 ReplicaSet(负责版本),ReplicaSet 控制 Pod(负责副本数)。这个两层控制关系是理解 Deployment 工作原理的关键。
通过这种方式,Deployment 不仅能保证 Pod 稳定地维持在指定数量,还能轻松实现滚动更新(Rolling Update)和版本回滚(Rollback)等高级功能,是 Kubernetes 无状态应用编排的最佳实践。
ReplicaSet (副本集)
ReplicaSet 是一个相对简单的控制器,它的核心职责只有一个:保证在任何时候都有指定数量的 Pod 副本在稳定运行。它是实现应用高可用的基础。
ReplicaSet 的工作机制同样遵循控制循环模式。它通过一个标签选择器(Label Selector)来识别它应该管理的 Pod 集合。
- 如果发现正在运行的 Pod 数量少于期望副本数,它就会根据定义的 Pod 模板(Pod Template) 来创建新的 Pod。
- 如果发现 Pod 数量多于期望副本数,它就会随机终止多余的 Pod。
一个 ReplicaSet 的 YAML 定义主要包含三个关键部分: replicas:期望的 Pod 副本数量。selector:标签选择器,用于识别哪些 Pod 属于该 ReplicaSet 的管理范围。template:Pod 模板,当需要创建新 Pod 时,会以此为蓝本

注:虽然 ReplicaSet 可以独立使用,但在现代 Kubernetes 实践中,用户几乎不应直接操作 ReplicaSet。我们应该始终通过 Deployment 来管理应用。Deployment 会自动创建和管理其背后的 ReplicaSet,从而为我们屏蔽了版本管理的复杂细节。
网络
Kubernetes教程(二)---集群网络之 Flannel 核心原理 - 指月小筑(探索云原生)
Flannel的VXLAN模式性能远好于UDP模式,原因是UDP模式需要频繁切换用户态和内核态