0. 前言

  • 最近在学习张磊老师的 深入剖析Kubernetes 系列课程,最近学到了 Kubernetes 容器持久化存储部分
  • 现对这一部分的相关学习和体会做一下整理,内容参考 深入剖析Kubernetes 原文,仅作为自己后续回顾方便
  • 希望详细了解的同学可以移步至原文支持一下原作者
  • 参考原文:深入剖析Kubernetes

1. PV、PVC、StorageClass 关系梳理

1.1 相关概念

  • Volume:其实就是将一个宿主机上的目录,跟一个容器里的目录绑定挂载在了一起
  • 持久化 Volume:指的就是这个宿主机上的目录,具备“持久性”
    • 即:这个目录里面的内容,既不会因为容器的删除而被清理掉,也不会跟当前的宿主机绑定
    • 这样,当容器被重启或者在其他节点上重建出来之后,它仍然能够通过挂载这个 Volume,访问到这些内容
    • 大多数情况下,持久化 Volume 的实现,往往依赖于一个远程存储服务,比如:远程文件存储(比如,NFS、GlusterFS)、远程块存储(比如,公有云提供的远程磁盘)等等
    • 而 Kubernetes 需要做的工作,就是使用这些存储服务,来为容器准备一个持久化的宿主机目录,以供将来进行绑定挂载时使用
    • 而所谓“持久化”,指的是容器在这个目录里写入的文件,都会保存在远程存储中,从而使得这个目录具备了“持久性”
  • PV:表示是持久化存储数据卷对象。这个 API 对象定义了一个持久化存储在宿主机上的目录(如 NFS 的挂载目录)
    • 通常情况下,PV 对象由运维人员事先创建在 Kubernetes 集群里,比如:
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
nfs:
server: 10.244.1.4
path: "/"
  • PVC:表示 Pod 所希望使用的持久化存储的属性(如:Volume 存储的大小、可读写权限等等)

    • PVC 对象通常由开发人员创建,或者以 PVC 模板的方式成为 StatefulSet 的一部分,然后由 StatefulSet 控制器负责创建带编号的 PVC。比如:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs
spec:
accessModes:
- ReadWriteMany
storageClassName: manual
resources:
requests:
storage: 1Gi
  • StorageClass:其实就是创建 PV 的模板。具体地说,StorageClass 对象会定义如下两个部分内容:

    • 第一,PV 的属性。比如,存储类型、Volume 的大小等等
    • 第二,创建这种 PV 需要用到的存储插件。比如,Ceph 等等
    • Kubernetes 只会将 StorageClass 相同的 PVC 和 PV 绑定起来
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: block-service
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-ssd

1.2 绑定条件

  • PVC 要真正被容器使用起来,就必须先和某个符合条件的 PV 通过两个条件进行绑定:

    • 首先是 PV 和 PVC 的 spec 字段,比如 PV 的存储(storage)大小,必须满足 PVC 的要求
    • 其次是 PV 和 PVC 的 storageClassName 字段必须一样
  • 在成功地将 PVC 和 PV 进行绑定之后,Pod 就能够像使用 hostPath 等常规类型的 Volume 一样,在自己的 YAML 文件里声明使用这个 PVC 了,如:
    • Pod 可以在 volumes 字段里声明自己要使用的 PVC 名字
    • 接下来,等这个 Pod 创建之后,kubelet 就会把这个 PVC 所对应的 PV,挂载在这个 Pod 容器内的目录上
apiVersion: v1
kind: Pod
metadata:
labels:
role: web-frontend
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort:
volumeMounts:
- name: nfs
mountPath: "/usr/share/nginx/html"
volumes:
- name: nfs
persistentVolumeClaim:
claimName: nfs

