2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

一文读懂 K8s 持久化存储流程

  • 2020-04-21
  • 本文字数:7730 字

    阅读完需:约 25 分钟

一文读懂 K8s 持久化存储流程

1 K8s 持久化存储基础

在进行 K8s 存储流程讲解之前,先回顾一下 K8s 中持久化存储的基础概念。

1.名词解释

  • in-tree:代码逻辑在 K8s 官方仓库中;

  • out-of-tree:代码逻辑在 K8s 官方仓库之外,实现与 K8s 代码的解耦;

  • PV:PersistentVolume,集群级别的资源,由 集群管理员 or External Provisioner 创建。PV 的生命周期独立于使用 PV 的 Pod,PV 的 .Spec 中保存了存储设备的详细信息;

  • PVC:PersistentVolumeClaim,命名空间(namespace)级别的资源,由 用户 or StatefulSet 控制器(根据 VolumeClaimTemplate) 创建。PVC 类似于 Pod,Pod 消耗 Node 资源,PVC 消耗 PV 资源。Pod 可以请求特定级别的资源(CPU 和内存),而 PVC 可以请求特定存储卷的大小及访问模式(Access Mode);

  • StorageClass:StorageClass 是集群级别的资源,由集群管理员创建。SC 为管理员提供了一种动态提供存储卷的“类”模板,SC 中的 .Spec 中详细定义了存储卷 PV 的不同服务质量级别、备份策略等等;

  • CSI:Container Storage Interface,目的是定义行业标准的“容器存储接口”,使存储供应商(SP)基于 CSI 标准开发的插件可以在不同容器编排(CO)系统中工作,CO 系统包括 Kubernetes、Mesos、Swarm 等。

2.组件介绍

  • PV Controller:负责 PV/PVC 绑定及周期管理,根据需求进行数据卷的 Provision/Delete 操作;

  • AD Controller:负责数据卷的 Attach/Detach 操作,将设备挂接到目标节点;

  • Kubelet:Kubelet 是在每个 Node 节点上运行的主要 “节点代理”,功能是 Pod 生命周期管理、容器健康检查、容器监控等;

  • Volume Manager:Kubelet 中的组件,负责管理数据卷的 Mount/Umount 操作(也负责数据卷的 Attach/Detach 操作,需配置 kubelet 相关参数开启该特性)、卷设备的格式化等等;

  • Volume Plugins:存储插件,由存储供应商开发,目的在于扩展各种存储类型的卷管理能力,实现第三方存储的各种操作能力,即是上面蓝色操作的实现。Volume Plugins 有 in-tree 和 out-of-tree 两种;

  • External Provioner:External Provioner 是一种 sidecar 容器,作用是调用 Volume Plugins 中的 CreateVolume 和 DeleteVolume 函数来执行 Provision/Delete 操作。因为 K8s 的 PV 控制器无法直接调用 Volume Plugins 的相关函数,故由 External Provioner 通过 gRPC 来调用;

  • External Attacher:External Attacher 是一种 sidecar 容器,作用是调用 Volume Plugins 中的 ControllerPublishVolume 和 ControllerUnpublishVolume 函数来执行 Attach/Detach 操作。因为 K8s 的 AD 控制器无法直接调用 Volume Plugins 的相关函数,故由 External Attacher 通过 gRPC 来调用。

3.持久卷使用

Kubernetes 为了使应用程序及其开发人员能够正常请求存储资源,避免处理存储设施细节,引入了 PV 和 PVC。创建 PV 有两种方式:


  1. 一种是集群管理员通过手动方式静态创建应用所需要的 PV;

  2. 另一种是用户手动创建 PVC 并由 Provisioner 组件动态创建对应的 PV。


下面我们以 NFS 共享存储为例来看二者区别。


静态创建存储卷


静态创建存储卷流程如下图所示:



第一步:集群管理员创建 NFS PV,NFS 属于 K8s 原生支持的 in-tree 存储类型。yaml 文件如下:


apiVersion: v1kind: PersistentVolumemetadata:  name: nfs-pvspec:  capacity:    storage: 10Gi  accessModes:    - ReadWriteOnce  persistentVolumeReclaimPolicy: Retain  nfs:    server: 192.168.4.1    path: /nfs_storag
复制代码


