Kubernetes 中有状态应用的管理方法
在 Kubernetes 中,有状态应用(Stateful Applications)通常指需要保存状态或持久化数据的应用,如数据库(MySQL, PostgreSQL 等)和消息队列(Kafka, RabbitMQ等)。
对于这类应用,Kubernetes 提供了一种叫做 StatefulSet 的工作负载 API 对象。StatefulSet 是为了解决有状态应用的特定问题(例如网络标识和持久存储)而创建的。与 Deployment 和 ReplicaSet 这类无状态应用控制器不同,StatefulSet 为每个 Pod 维护一个粘性身份,并保证 Pod 的部署和扩展顺序。
StatefulSet 提供的主要特性包括:
稳定、唯一的网络标识:每个 Pod 从 StatefulSet 创建时就拥有一个唯一的标识符,即使 Pod 被重新调度,这个标识符也不会改变。
稳定、持久的存储:StatefulSet 可以使用 PersistentVolume 提供持久化存储。当 Pod 被重新调度时,与之关联的 Persistent Volumes 会被重新挂载。
有序的、优雅的部署和扩展:当 Pod 被停止时,StatefulSet 会保证其在完全停止前不会启动其他的 Pod。
有序的、优雅的删除和终止:当需要删除 Pods,StatefulSet 会以逆序保证它们的安全删除。
有序的滚动更新:StatefulSet 的更新可以是有序的自动滚动更新。
在实际操作中,StatefulSet 通常与 PersistentVolume 和 PersistentVolumeClaim 一起使用,以保证数据的持久化存储。此外,有状态应用通常需要一个稳定的网络标识和发现机制,这可以通过 Kubernetes 的 Service 来提供。
例如,对于运行 MySQL 数据库的有状态应用,我们可能会创建一个名为 “mysql” 的 StatefulSet 和一个同样命名为 “mysql” 的 Service。此时,StatefulSet 中的每个 Pod 都会获得一个唯一的主机名,比如 “mysql-0”、”mysql-1”,等等。
当我们创建了一个 Service,该 Service 就会得到一个 DNS 名称,如 “mysql.default.svc.cluster.local”。Pods 可以通过这个 DNS 名称找到并连接到 MySQL 服务。此外,StatefulSet 中的每个 Pod 还会得到它们自己的 DNS 主机名,如 “mysql-0.mysql.default.svc.cluster.local”,”mysql-1.mysql.default.svc.cluster.local”。这样,每个 Pod 都有一个稳定的网络标识,而其他 Pods 可以使用这个网络标识找到并连接到特定的 Pod。
当创建一个 Service,Kubernetes 的 DNS 服务会为这个 Service 创建一个 DNS 记录。此 DNS 名称遵循如下的格式:·<service-name>.<namespace-name>.svc.cluster.local
。其中,<service-name>
是创建的 Service 的名称,<namespace-name>
是这个 Service 所在的命名空间名称,svc 和 cluster.local 是默认的后缀。
Pod 可以通过使用这个 DNS 名称来访问 Service,而不需要知道服务背后具体由哪些 Pod 提供。Kubernetes 会自动将网络请求转发到这个 Service 关联的一个或多个 Pod 上。
当创建一个 StatefulSet 时,如果同时为这个 StatefulSet 创建了一个同名的 Service,那么 Kubernetes 不仅会为这个 Service 创建一个 DNS 记录,还会为 StatefulSet 中的每个 Pod 创建一个 DNS 记录,这个记录的格式如下:<pod-name>.<service-name>.<namespace-name>.svc.cluster.local
。其中,<pod-name>
是 Pod 的名称,它是基于 StatefulSet 名称和 Pod 在 StatefulSet 中的索引号生成的。
这样,StatefulSet 中的每个 Pod 不仅有自己唯一的 DNS 名称,而且这个 DNS 名称是稳定的,不会因为 Pod 重启或被替换而改变。同时,Pod 还可以通过 Service 的 DNS 名称来访问 StatefulSet 中的其他 Pod,从而实现 Pod 间的通信。
当我们创建一个 StatefulSet,我们通常会为其创建一个同名的 Headless Service 来控制网络域。这个 Headless Service 不会有一个 ClusterIP,而是直接返回后端 Pods 的 IP 地址。举个例子:
apiVersion: v1
kind: Service
metadata:
name: web
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
然后,我们创建一个 StatefulSet,这个 StatefulSet 的名称也是 “web”:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "web"
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
我们创建了一个 StatefulSet,它有 3 个 Pod。这个 StatefulSet 使用的服务名称(serviceName)是 “web”,这是我们之前创建的 Headless Service 的名称。
由于我们的 Headless Service 和 StatefulSet 名称相同,Kubernetes 会为每个 Pod 创建一个唯一的 DNS 记录,格式为 <pod-name>.<service-name>.<namespace-name>.svc.cluster.local
。这样,这些 Pod 就可以通过 DNS 名称来互相发现和通信了。例如,第一个 Pod 的 DNS 名称会是 web-0.web.default.svc.cluster.local(假设这个 Pod 和 Service 都在默认命名空间)。
首先,要明确一点,一个 StatefulSet 下并不会有多个 Service。StatefulSet 是与一个 Headless Service(也就是没有 ClusterIP 的 Service)关联的,这个 Service 主要是为 StatefulSet 中的每个 Pod 提供一个稳定的网络标识。
web-0.web.default.svc.cluster.local 和 web-0.web1.default.svc.cluster.local 当然是可以通信的,前提是网络策略允许。这两个地址都是 DNS 名称,解析后会得到对应 Pod 的 IP 地址。在 Kubernetes 中,Pod 之间是可以进行网络通信的(除非被网络策略限制)。
但这里需要注意的是,web-0.web.default.svc.cluster.local 和 web-0.web1.default.svc.cluster.local 分别属于不同的 StatefulSet,web 和 web1 应该是两个不同的 StatefulSet(因为一个 StatefulSet 对应一个 Headless Service,StatefulSet 名称和对应的 Headless Service 名称通常是一致的)。他们之间的通信并不是 StatefulSet 为了实现 Pod 之间的稳定网络通信而设计的,而是 Kubernetes 网络模型的一部分:所有 Pod 都处在一个扁平的、共享的网络地址空间中,可以直接通过 IP 地址进行通信。
所以,即使不用 Service,只要知道了对方 Pod 的 IP 地址,Pod 之间也是可以进行通信的。Service(特别是 Headless Service)的主要作用是提供了一种基于 DNS 的、名字到地址的解析机制,使得 Pod 之间可以通过稳定的、易于理解的名字来进行通信,而不必关心对方的具体 IP 地址。
如何理解:基于 DNS 的名字到地址的解析机制? 当创建一个 Service 时,Kubernetes 控制平面会为该 Service 分配一个固定的 IP 地址,称为 Cluster IP。这个 Cluster IP 地址在整个集群的生命周期内都是不变的,无论背后的 Pods 如何更换。因此,其他的 Pods 或者节点可以通过这个 Cluster IP 来访问 Service,进而访问背后的 Pods。 但是,IP 地址并不是很直观,很难记住。这就是 DNS 的作用。在 Kubernetes 中,有一个组件叫做 Kube-DNS 或者 CoreDNS,它们会监听 Kubernetes API,当有新的 Service 创建时,就会为这个 Service 生成一个 DNS 记录。这个 DNS 记录的格式通常是
<service-name>.<namespace-name>.svc.cluster.local
,它会解析到对应 Service 的 Cluster IP。这样,其他的 Pods 就可以通过这个 DNS 名称来访问 Service,而不必记住复杂的 IP 地址。 让我们来看看 Headless Service。Headless Service 是没有 Cluster IP 的 Service。当为一个 StatefulSet 创建一个 Headless Service 时,Kube-DNS 或者 CoreDNS 不会为这个 Service 生成一个解析到 Cluster IP 的 DNS 记录,而是会为 StatefulSet 中的每一个 Pod 生成一个独立的 DNS 记录。这个 DNS 记录的格式是<pod-name>.<service-name>.<namespace-name>.svc.cluster.local
,它会解析到对应 Pod 的 IP 地址。 这就是我说的基于 DNS 的名字到地址的解析机制。这样,StatefulSet 中的每一个 Pod 都可以通过其他 Pod 的 DNS 名称来进行通信,而不必关心对方的具体 IP 地址。例如,如果 StatefulSet 的名字是 web,Headless Service 的名字也是 web,那么第一个 Pod(名字是 web-0)可以通过 web-1.web.default.svc.cluster.local 来访问第二个 Pod(名字是 web-1)。 因此,StatefulSet + Headless Service 提供了一种机制,让有状态应用中的每一个实例(即 Pod)都可以拥有一个稳定的网络标识(即 DNS 名称),并且这个网络标识在 Pod 重启或者迁移时不会改变.
例如,如果有一个 StatefulSet 叫做 web,并且为它创建了一个同名的 Headless Service,那么这个 StatefulSet 的第一个 Pod(名称为 web-0)就会拥有一个 DNS 记录 web-0.web.default.svc.cluster.local(这里假设他们在 default 命名空间),而这个 DNS 记录会解析到 Pod web-0 的 IP 地址。同样,第二个 Pod(名称为 web-1)就会拥有一个 DNS 记录 web-1.web.default.svc.cluster.local,这个 DNS 记录会解析到 Pod web-1 的 IP 地址。
这种机制确保了,即使 Pod 重新调度或者迁移到其他节点,它的网络标识(即 DNS 记录)仍然保持不变,因为 DNS 记录是基于 Pod 的名称,而 Pod 的名称在整个生命周期内是不变的。这对于一些需要稳定网络标识的有状态应用(如数据库和分布式存储系统)来说是非常重要的。
Operator 管理“有状态应用”
Operator 是由 CoreOS 提出的,用于扩展 Kubernetes API 以自动管理复杂状态应用的一种方法。简单来说,一个 Operator 是一个运行在 Kubernetes 集群中的自定义控制器,它使用自定义资源(Custom Resource)来表示被管理的应用或组件,并且定义了应用或组件的全生命周期管理逻辑。
例如,假设我们有一个基于分区(sharding)的数据库,该数据库有自己的特殊需求,比如在扩容或者缩容时需要按照一定的顺序进行,或者在进行数据备份和恢复时需要特殊的处理。这些需求超出了 Kubernetes 的 StatefulSet 和服务(Service)能提供的功能范围。
在这种情况下,我们就可以创建一个 Operator。首先,我们会定义一个 Custom Resource Definition(CRD),比如叫做 ShardedDatabase。这个 ShardedDatabase 就代表了我们的这个分区数据库。
然后,我们会实现并运行一个自定义的控制器,也就是 Operator。这个 Operator 会监控所有的 ShardedDatabase 对象,然后根据 ShardedDatabase 的规格(Spec)来创建或更新相应的资源,例如 StatefulSet、服务(Service)等等。
例如,当我们创建一个新的 ShardedDatabase 对象时,Operator 会看到这个新的对象,然后创建一系列的 StatefulSet 和服务(Service)来部署这个数据库。如果我们更新了 ShardedDatabase 对象的规格(例如,改变了分区数量),Operator 也会看到这个变化,然后按照数据库的需求来更新 StatefulSet(例如,按照正确的顺序添加或删除副本)。
此外,Operator 还可以管理数据库的其他生命周期事件,例如备份、恢复、升级等等。这些事件可以通过更新 ShardedDatabase 对象的状态(Status)或其他方法来触发。
总的来说,Operator 通过扩展 Kubernetes API,并定义和实现应用特定的管理逻辑,使得我们可以像管理无状态应用那样管理有状态应用。而且,由于 Operator 运行在 Kubernetes 集群中,并使用标准的 Kubernetes 工具和 API,我们可以使用相同的工具(如 kubectl 和 Dashboard)来管理 Operator 和它管理的应用。
PV、PVC、StorageClass
在Kubernetes中,PV(Persistent Volume)、PVC(Persistent Volume Claim)和StorageClass是用于管理存储的关键资源。以下是这三者的解释和区别,并附带一个实际的例子来说明如何使用它们。
PV (Persistent Volume) PV是集群中的一块持久存储空间。它可以是网络存储、云提供商的存储,或者本地物理磁盘。管理员通常负责创建和维护PV。
PVC (Persistent Volume Claim) PVC是用户对PV的请求或声明。它允许用户以一种抽象的方式请求存储,无需知道背后具体的存储实现。PVC可以指定所需的存储大小和访问模式(例如,读写一次或只读)。
StorageClass StorageClass是用于定义不同“类别”存储的模板。管理员可以定义一个或多个StorageClass来描述集群提供的不同类型的存储(例如,高性能、冷存储等)。用户可以在PVC中指定StorageClass,以便按需自动创建和配置PV。
假设想要在Kubernetes集群中为MySQL数据库提供一个10GB的持久存储空间。以下是步骤:
定义StorageClass:(可选)
如果想让PVC动态创建PV,可以先定义一个StorageClass。
apiVersion: storage.k8s.io/v1
kind: StorageClass
name: fast-storage
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-ssd
这将创建一个名为“fast-storage”的StorageClass,使用Google Cloud Platform的SSD持久磁盘。
创建PVC:
然后,可以创建一个PVC来请求10GB的存储,并选择先前定义的StorageClass。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-storage
resources:
requests:
storage: 10Gi
使用PVC在Pod中:
一旦PVC被创建和绑定到一个PV,可以在Pod的定义中引用它。
apiVersion: v1
kind: Pod
metadata:
name: mysql-pod
spec:
containers:
- name: mysql
image: mysql
volumeMounts:
- name: mysql-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-storage
persistentVolumeClaim:
claimName: mysql-pvc
PV是集群的实际存储资源,通常由管理员创建和管理。 PVC是对PV的请求,允许用户抽象地请求存储。 StorageClass定义了存储的“类别”,允许动态创建PV,为PVC提供了更大的灵活性。
在这个例子中:
spec.volumes 部分定义了一个名为 mysql-storage 的 Volume,该 Volume 指向一个名为 mysql-pvc 的 PVC。 spec.containers.volumeMounts 部分定义了一个挂载点,该挂载点将 Volume mysql-storage 挂载到容器的 /var/lib/mysql 目录。 这样,Pod 中的 mysql-pod 容器就可以通过路径 /var/lib/mysql 读写 PV 上的数据。 一旦这个 Pod 启动,Kubernetes 会确保该 PVC 指向的 PV(也就是实际的存储资源)被挂载到容器的 /var/lib/mysql 目录。这样,任何写入 /var/lib/mysql 目录的数据都会被写入 PV,从而实现数据的持久化。
这是一个基本的用法,Kubernetes 提供了更复杂的存储选项,例如多容器共享同一存储、使用只读存储等,可以根据实际需求进行选择。
Volume 类型是远程块存:
下面是一个使用 AWS EBS 作为远程块存储的 PV 示例。
apiVersion: v1
kind: PersistentVolume
metadata:
name: mypv
spec:
capacity:
storage: 10Gi
volumeMode: Block
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: my-sc
awsElasticBlockStore:
volumeID: "<VolumeId>"
spec.capacity 定义了 PV 的容量,这个值应该根据远程块存储的实际大小来设定。 spec.volumeMode: Block 指示 Kubernetes 这是一个块存储。 spec.accessModes 描述了 PV 的访问模式,这通常取决于远程存储的类型和配置。
Volume 类型是远程文件存储:
如 NFS(Network File System),在 Kubernetes 中,kubelet 的处理过程确实会更简单一些。下面就来具体说说这个过程:
在使用 NFS 类型的远程文件存储时,kubelet 可以跳过第一阶段(Attach)的操作。这是因为 NFS 本身就是一个分布式文件系统,不需要把存储设备挂载到宿主机上。相反,它允许客户端直接通过网络访问其上的文件。因此,kubelet 可以直接进入第二阶段(Mount)。
在这个阶段,kubelet 会在宿主机上准备一个目录作为 Volume。然后,kubelet 作为 NFS 客户端,会把远程 NFS 服务器的某个目录(例如,“/” 目录)挂载到这个宿主机上的 Volume 目录。这样,Pod 就可以通过这个 Volume 目录来访问 NFS 服务器上的文件了。
在 Kubernetes 中,可以通过 PersistentVolume (PV) 和 PersistentVolumeClaim (PVC) 来使用 NFS 存储。例如,可以创建一个 PV,指定其类型为 NFS,并提供 NFS 服务器的详细信息:
apiVersion: v1
kind: PersistentVolume
metadata:
name: mypv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /mydata
server: nfsserver.example.com
在上面的例子中,spec.nfs.path 是 NFS 服务器上的目录,spec.nfs.server 是 NFS 服务器的地址。然后,Pod 可以通过 PVC 来使用这个 PV,从而访问 NFS 服务器上的文件。
这种方式可以使得 Kubernetes 中的应用能够以一种统一的方式来访问各种类型的存储,包括远程文件存储,如 NFS。而且,由于 NFS 是一个分布式文件系统,它允许多个 Pod 同时读写同一个 Volume,这对于一些有共享存储需求的应用是非常有用的。
CSI-容器存储接口
CSI (Container Storage Interface):CSI 是一种标准化的接口,定义了 Kubernetes 和存储驱动之间的交互方式。
CSI 驱动:这是一种特殊类型的插件,实现了 CSI 接口,以此将存储系统接入 Kubernetes。
Kubelet 插件注册机制:这是一种用于发现和注册 Kubelet 插件的机制,包括 CSI 驱动。通过这个机制,Kubelet 能够知道哪些 CSI 驱动存在,以及如何与它们进行通信。
在这个过程中,CSI 驱动会在每个节点上运行,并使用 Kubelet 插件注册机制向 Kubelet 注册自己。CSI 驱动程序需要提供一个 Unix 域套接字,Kubelet 就通过这个套接字与 CSI 驱动进行通信。
当一个 Pod 请求使用某个 CSI 驱动提供的存储卷时,Kubelet 会通过该 Unix 域套接字向 CSI 驱动发送 CSI 调用,比如 NodeStageVolume(用于准备存储卷的使用)、NodePublishVolume(用于挂载存储卷)等,以此完成对存储卷的挂载和卸载操作。
因此,Kubelet 可以直接通过 Unix 域套接字与运行在同一个节点上的 CSI 驱动进行通信,无需通过网络进行远程调用。这样不仅提高了效率,也简化了网络配置,同时提高了系统的安全性。
CSI 驱动程序需要提供一个 Unix 域套接字,什么是Unix 域套接字?
Unix 域套接字是一种在同一台主机上的不同进程间进行通信的机制。可以将其视为本地主机上的进程间通信通道。与 TCP/IP 套接字在不同机器之间传输数据不同,Unix 域套接字只用于本地机器上的通信。
CSI 驱动和 Kubelet 如何使用 Unix 域套接字?
CSI 驱动创建 Unix 域套接字:CSI 驱动会在启动时创建一个 Unix 域套接字。这个套接字是一个特殊类型的文件,位于文件系统的某个位置。
CSI 驱动注册 Unix 域套接字:然后,CSI 驱动会通过 Kubelet 的插件注册机制告诉 Kubelet 套接字的位置。
Kubelet 使用 Unix 域套接字与 CSI 驱动通信:当 Kubelet 需要与 CSI 驱动通信时(例如挂载一个卷),它会使用该 Unix 域套接字连接到 CSI 驱动。此时,Unix 域套接字就像一个本地通信通道,使得 Kubelet 可以向 CSI 驱动发送请求并接收响应。
Unix 域套接字是一种允许在同一台计算机上的两个进程之间进行通信的机制。在 Kubernetes 中,CSI 驱动和 Kubelet 会使用 Unix 域套接字来实现他们之间的通信。这种方法比使用网络连接更有效,也更安全,因为通信仅限于本地计算机。
CSI组件为什么需要实现RPC接口?
RPC(Remote Procedure Call)是 CSI 使用的一种通信方式。CSI 中定义的 RPC 接口主要分为三类:Identity、Controller 和 Node。每一类都有其特定的用途,现在我来解释一下:
Identity 接口:这类接口用于 CSI 插件自我识别和向 Kubernetes 报告其功能。例如,GetPluginInfo RPC 接口用于返回 CSI 插件的名字和版本号。
Controller 接口:这类接口负责存储卷的生命周期管理,例如创建卷(CreateVolume)、删除卷(DeleteVolume)、挂载卷(ControllerPublishVolume)、卸载卷(ControllerUnpublishVolume)等。这些操作在控制平面执行,与具体的节点无关。
Node 接口:这类接口负责在特定的节点上对存储卷进行操作,例如挂载卷(NodeStageVolume、NodePublishVolume)和卸载卷(NodeUnstageVolume、NodeUnpublishVolume)等。这些操作需要在存储卷所挂载的节点上执行。
例如,当创建一个带有持久卷声明(PersistentVolumeClaim,PVC)的 Pod 时,Kubernetes 将调用 CSI 插件的 Controller 接口中的 CreateVolume RPC 创建一个新的存储卷。然后,当这个 Pod 被调度到一个节点上,Kubernetes 会调用 Node 接口中的 NodeStageVolume 和 NodePublishVolume RPC 来挂载这个存储卷到该节点的具体路径上。这个存储卷现在就可以被 Pod 中的容器作为一个卷来使用了。如果删除了这个 Pod,Kubernetes 就会调用 NodeUnpublishVolume 和 NodeUnstageVolume 来卸载这个存储卷,然后调用 DeleteVolume 来删除这个存储卷。
实现一个自己的CSI插件
导入 CSI 接口定义
安装 Protocol Buffers Compiler:可以从 https://github.com/protocolbuffers/protobuf/releases 下载适合的操作系统和架构的 protoc 编译器。
brew install protobuf
安装 Go Protobuf 插件:在命令行中运行以下命令以安装 Go 语言的 protobuf 插件:
go get -u github.com/golang/protobuf/protoc-gen-go
获取 CSI 接口定义:CSI 的接口定义是以 .proto 文件形式提供的,可以从 https://github.com/container-storage-interface/spec/tree/master/csi.proto 下载最新版本的接口定义。
生成 Go 源代码:使用以下命令将 CSI 接口定义转换为 Go 源代码:
修改csi.proto文件
// Code generated by make; DO NOT EDIT.
syntax = "proto3";
package csi.v1;
import "google/protobuf/descriptor.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option go_package = "/csi";
protoc --go_out=. csi.proto
这将生成一个名为 csi.pb.go 的文件,其中包含了 CSI 接口定义的 Go 语言表示。在的 Go 代码中,可以像使用其他 Go 源文件一样使用这个文件。
.
├── csi
│ └── csi.pb.go
├── csi.proto
├── go.mod
└── go.sum
2 directories, 4 files
实现 CSI 接口:
需要实现 CSI 描述的接口,这包括 Identity、Controller 和 Node 服务。
type LocalDriver struct {
// 这里可以放置插件需要的配置和状态
}
// 实现 Identity 服务接口
func (d *LocalDriver) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
// 返回插件信息
}
// 实现 Controller 服务接口
func (d *LocalDriver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
// 在本地文件系统上创建一个卷
}
// 实现 Node 服务接口
func (d *LocalDriver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
// 挂载卷到指定的目标路径
}
构建插件
将的代码编译为一个可执行文件。
部署插件
需要在 Kubernetes 集群的每个节点上部署插件,并确保 Kubernetes 能够通过 Unix 域套接字与插件通信。
创建 StorageClass
创建一个 Kubernetes StorageClass,指定 CSI 插件的名字。
测试插件
通过创建 PersistentVolumeClaim 和 Pod 来测试插件的功能。