1.3 绑定关系

  • 从面相对象的角度思考,PVC 可以理解为持久化存储的“接口”
  • 它提供了对某种持久化存储的描述,但不提供具体的实现
  • 而这个持久化存储的实现部分则由 PV 负责完成
  • 如果创建 Pod 的时候,系统里并没有合适的 PV 跟它定义的 PVC 绑定,Pod 的启动就会报错
  • 在 Kubernetes 中,实际上存在着一个专门处理持久化存储的控制器,叫作 Volume Controller
  • 这个 Volume Controller 维护着多个控制循环,其中有一个循环,扮演的就是撮合 PV 和 PVC 的“红娘”的角色:PersistentVolumeController
    • PersistentVolumeController 会不断地查看当前每一个 PVC,是不是已经处于 Bound(已绑定)状态
    • 如果不是,那它就会遍历所有可用的 PV,并尝试将其与这个未绑定的 PVC 进行绑定
    • 这样,Kubernetes 就可以保证用户提交的每一个 PVC,只要有合适的 PV 出现,它就能够很快进入绑定状态
    • 而所谓将一个 PV 与 PVC 进行绑定,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 spec.volumeName 字段上
    • 接下来 Kubernetes 只要获取到这个 PVC 对象,就一定能够找到它所绑定的 PV

1.4 持久化

  • 所谓容器的 Volume,其实就是将一个宿主机上的目录,跟一个容器里的目录绑定挂载在了一起
  • 而所谓的“持久化 Volume”,指的就是这个宿主机上的目录,具备“持久性”:
    • 这个目录里面的内容,既不会因为容器的删除而被清理掉,也不会跟当前的宿主机绑定
    • 这样,当容器被重启或者在其他节点上重建出来之后,它仍然能够通过挂载这个 Volume,访问到这些内容
  • 前面使用的 hostPath 和 emptyDir 类型的 Volume 并不具备这个特征:
    • 它们既有可能被 kubelet 清理掉,也不能被“迁移”到其他节点上
  • 所以,大多数情况下,持久化 Volume 的实现,往往依赖于一个远程存储服务,比如:远程文件存储(比如 NFS、GlusterFS)、远程块存储(比如公有云提供的远程磁盘)等等

1.4.1 两阶段处理

  • 而 Kubernetes 需要做的工作,就是使用这些存储服务,来为容器准备一个持久化的宿主机目录,以供将来进行绑定挂载时使用
  • 而所谓“持久化”,指的是容器在这个目录里写入的文件,都会保存在远程存储中,从而使得这个目录具备了“持久性”
  • 这个准备“持久化”宿主机目录的过程,称为“两阶段处理”:
    • 当一个 Pod 调度到一个节点上之后,kubelet 就要负责为这个 Pod 创建它的 Volume 目录
    • 默认情况下,kubelet 为 Volume 创建的目录是如下所示的一个宿主机上的路径:/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 类型 >/<Volume 名字 >

1.4.1.1 Attach

  • 如果 Volume 类型是远程块存储,那么 kubelet 就需要先调用相应的 API,将它所提供的 Persistent Disk 注册到 Pod 所在的宿主机上
  • 这一步为虚拟机注册远程磁盘的操作,对应的正是“两阶段处理”的第一阶段
  • 在 Kubernetes 中,我们把这个阶段称为 Attach
  • Kubernetes 提供的可用参数是 nodeName,即宿主机的名字

1.4.1.2 Mount

  • Attach 阶段完成后,为了能够使用这个远程磁盘,kubelet 还要进行第二个操作,即:格式化这个磁盘设备,然后将它挂载到宿主机指定的挂载点上
  • 这个挂载点,正是在前面反复提到的 Volume 的宿主机目录
  • 所以,这一步相当于执行:将磁盘设备格式化并挂载到 Volume 宿主机目录的操作,对应的正是“两阶段处理”的第二个阶段:Mount
  • Kubernetes 提供的可用参数是 dir,即 Volume 的宿主机目录
  • Mount 阶段完成后,这个 Volume 的宿主机目录就是一个“持久化”的目录了,容器在它里面写入的内容,会保存在远程磁盘中
  • 而如果你的 Volume 类型是远程文件存储(比如 NFS)的话,kubelet 的处理过程就会更简单一些
  • 因为在这种情况下,kubelet 可以跳过 Attach 阶段,因为一般来说,远程文件存储并没有一个“存储设备”需要注册在宿主机上
  • 所以,kubelet 会直接从 Mount 阶段开始准备宿主机上的 Volume 目录
  • 在这一步,kubelet 需要作为 client,将远端 NFS 服务器的目录(比如:“/”目录),挂载到 Volume 的宿主机目录上
  • 即相当于执行如下所示的命令:mount -t nfs <NFS 服务器地址 >:/ /var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 类型 >/<Volume 名字 >
  • 通过这个挂载操作,Volume 的宿主机目录就成为了一个远程 NFS 目录的挂载点
  • 后面你在这个目录里写入的所有文件,都会被保存在远程 NFS 服务器上。所以,我们也就完成了对这个 Volume 宿主机目录的“持久化”