第二步:用户创建 PVC,yaml 文件如下:


apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: nfs-pvcspec:  accessModes:  - ReadWriteOnce  resources:    requests:      storage: 10Gi
复制代码


通过 kubectl get pv 命令可看到 PV 和 PVC 已绑定:


[root@huizhi ~]# kubectl get pvcNAME      STATUS   VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGEnfs-pvc   Bound    nfs-pv-no-affinity   10Gi       RWO                           4s
复制代码


第三步:用户创建应用,并使用第二步创建的 PVC。


apiVersion: v1kind: Podmetadata:  name: test-nfsspec:  containers:  - image: nginx:alpine    imagePullPolicy: IfNotPresent    name: nginx    volumeMounts:    - mountPath: /data      name: nfs-volume  volumes:  - name: nfs-volume    persistentVolumeClaim:      claimName: nfs-pvc
复制代码


此时 NFS 的远端存储就挂载了到 Pod 中 nginx 容器的 /data 目录下。


动态创建存储卷


动态创建存储卷,要求集群中部署有 nfs-client-provisioner 以及对应的 storageclass。


动态创建存储卷相比静态创建存储卷,少了集群管理员的干预,流程如下图所示:



集群管理员只需要保证环境中有 NFS 相关的 storageclass 即可:


kind: StorageClassapiVersion: storage.k8s.io/v1metadata:  name: nfs-scprovisioner: example.com/nfsmountOptions:  - vers=4.1
复制代码


第一步:用户创建 PVC,此处 PVC 的 storageClassName 指定为上面 NFS 的 storageclass 名称:


kind: PersistentVolumeClaimapiVersion: v1metadata:  name: nfs  annotations:    volume.beta.kubernetes.io/storage-class: "example-nfs"spec:  accessModes:    - ReadWriteMany  resources:    requests:      storage: 10Mi  storageClassName: nfs-sc
复制代码


第二步:集群中的 nfs-client-provisioner 会动态创建相应 PV。此时可看到环境中 PV 已创建,并与 PVC 已绑定。


[root@huizhi ~]# kubectl get pvNAME                                       CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM         REASON    AGEpvc-dce84888-7a9d-11e6-b1ee-5254001e0c1b   10Mi        RWX           Delete          Bound       default/nfs             4s
复制代码


第三步:用户创建应用,并使用第二步创建的 PVC,同静态创建存储卷的第三步。

2 K8s 持久化存储流程

1.流程概览

此处借鉴 @郡宝 在云原生存储课程中的流程图



流程如下:


  1. 用户创建了一个包含 PVC 的 Pod,该 PVC 要求使用动态存储卷;

  2. Scheduler 根据 Pod 配置、节点状态、PV 配置等信息,把 Pod 调度到一个合适的 Worker 节点上;

  3. PV 控制器 watch 到该 Pod 使用的 PVC 处于 Pending 状态,于是调用 Volume Plugin(in-tree)创建存储卷,并创建 PV 对象(out-of-tree 由 External Provisioner 来处理);

  4. AD 控制器发现 Pod 和 PVC 处于待挂接状态,于是调用 Volume Plugin 挂接存储设备到目标 Worker 节点上

  5. 在 Worker 节点上,Kubelet 中的 Volume Manager 等待存储设备挂接完成,并通过 Volume Plugin 将设备挂载到全局目录:/var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV name](以 iscsi 为例);

  6. Kubelet 通过 Docker 启动 Pod 的 Containers,用 bind mount 方式将已挂载到本地全局目录的卷映射到容器中。


更详细的流程如下:


2.流程详解

不同 K8s 版本,持久化存储流程略有区别。本文基于 Kubernetes 1.14.8 版本。


从上述流程图中可看到,存储卷从创建到提供应用使用共分为三个阶段:Provision/Delete、Attach/Detach、Mount/Unmount。


provisioning volumes



PV 控制器中有两个 Worker


  • ClaimWorker:处理 PVC 的 add / update / delete 相关事件以及 PVC 的状态迁移;

  • VolumeWorker:负责 PV 的状态迁移。


