K8s系列之:共享存储原理和PV、PVC、StorageClass详解

一、共享存储机制概述

K8s对于有状态的容器应用或者对数据需要持久化的应用,不仅需要将容器内的目录挂载到宿主机的目录或者emptyDir临时存储卷,而且需要更加可靠的存储来保存应用产生的重要数据,以便容器应用在重建之后,仍然可以使用之前的数据。K8s引入PersistentVolume和PersistentVolumeClaim两个资源对象来实现对存储的管理子系统。

  • PersistentVolume(PV)是对底层网络共享存储的抽象,将共享存储定义为一种资源,比如节点也是一种容器应用可以消费的资源。PV由管理员进行创建和配置,与共享存储的具体实现直接相关,例如ClusterFS等提供的共享存储,通过插件式的机制完成与共享存储的对接,以供应用访问和使用。
  • PersistentVolumeClaim(PVC)则是用户对于存储资源的一个申请。就像Pod消费Node资源一样,PVC会消费PV资源。PVC可以申请特定的存储空间和访问模式。
  • 使用PVC申请到一定的存储空间仍然不足以满足应用对于存储设备的各种需求。通常应用程序都会对存储设备的特性和性能有不同的要求,包括读写速度、并发性能、数据冗余等更高的要求。K8s引入了新的资源对象StorageClass,用于标记存储资源的特性和性能。StorageClass和动态资源供应的机制得到了完善,实现了存储卷的按需创建,在共享存储的自动化管理进程中实现了重要的一步。
  • 通过StorageClass的定义,管理员可以将存储资源定义为某种类别(Class),正好存储设备对于自身的配置描述(Profile),例如快速存储、慢速存储、有数据冗余、无数据冗余等。用户根据StorageClass的描述就能够直观得知各种存储资源的特性,就可以根据应用对存储资源的需求去申请存储资源了。

二、PV详解

PV作为存储资源,主要包括存储能力、访问模式、存储类型、回收策略、后端存储类型等关键信息的设置。

下面的例子生命的PV具有如下属性:

  • 5Gi存储空间
  • 访问模式为ReadWriteOnce
  • 存储类型为slow(要求系统中存在名为slow的StorageClass)
  • 回收策略为Recyle
  • 后端存储类型为nfs(设置了NFS Server的IP地址和路径)
apiVersion: v1
kind: PersistentVolume
metadata: 
  name: pv1
spec:
  capacity: 5Gi
  	storage: 5Gi
  accessModes:
 - ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
storageClassName: slow
nfs:
  path: /tmp
  server: 172.17.0.2

K8s支持的PV类型如下:

  • gcePersistentDisk:GCE公有云提供的PersistentDisk
  • AWSElasticBlockStore:AWS公有云提供的ElasticBlockStore
  • NFS:网络文件系统
  • GlusterFS
  • HostPath:宿主机目录,仅用于单机测试

1.PV的关键配置参数

1)存储能力(Capacity)

  • 描述存储设备具备的能力。

2)访问模式(Access Modes)

  • 对PV进行访问模式的设置,用于描述用户应用对存储资源的访问的权限。访问模式如下:

  • ReadWriteOnce(RWO):读写权限,并且只能被单个Node挂载。

  • ReadOnlyMany(ROX):只读权限,允许被多个Node挂载。

  • ReadWriteMany(RWX):读写权限,允许被多个Node挂载。

某些PV可能支持多种访问模式,但PV在挂载时只能使用一种访问模式,多种访问模式不能同时生效。

不同的存储提供者支持的访问模式:

Volume PluginReadWriteOnceReadOnlyManyReadWriteMany
ClusterFS
HostPath
NFS

3)存储类别(Class)

  • PV可以设定其存储的类别(Class),通过storageClassName参数指定一个StorageClass资源对象的名称。
  • 具有特定类别的PV只能与请求了该类别的PVC进行绑定。
  • 未设定类别的PV则只能与不请求任何类别的PVC进行绑定。