1.4.2 后续工作

  • 经过两阶段处理,就得到了一个“持久化”的 Volume 宿主机目录
  • 接下来,kubelet 只要把这个 Volume 目录通过 CRI 里的 Mounts 参数,传递给 Docker,然后就可以为 Pod 里的容器挂载这个“持久化”的 Volume 了
  • 其实,这一步相当于执行了如下所示的命令:docker run -v /var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 类型 >/<Volume 名字 >:/< 容器内的目标目录 > 我的镜像 ...
  • 在 Kubernetes 中,上述关于 PV 的“两阶段处理”流程,是靠独立于 kubelet 主控制循环(Kubelet Sync Loop)之外的两个控制循环来实现的:
    • Attach(以及 Dettach)操作,是由 Volume Controller 负责维护的:AttachDetachController(不断地检查每一个 Pod 对应的 PV,和这个 Pod 所在宿主机之间挂载情况。从而决定,是否需要对这个 PV 进行操作)
    • 作为一个 Kubernetes 内置的控制器,Volume Controller 是 kube-controller-manager 的一部分
    • 所以,AttachDetachController 也一定是运行在 Master 节点上的
    • Mount(以及 Unmount)操作,必须发生在 Pod 对应的宿主机上,是 kubelet 组件的一部分,叫作 VolumeManagerReconciler,是一个独立于 kubelet 主循环的 Goroutine
  • 通过这样将 Volume 的处理同 kubelet 的主循环解耦,Kubernetes 就避免了这些耗时的远程挂载操作拖慢 kubelet 的主控制循环,进而导致 Pod 的创建效率大幅下降的问题

1.5 StorageClass

  • 一个大规模的 Kubernetes 集群里很可能有成千上万个 PVC,这就意味着运维人员必须得事先创建出成千上万个 PV
  • 更麻烦的是,随着新的 PVC 不断被提交,运维人员就不得不继续添加新的、能满足条件的 PV,否则新的 Pod 就会因为 PVC 绑定不到 PV 而失败
  • 在实际操作中,这几乎没办法靠人工做到
  • 所以,Kubernetes 提供了一套可以自动创建 PV 的机制,即:Dynamic Provisioning
  • 相比之下,前面人工管理 PV 的方式就叫作 Static Provisioning
  • Dynamic Provisioning 机制工作的核心,在于一个名叫 StorageClass 的 API 对象
  • 而 StorageClass 对象的作用,其实就是创建 PV 的模板
  • 具体地说,StorageClass 对象会定义如下两个部分内容:
    • PV 的属性。比如存储类型、Volume 的大小等等
    • 创建这种 PV 需要用到的存储插件。比如 Ceph 等等
  • 有了这样两个信息之后,Kubernetes 就能够根据用户提交的 PVC,找到一个对应的 StorageClass
  • 然后,Kubernetes 就会调用该 StorageClass 声明的存储插件,创建出需要的 PV。比如:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: block-service
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-ssd
  • 在这个 YAML 文件里,我们定义了一个名叫 block-service 的 StorageClass
  • provisioner 字段的值是:kubernetes.io/gce-pd,这正是 Kubernetes 内置的 GCE PD 存储插件的名字
  • parameters 字段,就是 PV 的参数。比如:上面例子里的 type=pd-ssd,指的是这个 PV 的类型是“SSD 格式的 GCE 远程磁盘”
  • 作为应用开发者,我们只需要在 PVC 里指定要使用的 StorageClass 名字即可,如:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claim1