PV 状态迁移(UpdatePVStatus)


  • PV 初始状态为 Available,当 PV 与 PVC 绑定后,状态变为 Bound;

  • 与 PV 绑定的 PVC 删除后,状态变为 Released;

  • 当 PV 回收策略为 Recycled 或手动删除 PV 的 .Spec.ClaimRef 后,PV 状态变为 Available;

  • 当 PV 回收策略未知或 Recycle 失败或存储卷删除失败,PV 状态变为 Failed;

  • 手动删除 PV 的 .Spec.ClaimRef,PV 状态变为 Available。


PVC 状态迁移(UpdatePVCStatus)


  • 当集群中不存在满足 PVC 条件的 PV 时,PVC 状态为 Pending。在 PV 与 PVC 绑定后,PVC 状态由 Pending 变为 Bound;

  • 与 PVC 绑定的 PV 在环境中被删除,PVC 状态变为 Lost;

  • 再次与一个同名 PV 绑定后,PVC 状态变为 Bound。


Provisioning 流程如下(此处模拟用户创建一个新 PVC):


静态存储卷流程(FindBestMatch):PV 控制器首先在环境中筛选一个状态为 Available 的 PV 与新 PVC 匹配。


  • DelayBinding:PV 控制器判断该 PVC 是否需要延迟绑定:1. 查看 PVC 的 annotation 中是否包含 volume.kubernetes.io/selected-node,若存在则表示该 PVC 已经被调度器指定好了节点(属于 ProvisionVolume),故不需要延迟绑定;2. 若 PVC 的 annotation 中不存在 volume.kubernetes.io/selected-node,同时没有 StorageClass,默认表示不需要延迟绑定;若有 StorageClass,查看其 VolumeBindingMode 字段,若为 WaitForFirstConsumer 则需要延迟绑定,若为 Immediate 则不需要延迟绑定;

  • FindBestMatchPVForClaim:PV 控制器尝试找一个满足 PVC 要求的环境中现有的 PV。PV 控制器会将所有的 PV 进行一次筛选,并会从满足条件的 PV 中选择一个最佳匹配的 PV。筛选规则:1. VolumeMode 是否匹配;2. PV 是否已绑定到 PVC 上;3. PV 的 .Status.Phase 是否为 Available;4. LabelSelector 检查,PV 与 PVC 的 label 要保持一致;5. PV 与 PVC 的 StorageClass 是否一致;6. 每次迭代更新最小满足 PVC requested size 的 PV,并作为最终结果返回;

  • Bind:PV 控制器对选中的 PV、PVC 进行绑定:1. 更新 PV 的 .Spec.ClaimRef 信息为当前 PVC;2. 更新 PV 的 .Status.Phase 为 Bound;3. 新增 PV 的 annotation :pv.kubernetes.io/bound-by-controller: “yes”;4. 更新 PVC 的 .Spec.VolumeName 为 PV 名称;5. 更新 PVC 的 .Status.Phase 为 Bound;6. 新增 PVC 的 annotation:pv.kubernetes.io/bound-by-controller: “yes” 和 pv.kubernetes.io/bind-completed: “yes”;


动态存储卷流程(ProvisionVolume):若环境中没有合适的 PV,则进入动态 Provisioning 场景:


  • Before Provisioning:1. PV 控制器首先判断 PVC 使用的 StorageClass 是 in-tree 还是 out-of-tree:通过查看 StorageClass 的 Provisioner 字段是否包含 “kubernetes.io/” 前缀来判断;2. PV 控制器更新 PVC 的 annotation:claim.Annotations[“volume.beta.kubernetes.io/storage-provisioner”] = storageClass.Provisioner;

  • in-tree Provisioning(internal provisioning):1. in-tree 的 Provioner 会实现 ProvisionableVolumePlugin 接口的 NewProvisioner 方法,用来返回一个新的 Provisioner;2. PV 控制器调用 Provisioner 的 Provision 函数,该函数会返回一个 PV 对象;3. PV 控制器创建上一步返回的 PV 对象,将其与 PVC 绑定,Spec.ClaimRef 设置为 PVC,.Status.Phase 设置为 Bound,.Spec.StorageClassName 设置为与 PVC 相同的 StorageClassName;同时新增 annotation:“pv.kubernetes.io/bound-by-controller”=“yes” 和 “pv.kubernetes.io/provisioned-by”=plugin.GetPluginName();

  • out-of-tree Provisioning(external provisioning):1. External Provisioner 检查 PVC 中的 claim.Spec.VolumeName 是否为空,不为空则直接跳过该 PVC;2. External Provisioner 检查 PVC 中的 claim.Annotations[“volume.beta.kubernetes.io/storage-provisioner”] 是否等于自己的 Provisioner Name(External Provisioner 在启动时会传入 --provisioner 参数来确定自己的 Provisioner Name);3. 若 PVC 的 VolumeMode=Block,检查 External Provisioner 是否支持块设备;4. External Provisioner 调用 Provision 函数:通过 gRPC 调用 CSI 存储插件的 CreateVolume 接口;5. External Provisioner 创建一个 PV 来代表该 volume,同时将该 PV 与之前的 PVC 做绑定。