4)回收策略(Reclaim Policy)
目前支持如下三种回收策略:

  • 保留(Retain):保留数据,需要手工处理
  • 回收空间(Recyle):简单清除文件的操作(例如rm -rf /thevolume/*命令)
  • 删除(Delete):与PV相连的后端存储完成volume的删除操作(如AWS EBS等设备的内部volume清理)

目前NFS和HostPath两种类型的存储支持Recyle策略。
AWS EBS、GCE PD、Azure Disk和Cinder volumes支持Delete策略。

2.PV生命周期的各个阶段

某个PV在生命周期中,可能处于以下4个阶段之一。

  • Available:可用状态,还未与某个PVC绑定。
  • Bound:已与某个PVC绑定。
  • Released:绑定的PVC已经删除,资源已释放,但没有被集群回收。
  • Failed:自动资源回收失败。

3.PV的挂载参数(Mount Options)

在将PV挂载到一个Node时,根据后端存储的特点,可能需要设置额外的挂载参数,可以通过在PV的定义中,设置一个名为"volume.beta.kubernetes.io/mount-options"的annotation来实现。

下面的例子对一个类型为gcePersistentDisk的PV设置了挂载参数"discard":

apiVersion: "v1"
kind: "PersistentVolume"
metadata:
  name: gce-disk-1
  annotations:
    volume.beta.kubernetes.io/mount-options: "discard"
spec:
  capacity:
  	storage: "10Gi"
  accessModes:
    - "ReadWriteOnce"
  gcePersistentDisk:
  	fsType: "ext4"
  	pdName: "gce-disk-1"

三、PVC详解

PVC作为用户对存储资源的需求申请,主要包括存储空间请求、访问模式、PV选择条件和存储类别等信息的设置。

下面的例子声明的PVC具有如下属性:

  • 申请8Gi存储空间
  • 访问模式为ReadWriteOnce
  • PV选择条件为包含标签"release=stable",并且包含条件为"environment In [dev]"的标签
  • 存储类别为slow
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 8Gi
  storageClassName: slow
  selector:
    matchLabels:
      release: "stable"
    matchExpressions:
      - {key: environment, operator: In,values: [dev]}

PVC的关键配置参数说明如下:

  • 资源请求(Resource):描述对存储资源的请求,目前仅支持request.storage的设置,即存储空间大小
  • 访问模式(Access Modes):PVC也可以设置访问模式,用于描述用户应用对存储资源的访问权限。可以设置的三种访问模式与PV的设置相同。
  • PV选择条件(Selector):通过Label Selector的设置,可使PVC对于系统中已存在的各种PV进行筛选。系统将根据标签选择出合适的PV与该PVC进行绑定。选择条件可以使用matchLables和matchExpressions进行设置,如果两个字段都设置了,则Selector的逻辑将是两组条件同时满足才能完成匹配。
  • 存储类别(Class):PVC在定义时可以设定需要的后端存储的类别(通过storageClassName字段指定),以降低对后端存储特性的详细信息的依赖。只有设置了该Class的PV才能被系统选出,并与该PVC进行绑定。

注意:

  • PVC和PV都受限于namespace,PVC在选择PV时受到namespace的限制,只有相同namespace中的PV才可能与PVC绑定。
  • Pod在引用PVC时同样受namespace的限制,只有相同namespace中的PVC才能挂载到Pod内。
  • 当Selector和Class都进行了设置时,系统将选择两个条件同时满足的PV与之匹配。
  • 在启用动态供应模式的情况下,一旦用户删除了PVC,与之绑定的PV将根据其默认的回收策略Delete也会被删除。如果需要保留PV(用户数据),则在动态绑定成功后,用户需要将系统自动生成PV的回收策略从Delete改成Retain。

四、PV和PVC的生命周期

PV可以看作可用的存储资源,PVC则是对存储资源的需求。
在这里插入图片描述

1.资源供应(Provisioning)

K8s支持两种资源供应模式:

  • 静态模式(Static):集群管理员手工创建许多PV,在定义PV时需要将后端存储的特性进行设置。
  • 动态模式(Dynamic):集群管理员无需手工创建PV,而是通过StorageClass的设置对后端存储进行描述,标记为某种类型(Class)。此时要求PVC对存储的类型进行声明,系统将自动完成PV的创建及与PVC的绑定。PVC可以声明Class为“”,说明该PVC禁止使用动态模式。

资源供应的结果就是创建好的PV。

2.资源绑定(Binding)

  • 在用户定义好PVC之后,系统将根据PVC对资源的请求(存储空间和访问模式)在已存在的PV中选择一个满足PVC要求的PV,一旦找到,就将该PV与用户定义的PVC进行绑定,然后用户的应用就可以使用这个PVC了。
  • 系统中没有满足PVC要求的PV,PVC则会无限期处于Pending状态,直到等到系统创建一个符合要求的PV。
  • PV一旦绑定到某个PVC上,就被这个PVC独占,不能再与其他PVC进行绑定了。在这种情况下,当PVC申请的存储空间比PV的少时,整个PV的空间都能够为PVC所用,可能会造成资源的浪费。
  • 如果资源供应使用的是动态模式,则系统在为PVC找到合适的StorageClass后,将自动创建一个PV并完成与PVC的绑定。

3.资源使用(Using)

  • Pod使用volume的定义,将PVC挂载到容器内的某个路径进行使用。
  • Volume的类型为persistentVolumeClaim,在容器应用挂载了一个PVC后,就能被持续独占使用。
  • 多个Pod可以挂载同一个PVC,应用程序需要考虑多个实例共同访问一块存储空间的问题。

4.资源释放(Releasing)

  • 当用户对存储资源使用完毕后,用户可以删除PVC,与该PVC绑定的PV将会被标记为已释放,但还不能立刻与其他PVC进行绑定。
  • 通过之前PVC写入的数据可能还留在存储设备上,只有在清除之后该PV才能再次使用。

5.资源回收(Reclaiming)

  • 对于PV,管理员可以设定回收策略(Reclaim Policy),用于设置与之绑定的PVC释放资源之后,对于遗留数据如何处理。
  • 只有PV的存储空间完成回收,才能供新的PVC绑定和使用。

在静态资源供应模式下,通过PV和PVC完成绑定,并供Pod使用的存储管理机制。

在动态资源供应模式下,通过StorageClass和PVC完成资源动态绑定(系统自动生成PV),并供Pod使用的存储管理机制。

五、StorageClass详解

StorageClass作为对存储资源的抽象定义,对用户设置的PVC申请屏蔽后端存储的细节。

  • 一方面减轻用户对于存储资源细节的关注
  • 另一方面也减轻了管理员手工管理PV的工作,由系统自动完成PV的创建和绑定,实现了动态的资源供应。

使用基于StorageClass的动态资源供应模式将逐步成为云平台的标准存储配置模式。

StorageClass的定义主要包括名称、后端存储的提供者(Provisioner)和后端存储的相关参数配置。StorageClass一旦被创建出来,将无法修改。如需更改,则只能删除原StorageClass的定义重建。

下面的例子定义了一个名为"standard"的StorageClass,提供者为aws-ebs,其参数设置了一个type=gp2。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2

1.StorageClass的关键配置参数

1)提供者(Provisioner)
描述存储资源的提供者,也可以看作后端存储驱动。目前K8s支持的Provisioner都以kubernetes.io/为开头。

2)参数(Parameters)
后端存储资源提供者的参数设置,不同的Provisioner包括不同的参数设置。

以GlusterFS存储卷为例子:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: slow
provisioner: kubernetes.io/glusterfs
parameters:
  resturl: "http://127.0.0.1:8081"
  clusterid: "630839984929283f99393"
  restauthenabled: "true"
  restuser: "admin"
  secretNamespace: "default"
  secretName: "heketi-secret"
  gidMin: "40000"
  gidMax: "50000"
  volumetype: "replicate:3"

2.设置默认的Default StorageClass

要在系统中设置一个默认的StorageClass,首先需要启用名为DefaultStorageClass的admission controller,即在kube-apiserver的命令行参数–admission-control中增加:

--admission-control=...,DefaultStorageClass

然后在StorageClass的定义中设置一个annotation:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gold
  annotations:
    storageclass.beta.kubernetes.io/is-default-class="true"
  provisioner: kubernetes.io/gce-pd
  parameters:
    type: pd-ssd

通过kubectl create命令创建成功后,查看StorageClass列表,可以看到名为gold的StorageClass被标记为"default":

kubectl get sc
NAME                      TYPE
gold (default)   kubernetes.io/gce-pd

六、动态存储管理实战:GlusterFS

本节以GlusterFS为例,从定义StorageClass、创建GlusterFS和Heketi服务、用户申请PVC到创建Pod使用存储资源,对StorageClass和动态资源分配进行详细说明,进一步剖析K8s的存储机制。

1.准备工作

为了能够使用GlusterFS,首先在计划用于GlusterFS的各Node上安装GlusterFS客户端:

yum install glusterfs glusterfs-fuse

GlusterFS管理服务容器需要以特权模式运行,在kube-apiserver的启动参数中增加:

--allow-privileged=true

给要部署GlusterFS管理服务的节点打上"storagenode=glusterfs"的标签,是为了将GlusterFS容器定向部署到安装了GlusterFS的Node:

kubectl label node k8s-node-1 storagenode=glusterfs
kubectl label node k8s-node-2 storagenode=glusterfs
kubectl label node k8s-node-3 storagenode=glusterfs

2.创建GlusterFS管理服务容器集群

GlusterFS管理服务容器以Daemonset的方式进行部署,确保每台Node上都运行一个GlusterFS管理服务。

glusterfs-daemonset.yaml内容如下:

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: glusterfs
  labels:
    glusterfs: daemonset
  annotations:
    description: GlusterFS DaemonSet
    tags: glusterfs
spec:
  template:
    metadata:
      name: glusterfs
      labels:
        glusterfs-node: pod
    spec:
      nodeSelector:
        storagenode: glusterfs
      hostNetwork: true
      containers:
      - image: gluster/gluster-centos:latest
        name: glusterfs
        volumeMounts:
        - name: glusterfs-heketi
          mountPath: "/var/lib/heketi"
        - name: glusterfs-run
          mountPath: "/run/lvm"
        - name: glusterfs-etc
          mountPath: "/etc/glusterfs"
        - name: glusterfs-logs
          mountPath: "/var/lib/glusterfs"
        - name: glusterfs-config
          mountPath: "/var/lib/glusterd"
        - name: glusterfs-dev
          mountPath: "/dev"
        - name: glusterfs-misc
          mountPath: "/var/lib/misc/glusterfsd"
        - name: glusterfs-cgroup
          mountPath: "/sys/fs/cgroup"
          readOnly: true
        - name: glusterfs-ssl
          mountPath: "/etc/ssl"
          readOnly: true
        securityContext:
          capabilities: {}
          privileged: true
        readinessProbe:
          timeoutSeconds: 3
          initialDelaySeconds: 60
          exec:
            command:
            - "/bin/bash"
            - "-c"
            - systemctl status glusterd.service
          livenessProbe:
            timeoutSeconds: 3
            initialDelaySeconds: 60
            exec:
              command:
              - "/bin/bash"
              - "-c"
              - systemctl status glusterd.service
        volumes:
        - name: glusterfs-heketi
          hostPath:
            path: "/var/lib/heketi"
        - name: glusterfs-run
        - name: glusterfs-lvm
          hostPath:
            path: "/run/lvm"
         - name: glusterfs-etc
           hostPath:
             path: "/etc/glusterfs"
         - name: glusterfs-logs
           hostPath:
             path: "/var/log/glusterfs"
         - name: glusterfs-config
           hostPath:
             path: "/var/lib/glusterd"
         - name: glusterfs-dev
           hostPath:
             path: "/dev"
         - name: glusterfs-misc
           hostPath:
             path: "/var/lib/misc/glusterfsd"
         - name: glusterfs-cgroup
           hostPath:
             path: "/sys/fs/cgroup"
         - name: glusterfs-ssl
           hostPath:
             path: "/etc/ssl"
kubectl create -f glusterfs-daemonset.yaml
daemonset "glusterfs" created
kubectl get po

3.创建Heketi服务

Heketi是一个提供RESTful API管理GlusterFS卷的框架,并能够在OpenStack、K8s等云平台上实现动态存储资源供应,支持GlusterFS多集群管理,便于管理GlusterFS进行操作。

在部署Heketi服务之前,需要为它创建ServiceAccount对象:
heketi-service-account.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: heketi-service-account
kubectl create -f heketi-service-account.yaml

部署Heketi服务:
heketi-deployment-svc.yaml

---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: deploy-heketi
  labels:
    glusterfs: heketi-deployment
    deploy-heketi: heketi-deployment
  annotations:
    description: Defines how to deploy Heketi
spec:
  replicas: 1
  template:
    metadata:
      name: deploy-heketi
      labels:
        name: deploy-heketi
        glusterfs: heketi-pod
    spec:
      serviceAccountName: heketi-service-account
      containers:
      - image: heketi/heketi:dev
        name: deploy-heketi
        env:
        - name: HEKEATI_EXECUTOR
          value: kubernetes
        - name: HEKETI_FSTAB
          value: "/var/lib/heketi/fstab"
        - name: HEKETI_SNAPSHOT_LIMIT
          value: '14'
        - name: HEKETI_KUBE_GLUSTER_DAEMONSET
          value: "y"
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: db
          mountPath: "/var/lib/heketi"
        readinessProbe:
          timeoutSeconds: 3
          initialDelaySeconds: 3
          httpGet:
            path: "/hello"
            port: 8080
        livenessProbe:
          timeoutSeconds: 3
          initialDelaySeconds: 30
          httpGet:
            path: "/hello"
            port: 8080
      volumes:
      - name: db
        hostPath:
          path: "/heketi-data"

---
kind: Service
apiVersion: v1
metadata:
  name: deploy-heketi
  labels:
    glusterfs: heketi-service
    deploy-heketi: support
  annotations:
    description: Exposes Heketi Service
spec:
  selector:
    name: deploy-heketi
  ports:
  - name: deploy-heketi
    port: 8080
    targetPort: 8080

注意:Heketi的db数据需要持久化保存,建议使用hostPath或其他共享存储进行保存。

kubectl create -f heketi-deployment-svc.yaml

4.为Heketi设置GlusterFS集群

在Heketi能够管理GlusterFS集群之前,首先要为其设置GlusterFS集群的信息。可以用一个topology.json配置文件来完成各个GlusterFS节点和设备的定义。Heketi要求一个GlusterFS集群至少有3个节点。

进入Heketi容器,使用命令行工具heketi-cli完成GlusterFS集群的创建:

kubectl-cli topology load --json=topology.json

经过这个操作,Heketi完成了GlusterFS集群的创建,同时在GlusterFS集群的各个节点的/dev/sdb盘上成功创建了PV和VG。

查看Heketi的topology信息,可以看到Node和Device的详细信息,包括磁盘空间的大小和剩余空间。Volumes和Bricks还未创建。

heketi-cli topology info

5.定义StorageClass

准备工作已经就绪,集群管理员可以在K8s集群定义一个StorageClass了。

storageclass-gluster-heketi.yaml配置文件内容如下:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gluster-heketi
provisioner: kubernetes.io/glusterfs
parameters:
  resturl: "http://172.17.2.2:8080"
  restauthenabled: "false"

Provisioner参数必须设置为:kubernetes.io/glusterfs
resturl的地址需要设置为API Server所在主机可以访问到的Heketi服务的某个地址,可以使用服务ClusterIP+端口号、容器IP地址+端口号或将服务映射到物理机,使用物理机IP+NodePort。

创建这个StorageClass资源对象:

kubectl create -f storageclass-gluster-heketi.yaml

6.定义PVC

用户可以申请一个PVC了。例如一个用户申请一个1Gi空间的共享存储资源。StorageClass使用gluster-heketi,未定义任何Selector,说明使用动态资源供应的模式。
pvc-gluster-heketi.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-gluster-heketi
spec:
  storageClassName: gluster-heketi
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
kubectl create -f pvc-gluster-heketi.yaml

PVC定义一旦生成,系统便将触发Heketi进行相应的操作,主要为在GlusterFS集群上创建brick,再创建并启动一个volume。

查看PVC的状态,可见其已经为Bound(已绑定):

kubectl get pvc

查看PV,可见系统自动创建PV

kubectl get pv

查看PV的详细信息,可以看到容量、引用的StorageClass等信息都已正确设置,状态也为Bound(已绑定),回收策略则为默认的Delete。同时Gluster的Endpoint和Path也由Heketi自动完成了设置。

至此一个可供Pod使用的PVC就创建成功了,接下来Pod就能通过volume宕设置将这个PVC挂载到容器内部进行使用。

7.Pod使用PVC的存储资源

在Pod中使用PVC定义的存储资源非常容易,只需设置一个volume,类型为persistentVolumeClaim,即可轻松引用一个PVC。

下例中使用一个busybox容器验证对PVC的使用,注意Pod需要与PVC属于同一个namespace:

pod-use-pvc.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-use-pvc
spec:
  containers:
  - name: pod-use-pvc
    image: busybox
    command:
    - sleep
    - "3600"
    volumeMounts:
    - name: gluster-volume
      mountPath: "/pv-data"
      readOnly: false
  volumes:
  - name: gluster-volume
    persistentVolumeClaim:
      claimName: pvc-gluster-heketi
kubectl create -f pod-use-pvc.yaml

进入容器pod-use-pvc,在/pv-data目录下创建一些文件:

kubectl exec -ti pod-use-pvc -- /bin/sh
cd /pv-data
touch a
echo "hello" > b

可以验证文件a和b在GlusterFS集群中正确生成。

在使用动态存储供应模式的情况下,相对于静态模式的优势至少包括以下两点:

  • 管理员无须预先创建大量的PV作为存储资源
  • 用户申请PVC时无法保证容量与预置PV的容量完全匹配,建议用户优先使用StorageClass的动态存储供应模式进行存储管理。
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
PV(PersistentVolume)和PVC(PersistentVolumeClaim)是Kubernetes中用于实现持久化存储的重要概念。 PV是集群中的一块存储,可以是NFS、iSCSI、本地存储等,由管理员进行配置或使用存储类进行动态配置。PV定义了存储的容量、访问模式、持久化存储的类型等属性。PV的生命周期是独立于Pod的,即使Pod被删除,PV仍然存在,可以被其他Pod继续使用。 PVC是一个持久化存储卷,用于访问各种类型的持久化存储,如本地存储、网络存储、云存储等。PVC的使用使应用程序更加灵活和可移植,同时也提高了存储资源的利用率。PVCPV是一一对应的关系,即一个PVC只能绑定一个PV,而一个PV也只能被一个PVC绑定。 下面是一个演示k8s持久化存储PVPVC的案例: 1. 创建PV: ```yaml apiVersion: v1 kind: PersistentVolume metadata: name: my-pv spec: capacity: storage: 1Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: my-storage-class nfs: path: /data server: nfs-server-ip ``` 2. 创建PVC: ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: my-storage-class ``` 3. 创建Pod,并挂载PVC卷: ```yaml apiVersion: v1 kind: Pod metadata: name: my-pod spec: containers: - name: my-container image: nginx volumeMounts: - name: my-volume mountPath: /data volumes: - name: my-volume persistentVolumeClaim: claimName: my-pvc ``` 4. 删除PVC的正确步骤: ```shell kubectl delete pod my-pod kubectl delete pvc my-pvc kubectl delete pv my-pv ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

最笨的羊羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值