spec:
accessModes:
- ReadWriteOnce
storageClassName: block-service
resources:
requests:
storage: 30Gi
  • 在 PVC 里添加了一个叫作 storageClassName 的字段,用于指定该 PVC 所要使用的 StorageClass 的名字是:block-service
  • 通过 kubectl create 创建上述 PVC 对象之后,Kubernetes 就会调用 Google Cloud 的 API,创建出一块 SSD 格式的 Persistent Disk。然后,再使用这个 Persistent Disk 的信息,自动创建出一个对应的 PV 对象
  • 这个自动创建出来的 PV 的 StorageClass 字段的值,也是 block-service
  • 这是因为,Kubernetes 只会将 StorageClass 相同的 PVC 和 PV 绑定起来
  • 有了 Dynamic Provisioning 机制,运维人员只需要在 Kubernetes 集群里创建出数量有限的 StorageClass 对象就可以了
  • 当开发人员提交了包含 StorageClass 字段的 PVC 之后,Kubernetes 就会根据这个 StorageClass 创建出对应的 PV

1.6 小结

  • PVC 描述的是 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等
  • PV 描述的,则是一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储服务器地址等
  • 而 StorageClass 的作用,则是充当 PV 的模板。并且,只有同属于一个 StorageClass 的 PV 和 PVC,才可以绑定在一起
  • 当然,StorageClass 的另一个重要作用,是指定 PV 的 Provisioner(存储插件)
  • 如果你的存储插件支持 Dynamic Provisioning 的话,Kubernetes 就可以自动为你创建 PV 了

2. CSI 插件体系的设计原理

  • 存储插件实际担任的角色,仅仅是 Volume 管理中的 Attach 阶段和 Mount 阶段的具体执行者
  • 而像 Dynamic Provisioning 这样的功能,不是存储插件的责任,而是 Kubernetes 本身存储管理功能的一部分,如图:

  • CSI 插件体系的设计思想,就是把这个 Provision 阶段,以及 Kubernetes 里的一部分存储管理功能,从主干代码里剥离出来,做成了几个单独的组件
  • 这些组件会通过 Watch API 监听 Kubernetes 里与存储相关的事件变化,比如 PVC 的创建,来执行具体的存储管理动作
  • 而这些管理动作,比如 Attach 阶段和 Mount 阶段的具体操作,实际上就是通过调用 CSI 插件来完成的。设计思路如图:

  • 这套存储插件体系多了三个独立的外部组件(External Components),即:Driver Registrar、External Provisioner 和 External Attacher
  • 对应的正是从 Kubernetes 项目里面剥离出来的那部分存储管理功能
  • 需要注意的是,External Components 虽然是外部组件,但依然由 Kubernetes 社区来开发和维护
  • 而右侧的部分,就是需要编写代码来实现的 CSI 插件
    • 一个 CSI 插件只有一个二进制文件,但它会以 gRPC 的方式对外提供三个服务(gRPC Service),分别叫作:CSI Identity、CSI Controller 和 CSI Node

2.1 External Components

2.1.1 Driver Registrar

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

2.1.2 External Provisioner

  • External Provisioner 组件,负责的正是 Provision 阶段
  • 在具体实现上,External Provisioner 监听了 APIServer 里的 PVC 对象
  • 当一个 PVC 被创建时,它就会调用 CSI Controller 的 CreateVolume 方法,为你创建对应 PV
  • 此外,如果你使用的存储是公有云提供的磁盘(或者块设备)的话,这一步就需要调用公有云(或者块设备服务)的 API 来创建这个 PV 所描述的磁盘(或者块设备)
  • 不过,由于 CSI 插件是独立于 Kubernetes 之外的,所以在 CSI 的 API 里不会直接使用 Kubernetes 定义的 PV 类型,而是会自己定义一个单独的 Volume 类型