deleting volumes


Deleting 流程为 Provisioning 的反操作


用户删除 PVC,删除 PV 控制器改变 PV.Status.Phase 为 Released。


当 PV.Status.Phase == Released 时,PV 控制器首先检查 Spec.PersistentVolumeReclaimPolicy 的值,为 Retain 时直接跳过,为 Delete 时:


  • in-tree Deleting:1. in-tree 的 Provioner 会实现 DeletableVolumePlugin 接口的 NewDeleter 方法,用来返回一个新的 Deleter;2. 控制器调用 Deleter 的 Delete 函数,删除对应 volume;3. 在 volume 删除后,PV 控制器会删除 PV 对象;

  • out-of-tree Deleting:1. External Provisioner 调用 Delete 函数,通过 gRPC 调用 CSI 插件的 DeleteVolume 接口;2. 在 volume 删除后,External Provisioner 会删除 PV 对象


Attaching Volumes


Kubelet 组件和 AD 控制器都可以做 attach/detach 操作,若 Kubelet 的启动参数中指定了 --enable-controller-attach-detach,则由 Kubelet 来做;否则默认由 AD 控制起来做。下面以 AD 控制器为例来讲解 attach/detach 操作。



AD 控制器中有两个核心变量


  • DesiredStateOfWorld(DSW):集群中预期的数据卷挂接状态,包含了 nodes->volumes->pods 的信息;

  • ActualStateOfWorld(ASW):集群中实际的数据卷挂接状态,包含了 volumes->nodes 的信息。


Attaching 流程如下


AD 控制器根据集群中的资源信息,初始化 DSW 和 ASW。


AD 控制器内部有三个组件周期性更新 DSW 和 ASW:


  • Reconciler。通过一个 GoRoutine 周期性运行,确保 volume 挂接 / 摘除完毕。此期间不断更新 ASW:


in-tree attaching:1. in-tree 的 Attacher 会实现 AttachableVolumePlugin 接口的 NewAttacher 方法,用来返回一个新的 Attacher;2. AD 控制器调用 Attacher 的 Attach 函数进行设备挂接;3. 更新 ASW。


out-of-tree attaching:1. 调用 in-tree 的 CSIAttacher 创建一个 VolumeAttachement(VA)对象,该对象包含了 Attacher 信息、节点名称、待挂接 PV 信息;2. External Attacher 会 watch 集群中的 VolumeAttachement 资源,发现有需要挂接的数据卷时,调用 Attach 函数,通过 gRPC 调用 CSI 插件的 ControllerPublishVolume 接口。


  • DesiredStateOfWorldPopulator。通过一个 GoRoutine 周期性运行,主要功能是更新 DSW:


findAndRemoveDeletedPods - 遍历所有 DSW 中的 Pods,若其已从集群中删除则从 DSW 中移除;


findAndAddActivePods - 遍历所有 PodLister 中的 Pods,若 DSW 中不存在该 Pod 则添加至 DSW。


  • PVC Worker。watch PVC 的 add/update 事件,处理 PVC 相关的 Pod,并实时更新 DSW。


Detaching Volumes


Detaching 流程如下:


  1. 当 Pod 被删除,AD 控制器会 watch 到该事件。首先 AD 控制器检查 Pod 所在的 Node 资源是否包含"volumes.kubernetes.io/keep-terminated-pod-volumes"标签,若包含则不做操作;不包含则从 DSW 中去掉该 volume;

  2. AD 控制器通过 Reconciler 使 ActualStateOfWorld 状态向 DesiredStateOfWorld 状态靠近,当发现 ASW 中有 DSW 中不存在的 volume 时,会做 Detach 操作:


in-tree detaching:1. AD 控制器会实现 AttachableVolumePlugin 接口的 NewDetacher 方法,用来返回一个新的 Detacher;2. 控制器调用 Detacher 的 Detach 函数,detach 对应 volume;3. AD 控制器更新 ASW。


out-of-tree detaching:1. AD 控制器调用 in-tree 的 CSIAttacher 删除相关 VolumeAttachement 对象;2. External Attacher 会 watch 集群中的 VolumeAttachement(VA)资源,发现有需要摘除的数据卷时,调用 Detach 函数,通过 gRPC 调用 CSI 插件的 ControllerUnpublishVolume 接口;3. AD 控制器更新 ASW。


Mounting/Unmounting Volumes



Volume Manager 中同样也有两个核心变量:


  • DesiredStateOfWorld(DSW):集群中预期的数据卷挂载状态,包含了 volumes->pods 的信息;

  • ActualStateOfWorld(ASW):集群中实际的数据卷挂载状态,包含了 volumes->pods 的信息。


Mounting/UnMounting 流程如下:


全局目录(global mount path)存在的目的:块设备在 Linux 上只能挂载一次,而在 K8s 场景中,一个 PV 可能被挂载到同一个 Node 上的多个 Pod 实例中。若块设备格式化后先挂载至 Node 上的一个临时全局目录,然后再使用 Linux 中的 bind mount 技术把这个全局目录挂载进 Pod 中对应的目录上,就可以满足要求。上述流程图中,全局目录即 /var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV name]


VolumeManager 根据集群中的资源信息,初始化 DSW 和 ASW。


VolumeManager 内部有两个组件周期性更新 DSW 和 ASW:


  • DesiredStateOfWorldPopulator:通过一个 GoRoutine 周期性运行,主要功能是更新 DSW;

  • Reconciler:通过一个 GoRoutine 周期性运行,确保 volume 挂载 / 卸载完毕。此期间不断更新 ASW:


unmountVolumes:确保 Pod 删除后 volumes 被 unmount。遍历一遍所有 ASW 中的 Pod,若其不在 DSW 中(表示 Pod 被删除),此处以 VolumeMode=FileSystem 举例,则执行如下操作:


  1. Remove all bind-mounts:调用 Unmounter 的 TearDown 接口(若为 out-of-tree 则调用 CSI 插件的 NodeUnpublishVolume 接口);

  2. Unmount volume:调用 DeviceUnmounter 的 UnmountDevice 函数(若为 out-of-tree 则调用 CSI 插件的 NodeUnstageVolume 接口);

  3. 更新 ASW。


mountAttachVolumes:确保 Pod 要使用的 volumes 挂载成功。遍历一遍所有 DSW 中的 Pod,若其不在 ASW 中(表示目录待挂载映射到 Pod 上),此处以 VolumeMode=FileSystem 举例,执行如下操作:


  1. 等待 volume 挂接到节点上(由 External Attacher or Kubelet 本身挂接);

  2. 挂载 volume 到全局目录:调用 DeviceMounter 的 MountDevice 函数(若为 out-of-tree 则调用 CSI 插件的 NodeStageVolume 接口);

  3. 更新 ASW:该 volume 已挂载到全局目录;

  4. bind-mount volume 到 Pod 上:调用 Mounter 的 SetUp 接口(若为 out-of-tree 则调用 CSI 插件的 NodePublishVolume 接口);

  5. 更新 ASW。


unmountDetachDevices:确保需要 unmount 的 volumes 被 unmount。遍历一遍所有 ASW 中的 UnmountedVolumes,若其不在 DSW 中(表示 volume 已无需使用),执行如下操作:


  1. Unmount volume:调用 DeviceUnmounter 的 UnmountDevice 函数(若为 out-of-tree 则调用 CSI 插件的 NodeUnstageVolume 接口);

  2. 更新 ASW。

3 总结

