Kubernetes存储详解

概述

由于容器默认情况下rootfs系统树没有一个与之对应的存储设备,因此可以认为容器中任何文件操作都是临时性的,这样的设计带来了两个问题,其一是如果容器因为某些原因被kubelet重启后,会丢失这些文件;其二是不同的容器之间无法共享文件。为了解决上述问题,Kubernetes 设计了卷(Volume) 这一抽象来管理容器中所有需要使用到的外部文件。

并且由于容器本身存在声明周期以及容器的存储来源有多样性的特点,卷本身带有独立的状态标识实现其生命周期循环,根绝业务场景的不同,卷又细分为持久卷、临时卷、投射卷三大类。

为了让集群管理员可以管理更多不同特性的持久卷,Kubernetes 又设计了存储类(StorageClass) 来管理每一类具有相同特性的持久卷,在后续的 Kubernetes 版本迭代过程中陆续加入了一些其他的特性,例如:为了可以将任意第三方存储暴露给容器,增加了容器存储接口 (CSI);为了增加存储的可靠性,增加类似传统文件系统中快照概念的卷快照 (Snapshot) 资源;为了可以对接任意第三方对象存储,增加了容器对象存储接口(COSI)。

持久卷(Persistent Volume)

PersistentVolume 子系统为用户和管理员提供了一组 API,通过引入了两个新的 API 资源:PersistentVolume 存储提供者进行管理; PersistentVolumeClaim 被存储使用者引用。

持久卷(PersistentVolume,PV) 是集群中的一块存储,可以由管理员事先制备 (Provision) , 或者使用存储类(Storage Class)来动态制备 (Provision) 。 持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样, 也是使用卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: foo-pv
spec:
  storageClassName: ""
  claimRef:
    name: foo-pvc
    namespace: foo

持久卷申领(PersistentVolumeClaim,PVC) 表达的是用户对存储的请求。概念上与 Pod 类似。 Pod 会耗用节点资源,而 PVC 申领会耗用 PV 资源。Pod 可以请求特定数量的资源(CPU 和内存);同样 PVC 申领也可以请求特定的大小和访问模式 (例如,可以要求 PV 卷能够以 ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany 模式之一来挂载,参见访问模式)。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: foo-pvc
  namespace: foo
spec:
  storageClassName: ""
  volumeName: foo-pv

尽管 PersistentVolumeClaim 允许用户消耗抽象的存储资源, 常见的情况是针对不同的问题用户需要的是具有不同属性(如,性能)的 PersistentVolume 卷。 集群管理员需要能够提供不同性质的 PersistentVolume, 并且这些 PV 卷之间的差别不仅限于卷大小和访问模式,同时又不能将卷是如何实现的这些细节暴露给用户。 为了满足这类需求,就有了存储类(StorageClass) 资源。

存储卷生命周期管理

1. 制备 (Provision)

制备(Provision)一般是只在 Kubernetes API Server中的 PV 资源以及在存储系统中申请存储空间。PV 卷的制备有两种方式:静态制备或动态制备。

静态制备时,集群管理员创建若干 PV 卷,这些卷对象带有真实存储的细节信息。

动态制备时,集群管理员在Kubernetes上创建一个存储类,当集群发现没有PV可以与PVC匹配时,由存储类来管理 PV 资源的创建和存储空间的创建。

为了基于存储类完成动态的存储制备,集群管理员需要在 API 服务器上启用 DefaultStorageClass 准入控制器。

2. 绑定 (Bind)

前面我们讲过 Pod 中每声明一个PVC 资源,则消费一个与之对应的 PV 资源,这一过程也被我们成为绑定 (Bind) 。集群控制平面监测到新的 PVC 对象,并寻找一个与之匹配的 PV 卷, 并将二者绑定到一起(将 PV 信息写入 PVC 对象的 ClaimRef 信息中,并将 PV 状态修改 Bound )。

如果找不到匹配的 PV 卷,PVC 会无限期地处于未绑定状态。例如,即使某集群上制备了很多 50 Gi 大小的 PV 卷,也无法与请求 100 Gi 大小的存储的 PVC 匹配。当新的 100 Gi PV 卷被加入到集群时, 该 PVC 才有可能被绑定。

3. 使用 (Using)

Pod 将 PVC 申领当做存储卷来使用。集群会检视 PVC 申领,找到所绑定的卷, 并为 Pod 挂载该卷。对于支持多种访问模式的卷, 用户要在 Pod 中以卷的形式使用申领时指定期望的访问模式。

一旦用户有了申领对象并且该申领已经被绑定, 则所绑定的 PV 卷在用户仍然需要它期间一直属于该用户。 用户通过在 Pod 的 volumes 块中包含 persistentVolumeClaim 节区来调度 Pod,访问所申领的 PV 卷。 相关细节可参阅使用申领作为卷。

apiVersion: v1
kind: Pod
metadata:
  name: volume-test
spec:
  containers:
  - name: container-test
    image: busybox:1.28
    volumeMounts:
    - name: foo
      mountPath: "/mnt/foo"
  volumes:
  - name: foo
    persistentVolumeClaim:
        claimName: foo-pvc

回收策略 (Reclaim Policy)

当用户不再使用其存储卷时,他们可以从 API 中将 PVC 对象删除, 从而允许该资源被回收再利用。PersistentVolume 对象的回收策略告诉集群, 当其被从申领中释放时如何处理该数据卷。 目前,数据卷可以被 Retained(保留)、Recycled(回收)或 Deleted(删除)。

1. 保留(Retain)

回收策略 Retain 使得用户可以手动回收资源。当 PersistentVolumeClaim 对象被删除时,PersistentVolume 卷仍然存在,对应的数据卷被视为"已释放(released)"。 由于卷上仍然存在这前一申领人的数据,该卷还不能用于其他申领。 管理员可以通过下面的步骤来手动回收该卷:

删除 PersistentVolume 对象。与之相关的、位于外部基础设施中的存储资产 (例如 AWS EBS、GCE PD、Azure Disk 或 Cinder 卷)在 PV 删除之后仍然存在。

根据情况,手动清除所关联的存储资产上的数据。

手动删除所关联的存储资产。

如果你希望重用该存储资产,可以基于存储资产的定义创建新的 PersistentVolume 卷对象。

2. 删除(Delete)