2.1.3 External Attacher

  • 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 插件的时候,我们会将这三个 External Components 作为 sidecar 容器和 CSI 插件放置在同一个 Pod 中。由于 External Components 对 CSI 插件的调用非常频繁,所以这种 sidecar 的部署方式非常高效

2.2 CSI 插件服务

2.2.1 CSI IdentityCSI 插件的 CSI Identity 服务,负责对外暴露这个插件本身的信息

2.2.2 CSI Controller

  • CSI Controller 服务,定义的则是对 CSI Volume 的管理接口,比如:创建和删除 CSI Volume、对 CSI Volume 进行 Attach/Dettach,以及对 CSI Volume 进行 Snapshot 等
  • CSI Controller 服务里定义的这些操作有个共同特点,那就是它们都无需在宿主机上进行,而是属于 Kubernetes 里 Volume Controller 的逻辑,也就是属于 Master 节点的一部分
  • CSI Controller 服务的实际调用者,并不是 Kubernetes(即:通过 pkg/volume/csi 发起 CSI 请求),而是 External Provisioner 和 External Attacher
  • 这两个 External Components,分别通过监听 PVC 和 VolumeAttachement 对象,来跟 Kubernetes 进行协作

2.2.3 CSI Node

  • 而 CSI Volume 需要在宿主机上执行的操作,都定义在了 CSI Node 服务里面

2.3 小节

  • CSI 的设计思想,把插件的职责从两阶段处理,扩展成了 Provision、Attach 和 Mount 三个阶段
  • 其中,Privision 等价于“创建远程磁盘块”,Attach 等价于“注册磁盘到虚拟机”,Mount 等价于“将该磁盘格式化后,挂载在 Volume 的宿主机目录上”
  • 当 AttachDetachController 需要进行 Attach 操作时,它实际上会执行到 pkg/volume/csi 目录中,创建一个 VolumeAttachment 对象,从而触发 External Attacher 调用 CSI Controller 服务的 ControllerPublishVolume 方法
  • 当 VolumeManagerReconciler 需要进行 Mount 操作时,它实际上也会执行到 pkg/volume/csi 目录中,直接向 CSI Node 服务发起调用 NodePublishVolume 方法的请求。
  • 以上,就是 CSI 插件最基本的工作原理了

3. CSI 插件部署

3.1 常用原则

  • 第一,通过 DaemonSet 在每个节点上都启动一个 CSI 插件,来为 kubelet 提供 CSI Node 服务
  • 这是因为,CSI Node 服务需要被 kubelet 直接调用,所以它要和 kubelet“一对一”地部署起来
  • 此外,在上述 DaemonSet 的定义里面,除了 CSI 插件,我们还以 sidecar 的方式运行着 driver-registrar 这个外部组件
  • 它的作用,是向 kubelet 注册这个 CSI 插件
  • 这个注册过程使用的插件信息,则通过访问同一个 Pod 里的 CSI 插件容器的 Identity 服务获取到
  • 需要注意的是,由于 CSI 插件运行在一个容器里,那么 CSI Node 服务在 Mount 阶段执行的挂载操作,实际上是发生在这个容器的 Mount Namespace 里的
  • 可是,我们真正希望执行挂载操作的对象,都是宿主机 /var/lib/kubelet 目录下的文件和目录
  • 所以,在定义 DaemonSet Pod 的时候,我们需要把宿主机的 /var/lib/kubelet 以 Volume 的方式挂载进 CSI 插件容器的同名目录下
  • 然后设置这个 Volume 的 mountPropagation=Bidirectional,即开启双向挂载传播,从而将容器在这个目录下进行的挂载操作“传播”给宿主机,反之亦然
  • 第二,通过 StatefulSet 在任意一个节点上再启动一个 CSI 插件,为 External Components 提供 CSI Controller 服务
  • 所以,作为 CSI Controller 服务的调用者,External Provisioner 和 External Attacher 这两个外部组件,就需要以 sidecar 的方式和这次部署的 CSI 插件定义在同一个 Pod 里
  • 而像我们上面这样将 StatefulSet 的 replicas 设置为 1 的话,StatefulSet 就会确保 Pod 被删除重建的时候,永远有且只有一个 CSI 插件的 Pod 运行在集群中
  • 这对 CSI 插件的正确性来说,至关重要