本文先对 K8s 持久化存储基础概念及使用方法进行了介绍,并对 K8s 内部存储流程进行了深度解析。在 K8s 上,使用任何一种存储都离不开上面的流程(有些场景不会用到 attach/detach),环境上的存储问题也一定是其中某个环节出现了故障。


参考链接:


https://github.com/kubernetes/kubernetes


https://edu.aliyun.com/lesson_1651_13092#_13092


https://edu.aliyun.com/lesson_1651_13085#_13085


https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/volume-provisioning.md


https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md


原文链接


https://mp.weixin.qq.com/s?__biz=MzIzNjUxMzk2NQ==&mid=2247493879&idx=2&sn=8f9dc34c8c46f70bf80121b2f155b00c&chksm=e8d41735dfa39e23ece26f4101f38000a6fda196ae3290c793163ca05428d063068d31efce9b&token=1951287211&lang=zh_CN#rd


2020-04-21 10:063322

评论

发布
暂无评论
发现更多内容

阿里云低延时直播 RTS 能力升级 让直播推流效果更佳

阿里云CloudImagine

阿里云 音视频 直播技术 视频云

华为首次发布HarmonyOS职业认证,助力开发者实现职业进阶

科技汇

WorkPlus高端制造业数字化解决方案—中集集团

BeeWorks

企业 即时通讯 协同办公 开源软件

科创人|决策易赵祝维:从满足应用需求到服务业务目标,从SaaS服务商到业务合作伙伴

科创人

名列GitHub必看榜!腾讯架构师纯手敲Spring Boot高级进阶笔记

Java架构追梦

Java 架构 腾讯 面试 springboot

PHA挖矿|PHA云算力挖矿系统开发案例

Geek_23f0c3

区块链 云算力挖矿系统开发详解 PHA矿机挖矿

优评海洋APP系统开发模板

BTAU比特金盾系统软件开发内容

Pravega Flink connector 的过去、现在和未来

Apache Flink

flink

我可以减肥失败,但我的 Docker 镜像一定要瘦身成功!

尔达Erda

Docker 开源 云原生 镜像 瘦身

商业智能BI,会成为下一个风口吗?

瓴羊企业智能服务

阿里云 数据中台 数据分析 BI 商业智能

速拼商城APP系统开发介绍

干掉 Postman?测试接口直接生成API文档,这个工具贼好用

程序员小富

Java 编程 程序员 开发工具 大学生

Redis on AEP 实践

BUG侦探

redis 傲腾AEP

网络攻防学习笔记 Day76

穿过生命散发芬芳

网络攻防 7月日更

《计算机网络 PDF》搞起!

苹果看辽宁体育

大前端 后端 计算机网络

喜讯:恒拓高科荣获“2020年度华侨城集团优秀数字化服务商”称号

BeeWorks

开源 解决方案 即时通讯 开源软件

WorkPlus综合企业数字化解决方案—华侨城

BeeWorks

企业 移动开 开源软件

云原生数据库的幕后英雄—浅谈分布式数据库的计算和存储分离

从Encoder-Decoder模型入手,探索语境偏移解决之道

华为云开发者联盟

神经网络 ASR 语境偏移 CLAS ASR模型

深度解析HashMap底层实现架构

华为云开发者联盟

Java hashmap 底层 底层架构 Map接口

Ubuntu Server 20.04搭建Redis集群

玏佾

redis redis集群 搭建 redis cluster

HarmonyOS Connect伙伴峰会重庆站举办 生态建设持续完善

科技汇

Gemini Mining双子矿业系统APP开发模板

膜拜!阿里内部都在强力进阶学习springboot实战派文档

Java spring 程序员 架构 面试

如何实现70%丢包下音视频的高可用-信令篇

ZEGO即构

音视频 弱网 QUIC协议

视觉生产技术入门篇

若尘

视觉 7月日更

面试官:order by 是怎样排序的?怎么优化?

一个优秀的废人

Java MySQL 数据库 后端 order by

差点跳起来了!阿里首推22w字Java面试复盘宝典成功助我入职美团

白亦杨

Java 编程 程序员

北鲲云超算与传统的超算中心有什么不同?

北鲲云

国内首发!阿里高工手码分布式系统速成笔记!

Java 编程 程序员

一文读懂 K8s 持久化存储流程_容器_孙志恒(惠志)_InfoQ精选文章