对于支持 Delete 回收策略的卷插件,删除动作会将 PersistentVolume 对象从 Kubernetes 中移除,同时也会从外部基础设施(如 AWS EBS、GCE PD、Azure Disk 或 Cinder 卷)中移除所关联的存储资产。 动态制备的卷会继承其 StorageClass 中设置的回收策略, 该策略默认为 Delete。管理员需要根据用户的期望来配置 StorageClass; 否则 PV 卷被创建之后必须要被编辑或者修补。 参阅更改 PV 卷的回收策略。

3. 回收(Recycle)

如果下层的卷插件支持,回收策略 Recycle 会在卷上执行一些基本的擦除 (rm -rf /thevolume/*)操作,之后允许该卷用于新的 PVC 。

临时卷

有些应用程序需要额外的存储,但并不关心数据在重启后是否仍然可用。 例如,缓存服务经常受限于内存大小,而且可以将不常用的数据转移到比内存慢的存储中,对总体性能的影响并不大。

另有些应用程序需要以文件形式注入的只读数据,比如配置数据或密钥。

临时卷 就是为此类用例设计的。因为卷会遵从 Pod 的生命周期,与 Pod 一起创建和删除, 所以停止和重新启动 Pod 时,不会受持久卷在何处可用的限制。

临时卷在 Pod 规约中以 内联 方式定义,这简化了应用程序的部署和管理。

Kubernetes 为了不同的用途,支持几种不同类型的临时卷:

emptyDir:

Pod 启动时为空,存储空间来自本地的 kubelet 根目录(通常是根磁盘)或内存。

configMap、 downwardAPI、 secret:

将不同类型的 Kubernetes 数据注入到 Pod 中

CSI 临时卷:

类似于前面的卷类型,但由专门支持此特性 的指定 CSI 驱动程序提供。

通用临时卷:

它可以由所有支持持久卷的存储驱动程序提供。

emptyDir、configMap、downwardAPI、secret 是作为 本地临时存储 提供的。它们由各个节点上的 kubelet 管理。

CSI 临时卷 必须 由第三方 CSI 存储驱动程序提供。

通用临时卷 可以 由第三方 CSI 存储驱动程序提供,也可以由支持动态制备的任何其他存储驱动程序提供。 一些专门为 CSI 临时卷编写的 CSI 驱动程序,不支持动态制备:因此这些驱动程序不能用于通用临时卷。

使用第三方驱动程序的优势在于,它们可以提供 Kubernetes 本身不支持的功能, 例如,与 kubelet 管理的磁盘具有不同性能特征的存储,或者用来注入不同的数据。

投射卷

一个 projected 卷可以将若干现有的卷源映射到同一个目录之上。

目前,源可以被投射的卷包括:secret, downwardAPI, configMap, serviceAccountToken。

所有的卷源都要求处于 Pod 所在的同一个名字空间内。进一步的详细信息,可参考 一体化卷设计文档。

apiVersion: v1
kind: Pod
metadata:
  name: volume-test
spec:
  containers:
  - name: container-test
    image: busybox:1.28
    volumeMounts:
    - name: all-in-one
      mountPath: "/projected-volume"
      readOnly: true
  volumes:
  - name: all-in-one
    projected:
      sources:
      - secret:
          name: mysecret
          items:
            - key: username
              path: my-group/my-username
      - downwardAPI:
          items:
            - path: "labels"
              fieldRef:
                fieldPath: metadata.labels
            - path: "cpu_limit"
              resourceFieldRef:
                containerName: container-test
                resource: limits.cpu
      - configMap:
          name: myconfigmap
          items:
            - key: config
              path: my-group/my-config

存储架构

存储架构

为了存储卷管理的功能,Kubernetes 设计了4大组件,从顶层向一下依次为:

Volume Plugins — 存储提供的扩展接口, 包含了各类存储提供者的plugin实现。其中Volume Plugins是一个基础部件,功能上来说Volume Manager、PV Controller、AD Controller三部分。

Volume Manager — 运行在kubelet 里让存储Ready的部件,负责管理数据卷的 Mount/Umount 操作(也负责数据卷的 Attach/Detach 操作,需配置 kubelet 相关参数开启该特性)、卷设备的格式化等等。

PV Controller — 运行在控制平面的 kube-controller-manager 上的组件,主要负责 PV/PVC 绑定及周期管理,根据需求进行对 PVC 的 Provision/Delete 操作;所谓将一个 PV 与 PVC 进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 spec.volumeName 字段上。

AD(Attach/Detach) Controller — 运行在控制平面的 kube-controller-manager 上的组件,负责数据卷的 Attach/Detach 操作,将设备挂接到目标节点。

存储卷挂载

PV Controller和K8S其它组件一样监听API Server中的资源更新,对于卷管理主要是监听PV,PVC, SC三类资源,当监听到这些资源的创建、删除、修改时,PV Controller经过判断是需要做创建、删除、绑定、回收等动作(后续会展开介绍内部逻辑),然后根据需要调用Volume Plugins进行业务处理。

存储管理

存储卷管理核心做三个动作:

Provision/Delete (创建/删除存储卷,处理pv和pvc之间的关系)

Attach/Detach (挂接和摘除存储卷,处理的是volumes和node上目录之间的关系)

Mount/Unmount (挂载和摘除目录,处理的是volumes和pod之间的关系)

CSI存储插件流程分析

以 nfs plugin 插件为例,分析 Kubernetes 集群如何为用户提供存储资源。

CSI 简介

csi插件的实现,官方已经封装好的lib,我们只要实现对应的接口就可以了,目前我们可以实现的插件有两种类型,如下:

Controller Plugin,负责存储对象(Volume)的生命周期管理,在集群中仅需要有一个即可;

Node Plugin,在必要时与使用 Volume 的容器所在的节点交互,提供诸如节点上的 Volume 挂载/卸载等动作支持,如有需要则在每个服务节点上均部署。

官方提供了rpc接口实现上面两个插件,如下:

CSI Identity Service(身份服务)

Node Plugin和Controller Plugin都必须实现这些RPC集,接口如下:

service Identity {
    // GetPluginInfo, 获取 Plugin 基本信息
  rpc GetPluginInfo(GetPluginInfoRequest)
    returns (GetPluginInfoResponse) {}

    // GetPluginCapabilities,获取 Plugin 支持的能力
  rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
    returns (GetPluginCapabilitiesResponse) {}

    //Probe,探测 Plugin 的健康状态
  rpc Probe (ProbeRequest)
    returns (ProbeResponse) {}
}
CSI Controller Service(控制器服务)

控制器服务,Controller Plugin 必须实现这些RPC集。CSI Controller 服务里定义的这些操作有个共同特点,那就是它们都无需在宿主机上进行,而是属于 Kubernetes 里 Volume Controller 的逻辑,也就是属于 Master 节点的一部分。需要注意的是,正如我在前面提到的那样,CSI Controller 服务的实际调用者,并不是 Kubernetes(即:通过 pkg/volume/csi 发起 CSI 请求),而是 External Provisioner 和 External Attacher。这两个 External Components,分别通过监听 PVC 和 VolumeAttachement 对象,来跟 Kubernetes 进行协作。

service Controller {
    // Volume CRUD,包括了扩容和容量探测等 Volume 状态检查与操作接口
  rpc CreateVolume (CreateVolumeRequest)
    returns (CreateVolumeResponse) {}

  rpc DeleteVolume (DeleteVolumeRequest)
    returns (DeleteVolumeResponse) {}

    // Publish/Unpublish ,也就是对 CSI Volume 进行 Attach/Dettach,还包括Node 对 Volume 的访问权限管理
  rpc ControllerPublishVolume (ControllerPublishVolumeRequest)
    returns (ControllerPublishVolumeResponse) {}

  rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)
    returns (ControllerUnpublishVolumeResponse) {}

  rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)
    returns (ValidateVolumeCapabilitiesResponse) {}

  rpc ListVolumes (ListVolumesRequest)
    returns (ListVolumesResponse) {}

  rpc GetCapacity (GetCapacityRequest)
    returns (GetCapacityResponse) {}

  rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
    returns (ControllerGetCapabilitiesResponse) {}

    // Snapshot CRD,快照的创建和删除操作,目前 CSI 定义的 Snapshot 仅用于创建 Volume,未提供回滚的语义
  rpc CreateSnapshot (CreateSnapshotRequest)
    returns (CreateSnapshotResponse) {}

  rpc DeleteSnapshot (DeleteSnapshotRequest)
    returns (DeleteSnapshotResponse) {}

  rpc ListSnapshots (ListSnapshotsRequest)
    returns (ListSnapshotsResponse) {}

  rpc ControllerExpandVolume (ControllerExpandVolumeRequest)
    returns (ControllerExpandVolumeResponse) {}

  rpc ControllerGetVolume (ControllerGetVolumeRequest)
    returns (ControllerGetVolumeResponse) {
        option (alpha_method) = true;
    }
}
CSI Controller Service(控制器服务)

节点服务:Node Plugin必须实现这些RPC集。CSI Volume 需要在宿主机上执行的操作,都定义在了 CSI Node 服务里面

service Node {
    // Node Stage/Unstage/Publish/Unpublish/GetStats Volume,节点上 Volume 的连接状态管理,也就是mount是由 NodeStageVolume 和 NodePublishVolume 两个接口共同实现的。
rpc NodeStageVolume (NodeStageVolumeRequest)
    returns (NodeStageVolumeResponse) {}

  rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
    returns (NodeUnstageVolumeResponse) {}

  rpc NodePublishVolume (NodePublishVolumeRequest)
    returns (NodePublishVolumeResponse) {}

  rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
    returns (NodeUnpublishVolumeResponse) {}

  rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
    returns (NodeGetVolumeStatsResponse) {}

    // Node Expand Volume, 节点上的 Volume 扩容操作,在 volume 逻辑大小扩容之后,可能还需要同步的扩容 Volume 之上的文件系统并让使用 Volume 的 Container 感知到,所以在 Node Plugin 上需要有对应的接口
  rpc NodeExpandVolume(NodeExpandVolumeRequest)
    returns (NodeExpandVolumeResponse) {}

    // Node Get Capabilities/Info, Plugin 的基础属性与 Node 的属性查询

  rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
    returns (NodeGetCapabilitiesResponse) {}

  rpc NodeGetInfo (NodeGetInfoRequest)
    returns (NodeGetInfoResponse) {}
}

csi-call

Driver Registrar 组件,负责将插件注册到 kubelet 里面(这可以类比为,将可执行文件放在插件目录下)。而在具体实现上,Driver Registrar 需要请求 CSI 插件的 Identity 服务来获取插件信息。

External Provisioner 组件,负责的正是 Provision 阶段。在具体实现上,External Provisioner 监听(Watch)了 APIServer 里的 PVC 对象。当一个 PVC 被创建时,它就会调用 CSI Controller 的 CreateVolume 方法,为你创建对应 PV。

External Attacher 组件,负责的正是“Attach 阶段”。在具体实现上,它监听了 APIServer 里 VolumeAttachment 对象的变化。VolumeAttachment 对象是 Kubernetes 确认一个 Volume 可以进入“Attach 阶段”的重要标志。一旦出现了 VolumeAttachment 对象,External Attacher 就会调用 CSI Controller 服务的 ControllerPublish 方法,完成它所对应的 Volume 的 Attach 阶段。

Volume 的“Mount 阶段”,并不属于 External Components 的职责。当 kubelet 的

VolumeManagerReconciler 控制循环检查到它需要执行 Mount 操作的时候,会通过 pkg/volume/csi 包,直接调用 CSI Node 服务完成 Volume 的“Mount 阶段”。

如果你要实现一个自己的 CSI Driver 你需要至少提供两个 gRPC 服务:CSI Identity service,负责 CSI 插件的识别工作;CSI Node Driver 负责

nfs plugin 项目路径: https://github.com/kubernetes-sigs/csi-driver-nfs

nfs controller 项目路径:https://github.com/kubernetes-sigs/external-provisioner

部署

部署方式

# 安装存储插件
curl -skSL https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/install-driver.sh | bash -s master --

# 查看 Pod 工作状态
kubectl -n kube-system get pod -o wide -l app=csi-nfs-controller
kubectl -n kube-system get pod -o wide -l app=csi-nfs-node

# 结果
NAME                                       READY   STATUS    RESTARTS   AGE     IP             NODE
csi-nfs-controller-56bfddd689-dh5tk       4/4     Running   0          35s     10.240.0.19    k8s-agentpool-22533604-0
csi-nfs-node-cvgbs                        3/3     Running   0          35s     10.240.0.35    k8s-agentpool-22533604-1
csi-nfs-node-dr4s4                        3/3     Running   0          35s     10.240.0.4     k8s-agentpool-22533604-0

csi nfs driver 包含 controller 和 nodeplugin 两部分,controller 负责存储资源管理,nodeplugin 负责将存储卷加载到容器中。

controller pod 中有两个工作组件,一个是 provisioner 容器,负责的正是 Provision 阶段;一个 nfsplugin 容器,负责存储卷声明周期管理。

node pod 中有两个工作组件,一个 node-driver-registrar 容器,负责向 Kubelet 注册一个CSI 插件;一个 nfs 容器,负责 PV 挂载。

nfscontroller 源码解析

1. 服务入口

controller 服务的主流程直接写在了main函数中,直接看代码:

func main() {
	// ...

	// 客户端初始化
	grpcClient, err := ctrl.Connect(*csiEndpoint, metricsManager)
	if err != nil {
		klog.Error(err.Error())
		os.Exit(1)
	}

	err = ctrl.Probe(grpcClient, *operationTimeout)
	if err != nil {
		klog.Error(err.Error())
		os.Exit(1)
	}

	// 查询集群中 driver 对应的 provisionerName
	provisionerName, err := ctrl.GetDriverName(grpcClient, *operationTimeout)
	if err != nil {
		klog.Fatalf("Error getting CSI driver name: %s", err)
	}
	klog.V(2).Infof("Detected CSI driver %s", provisionerName)
	metricsManager.SetDriverName(provisionerName)

	translator := csitrans.New()
	supportsMigrationFromInTreePluginName := ""
	if translator.IsMigratedCSIDriverByName(provisionerName) {
		supportsMigrationFromInTreePluginName, err = translator.GetInTreeNameFromCSIName(provisionerName)
		if err != nil {
			klog.Fatalf("Failed to get InTree plugin name for migrated CSI plugin %s: %v", provisionerName, err)
		}
		klog.V(2).Infof("Supports migration from in-tree plugin: %s", supportsMigrationFromInTreePluginName)

		// Create a new connection with the metrics manager with migrated label
		metricsManager = metrics.NewCSIMetricsManagerWithOptions(provisionerName,
			// Will be provided via default gatherer.
			metrics.WithProcessStartTime(false),
			metrics.WithMigration())
		migratedGrpcClient, err := ctrl.Connect(*csiEndpoint, metricsManager)
		if err != nil {
			klog.Error(err.Error())
			os.Exit(1)
		}
		grpcClient.Close()
		grpcClient = migratedGrpcClient

		err = ctrl.Probe(grpcClient, *operationTimeout)
		if err != nil {
			klog.Error(err.Error())
			os.Exit(1)
		}
	}

	// 准备资源采集、选主、监控检查
	mux := http.NewServeMux()
	gatherers := prometheus.Gatherers{
		legacyregistry.DefaultGatherer,
		metricsManager.GetRegistry(),
	}

	pluginCapabilities, controllerCapabilities, err := ctrl.GetDriverCapabilities(grpcClient, *operationTimeout)
	if err != nil {
		klog.Fatalf("Error getting CSI driver capabilities: %s", err)
	}

	// 为 provisioner 生成唯一的ID
	timeStamp := time.Now().UnixNano() / int64(time.Millisecond)
	identity := strconv.FormatInt(timeStamp, 10) + "-" + strconv.Itoa(rand.Intn(10000)) + "-" + provisionerName
	if *enableNodeDeployment {
		identity = identity + "-" + node
	}

	// 创建一个共享的 informer
	factory := informers.NewSharedInformerFactory(clientset, ctrl.ResyncPeriodOfCsiNodeInformer)
	var factoryForNamespace informers.SharedInformerFactory 

	// 监听 StorageClass 和 PVC
	scLister := factory.Storage().V1().StorageClasses().Lister()
	claimLister := factory.Core().V1().PersistentVolumeClaims().Lister()

	var vaLister storagelistersv1.VolumeAttachmentLister
	if controllerCapabilities[csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME] {
		klog.Info("CSI driver supports PUBLISH_UNPUBLISH_VOLUME, watching VolumeAttachments")
		vaLister = factory.Storage().V1().VolumeAttachments().Lister()
	} else {
		klog.Info("CSI driver does not support PUBLISH_UNPUBLISH_VOLUME, not watching VolumeAttachments")
	}

    // 默认不开启 enableNodeDeployment
	var nodeDeployment *ctrl.NodeDeployment
	if *enableNodeDeployment {
		nodeDeployment = &ctrl.NodeDeployment{
			NodeName:         node,
			ClaimInformer:    factory.Core().V1().PersistentVolumeClaims(),
			ImmediateBinding: *nodeDeploymentImmediateBinding,
			BaseDelay:        *nodeDeploymentBaseDelay,
			MaxDelay:         *nodeDeploymentMaxDelay,
		}
		nodeInfo, err := ctrl.GetNodeInfo(grpcClient, *operationTimeout)
		if err != nil {
			klog.Fatalf("Failed to get node info from CSI driver: %v", err)
		}
		nodeDeployment.NodeInfo = *nodeInfo
	}

	// 节点监听
	var nodeLister listersv1.NodeLister
	var csiNodeLister storagelistersv1.CSINodeLister
	if ctrl.SupportsTopology(pluginCapabilities) {
		if nodeDeployment != nil {
			// ...
		} else {
			csiNodeLister = factory.Storage().V1().CSINodes().Lister()
			nodeLister = factory.Core().V1().Nodes().Lister()
		}
	}

	// PVC Informer
	rateLimiter := workqueue.NewItemExponentialFailureRateLimiter(*retryIntervalStart, *retryIntervalMax)
	claimQueue := workqueue.NewNamedRateLimitingQueue(rateLimiter, "claims")
	claimInformer := factory.Core().V1().PersistentVolumeClaims().Informer()

	// 为 external-provisioner 添加 options
	provisionerOptions := []func(*controller.ProvisionController) error{
		controller.LeaderElection(false), // Always disable leader election in provisioner lib. Leader election should be done here in the CSI provisioner level instead.
		controller.FailedProvisionThreshold(0),
		controller.FailedDeleteThreshold(0),
		controller.RateLimiter(rateLimiter),
		controller.Threadiness(int(*workerThreads)),
		controller.CreateProvisionedPVLimiter(workqueue.DefaultControllerRateLimiter()),
		controller.ClaimsInformer(claimInformer),
		controller.NodesLister(nodeLister),
	}

	if utilfeature.DefaultFeatureGate.Enabled(features.HonorPVReclaimPolicy) {
		provisionerOptions = append(provisionerOptions, controller.AddFinalizer(true))
	}

	if supportsMigrationFromInTreePluginName != "" {
		provisionerOptions = append(provisionerOptions, controller.AdditionalProvisionerNames([]string{supportsMigrationFromInTreePluginName}))
	}

	// 创建一个 provisioner 和两个 controller 
	csiProvisioner := ctrl.NewCSIProvisioner(
		clientset,
		*operationTimeout,
		identity,
		*volumeNamePrefix,
		*volumeNameUUIDLength,
		grpcClient,
		snapClient,
		provisionerName,
		pluginCapabilities,
		controllerCapabilities,
		supportsMigrationFromInTreePluginName,
		*strictTopology,
		*immediateTopology,
		translator,
		scLister,
		csiNodeLister,
		nodeLister,
		claimLister,
		vaLister,
		*extraCreateMetadata,
		*defaultFSType,
		nodeDeployment,
		*controllerPublishReadOnly,
		*preventVolumeModeConversion,
	)

	// ...

	provisionController = controller.NewProvisionController(
		clientset,
		provisionerName,
		csiProvisioner,
		provisionerOptions...,
	)

	csiClaimController := ctrl.NewCloningProtectionController(
		clientset,
		claimLister,
		claimInformer,
		claimQueue,
		controllerCapabilities,
	)

	// 启动 http 服务
	if addr != "" {
		// ...

		go func() {
			klog.Infof("ServeMux listening at %q", addr)
			err := http.ListenAndServe(addr, mux)
			if err != nil {
				klog.Fatalf("Failed to start HTTP server at specified address (%q) and metrics path (%q): %s", addr, *metricsPath, err)
			}
		}()
	}

	run := func(ctx context.Context) {
		factory.Start(ctx.Done())
		if factoryForNamespace != nil {
			// Starting is enough, the capacity controller will
			// wait for sync.
			factoryForNamespace.Start(ctx.Done())
		}

		// informer 缓存同步
		cacheSyncResult := factory.WaitForCacheSync(ctx.Done())
		for _, v := range cacheSyncResult {
			if !v {
				klog.Fatalf("Failed to sync Informers!")
			}
		}

        // 启动控制器
		if capacityController != nil {
			go capacityController.Run(ctx, int(*capacityThreads))
		}
		if csiClaimController != nil {
			go csiClaimController.Run(ctx, int(*finalizerThreads))
		}
		provisionController.Run(ctx)
	}

	// ...
}

provisionController 是定义在 external-provisioner 官方lib库中所实现的控制器,下面看一下源码:

func (ctrl *ProvisionController) Run(ctx context.Context) {
	run := func(ctx context.Context) {
		klog.Infof("Starting provisioner controller %s!", ctrl.component)
		defer utilruntime.HandleCrash()
		defer ctrl.claimQueue.ShutDown()
		defer ctrl.volumeQueue.ShutDown()

		ctrl.hasRunLock.Lock()
		ctrl.hasRun = true
		ctrl.hasRunLock.Unlock()
		if ctrl.metricsPort > 0 {
			// ...
		}

		// 将 controller 初始化传入的 Informer 启动
		if !ctrl.customClaimInformer {
			go ctrl.claimInformer.Run(ctx.Done())
		}
		if !ctrl.customVolumeInformer {
			go ctrl.volumeInformer.Run(ctx.Done())
		}
		if !ctrl.customClassInformer {
			go ctrl.classInformer.Run(ctx.Done())
		}

		// informer 缓存同步
		if !cache.WaitForCacheSync(ctx.Done(), ctrl.claimInformer.HasSynced, ctrl.volumeInformer.HasSynced, ctrl.classInformer.HasSynced) {
			return
		}

		// 启动 goroutine 处理由于被监听到发生了增删改查事件的 PV 和 PVC 队列
		for i := 0; i < ctrl.threadiness; i++ {
			go wait.Until(func() { ctrl.runClaimWorker(ctx) }, time.Second, ctx.Done())
			go wait.Until(func() { ctrl.runVolumeWorker(ctx) }, time.Second, ctx.Done())
		}

		klog.Infof("Started provisioner controller %s!", ctrl.component)

		select {}
	}

	go ctrl.volumeStore.Run(ctx, DefaultThreadiness)

	if ctrl.leaderElection {
		// ...
	} else {
		run(ctx)
	}
}

事件处理的 runClaimWorker 和 runVolumeWorker 会去调用我们声明在 external-provsioner 中的Controller Service 接口,具体的调用过程这里不展开,如果有兴趣可以到官方仓库查看源码

2. 控制器服务结构

func (p *csiProvisioner) Provision(ctx context.Context, options controller.ProvisionOptions) (*v1.PersistentVolume, controller.ProvisioningState, error) {
	claim := options.PVC
	provisioner, ok := claim.Annotations[annStorageProvisioner]
	if !ok {
		provisioner = claim.Annotations[annBetaStorageProvisioner]
	}

	// ...

	// 检测工作节点
	owned, err := p.checkNode(ctx, claim, options.StorageClass, "provision")
	if err != nil {
		return nil, controller.ProvisioningNoChange,
			fmt.Errorf("node check failed: %v", err)
	}
	if !owned {
		return nil, controller.ProvisioningNoChange, &controller.IgnoredError{
			Reason: fmt.Sprintf("not responsible for provisioning of PVC %s/%s because it is not assigned to node %q", claim.Namespace, claim.Name, p.nodeDeployment.NodeName),
		}
	}

	// provision 预处理
	result, state, err := p.prepareProvision(ctx, claim, options.StorageClass, options.SelectedNode)
	if result == nil {
		return nil, state, err
	}

	// ...

	// 创建 PV
	rep, err := p.csiClient.CreateVolume(createCtx, req)
	if err != nil {
		mayReschedule := p.supportsTopology() &&
			options.SelectedNode != nil
		state := checkError(err, mayReschedule)
		klog.V(5).Infof("CreateVolume failed, supports topology = %v, node selected %v => may reschedule = %v => state = %v: %v",
			p.supportsTopology(),
			options.SelectedNode != nil,
			mayReschedule,
			state,
			err)
		return nil, state, err
	}

	// ...


	// 将 PV 创建得到的返回值写回到预处理结果中
	if len(volCaps) == 1 && volCaps[0].GetAccessMode().GetMode() == csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY && p.controllerPublishReadOnly {
		pvReadOnly = true
	}

	result.csiPVSource.VolumeHandle = p.volumeIdToHandle(rep.Volume.VolumeId)
	result.csiPVSource.VolumeAttributes = volumeAttributes
	result.csiPVSource.ReadOnly = pvReadOnly
	pv := &v1.PersistentVolume{
		ObjectMeta: metav1.ObjectMeta{
			Name: pvName,
		},
		Spec: v1.PersistentVolumeSpec{
			AccessModes:  options.PVC.Spec.AccessModes,
			MountOptions: options.StorageClass.MountOptions,
			Capacity: v1.ResourceList{
				v1.ResourceName(v1.ResourceStorage): bytesToQuantity(respCap),
			},
			// TODO wait for CSI VolumeSource API
			PersistentVolumeSource: v1.PersistentVolumeSource{
				CSI: result.csiPVSource,
			},
		},
	}

	// 修改 PV 资源的 annDeletionSecretRefName 和 namespace
	if result.provDeletionSecrets != nil {
		klog.V(5).Infof("createVolumeOperation: set annotation [%s/%s] on pv [%s].", annDeletionProvisionerSecretRefNamespace, annDeletionProvisionerSecretRefName, pv.Name)
		metav1.SetMetaDataAnnotation(&pv.ObjectMeta, annDeletionProvisionerSecretRefName, result.provDeletionSecrets.name)
		metav1.SetMetaDataAnnotation(&pv.ObjectMeta, annDeletionProvisionerSecretRefNamespace, result.provDeletionSecrets.namespace)
	} else {
		metav1.SetMetaDataAnnotation(&pv.ObjectMeta, annDeletionProvisionerSecretRefName, "")
		metav1.SetMetaDataAnnotation(&pv.ObjectMeta, annDeletionProvisionerSecretRefNamespace, "")
	}

	if options.StorageClass.ReclaimPolicy != nil {
		pv.Spec.PersistentVolumeReclaimPolicy = *options.StorageClass.ReclaimPolicy
	}

	// ...

	return pv, controller.ProvisioningFinished, nil
}

处理好了 PV 和 PVC 的绑定以后,provisioner 的工作到这里就结束了,后需要处理将由 kube-controller-manager 中的 AD controller 来接管。

nfsplugin 源码解析

1. 程序入口

从程序入口出发,实际的服务启动逻辑在driver.Run函数中。

我们可以看到服务启动时,三个CSI Service(CSI Identity , CSI Controller , CSI Node)全都被初始化了,一般我们开发插件时会把三者放在一个二进制程序中。

func (n *Driver) Run(testMode bool) {
	// ...

	n.ns = NewNodeServer(n, mount.New(""))
	s := NewNonBlockingGRPCServer()
	s.Start(n.endpoint,
		NewDefaultIdentityServer(n),
		// NFS plugin has not implemented ControllerServer
		// using default controllerserver.
		NewControllerServer(n),
		n.ns,
		testMode)
	s.Wait()
}

2. 服务启动

func (s *nonBlockingGRPCServer) serve(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer, testMode bool) {

	proto, addr, err := ParseEndpoint(endpoint)
	if err != nil {
		klog.Fatal(err.Error())
	}

	if proto == "unix" {
		addr = "/" + addr
		if err := os.Remove(addr); err != nil && !os.IsNotExist(err) {
			klog.Fatalf("Failed to remove %s, error: %s", addr, err.Error())
		}
	}

	listener, err := net.Listen(proto, addr)
	if err != nil {
		klog.Fatalf("Failed to listen: %v", err)
	}

	opts := []grpc.ServerOption{
		grpc.UnaryInterceptor(logGRPC),
	}
	server := grpc.NewServer(opts...)
	s.server = server

    // 注册 gRPC 服务
	if ids != nil {
		csi.RegisterIdentityServer(server, ids)
	}
	if cs != nil {
		csi.RegisterControllerServer(server, cs)
	}
	if ns != nil {
		csi.RegisterNodeServer(server, ns)
	}

	// ...

	err = server.Serve(listener)
	if err != nil {
		klog.Fatalf("Failed to serve grpc server: %v", err)
	}
}

3. 存储卷生命周期管理

经过AD Controller的调度后,Kubelet知道了当前节点有需要调用 nfscsiplugin 所注册的接口进行生命周期管理的存储卷存储,这个存储卷对应的生命周期是怎样的呢,根据官网上的定义,默认情况下,一个存储卷的生命周期如下:

   CreateVolume +------------+ DeleteVolume
 +------------->|  CREATED   +--------------+
 |              +---+----^---+              |
 |       Controller |    | Controller       v
+++         Publish |    | Unpublish       +++
|X|          Volume |    | Volume          | |
+-+             +---v----+---+             +-+
                | NODE_READY |
                +---+----^---+
               Node |    | Node
            Publish |    | Unpublish
             Volume |    | Volume
                +---v----+---+
                | PUBLISHED  |
                +------------+

接口下来看一下图表中的业务锁对应的代码

CreateVolume/DeleteVolume, ControllerPublishVolume/ControllerUnpublishVolume这两组接口声明在

ControllerServer 服务中,代码如下:

func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {

    // ...

    // 创建存储卷
	nfsVol, err := newNFSVolume(name, reqCapacity, parameters)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	var volCap *csi.VolumeCapability
	if len(req.GetVolumeCapabilities()) > 0 {
		volCap = req.GetVolumeCapabilities()[0]
	}
	// Mount nfs base share so we can create a subdirectory
	if err = cs.internalMount(ctx, nfsVol, parameters, volCap); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to mount nfs server: %v", err.Error())
	}
	defer func() {
		if err = cs.internalUnmount(ctx, nfsVol); err != nil {
			klog.Warningf("failed to unmount nfs server: %v", err.Error())
		}
	}()

    
	fileMode := os.FileMode(mountPermissions)
	internalVolumePath := getInternalVolumePath(cs.Driver.workingMountDir, nfsVol)
	if err = os.Mkdir(internalVolumePath, fileMode); err != nil && !os.IsExist(err) {
		return nil, status.Errorf(codes.Internal, "failed to make subdirectory: %v", err.Error())
	}
	if err = os.Chmod(internalVolumePath, fileMode); err != nil {
		klog.Warningf("failed to chmod subdirectory: %v", err.Error())
	}

	setKeyValueInMap(parameters, paramSubDir, nfsVol.subDir)
	return &csi.CreateVolumeResponse{
		Volume: &csi.Volume{
			VolumeId:      nfsVol.id,
			CapacityBytes: 0,
			VolumeContext: parameters,
		},
	}, nil
}

// 没实现
func (cs *ControllerServer) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
	return nil, status.Error(codes.Unimplemented, "")
}

NodePublishVolume/NodeUnpublishVolume 这组接口定义在 nodeserver 服务中,代码如下:

func (ns *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
	// 在节点上挂存储卷
	mountOptions := volCap.GetMount().GetMountFlags()
	if req.GetReadonly() {
		mountOptions = append(mountOptions, "ro")
	}

	var server, baseDir, subDir string
	subDirReplaceMap := map[string]string{}

	mountPermissions := ns.Driver.mountPermissions
	performChmodOp := (mountPermissions > 0)
	for k, v := range req.GetVolumeContext() {
		switch strings.ToLower(k) {
		case paramServer:
			server = v
		case paramShare:
			baseDir = v
		case paramSubDir:
			subDir = v
		case pvcNamespaceKey:
			subDirReplaceMap[pvcNamespaceMetadata] = v
		case pvcNameKey:
			subDirReplaceMap[pvcNameMetadata] = v
		case pvNameKey:
			subDirReplaceMap[pvNameMetadata] = v
		case mountOptionsField:
			if v != "" {
				mountOptions = append(mountOptions, v)
			}
		case mountPermissionsField:
			if v != "" {
				var err error
				var perm uint64
				if perm, err = strconv.ParseUint(v, 8, 32); err != nil {
					return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid mountPermissions %s", v))
				}
				if perm == 0 {
					performChmodOp = false
				} else {
					mountPermissions = perm
				}
			}
		}
	}

	if server == "" {
		return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("%v is a required parameter", paramServer))
	}
	if baseDir == "" {
		return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("%v is a required parameter", paramShare))
	}
	server = getServerFromSource(server)
	source := fmt.Sprintf("%s:%s", server, baseDir)
	if subDir != "" {
		// replace pv/pvc name namespace metadata in subDir
		subDir = replaceWithMap(subDir, subDirReplaceMap)

		source = strings.TrimRight(source, "/")
		source = fmt.Sprintf("%s/%s", source, subDir)
	}

	notMnt, err := ns.mounter.IsLikelyNotMountPoint(targetPath)
	if err != nil {
		if os.IsNotExist(err) {
			if err := os.MkdirAll(targetPath, os.FileMode(mountPermissions)); err != nil {
				return nil, status.Error(codes.Internal, err.Error())
			}
			notMnt = true
		} else {
			return nil, status.Error(codes.Internal, err.Error())
		}
	}
	if !notMnt {
		return &csi.NodePublishVolumeResponse{}, nil
	}

	klog.V(2).Infof("NodePublishVolume: volumeID(%v) source(%s) targetPath(%s) mountflags(%v)", volumeID, source, targetPath, mountOptions)
	err = ns.mounter.Mount(source, targetPath, "nfs", mountOptions)
	if err != nil {
		if os.IsPermission(err) {
			return nil, status.Error(codes.PermissionDenied, err.Error())
		}
		if strings.Contains(err.Error(), "invalid argument") {
			return nil, status.Error(codes.InvalidArgument, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	if performChmodOp {
		if err := chmodIfPermissionMismatch(targetPath, os.FileMode(mountPermissions)); err != nil {
			return nil, status.Error(codes.Internal, err.Error())
		}
	} else {
		klog.V(2).Infof("skip chmod on targetPath(%s) since mountPermissions is set as 0", targetPath)
	}
	klog.V(2).Infof("volume(%s) mount %s on %s succeeded", volumeID, source, targetPath)
	return &csi.NodePublishVolumeResponse{}, nil
}

更多技术分享浏览我的博客:

https://thierryzhou.github.io

原文地址:https://cloud.tencent.com/developer/article/2165884

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


文章浏览阅读942次。kube-controller-manager 和 kubelet 是异步工作的,这意味着延迟可能包括任何的网络延迟、apiserver 的延迟、etcd 延迟,一个节点上的负载引起的延迟等等。当 Kubernetes 中 Node 节点出现状态异常的情况下,节点上的 Pod 会被重新调度到其他节点上去,但是有的时候我们会发现节点 Down 掉以后,Pod 并不会立即触发重新调度,这实际上就是和 Kubelet 的状态更新机制密切相关的,Kubernetes 提供了一些参数配置来触发重新调度的时间。_node-monitor-period
文章浏览阅读3.8k次。上篇文章详细介绍了弹性云混部的落地历程,弹性云是滴滴内部提供给网约车等核心服务的容器平台,其基于 k8s 实现了对海量 node 的管理和 pod 的调度。本文重点介绍弹性云的调度能力,分为以下部分:调度链路图:介绍当前弹性云调度体系链路,对架构体系有一个初步的认知k8s 调度能力的运用:整体介绍弹性云现在用到的 k8s 调度能力和对其的增强k8s 版本的升级:介绍到从 k8s 1.12 到 1...._滴滴机房 腾讯
文章浏览阅读897次。对于cpu来说,这种分配方式并不会有太大问题,因为cpu可以灵活调度,numa调度时我们只计算绑定了numa cpu的pod是可以接受的,但是对于内存来说,numa node上申请了的内存无法做到随时迁移,这就会导致调度器视角numa node的mem资源足够,但是等到pod真正使用时,由于没有绑定numa node的pod申请的内存,导致numa node的mem资源不足,造成swap中断或者远端内存申请,这会对绑定mem的pod来带来性能损耗。忽略了没有绑定numa node的pod资源。_kubectl numa
文章浏览阅读796次,点赞17次,收藏15次。只要在Service定义中设置了ClusterIp:None,就定义了一个HeadLess Service, 它与普通的Service关键区别在于它没有ClusterIp地址,如果解析HeadLess Service的DNS域名,则会返回该Service对应的全部Pod的EndPoint列表,这就意味着客户端是直接与后端的pod建立了TCP/IP链接进行通信的。一个Label是一个键值对。注解:属于资源对象的元数据,可以被理解为一种特殊的标签,不过更多的是与程序挂钩,通常用于实现资源对象属性的自定义扩展。
文章浏览阅读763次。但是此时如果配置成 NONE, 租户创建成功了,但是无法创建资源文件,也就是无法上传文件,可能 dolphinscheduler 团队就想着将文件上传到 hdfs,暂不支持本地。需要将 resource.storage.type 置为 NONE, 因为我之前用的 1.3.6 版本的时候,即使资源文件存在本地文件也需要配置成 hdfs。_[error] 2023-10-24 18:10:43.762 +0800 org.apache.dolphinscheduler.api.servic
文章浏览阅读2.7k次,点赞2次,收藏13次。公司使用的是交老的k8s版本(1.16),由于老版本的K8s对于现在很多新特性不支持,所以需要升级到新版本。目前2023年7月11日最新版本的k8s是v1.27.3。通过参考官方文档进行k8s部署工作。其中涉及到操作系统配置、防火墙配置、私有镜像仓库等。_k8s最新版本
文章浏览阅读1.8w次,点赞14次,收藏27次。能节省你在kubeadm init 时遇到问题的排错时间⌚️。整合了网上大佬
文章浏览阅读1.1k次,点赞2次,收藏7次。具体操作步骤可以参考之前的教程,建议是先安装一台,然后克隆虚拟机,这样速度快。注意:在克隆时记得修改Mac地址、IP地址、UUID和主机名。(最后别忘了保存下快照~)_部署k8s集群
文章浏览阅读863次,点赞23次,收藏16次。当部署完 Kubernetes,便拥有了一个完整的集群。一组工作机器,称为节点, 会运行容器化应用程序。每个集群至少有一个工作节点。工作节点会 托管Pod ,而 Pod 就是作为应用负载的组件。控制平面管理集群中的工作节点和Pod。说人话版本:集群:cluster,多个几点被组织到一起共同为系统提供服务过程称之为集群。本质上是将承载同一个软件服务节点组织到一起,称之为该软件(服务)的集群,当然集群中的节点身份地位是不一样的。k8s集群也是如此,他也是多个节点组成。
文章浏览阅读943次。Rancher是一个开源的企业级多集群Kubernetes管理平台,实现了Kubernetes集群在混合云+本地数据中心的集中部署与管理,以确保集群的安全性,加速企业数字化转型。Rancher 1.0版本在2016年就已发布,时至今日,Rancher已经成长为企业在生产环境中运行容器和Kubernetes的首要选择。_rancher管理k8s
文章浏览阅读742次,点赞2次,收藏3次。本篇来讲解如何在centos下安装部署高可用k8s集群。_kubeadm ha keepalived + nginx
文章浏览阅读1.9k次,点赞21次,收藏25次。那么这个空间设置成内存的2倍大小。点击IPv4设置--手动--添加--设置ip--设置DNS服务器,最后点击--“保存”;首先选中--“本地标准磁盘”,存储配置--自定义分区,点击--“完成”;在--主机名--设置主机名:(例如k8s-master01),点击--点击+,设置--挂载点/boot--期望容量,点击--添加挂载点;点击--+--挂载点swap--期望容量,点击--“添加挂载点”;默认选择--亚洲--上海,并调整日期和时间,点击--“完成”;设备类型--确认--LVM,卷组--选择“修改”;_euler 服务器搭建
文章浏览阅读1k次。在1.25版本的k8s集群中部署gpu-manage时,虽然显示gpu节点上gpu-manage的pod实例都是running状态,但是给pod申领。既可以用源码的Makefile自动编译打包成新的镜像,但是源码的。说明gpu-manager和容器运行时接口通信失败了。编译后的镜像在1.25版本的k8s中可以正常使用。,但是在k8s1.23版本之后,接口路径已经改为。资源时,却始终找不到有资源的节点。,另外有一些依赖需要国际上的支持。可以看到这里用的运行时接口是。查看节点的详情时,返回的。_launch gpu manager 报错 can't create container runtime manager: context dead
文章浏览阅读1k次,点赞18次,收藏16次。SelfLink:API的资源对象之一,表示资源对象在集群当中自身的一个连结,self-Link是一个唯一的标识号,可以用于标识k8s集群当中的每个资源的对象。容器里使用的配置,在provisioner当中定义好环境变量,传给容器,storageclass的名称,NFS服务器的地址,NFS的目录。NFS的provisionner的客户端以pod的方式运行在集群当中,监听k8s集群当中PV的请求,然后动态的创建于NFS相关的PV。命名为 nfs-client-provisioner-clusterrole。
文章浏览阅读6.3k次,点赞2次,收藏20次。k8s证书过期解决方案之替换证书_k8s证书过期如何更换
文章浏览阅读1k次。KMS,Key Management Service,即密钥管理服务,在K8S集群中,以驱动和插件的形式启用对Secret,Configmap进行加密。以保护敏感数据
文章浏览阅读888次。exporter对于云服务的监控还是很不完美,毕竟每家都有自己的护城河。自动发现多实例这样的借助consul 阿波罗这样的会简单一些。aws可以借助cloudwatch这样的导入模板到grafana中。还是希望能将类似腾讯云云监控中的这些指标采集到prometheus中,但是这过程应该还很遥远grafana出图 prometheus查询语法这些东西有时间的好好研究一下。报警有必要进行分级别,收敛配置一下!_command: - "-redis.password-file=/redis_passwd.json
文章浏览阅读1k次。可以在此处(https://cloud.google.com/kubernetes-engine/docs/how-to/kube-dns)和此处(https://www.digitalocean.com/community/tutorials/an-introduction-to-the-kubernetes-dns-service)找到更多的详细信息。-or-ipvs/)和此处(https://arthurchiao.art/blog/cracking-k8s-node-proxy/)。_k8s默认命名空间
文章浏览阅读4.9k次,点赞11次,收藏32次。如果运行runc命令时提示:runc: error while loading shared libraries: libseccomp.so.2: cannot open shared object file: No such file or directory,则表明runc没有找到libseccomp,需要检查libseccomp是否安装,本次安装默认就可以查询到。所有主机均需要操作。所有主机均需要操作。所有主机均需要操作。所有主机均需要操作。所有主机均需要操作。所有主机均需要操作。_kubernetes 1.28
文章浏览阅读3.6w次,点赞118次,收藏144次。Canal 提供了网络功能,使得 Kubernetes 集群中的 Pod 可以相互通信,并与集群外部的服务进行通信。它通过网络插件的方式,为每个 Pod 分配唯一的 IP 地址,并管理网络流量的路由和转发。此外,Canal 还支持网络策略,用于定义 Pod 之间的通信规则和安全策略。Canal 基于 Calico 和 Flannel 项目,结合了二者的优点。它使用 Calico 的数据平面,提供高性能的网络转发和安全特性,同时使用 Flannel 的控制平面,实现 IP 地址管理和网络策略的配置。_k8s canal