9 月 13 日,2025 Inclusion・外滩大会「开源嘉年华」正在限量报名中! 了解详情
写点什么

一文读懂 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:063205

评论

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

《css大法》之使用伪元素实现超实用的图标库(附源码)

徐小夕

CSS css3 大前端 CSS小技巧

原创 | 使用JPA实现DDD持久化-O/R映射元数据:映射注解分组

编程道与术

Java hibernate 编程 mybatis jpa

可见性是什么?(通俗易懂)

叫练

volatile JMM 多线程 synchronized

区块链司法存证应用落地应用解决方案

t13823115967

区块链落地开发

Mybatis 中xml和注解映射,这次终于搞明白了

田维常

mybatis

甲方日常 65

句子

工作 随笔杂谈 日常

一文教你看懂缓存穿透、击穿、雪崩、降级等异常

鄙人薛某

Java 缓存 后端 缓存击穿 缓存雪崩

话题讨论 | 立一个近期的flag,你会想到什么?

xcbeyond

话题讨论

operator-sdk & kubebuilder

QiLab

k8s operator-sdk kubebuilder crd

网咯请求中的 connectTimeout 和 soTimeout

不在调上

《算法导论》.pdf

田维常

原创 | 使用JPA实现DDD持久化-O/R映射元数据:类级映射-实体和值对象

编程道与术

Java hibernate 编程 mybatis jpa

使用JPA实现DDD持久化-O/R映射元数据-特殊属性映射:ID、Version和Transient

编程道与术

Java hibernate 编程 mybatis jpa

没有它你的DevOps是玩不转的,你信不?

华为云开发者联盟

容器 DevOps 微服务

人脸识别是如何实现的

anyRTC开发者

ios 音视频 WebRTC 人脸识别 安卓

情报合成研判系统开发,智慧警务解决方案

t13823115967

智慧公安

安装MySQL后,需要调整的10个性能配置项

Simon

MySQL percona server

P8架构挑战:七大专题1425页考点,你能成功吗?

小Q

Java 学习 程序员 架构 面试

北京奥森小景

小马哥

摄影 美景 奥森 28天写作

Java架构速成笔记:七大专题,1425页考点,挑战P8岗

Java架构追梦

Java 学习 面试 java架构

关于Kubernetes和Docker关系的八个问题

杨明越

话题讨论 | 选择做一个程序员,你后悔过吗?

xcbeyond

话题讨论

业务中台建设-数据

孝鹏

架构 中台 数据 赋能

原创 | 使用JPA实现DDD持久化-领域模型:对象的世界

编程道与术

Java hibernate 编程 mybatis jpa

全球熵ETV系统APP软件开发

系统开发

不满意社区的轮子,我们自创了一套 React Hooks 风格的数据加载方案

LeanCloud

API React Hooks

时序数据库DolphinDB与Druid的对比测试

DolphinDB

数据分析 时序数据库 Druid 数据库选择 DolphinDB

开发实践丨用小熊派STM32开发板模拟自动售货机

华为云开发者联盟

物联网 小熊派 开发板

英特尔携手德晟达、游密,发布云会议终端解决方案,打造视听新体验

E科讯

如何预防勒索攻击事件?这份安全自查指南请查收

京东科技开发者

数据安全 数据加密 系统安全

话题讨论 | 30 张图解高并发服务模型你必须这些

程序员柠檬

话题讨论

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