3.2 小结

  • 当用户创建了一个 PVC 之后,部署的 StatefulSet 里的 External Provisioner 容器,就会监听到这个 PVC 的诞生
  • 然后调用同一个 Pod 里的 CSI 插件的 CSI Controller 服务的 CreateVolume 方法,为你创建出对应的 PV
  • 这时候,运行在 Kubernetes Master 节点上的 Volume Controller,就会通过 PersistentVolumeController 控制循环,发现这对新创建出来的 PV 和 PVC,并且看到它们声明的是同一个 StorageClass
  • 所以,它会把这一对 PV 和 PVC 绑定起来,使 PVC 进入 Bound 状态
  • 然后,用户创建了一个声明使用上述 PVC 的 Pod,并且这个 Pod 被调度器调度到了宿主机 A 上
  • 这时候,Volume Controller 的 AttachDetachController 控制循环就会发现,上述 PVC 对应的 Volume,需要被 Attach 到宿主机 A 上
  • 所以,AttachDetachController 会创建一个 VolumeAttachment 对象,这个对象携带了宿主机 A 和待处理的 Volume 的名字
  • 这样,StatefulSet 里的 External Attacher 容器,就会监听到这个 VolumeAttachment 对象的诞生
  • 于是,它就会使用这个对象里的宿主机和 Volume 名字,调用同一个 Pod 里的 CSI 插件的 CSI Controller 服务的 ControllerPublishVolume 方法,完成 Attach 阶段
  • 上述过程完成后,运行在宿主机 A 上的 kubelet,就会通过 VolumeManagerReconciler 控制循环,发现当前宿主机上有一个 Volume 对应的存储设备(比如磁盘)已经被 Attach 到了某个设备目录下
  • 于是 kubelet 就会调用同一台宿主机上的 CSI 插件的 CSI Node 服务的 NodeStageVolume 和 NodePublishVolume 方法,完成这个 Volume 的 Mount 阶段
  • 至此,一个完整的持久化 Volume 的创建和挂载流程就结束了

4. 总结

  • 通过学习,基本了解了 Kubernetes 持久化存储的基本原理和流程
  • 当前内容还是以张磊老师的原文为主,后续还需要继续思考和提炼
  • 本文所有涉及的知识点汇总至图 Kubernetes 容器持久化存储 中,刚兴趣的同学可以点击查看

5. 参考文献

最新文章

  1. jquery+ajax实现分页
  2. 360wifi使用方法|360wifi使用教程
  3. android Gui系统之SurfaceFlinger(2)---BufferQueue
  4. oracle数据匹配merge into
  5. C#(winform)为button添加背景图片
  6. ios oc 和 swfit 用dispatch_once 创建单例
  7. 制作圆角:《CSS3 Border-radius》
  8. delphi 10 seattle 安卓服务开发(二)
  9. step byt step之餐饮管理系统一
  10. HDU 1069 Monkey and Banana (DP)
  11. 使用 gradle 编译多版本 android 应用
  12. jasper2
  13. 浅谈API设计
  14. Layer弹层组件 二次扩展,项目中直接使用。
  15. java暴力递归回溯算法
  16. [多线程] 生产者消费者模型的BOOST实现
  17. 【C++】一篇文章,让你不再害怕指针
  18. u-boot移植(二)---修改前工作:代码流程分析1
  19. taro 微信小程序原生作用域获取
  20. 学JS的心路历程-JS支持面向对象?(二)

热门文章

  1. springboot 1.4 CXF配置
  2. Enum.GetUnderlyingType(obj.GetType())
  3. 在 React 项目中引入 GG-Editor 编辑可视化流程
  4. 技能篇丨FineCMS 5.0.10 多个漏洞详细分析
  5. c++的explicit理解
  6. 10.Redis 主从架构
  7. [Linux]F5负载均衡器
  8. 005-OpenStack-网络服务
  9. 虚拟机安装苹果macOS系统
  10. C语言之++和--