云原生之深入解析Kubernetes本地持久化存储方案OpenEBS LocalPV的最佳实践
一、K8s 本地存储
- K8s 支持多达 20+ 种类型的持久化存储,如常见的 CephFS 、Glusterfs 等,不过这些大都是分布式存储,随着社区的发展,越来越多的用户期望将 K8s 集群中工作节点上挂载的数据盘利用起来,于是就有了 local 类型持久卷的支持。
- 可以把 local 类型持久卷称作:Local Persistent Volume,简称 LocalPV。LocalPV 所代表的是某个被挂载的本地(工作节点)存储设备,例如磁盘、分区或者目录,因此 LocalPV 并不能像分布式存储一样可靠,但速度极快,这也决定了 LocalPV 使用场景:I/O 敏感度高且能够容忍小概率数据丢失现象。
- K8s 官方文档里有一个使用 LocalPV 的简单示例,简单总结下 K8s LocalPV 的特点:
-
- 只能用作静态创建的持久卷,不支持动态供应,也就是说必须通过手动的方式创建 PV;
-
- 与 hostPath 卷相比,LocalPV 能够以持久和可移植的方式使用,而无需手动将 Pod 调度到节点。系统通过查看 PV 的节点亲和性(nodeAffinity)配置,就能了解卷的节点约束。
-
- 如果想使用存储类来自动绑定 PVC 和 PV,则必须将 StorageClass 配置成延迟绑定。
- 示例如下:
apiVersion: storage.K8s.io/v1
kind: StorageClass
metadata:name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
- 其中 volumeBindingMode: WaitForFirstConsumer 属性即为延迟卷绑定,它使得调度器在为 PVC 选择一个合适的 PV 时能考虑到所属 Pod 的调度限制。举个例子,假设创建了两个 PV 分别为 PV1、PV2,然后创建一个 Pod 并申明一个 PVC 叫 PVC1 ,PV1 和 PV2 同时满足 PVC1 的要求,但此时存储类并不能马上将 PVC1 与 PV1 或 PV2 中任何一个 PV 进行绑定,而是要考虑 Pod 的调度策略。如果 Pod 指定了节点亲和性必须要部署到 PV1 所在节点,则 PVC1 就需要跟 PV1 进行绑定,而不能与 PV2 进行绑定。
- 可以发现,当 Pod 需要使用 LocalPV 时,PVC 与 PV 的绑定就需要考虑 Pod 的调度情况,因此 LocalPV 的存储类无法支持立即绑定,只能将绑定时机延迟到 Pod 调度时进行(WaitForFirstConsumer)。
二、OpenBES 本地存储
① OpenBES 简介
- 由于 K8s LocalPV 的使用限制无法满足生产需求,因此就需要寻找替代方案,好在已经有人实现了更强大的 LocalPV 存储方案:OpenEBS LocalPV。
- OpenEBS 官网地址为:OpenEBS 官网。
- OpenEBS 可以将 K8s 工作节点上的任何可用存储转换为本地或分布式(也称为复制)持久卷。
- OpenEBS 最初由 MayaData 构建并捐赠给 CNCF,现在是 CNCF 沙盒项目。
② 卷类型
- OpenEBS 支持两种卷类型:本地卷、复制卷,架构如下:
- 本地卷能够直接将工作节点上插入的数据盘抽象为持久卷,供当前节点上的 Pod 挂载使用。而复制卷则相对复杂一些,OpenEBS 使用其内部的引擎为每个复制卷创建一个微服务,在使用时,有状态服务将数据写入 OpenEBS 引擎,引擎将数据同步复制到集群中的多个节点,从而实现了高可用。
③ 本地卷
- 由于本次 OpenEBS 的落地实践针对于本地持久化存储,故主要分析 OpenEBS 本地卷的使用,OpenEBS 本地卷支持多种类型:Hostpath、Device、LVM、ZFS、Rawfile。每种类型各有特点,都有自己的适用场景,比如相较于 K8s 原生 Hostpath,OpenEBS 的 Hostpath 能够支持将外挂的数据盘目录作为 Hostpath 目录,从而避免 Pod 可能将宿主机目录写满的问题。Device 能够将块设备用于 LocalPV 的使用,速度极快。而借助 LVM 的能力,则可以更灵活的使用 LocalPV,它可以支持 PV 的动态扩缩容操作。
- OpenEBS 为其支持的每种类型都实现了一个单独的项目,以使用块设备为例,介绍 OpenEBS LocalPV 使用实践,项目地址为:device-localpv,接下来以 Device-LocalPV 来指代它。
④ 实践
- 环境准备:
-
- 使用 minikube 来搭建 K8s 集群环境,OpenEBS 官方要求 K8s 版本为 1.20+,实测下来 1.19 版本也是没有问题的,不过建议优先选用推荐版本:
-
- 使用 VirtualBox 作为驱动启动两个 minikube 节点:minikube、minikube-m02(可以参考:https://minikube.sigs.k8s.io/docs/drivers/virtualbox/);
-
- minikube 节点挂载一块 4GB 磁盘,minikube-m02 节点分别挂载一块 4GB、一块 8GB 磁盘。
- 安装 Device-LocalPV:
-
- 由于 OpenEBS Device-LocalPV 本身即为云原生而开发的应用,因此安装起来非常简单,只需要一条 kubectl apply 命令即可。
kubectl apply -f https://raw.githubusercontent.com/openebs/device-localpv/develop/deploy/device-operator.yaml
- 执行以上命令后,会得到如下几个相关 Pod:
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system openebs-device-controller-0 2/2 Running 0 2m23s
kube-system openebs-device-node-4wld7 2/2 Running 0 2m23s
kube-system openebs-device-node-p2r6m 2/2 Running 0 2m23s
-
- 确保这几个 Pod 全部处于 Running 状态,就表示 Device-LocalPV 安装成功,如果安装失败,则需要根据 kubectl describe 命令的描述信息进行排查。
- 准备磁盘:
-
- Device-LocalPV 能够直接接管节点上的块设备,有时候节点上可能同时插入多块数据盘,而这些数据盘中,也许某些数据盘我们不想当作 LocalPV 来使用,为了能够区分哪些块设备可以供 Device-LocalPV 来使用,需要在对应的块设备上创建一个(~10MiB)Meta 分区,用于存储磁盘标识信息,Meta 分区有如下要求:
-
-
- 是块设备的第一个分区(ID_PART_ENTRY_NUMBER=1);
-
-
-
- 不能被格式化成任何文件系统;
-
-
-
- 不能设置 flags 分区标记。
-
-
- 操作命令如下:
## 在 minikube 节点上执行如下命令
$ sudo parted /dev/sdb mklabel gpt
$ sudo parted /dev/sdb mkpart test-device 1MiB 10MiB## 在 minikube-m02 节点执行如下命令
$ sudo parted /dev/sdb mklabel gpt
$ sudo parted /dev/sdb mkpart test-device 1MiB 10MiB
$ sudo parted /dev/sdc mklabel gpt
$ sudo parted /dev/sdc mkpart test-device 1MiB 10MiB
-
- 以上分别对 minikube、minikube-m02 两个节点上的块设备进行了 Meta 分区的操作,其中 /dev/sdb、/dev/sdc 就是节点上挂载的块设备名,需要根据自己实际情况来指定。其中三块盘的 Meta 分区名都叫 test-device 是有意而为之,接下来创建存储类时会用到。
- 创建存储类:
-
- 既然 Device-LocalPV 支持动态供应,那么必然少不了创建存储类的步骤,将以下 yaml 文件保存为 sc.yaml 然后通过 kubectl apply -f sc.yaml 创建存储类:
apiVersion: storage.K8s.io/v1
kind: StorageClass
metadata:name: openebs-device-sc
allowVolumeExpansion: false
parameters:devname: "test-device"
provisioner: device.csi.openebs.io
volumeBindingMode: WaitForFirstConsumer
-
- 存储类 parameters 字段需要指定 devname,其值为上面在为块设备分区时指定的分区名称 test-device,存储类在创建 PV 的时候正是根据这个分区名的匹配,来找到哪些块设备是供 Device-LocalPV 来使用的。
- 创建 StatefulSet 来申请使用 LocalPV:
-
- StatefulSet 以及相关资源定义如下:
apiVersion: v1
kind: Service
metadata:name: nginxlabels:app: nginx
spec:ports:- port: 80name: webclusterIP: Noneselector:app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:name: hello
spec:selector:matchLabels:app: helloserviceName: "nginx"replicas: 2template:metadata:labels:app: hellospec:terminationGracePeriodSeconds: 1containers:- name: htmlimage: busyboximagePullPolicy: IfNotPresentcommand:- sh- -c- 'while true; do echo "`date` [`hostname`] Hello from OpenEBS Local PV." >> /mnt/store/index.html; sleep $(($RANDOM % 5 + 300)); done'volumeMounts:- mountPath: /mnt/storename: csi-devicepv- name: webimage: K8s.gcr.io/nginx-slim:0.8imagePullPolicy: IfNotPresentports:- containerPort: 80name: webvolumeMounts:- name: csi-devicepvmountPath: /usr/share/nginx/htmlvolumeClaimTemplates:- metadata:name: csi-devicepvspec:accessModes: ["ReadWriteOnce"]storageClassName: "openebs-device-sc"resources:requests:storage: 1Gi
-
- 创建一个 Service 和一个名叫 hello 的 StatefulSet,重点关注 StatefulSet,它启动两个副本,并通过 volumeClaimTemplates 来申请 1Gi 大小的 PVC。通过 kubectl apply 命令安装以上文件资源后,可以看到,两个 Pod 分别被调度到了不同节点上:
# 查看 Pod 调度情况
➜ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hello-0 2/2 Running 0 4m13s 10.244.1.3 minikube-m02 <none> <none>
hello-1 2/2 Running 0 2m42s 10.244.0.3 minikube <none> <none># 查看 PVC 资源
➜ kubectl get pvc -o wide
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
csi-devicepv-hello-0 Bound pvc-042661c8-c000-4dde-9950-2b6859d5f273 1Gi RWO openebs-device-sc 4m46s Filesystem
csi-devicepv-hello-1 Bound pvc-26f92829-e0d4-4520-86da-2d7741cd68c2 1Gi RWO openebs-device-sc 3m15s Filesystem# 查看 PV 资源
➜ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
pvc-042661c8-c000-4dde-9950-2b6859d5f273 1Gi RWO Delete Bound default/csi-devicepv-hello-0 openebs-device-sc 18m Filesystem
pvc-26f92829-e0d4-4520-86da-2d7741cd68c2 1Gi RWO Delete Bound default/csi-devicepv-hello-1 openebs-device-sc 17m Filesystem
-
- 现在分别登录两台 minikube 主机节点,使用 fdisk 命令查看两个节点磁盘使用情况:
-
- 左侧为 minikube 节点,右侧为 minikube-m02 节点,可以看到 PV 创建成功后会在对应节点的块设备上创建与 PV 相同大小的分区来提供 LocalPV 的支持,而这个分区的生命周期管理工作都是由 Device-LocalPV 来完成的,它会随着 PV 的创建而创建,随着 PV 的删除而销毁。
-
- 至此,就完成了 OpenEBS Device-LocalPV 实践演练。正常来说,通过上面的实践步骤,应该能顺利的搭建并使用 OpenEBS Device-LocalPV。但也许会遇到一些奇怪的问题,接下来就分析几个在 OpenEBS Device-LocalPV 落地实践过程可能会遇到的坑。
⑤ 问题分析
- parted 命令版本问题:
-
- 根据经验,parted 命令不同版本表现可能不一致。如下所示,使用两个不同版本的 parted 工具执行相同命令,来对块设备进行 Meta 分区操作,得到的结果却不一样:
-
- 可以发现,截图中 3.3 版本 parted 命令分区会产生 flag(msftdata),下面 3.1 版本的 parted 命令分区则没有产生 flag。OpenEBS Device-LocalPV 规定 Meta 分区不能有 flag,如果产生 flag 则 Device-LocalPV 会将这个块设备忽略,不进行使用。
-
- 为了保证执行 parted 命令对磁盘进行分区时,得到预期结果,可以总是进入 OpenEBS Daemonset 的容器内部来使用 OpenEBS 提供的 parted 命令进行分区。这样就保证了与 Device-LocalPV 工作时内部使用的 parted 命令版本一致,不会出现一些意料之外的问题。Daemonset 所对应的 Pod 即为上面的 openebs-device-node-4wld7、openebs-device-node-p2r6m 两个 Pod,OpenEBS 启动时会在每个节点上启动一个 Daemonset 工作负载,用来操作节点块设备提供 LocalPV 支持。
- PV 动态扩容问题:OpenEBS Device-LocalPV 目前还不支持扩容操作,因此在创建存储类时需要指定 allowVolumeExpansion 属性值为 false,以此来标记这个存储类所创建出来的 PV 不支持动态扩容操作。
- PV 可能无法创建成功问题:
-
- 可能会遇到 PVC 所申请的容量刚好等于块设备剩余容量时,PV 无法创建成功,PVC 一直处于 Pending 状态的问题。
-
- 假设现在只有一个节点,节点上也仅仅只有一个块设备供 Device-LocalPV 使用,其容量为 10Gi。如果连续申请 4 个 PVC,其容量依次为 1G、3G、3G、1G,分别对应如下截图(fdisk 命令显示结果)中 sdb2、sdb3、sdb4、sdb5 4 个分区。
-
- 现在如果删除第一个容量为 3G 的 PVC,Device-LocalPV 则会自动删除 /dev/sdb3 这个分区。可是,此时如果尝试再次创建一个 3G 的 PVC,这个 PVC 将永远无法创建成功,一直处于 Pending 状态。当每创建一个 PVC 时,存储类都会通过 Device-LocalPV 在节点的块设备上新创建一个和 PVC 中申请的容量相等的一个磁盘分区出来,从截图中可以看出,这个分区的 Start 、End 是从小到大且连续的。新创建的 3G 的 PVC 容量等于 /dev/sdb3 分区容量,理论上应该是可以创建成功的。
-
- 分析 Device-LocalPV 源码可以发现,在计算节点上块设备剩余可用容量是否满足 PVC 申请的容量大小时,Device-LocalPV 通过 if tmp.SizeMiB > partSize 这条语句来进行判断,其中 tmp.SizeMiB 为块设备剩余可用容量,partSize 为 PVC 申请容量,只有块设备剩余容量大于 PVC 申请容量时,才会进行分区操作,如果没有满足 PVC 申请容量的可用分区,PVC 就会一直处于 Pending 状态。
-
- 由此可见,将 if tmp.SizeMiB > partSize 改成 if tmp.SizeMiB >= partSize 即可解决这个问题,另外,在阅读源码的过程中,还可以发现 Device-LocalPV 在计算块设备可用分区时,对于分区大小的计算会涉及从 Bytes 到 Mib 的单位转换操作:
-
- beginBytes 、endBytes 对应的就是上面 fdisk 命令截图中的 Start 、End,此时如果分区没有对齐,则会出现浮点数计算精度丢失问题,其中 beginMib 会被 math.Ceil 向上取整,endMib 会被 math.Floor 向下取整,最终得到的 sizeMib 有可能小于实际剩余可用空间,这样就导致可能会出现磁盘剩余容量满足 PVC 申请容量,而 PVC 却无法创建成功现象,因此在创建 PVC 时应尽量申请 1024 整数倍大小的容量。
三、CSI
- 在 K8s 中,如果 K8s 内置的存储功能不满足我们的生产需求,则可以通过一种叫作 CSI 的插件机制来对其进行扩展,而 Device-LocalPV 正是采用这种机制来实现的。
- CSI 是 Container Storage Interface 的简称,是 K8s 官方定义的容器存储接口规范,它试图定义一个统一的业界标准,专门用来扩展容器编排系统的存储能力。
① 基础架构
- 一个 CSI 插件包含两个主体部分 External Components 和 Custom Components,其中 External Components 由 K8s 官方提供,而 Custom Components 则由编写插件的作者来提供。这两个 Components 又各自包含 3 个组件,一起协同工作。
- CSI 基本架构如下:
② 工作流程
- 首先,在 CSI 插件启动时,External Components 中的 Driver Registrar 组件最先开始工作,它通过与 Custom Components 中的 Identity 组件进行通信,获取到 CSI 插件的基本信息,并将其注册到 kubelet 中。
- 而 External Components 中的 Provisioner 组件则通过 Watch 机制,监听了 APIServer 中 PVC 对象的创建,一旦有新的 PVC 被创建,Provisioner 就会与 Custom Components 中的 Controller 组件进行通信,让其创建 PV 相关资源。
- 创建完 PV,下一步就到了 Attach 阶段,而 Attach 操作正对应了 External Components 中的 Attacher 组件,它同样会与 Custom Components 中的 Controller 组件进行通信,协同完成 Attach 操作。
- 而最后一步 Mount 操作,则由 Node 节点上的 kubelet 直接调用 External Components 中的 Node 组件来完成。至此,Pod 内部应用就可以使用主机节点上挂载的 LocalPV 了。
四、Device-LocalPV
① 部署
- 要分析一个程序的执行流程,当然要从程序启动入口开始,而 OpenEBS 是一个面向云原生的应用,那么首先要看的,就是项目的部署方式,OpenEBS Device-LocalPV 项目部署 yaml 文件也在其项目的 git 仓库中。
- 可以看到,部署文件中最重要的两个资源分别是一个名为 openebs-device-controller 的 StatefulSet,和一个名为 openebs-device-node 的 DaemonSet。
- 在 StatefulSet 中启动了两个容器,分别是 K8s 官方提供的 External Provisioner 组件,和由 OpenEBS 开发的 Custom Controller 组件,这两个组件被放在一个 Pod 中协同工作。
- 而在 DaemonSet 中同样也启动了两个容器,分别是 K8s 官方提供的 External Driver Registrar 组件,和由 OpenEBS 开发的 Custom Node 组件。那么,CSI 机制中的 External Attacher 组件去哪里了?实际上 LocalPV 并没有 Attach 操作,创建出来后只需要一步 Mount 操作即可使用,所以也就没有必要部署这个组件了。
- 可能我们会好奇 External 组件和 Custom 对应组件之间如何来进行通信?根据上面提供的 Device-LocalPV 项目部署 yaml 文件中的内容,就不难发现,里面有 unix:///xxx/csi.sock 字样,实际上它们之间的通信都是依靠基于 Unix socket 的 gRPC 来进行的,这样即实现了组件间的解耦,又能高效进行通信。
② 组件
- 知道 OpenEBS Device-LocalPV 项目都部署了哪些组件,接下来通过阅读源码来分析程序执行流程。无论是 StatefulSet 还是 DaemonSet,Device-LocalPV 程序启动入口文件都是同一个,程序启动后,在 main 函数里会调用 run 函数,run 函数定义如下:
func run(config *config.Config) {if config.Version == "" {config.Version = version.Current()}klog.Infof("Device Driver Version :- %s - commit :- %s", version.Current(), version.GetGitCommit())klog.Infof("DriverName: %s Plugin: %s EndPoint: %s NodeID: %s",config.DriverName,config.PluginType,config.Endpoint,config.NodeID,)if len(config.IgnoreBlockDevicesRegex) > 0 {device.DeviceConfiguration.IgnoreBlockDevicesRegex = regexp.MustCompile(config.IgnoreBlockDevicesRegex)}err := driver.New(config).Run()if err != nil {log.Fatalln(err)}os.Exit(0)
}
- 值得注意的是 err := driver.New(config).Run() 这句代码,通过 config 参数启动了一个 Driver 并执行 Run 方法,可以跟踪到 New 函数内部来查看其实现:
func New(config *config.Config) *CSIDriver {driver := &CSIDriver{config: config,cap: GetVolumeCapabilityAccessModes(),}switch config.PluginType {case "controller":driver.cs = NewController(driver)case "agent":driver.ns = NewNode(driver)}driver.ids = NewIdentity(driver)return driver
}
- 可以发现,New 函数内部会通过 config 创建一个 CSIDriver 对象,这个对象会根据配置参数注册 Controller 组件或 Node 组件(在 Device-LocalPV 项目中 agent 和 Node 等价),而这两个组件分别对应的就是 StatefulSet 和 DaemonSet,也就是说,Controller 组件会以 StatefulSet 的方式启动,Node 组件则会以 DaemonSet 方式启动。
- ** Identity 组件**:
-
- 根据上面的源码可以发现,不管启动 Controller 或 Node 中的哪个组件,Identity 组件都会被注册进来(driver.ids = NewIdentity(driver)),因为将 Device-LocalPV 实现的 CSI 插件分开成两个工作负载来部署,而它们都需要注册给 K8s,Identity 组件正是干这件事情的。
-
- Identity 实现如下,定义了 3 个方法,分别是 GetPluginInfo 、Probe 、GetPluginCapabilities:
package driverimport ("github.com/container-storage-interface/spec/lib/go/csi""github.com/openebs/device-localpv/pkg/version""golang.org/x/net/context""google.golang.org/grpc/codes""google.golang.org/grpc/status"
)// identity is the server implementation
// for CSI IdentityServer
type identity struct {driver *CSIDriver
}// NewIdentity returns a new instance of CSI
// IdentityServer
func NewIdentity(d *CSIDriver) csi.IdentityServer {return &identity{driver: d,}
}// GetPluginInfo returns the version and name of
// this service
//
// This implements csi.IdentityServer
func (id *identity) GetPluginInfo(ctx context.Context,req *csi.GetPluginInfoRequest,
) (*csi.GetPluginInfoResponse, error) {if id.driver.config.DriverName == "" {return nil, status.Error(codes.Unavailable, "missing driver name")}if id.driver.config.Version == "" {return nil, status.Error(codes.Unavailable, "missing driver version")}return &csi.GetPluginInfoResponse{Name: id.driver.config.DriverName,// TODO// verify which version needs to be used:// config.version or version.Current()VendorVersion: version.Current(),}, nil
}// TODO
// Need to implement this
//
// # Probe checks if the plugin is running or not
//
// This implements csi.IdentityServer
func (id *identity) Probe(ctx context.Context,req *csi.ProbeRequest,
) (*csi.ProbeResponse, error) {return &csi.ProbeResponse{}, nil
}// GetPluginCapabilities returns supported capabilities
// of this plugin
//
// Currently it reports whether this plugin can serve
// the Controller interface. Controller interface methods
// are called dependant on this
//
// This implements csi.IdentityServer
func (id *identity) GetPluginCapabilities(ctx context.Context,req *csi.GetPluginCapabilitiesRequest,
) (*csi.GetPluginCapabilitiesResponse, error) {return &csi.GetPluginCapabilitiesResponse{Capabilities: []*csi.PluginCapability{{Type: &csi.PluginCapability_Service_{Service: &csi.PluginCapability_Service{Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,},},},{Type: &csi.PluginCapability_Service_{Service: &csi.PluginCapability_Service{Type: csi.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS,},},},},}, nil
}
-
- 其中 GetPluginInfo 方法返回插件的名称和版本号,Probe 顾名思义是一个探针程序,K8s 可以根据这个探针检查插件是否正常工作。
-
- 而 GetPluginCapabilities 方法返回当前插件的能力,用告诉 K8s 这个 CSI 插件实现了哪些功能,比如 Device-LocalPV 项目没有实现 Attach 功能,当我们在创建 PVC 时指定了这个 CSI 插件作为存储类的 provisioner 时,K8s 就会自动跳过 Attach 阶段,直接进入 Mount 阶段。
-
- 细心的读者可能已经发现每个方法上都有一行 // This implements csi.ControllerServer 注释,实际上这些方法的名称都是固定的,已经被 CSI 规范所定义,而编写 CSI 插件的作者只需要按照规范实现对应方法即可。
- Controller 组件:
-
- 上面分析 CSI 基本架构的时候,讲到 External Provisioner 组件通过 Watch 机制监听 APIServer 中 PVC 对象的创建,一旦有新的 PVC 被创建,Provisioner 组件就会与 Custom Controller 组件进行通信,让其创建 PV 相关资源。这里我说的是创建 PV 相关资源,而不是创建 PV。所谓的 PV 相关资源则是一个 CRD,是编写 CSI 组件的开发人员自定义的一种资源类型,一个 CRD 会与一个 PV 对应,其生命周期也基本相同。这么做的原因是 PV 属于 K8s 内部资源,而 CSI 是一个通用规范,不止适用于 K8s,还适用于任何容器编排系统,因此 CSI 插件应该自己定义一种资源类型,来与 PV 进行对应。
-
- 而这个与 PV 相对应的 CRD 叫作 devicevolumes.local.openebs.io,它定义在 Device-LocalPV 项目部署文件中,相当于 Device-LocalPV 自己管理的 PV 资源。一个典型的 devicevolumes.local.openebs.io 资源定义如下:
apiVersion: v1
items:
- apiVersion: local.openebs.io/v1alpha1kind: DeviceVolumemetadata:creationTimestamp: "2022-08-01T08:45:51Z"finalizers:- device.openebs.io/finalizergeneration: 3labels:kubernetes.io/nodename: minikube-m02name: pvc-8e659633-e052-439a-85eb-30a6d385a12bnamespace: openebsresourceVersion: "1715"uid: 540aa699-4f6f-487c-b38b-38f8b414bd7cspec:capacity: "1073741824"devname: device-localpvownerNodeID: minikubestatus:state: Ready
kind: List
metadata:resourceVersion: ""
-
- 为了方便对节点块设备进行管理,Device-LocalPV 还定义了一个叫 devicenodes.local.openebs.io 的 CRD,这个 CRD 对应的是 K8s 工作节点,有几个供 Device-LocalPV 使用的节点,就有几个 devicenodes.local.openebs.io 资源,这个 CRD 中记录了当前节点上所有可用块设备。那么 devicevolumes.local.openebs.io 是在什么时候创建的呢?在 Custom Controller 组件中有一个关键方法叫 Createvolume,正是这个方法负责 PV 相关 CRD 的创建。
-
- External Provisioner 组件监听到有新的 PVC 创建,就会执行组件内部的 Provision 方法,而在 Provision 方法内部,会通过 gRPC 的方式调用 Custom Controller 组件的 Createvolume 方法来创建 CRD,CRD 创建完成后,会由 Provisioner 来创建 PV 对象。相关代码实现如下:
- Agent 组件:
-
- Device-LocalPV 组件是 Agent,它实际上就是 CSI 插件中的 Node 组件,从这个名称不难猜测,所有在节点宿主机上的操作,都会通过这个组件来完成。
-
- 当 Provisioner 组件和 Controller 组件分别创建好 PV 和 CRD 以后,那么还剩下 2 个步骤,分别是在块设备上创建分区和将分区 Mount 到容器内部,这两个操作正是 Agent 组件的职责。
-
- Agent 组件其构造函数如下:
// NewNode returns a new instance
// of CSI NodeServer
func NewNode(d *CSIDriver) csi.NodeServer {var ControllerMutex = sync.RWMutex{}// set up signals so we handle the first shutdown signal gracefullystopCh := signals.SetupSignalHandler()// start the device node resource watchergo func() {err := devicenode.Start(&ControllerMutex, stopCh)if err != nil {klog.Fatalf("Failed to start Device node controller: %s", err.Error())}}()// start the device volume watchergo func() {err := volume.Start(&ControllerMutex, stopCh)if err != nil {klog.Fatalf("Failed to start Device volume management controller: %s", err.Error())}}()if d.config.ListenAddress != "" {exposeMetrics(d.config, stopCh)}return &node{driver: d,}
}
-
- 可以发现在 Agent 组件内部,启动了两个 Goroutine,它们分别用来监听 devicenodes.local.openebs.io 和 devicevolumes.local.openebs.io 这两个 CRD,其内部都实现了 syncHandler 方法,根据 CRD 状态进行相应操作。
-
- volume 的 syncHandler 主要逻辑如下:
func (c *VolController) syncHandler(key string) error {...// Get the Vol resource with this namespace/nameVol, err := c.VolLister.DeviceVolumes(namespace).Get(name)if K8serror.IsNotFound(err) {runtime.HandleError(fmt.Errorf("devicevolume '%s' has been deleted", key))return nil}if err != nil {return err}VolCopy := Vol.DeepCopy()err = c.syncVol(VolCopy)return err
}func (c *VolController) syncVol(vol *apis.DeviceVolume) error {...// if the status Pending means we will try to create the volumeif vol.Status.State == device.DeviceStatusPending {err = device.CreateVolume(vol)if err == nil {err = device.UpdateVolInfo(vol, device.DeviceStatusReady)} else if custError, ok := err.(*apis.VolumeError); ok && custError.Code == apis.InsufficientCapacity {vol.Status.Error = custErrorreturn device.UpdateVolInfo(vol, device.DeviceStatusFailed)}}return err
}
-
- 可以看到,syncHandler 方法会将查询到的 devicevolumes.local.openebs.io 信息传递给 syncVol 方法,而这个方法中有一行非常关键的代码 device.CreateVolume(vol),调用此方法的作用正是根据 CRD 的信息,在块设备上创建出真正的分区。
-
- 分区一旦被成功创建,那么就只剩下最后一个步骤 Mount 操作了,Mount 操作由 PV 所在节点的 kubelet 直接调用 Agent 组件的 NodePublishVolume 方法来完成。值得注意的是,在部署 Device-LocalPV 项目的 yaml 文件中,Agent 组件所在容器的 volumeMounts 属性中有一个 mountPropagation: “Bidirectional” 配置,其作用是为了使在容器内部执行的 Mount 命令能够向上传播到宿主机上。所以尽管Agent 组件运行在容器中,但在其内部执行的 Mount 命令依然能够在节点上生效。
③ 调度策略
- Device-LocalPV 提供了两种调度策略:CapacityWeighted、VolumeWeighted,可以在存储类中通过参数指定调度策略:
parameters:scheduler: "VolumeWeighted"devname: "test-device"
- CapacityWeighted:
-
- CapacityWeighted 为默认调度策略,即根据使用容量调度,它会查找已部署了 OpenEBS 的节点,按节点上块设备已使用容量进行打分,优先调度到已使用容量较小的节点。
-
- 如下所示的图中,Node1 节点上已经存在 3 个 PV,Node2 节点上存在 2 个 PV,Node3 节点上没有部署 OpenEBS,如果此时新创建一个 PVC,那么 PV 会如何调度呢?
-
- 根据 CapacityWeighted 调度策略来看,Node3 节点第一个被排除,尽管 Node1 节点上已经存在的 PV 数量比 Node2 节点上的多,但已使用容量较少,故根据已使用容量来排序,显然这个新创建的 PV 将会被调度到 Node1 节点。
- VolumeWeighted:
-
- VolumeWeighted 调度策略则根据使用卷数量来进行调度,查找已部署了 OpenEBS 的节点,按节点上块设备已分配卷数量进行打分,优先调度到已使用卷数量较小的节点。
-
- 那么分析下来,在跟上图中同样的情况下,使用 VolumeWeighted 调度策略后,新创建的 PV 将会被调度到 Node2 节点:
- 自定义调度策略:
-
- 如果事情按照理想化方向发展,那么上面两种 Device-LocalPV 提供的调度策略没有任何问题,但真实场景中,也许会遇到如下情况:
-
- 现在部署了 OpenEBS 的节点从 2 个扩展成 3 个,但是 Node3 上仅仅部署了 OpenEBS,还没有插入块设备供 OpenEBS 使用。
在这种情况下,如果新创建 PVC,那么这个 PVC 将一直处于 Pending 状态,PV 无法完成创建。因为无论是哪种调度策略,Device-LocalPV 在为节点打分阶段,总是会将使块设备用量为 0 的节点排序在最前面,所以两种策略最终都会将 PV 调度到 Node3 节点,而又因为 Node3 节点上没有供 OpenEBS 使用的块设备,无法进行磁盘分区,PV 也就无法成功创建。
- 现在部署了 OpenEBS 的节点从 2 个扩展成 3 个,但是 Node3 上仅仅部署了 OpenEBS,还没有插入块设备供 OpenEBS 使用。
④ 故障处理
- 节点故障:
-
- 如果某个正在被 LocalPV 使用的节点出现故障,可以通过迁移块设备来让 LocalPV 恢复使用,具体步骤如下:
-
-
- 将数据盘移动到新节点上;
-
-
-
- 如果新节点没有部署 OpenEBS 则需要先部署 Device-LocalPV 的 DaemonSet 到新节点上;
-
-
-
- 修改 CRD 资源 devicevolumes.local.openebs.io 所属的节点,即修改 spec.ownerNodeID 属性到新的节点;
-
-
-
- 修改 PV 的节点信息,即修改 spec.nodeAffinity 属性到新的节点;
-
-
-
- 删除使用 PV 的 Pod 让其自动重启,并调度到 PV 所指定的新节点上。
-
- 磁盘故障:如果某个正在被 LocalPV 使用的块设备出现故障,则会造成数据丢失。
-
- 需要注意的是,当某个 PV 所使用的磁盘分区出现故障时,PV 无法感知,只有部署在 Pod 内的程序可以感知到,可以尝试在出现故障的 Pod 容器内部执行读写操作,会得到如下错误:
/mnt/store # echo abc > a.txt
sh: can't create a.txt: Input/output error
-
- 因此使用 LocalPV 的程序要有比较完善的异常处理机制以应对可能出现的故障问题。
相关文章:
云原生之深入解析Kubernetes本地持久化存储方案OpenEBS LocalPV的最佳实践
一、K8s 本地存储 K8s 支持多达 20 种类型的持久化存储,如常见的 CephFS 、Glusterfs 等,不过这些大都是分布式存储,随着社区的发展,越来越多的用户期望将 K8s 集群中工作节点上挂载的数据盘利用起来,于是就有了 loca…...
设计模式-策略(Strategy)模式
又被称为政策(方针)模式策略模式(Strategy Design Pattern):封装可以互换的行为,并使用委托来决定要使用哪一个策略模式是一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中&#x…...
Star 4.1k!Gitee GVP开源项目!新一代桌面应用开发框架 ElectronEgg!
前言 随着现代技术的快速升级迭代及发展,桌面应用开发已经变得越来越普及。然而对于非专业桌面应用开发工程师在面对这项任务时,可能会感到无从下手,甚至觉得这是一项困难的挑战。 本篇文章将分享一种新型桌面应用开发框架 ElectronEgg&…...
node.js学习(简单聊天室)
在掘金查看该文章 1. TCP服务搭建 1.1 socket 先来粗略了解下socket 套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中&am…...
cfa一级考生复习经验分享系列(四)
备考CFA一级满打满算用了一个多月,每天八个小时以上。可能如果仅以通过为目标的话完全不用这样,看过太多类似于只看了一周就通过了考试又或是放弃了好几门飘过了考试的情况,我觉得这是不正确的考试状态,完全不必惊叹,踏…...
PPT插件-好用的插件-放映笔、绘图板-大珩助手
放映笔 幻灯片放映时,工具在幻灯片的左下方,本工具在幻灯片的右侧,可以移动,可以方便在右侧讲课时候使用 绘图板 可在绘图板上写签名、绘制图画、写字等等,点画笔切换橡皮擦,点插入绘图,将背景…...
弧形导轨的安装注意事项
随着弧形导轨的应用日渐普遍,在日常使用中总会遇到很多各种各样的问题,原因很多是安装不正确或者使用不恰当。不合理的使用不但不能充分发挥其价值还会导致使用寿命大打折扣,使企业造成不必要的损失,因此大伙有必要了解一些安装的…...
Elasticsearch优化-04
Elasticsearch优化 1、优化-硬件选择 Elasticsearch 的基础是 Lucene,所有的索引和文档数据是存储在本地的磁盘中,具体的路径可在 ES 的配置文件…/config/elasticsearch.yml中配置,如下: # #Path to directory where to store …...
Springboot+vue的公寓报修管理系统(有报告)。Javaee项目,springboot vue前后端分离项目
演示视频: Springbootvue的公寓报修管理系统(有报告)。Javaee项目,springboot vue前后端分离项目 项目介绍: 本文设计了一个基于Springbootvue的前后端分离的公寓报修管理系统,采用M(model&…...
uniapp腾讯地图路线规划
在uniapp中使用腾讯地图进行路线规划需要通过腾讯地图API进行操作。以下是基本的步骤: 在腾讯地图开放平台上注册账号,并创建应用获取API key。 在uniapp的项目中引入腾讯地图API的JS文件,例如在index.html中添加以下代码: <…...
Python 全栈体系【四阶】(五)
第四章 机器学习 三、数据预处理 1. 数据预处理的目的 去除无效数据、不规范数据、错误数据 补齐缺失值 对数据范围、量纲、格式、类型进行统一化处理,更容易进行后续计算 2. 预处理方法 2.1 标准化(均值移除) 让样本矩阵中的每一列的…...
原点处可微问题
文章目录 原点可微问题例例 原点可微问题 lim x → 0 , y → 0 f ( x , y ) − f ( 0 , 0 ) x 2 y 2 \lim\limits_{x\to{0},y\to{0}} \frac{f(x,y)-f(0,0)}{\sqrt{x^2y^2}} x→0,y→0limx2y2 f(x,y)−f(0,0) 0 0 0(1)是函数 f ( x , y ) f(x,y) f(x,y)在 ( 0 , 0 ) (…...
Flink+Kafka消费
引入jar <dependency><groupId>org.apache.flink</groupId><artifactId>flink-java</artifactId><version>1.8.0</version> </dependency> <dependency><groupId>org.apache.flink</groupId><artifactI…...
Seconds_Behind_Master越来越大,主从同步延迟
问题现象 发现从库mysql_slave的参数Seconds_Behind_Master越来越大。已排除主从服务器时间不一致;那么主要就判断两点:是io thread慢还是 sql thread慢?先观察show slave status\G 。 判断3个参数(参数后面的值是默认空闲时候的…...
除法求值[中等]
一、题目 给你一个变量对数组equations和一个实数值数组values作为已知条件,其中equations[i] [Ai, Bi]和values[i]共同表示等式Ai / Bi values[i]。每个Ai或Bi是一个表示单个变量的字符串。另有一些以数组queries表示的问题,其中queries[j] [Cj, Dj…...
新时代商业市场:AR技术的挑战与机遇并存
随着科技的不断发展,增强现实(AR)技术逐渐成为当今社会的一个重要组成部分。AR技术能够将虚拟世界与现实世界相结合,为人们提供更加丰富、多样化的体验。在新时代的社会商业市场中,AR技术也正逐渐被应用于各种商业活动…...
RHEL8中ansible的使用
编写ansible.cfg和清单文件ansible的基本用法 本章实验三台RHEL8系统(rhel801,rhel802,rhel803),其中rhel801是ansible主机 这里要确保ansible主机能够解析所有被管理的机器,这里通过配置/etc/hosts来实现…...
【1.6计算机组成与体系结构】存储系统
目录 1.层次化存储结构2.Cache2.1 Cache的介绍2.2 局部性原理2.3 Cache应用 1.层次化存储结构 由 ⬆ CPU:寄存器。 快 ⬆ Cache:按内容存取(相联存储器)。 到 ⬆内存(主存):DRAM。 慢 ⬆ 外存(辅存&#…...
TCP/UDP 协议
目录 一.TCP协议 1.介绍 2.报文格式 编辑 确认号 控制位 窗口大小 3.TCP特性 二.TCP协议的三次握手 1.tcp 三次握手的过程 三.四次挥手 2.有限状态机 四.tcp协议和udp协议的区别 五.udp协议 UDP特性 六.telnet协议 一.TCP协议 1.介绍 TCP(Transm…...
如何正确理解和使用 Golang 中 nil ?
目录 指针中的 nil 切片中的 nil map 中的 nil 通道中的 nil 函数中的 nil 接口中的 nil 避免 nil 相关问题的最佳实践 小结 在 Golang 中,nil 是一个预定义的标识符,在不同的上下文环境中有不同的含义,但通常表示“无”、“空”或“…...
IDEA新建jdk8 spring boot项目
今天新建spring boot项目发现JDK版本最低可选17。 但是目前用的最多的还是JDK8啊。 解决办法 Server URL中设置: https://start.aliyun.com/设置完成后,又可以愉快的用jdk8创建项目了。 参考 https://blog.csdn.net/imbzz/article/details/13469117…...
Qt/C++音视频开发59-使用mdk-sdk组件/原qtav作者力作/性能凶残/超级跨平台
一、前言 最近一个月一直在研究mdk-sdk音视频组件,这个组件是原qtav作者的最新力作,提供了各种各样的示例demo,不仅限于支持C,其他各种比如java/flutter/web/android等全部支持,性能上也是杠杠的,目前大概…...
智安网络|企业网络安全工具对比:云桌面与堡垒机,哪个更适合您的需求
随着云计算技术的快速发展,越来越多的企业开始采用云计算解决方案来提高效率和灵活性。在云计算环境下,云桌面和堡垒机被广泛应用于企业网络安全和办公环境中。尽管它们都有助于提升企业的安全和效率,但云桌面和堡垒机在功能和应用方面存在着…...
Git忽略已经提交的文件
原理类似于 Android修改submodule的lib包名...
MVVM和MVC以及MVP的原理以及它们的区别
MVVM、MVC 和 MVP 都是前端架构模式,它们各自有不同的原理和特点。 MVC(Model-View-Controller) 原理:MVC 将应用程序分为三个部分:模型(Model)、视图(View)和控制器&a…...
WeChatMsg: 导出微信聊天记录 | 开源日报 No.108
Mozilla-Ocho/llamafile Stars: 3.5k License: NOASSERTION llamafile 是一个开源项目,旨在通过将 lama.cpp 与 Cosmopolitan Libc 结合成一个框架,将 LLM (Large Language Models) 的复杂性折叠到单个文件可执行程序中,并使其能够在大多数…...
Python学习之复习MySQL-Day3(DQL)
目录 文章声明⭐⭐⭐让我们开始今天的学习吧!DQL简介基本查询查询多个/全部字段设置别名去除重复记录 条件查询条件查询介绍实例演示 聚合函数什么是聚合函数?常见的聚合函数实例演示 分组查询分组查询语法where 和 having 的区别实例演示 排序查询语法实…...
AI超级个体:ChatGPT与AIGC实战指南
目录 前言 一、ChatGPT在日常工作中的应用场景 1. 客户服务与支持 2. 内部沟通与协作 3. 创新与问题解决 二、巧用ChatGPT提升工作效率 1. 自动化工作流程 2. 信息整合与共享 3. 提高决策效率 三、巧用ChatGPT创造价值 1. 优化产品和服务 2. 提高员工满意度和留任率…...
SpringBoot集成websocket(5)|(使用OkHttpClient实现websocket以及详细介绍)
SpringBoot集成websocket(5)|(使用OkHttpClient实现websocket以及详细介绍) 文章目录 SpringBoot集成websocket(5)|(使用OkHttpClient实现websocket以及详细介绍)[TOC] 前言一、初始…...
Kafka-Kafka基本原理与集群快速搭建(实践)
Kafka单机搭建 下载Kafka Apache Download Mirrors 解压 tar -zxvf kafka_2.12-3.4.0.tgz -C /usr/local/src/software/kafkakafka内部bin目录下有个内置的zookeeper(用于单机) 启动zookeeper(在后台启动) nohup bin/zookeeper-server-start.sh conf…...
做古建的那些网站比较适合/关键词整站优化
原文链接:http://www.360doc.com/content/17/1231/22/9200790_718001949.shtml 转载于:https://www.cnblogs.com/ys-01/p/11490069.html...
江门企业网站建设/seo赚钱培训
资源管理 Q1:我们有了解到,卸载AssetBundle后, 从这个AB读出资源的Instance ID和guid的映射关系会被删除。在我们的项目中, 优化策略是这样的: 进场景时, 预加载Prefab,并创建一些对象实例缓存,然后会把AB删…...
极验验证 wordpress/中山排名推广
文章目录0x01 DLL简介0x02 DLL 调用0x03 与 lib 文件区别0x04 DLL 编写0x01 DLL简介 动态链接库(Dynamic-Link-Library,缩写dll), 是微软公司在微软视窗操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是 .DLL、.OCX&am…...
pt网站怎么做/百度关键词推广方案
即如下: 【想做到点击nav侧边栏,仅替换右边div中的内容,而不是跳转到新的页面,这样的话,其实整个项目中就只有一个完整的页面,其他的页面均只写<body>内的部分即可,或者仅仅写要替换的<div>内的…...
网站制作公司茂名/无限制访问国外的浏览器
最近打开了交接过来的旧代码,编译了一下,出现以下错误: Error[Li005]: no definition for "__disable_interrupt" Error[Li005]: no definition for "__enable_interrupt"解决方法:添加头文件#include <…...
淘宝联盟返利网站怎么做/天津抖音seo
如何在visio中插入公式? 最方便的方法是安装mathtype公式编辑器,在里面输入完公式之后复制,在visio空白处点击右键,选择“选择性粘贴”,然后会出现mathtype 6.0 Equation,单击确定就把mathtype中的公式顺利…...