crio/podman镜像存储架构

前面讲解了关于docker镜像存储之overlayfs,对docker的镜像、容器存储有了基本的了解。但不同的公司所用容器运行时的侧重点不同,一部分在使用containerd,一部分在使用cri-o/podman(podman和cri-o),cri-o/podman镜像存储使用的是同一个架构。我决定对这两种运行时的镜像存储做一个分析。本期先讲解cri-o/podman,当然他们的存储驱动依然使用overlayfs

什么是podman

正如podman官网所说,Podman是一个无守护进程的容器引擎,用于在Linux系统上运行和管理OCI兼容的容器和镜像。它可以作为root和非特权用户运行容器。最好的部分是命令行与Docker兼容,也可以使用Dockerfiles构建镜像。
源码链接安装链接

镜像存储位置

当你运行 podman images 时,podman 会查找系统中已经拉取的镜像。podman 和 docker 的镜像存储在不同的位置。因此, podman images 不会列出之前 docker 拉取的镜像。podman 的镜像存储在 root 用户的 /var/lib/containers 中,非特权用户的 ~/.local/share/containers 中。存储位置与 docker ( /var/lib/docker ) 不同,因为它的存储结构是基于 OCI 标准的。docker 和 podman 的镜像是兼容的,但它们在系统中存储的方式不同。
Podman使用containers/storage作为存储后端,使用containers/image来操作镜像。
containers/storage的核心对象是store,它封装了存储结构,并存储着系统中的所有镜像和容器层(读写层copy-on-write)以及其他相关的元数据。image对象layer对象分别封装了与j镜像和容器层相关的所有信息。

storage(podman 存储结构)

接下来在该部分讨论podman overlay驱动的存储结构。简单看一下/var/lib/containers/storage的示例结构。

为了易于理解,拉取的centos:latest镜像,该镜像只有一层,且环境未启动任何容器

root@test:/var/lib/containers/storage# tree -L 4
.
├── libpod
│   ├── bolt_state.db
│   └── defaultCNINetExists
├── mounts
├── overlay
│   ├── 2653d992f4ef2bfd27f94db643815aa567240c37732cae1405ad1c1309ee9859
│   │   ├── diff
│   │   │   ├── bin -> usr/bin
│   │   │   ├── dev
│   │   │   ├── etc
│   │   │   ├── home
│   │   │   ├── lib -> usr/lib
│   │   │   ├── lib64 -> usr/lib64
│   │   │   ├── lost+found
│   │   │   ├── media
│   │   │   ├── mnt
│   │   │   ├── opt
│   │   │   ├── proc
│   │   │   ├── root
│   │   │   ├── run
│   │   │   ├── sbin -> usr/sbin
│   │   │   ├── srv
│   │   │   ├── sys
│   │   │   ├── tmp
│   │   │   ├── usr
│   │   │   └── var
│   │   ├── empty
│   │   ├── link
│   │   ├── merged
│   │   └── work
│   └── l
│       └── 57MIHXCXZI4TMYKQRHKJAAIU2Q -> ../2653d992f4ef2bfd27f94db643815aa567240c37732cae1405ad1c1309ee9859/diff
├── overlay-containers
│   ├── containers.json
│   └── containers.lock
├── overlay-images
│   ├── 300e315adb2f96afe5f0b2780b87f28ae95231fe3bdd1e16b9ba606307728f55
│   │   ├── =bWFuaWZlc3Qtc2hhMjU2OjgzMDFkMTAwMDIwZmZhZWRjOTNmNTdkOGM4YmIwZThlODgwMDY4NjJiY2I0OGViYzVkZmJlMWQxY2I4MzA2MGM=
│   │   ├── =bWFuaWZlc3Qtc2hhMjU2OmRiYmFjZWNjNDliMDg4NDU4NzgxYzE2ZjM3NzVmMmEyZWM3NTIxMDc5MDM0YTdiYTQ5OWM4YjBiYjdmODY4NzU=
│   │   ├── =c2hhMjU2OjMwMGUzMTVhZGIyZjk2YWZlNWYwYjI3ODBiODdmMjhhZTk1MjMxZmUzYmRkMWUxNmI5YmE2MDYzMDc3MjhmNTU=
│   │   ├── =c2lnbmF0dXJlLWRiYmFjZWNjNDliMDg4NDU4NzgxYzE2ZjM3NzVmMmEyZWM3NTIxMDc5MDM0YTdiYTQ5OWM4YjBiYjdmODY4NzU=
│   │   └── manifest
│   ├── images.json
│   └── images.lock
├── overlay-layers
│   ├── layers.json
│   └── layers.lock
├── storage.lock
├── tmp
└── userns.lock

讲讨论上面目录树上比较重要的几个目录。
**overlay:**镜像解压后的内容就存储在该目录中。对于每一层,其分解的rootfs存储在overlay/digest/diff中,其中的digest是该层未压缩内容的sha256摘要。对于每一层,其随机id存储在overlay/digest/link文件内。overlay/l 文件夹中包含指向 overlay//diff 目录中已解压的各个层的符号链接。符号链接的名称与存储在相应的 overlay//link 文件中的层的随机 ID 相同

overlay/l 文件夹中的每个符号链接代表一个已解压的层,并且其名称与该层的随机 ID 相匹配。这些符号链接提供了对相应层的访问路径,而不需要访问原始的 overlay//diff 目录。

root@test:/var/lib/containers/storage# cd overlay
root@test:/var/lib/containers/storage/overlay# ls
2653d992f4ef2bfd27f94db643815aa567240c37732cae1405ad1c1309ee9859  l                                                                                                               
root@test:/var/lib/containers/storage/overlay# ls ./l/
57MIHXCXZI4TMYKQRHKJAAIU2Q                                                                                                                                                        
root@test:/var/lib/containers/storage/overlay# ls -alh ./l/57MIHXCXZI4TMYKQRHKJAAIU2Q
lrwxrwxrwx 1 root root 72 Apr  9 09:05 ./l/57MIHXCXZI4TMYKQRHKJAAIU2Q -> ../2653d992f4ef2bfd27f94db643815aa567240c37732cae1405ad1c1309ee9859/diff                                 
root@test:/var/lib/containers/storage/overlay# cat 2653d992f4ef2bfd27f94db643815aa567240c37732cae1405ad1c1309ee9859/link

overlay/digest/lower文件存存储了父层的随机 ID(以及父层的父层的随机 ID,包含该层的所有父层的随机id),并以冒号(:)分隔。

root@test:/var/lib/containers/storage/overlay/8fbe574b9abff4af73b9c2d1967fb4a5beec7f5b1ad67a266d2991979ab3c6b4# cat lower 
l/2MKTBNDAEOVGZUHR5EVY7VO7ZM:l/JLCPV7U4QQOCICTU2OJWVX3ATP:l/XCK37LZAGQXCOSRJTK7WVMEBDQ:l/X4XV2WKDKGFEIKBUTA74AS2DCX:l/5MMB4TCVPUW2KPCTOPF4OS4KTT:l/OYNEDX46JG7AV4HGKT3NRVWEHY:l/O3
ZCLMX45E4DF35JVCEY7J5OYE:l/3IQ75FBG46YFQE5FWHQKWDDUGN:l/BEVMCWEIHRRJBJNM3ASJHVU4AC:l/YGU5H4ENWXYQ3N2X6UIS73RUSI:l/T3NDBMPOFOHZO7NH3A2YNRWKA5:l/LPCOWEU2TKSCJSVIXDTUCVXE23:l/RSVIMV
LI4AOOURAXKIOD3ADRZQ:l/G3HOZNNQA4JKGN22V2BDZO6K3R:l/6KKRJKXL6ZP7RV7SHNNXPZFTLF:l/57V4YRQLTWACKIQLR7HUINZJ2G:l/B6B4VTZ33TVPHDIMHLRBAG72AF:l/B35SNKWQ2KMVNYEIEDZLVOXMBY

overlay//(merge/upper/work) 目录则用于 overlay driver进行相关挂载操作。

overlay-containers目录存储正在运行的容器的读写层。

overlay-images 的文件夹用于存储本地系统中镜像的相关元数据。在 overlay-images 文件夹中,针对本地存储中的每个镜像,会在 overlay-images/ 目录下存储各种配置和清单文件,其中 是镜像的ID (ha256 sum of image)。通过将镜像的元数据存储在 overlay-images 文件夹中,可以方便地访问和管理本地系统中镜像的相关信息。这些元数据文件对于容器镜像的操作和管理非常重要,例如查找、验证、比较或删除镜像等。

root@test:/var/lib/containers/storage/overlay-images/300e315adb2f96afe5f0b2780b87f28ae95231fe3bdd1e16b9ba606307728f55# ls
'=bWFuaWZlc3Qtc2hhMjU2OjgzMDFkMTAwMDIwZmZhZWRjOTNmNTdkOGM4YmIwZThlODgwMDY4NjJiY2I0OGViYzVkZmJlMWQxY2I4MzA2MGM='
'=bWFuaWZlc3Qtc2hhMjU2OmRiYmFjZWNjNDliMDg4NDU4NzgxYzE2ZjM3NzVmMmEyZWM3NTIxMDc5MDM0YTdiYTQ5OWM4YjBiYjdmODY4NzU='
'=c2hhMjU2OjMwMGUzMTVhZGIyZjk2YWZlNWYwYjI3ODBiODdmMjhhZTk1MjMxZmUzYmRkMWUxNmI5YmE2MDYzMDc3MjhmNTU='
'=c2lnbmF0dXJlLWRiYmFjZWNjNDliMDg4NDU4NzgxYzE2ZjM3NzVmMmEyZWM3NTIxMDc5MDM0YTdiYTQ5OWM4YjBiYjdmODY4NzU='
 manifest

root@test:/var/lib/containers/storage/overlay-images/300e315adb2f96afe5f0b2780b87f28ae95231fe3bdd1e16b9ba606307728f55# echo "bWFuaWZlc3Qtc2hhMjU2OjgzMDFkMTAwMDIwZmZhZWRjOTNmNTdkO
GM4YmIwZThlODgwMDY4NjJiY2I0OGViYzVkZmJlMWQxY2I4MzA2MGM=" |base64 -d
manifest-sha256:8301d100020ffaedc93f57d8c8bb0e8e88006862bcb48ebc5dfbe1d1cb83060c

root@test:/var/lib/containers/storage/overlay-images/300e315adb2f96afe5f0b2780b87f28ae95231fe3bdd1e16b9ba606307728f55# echo "bWFuaWZlc3Qtc2hhMjU2OmRiYmFjZWNjNDliMDg4NDU4NzgxYzE2Z
jM3NzVmMmEyZWM3NTIxMDc5MDM0YTdiYTQ5OWM4YjBiYjdmODY4NzU=" |base64 -d
manifest-sha256:dbbacecc49b088458781c16f3775f2a2ec7521079034a7ba499c8b0bb7f86875

root@test:/var/lib/containers/storage/overlay-images/300e315adb2f96afe5f0b2780b87f28ae95231fe3bdd1e16b9ba606307728f55# echo "c2hhMjU2OjMwMGUzMTVhZGIyZjk2YWZlNWYwYjI3ODBiODdmMjhhZ
Tk1MjMxZmUzYmRkMWUxNmI5YmE2MDYzMDc3MjhmNTU=" |base64 -d
sha256:300e315adb2f96afe5f0b2780b87f28ae95231fe3bdd1e16b9ba606307728f55


root@test:/var/lib/containers/storage/overlay-images/300e315adb2f96afe5f0b2780b87f28ae95231fe3bdd1e16b9ba606307728f55# echo "c2lnbmF0dXJlLWRiYmFjZWNjNDliMDg4NDU4NzgxYzE2ZjM3NzVmM
mEyZWM3NTIxMDc5MDM0YTdiYTQ5OWM4YjBiYjdmODY4NzU=" |base64 -d
signature-dbbacecc49b088458781c16f3775f2a2ec7521079034a7ba499c8b0bb7f86875


podman如何使用Store

举几个实力命令来了解podman如何使用不同的文件:
当运行podman images时,podman会从overlay-images目录中的images.json文件中提取信息,然后打印镜像列表。

root@test:/var/lib/containers/storage/overlay-images# inotifywait -m -e access /var/lib/containers/storage/overlay-images/images.json
Setting up watches.
Watches established.
/var/lib/containers/storage/overlay-images/images.json ACCESS 
/var/lib/containers/storage/overlay-images/images.json ACCESS 
/var/lib/containers/storage/overlay-images/images.json ACCESS 
^C

当运行 podman run ubuntu:latest 命令时,它会在 images.json 文件中搜索 Ubuntu 镜像。如果镜像不存在,它会从镜像仓库中拉取该镜像。一旦镜像存在于本地系统中,它会从 images.json 和 layers.json 文件中获取相关的元数据。然后,它会挂载所有的镜像层,并在顶部添加一个读写层。这些镜像层包括只读的基础镜像层和读写的顶层。对于挂载镜像层,它会查找镜像的最顶层layer(存储在 images.json 中)。它从 overlay//link 文件中获取该层的随机 ID,从 overlay//lower 文件中获取所有父层的 ID。然后,它会从 overlay/l/ 目录中挂载这些镜像层。
最顶层layer如下:

  {
    "id": "bacbb4f6531eea67a52fc10fa4af0657ff3dbee71c5ef31cf10b32bf4d615562",                                                                                                     
    "digest": "sha256:c0114756bc381135afefddc026ba5106029fbc9f1fec0063ba7fc5cdee25356c",                                                                                          
    "names-history": [                                                                                                                                                            
      "docker.io/library/dc21594874aaed65754273a27a984e89556b8888a03811f9e1c58983c1c85519-tmp:latest"                                                                             
    ],
    "layer": "3ee8154a3ab87f4d350b88ac62b9f4c95f32a7916d80837d8a0af84f5d092fef",  
	......
}  

# 对应的layer层父目录软连接
root@test:/var/lib/containers/storage/overlay/3ee8154a3ab87f4d350b88ac62b9f4c95f32a7916d80837d8a0af84f5d092fef# cat lower 
l/SZQBY23EDAFYWBJ263MFX7DNU4:l/Q2GL55ZACOG6LMSLTQPS7QDFLL:l/SAJEHSHXNE2NKYN4M33V73HQWG:l/QKYDR52UCEDAFJQCVN5ESUWI4P:l/ICMMCAN2VCVS4URLUS5WEMXJA5:l/WEKUEFPNUDLA7UOXVWUJZLCJGO

# 对应本层数据的软连接
root@test:/var/lib/containers/storage/overlay/3ee8154a3ab87f4d350b88ac62b9f4c95f32a7916d80837d8a0af84f5d092fef# cat link 
WJP7FVIYD7FHVYA54NKZY52RBR

# 启动容器
root@test:/var/lib/containers/storage/overlay/3ee8154a3ab87f4d350b88ac62b9f4c95f32a7916d80837d8a0af84f5d092fef# podman run -it bacbb4f6531e /bin/sh

# 查看挂载,注意短链接名称
mount |grep overlay |grep SZQBY23EDAFYWBJ263MFX7DNU4
overlay on /var/lib/containers/storage/overlay/2df09bddc01b6bb1ae65eae790af5a4575df1f975fc486978cd8ac97d79568a5/merged type overlay (rw,nodev,relatime,lowerdir=/var/lib/container
s/storage/overlay/l/WJP7FVIYD7FHVYA54NKZY52RBR:/var/lib/containers/storage/overlay/l/SZQBY23EDAFYWBJ263MFX7DNU4:/var/lib/containers/storage/overlay/l/Q2GL55ZACOG6LMSLTQPS7QDFLL:/
var/lib/containers/storage/overlay/l/SAJEHSHXNE2NKYN4M33V73HQWG:/var/lib/containers/storage/overlay/l/QKYDR52UCEDAFJQCVN5ESUWI4P:/var/lib/containers/storage/overlay/l/ICMMCAN2VCV
S4URLUS5WEMXJA5:/var/lib/containers/storage/overlay/l/WEKUEFPNUDLA7UOXVWUJZLCJGO,upperdir=/var/lib/containers/storage/overlay/2df09bddc01b6bb1ae65eae790af5a4575df1f975fc486978cd8
ac97d79568a5/diff,workdir=/var/lib/containers/storage/overlay/2df09bddc01b6bb1ae65eae790af5a4575df1f975fc486978cd8ac97d79568a5/work,xino=off,metacopy=on)

当运行podman ps命令的时候,会查找overlay-containers/containers.json文件,并列出对应的所有正在运行的容器。
运行容器以后的存储目录结构如下:

root@test:/var/lib/containers/storage# tree -L 4
.
├── libpod
│   ├── bolt_state.db
│   └── defaultCNINetExists
├── mounts
├── overlay
│   ├── 2653d992f4ef2bfd27f94db643815aa567240c37732cae1405ad1c1309ee9859
│   │   ├── diff
│   │   │   ├── bin -> usr/bin
│   │   │   ├── dev
│   │   │   ├── etc
│   │   │   ├── home
│   │   │   ├── lib -> usr/lib
│   │   │   ├── lib64 -> usr/lib64
│   │   │   ├── lost+found
│   │   │   ├── media
│   │   │   ├── mnt
│   │   │   ├── opt
│   │   │   ├── proc
│   │   │   ├── root
│   │   │   ├── run
│   │   │   ├── sbin -> usr/sbin
│   │   │   ├── srv
│   │   │   ├── sys
│   │   │   ├── tmp
│   │   │   ├── usr
│   │   │   └── var
│   │   ├── empty
│   │   ├── link
│   │   ├── merged
│   │   └── work
│   ├── b8648c033100732de66cf583971c08cd862aff24086e92a8ae4ff7fdbebe7bbf(新增的容器层,也就是所谓的读写层)
│   │   ├── diff
│   │   │   └── run
│   │   ├── link
│   │   ├── lower
│   │   ├── merged
│   │   │   ├── bin -> usr/bin
│   │   │   ├── dev
│   │   │   ├── etc
│   │   │   ├── home
│   │   │   ├── lib -> usr/lib
│   │   │   ├── lib64 -> usr/lib64
│   │   │   ├── lost+found
│   │   │   ├── media
│   │   │   ├── mnt
│   │   │   ├── opt
│   │   │   ├── proc
│   │   │   ├── root
│   │   │   ├── run
│   │   │   ├── sbin -> usr/sbin
│   │   │   ├── srv
│   │   │   ├── sys
│   │   │   ├── tmp
│   │   │   ├── usr
│   │   │   └── var
│   │   └── work
│   │       └── work
│   └── l
│       ├── 57MIHXCXZI4TMYKQRHKJAAIU2Q -> ../2653d992f4ef2bfd27f94db643815aa567240c37732cae1405ad1c1309ee9859/diff
│       └── TJT7UBJNLFG43TKK35H2CM4NJN -> ../b8648c033100732de66cf583971c08cd862aff24086e92a8ae4ff7fdbebe7bbf/diff
├── overlay-containers
│   ├── c7941dc49b388654c169e19621290e297a73e6077be67a89e4391497d29d36de
│   │   └── userdata
│   │       ├── artifacts
│   │       ├── attach
│   │       ├── config.json
│   │       ├── ctl
│   │       ├── secrets
│   │       ├── shm
│   │       └── winsz
│   ├── containers.json
│   └── containers.lock
├── overlay-images
│   ├── 300e315adb2f96afe5f0b2780b87f28ae95231fe3bdd1e16b9ba606307728f55
│   │   ├── =bWFuaWZlc3Qtc2hhMjU2OjgzMDFkMTAwMDIwZmZhZWRjOTNmNTdkOGM4YmIwZThlODgwMDY4NjJiY2I0OGViYzVkZmJlMWQxY2I4MzA2MGM=
│   │   ├── =bWFuaWZlc3Qtc2hhMjU2OmRiYmFjZWNjNDliMDg4NDU4NzgxYzE2ZjM3NzVmMmEyZWM3NTIxMDc5MDM0YTdiYTQ5OWM4YjBiYjdmODY4NzU=
│   │   ├── =c2hhMjU2OjMwMGUzMTVhZGIyZjk2YWZlNWYwYjI3ODBiODdmMjhhZTk1MjMxZmUzYmRkMWUxNmI5YmE2MDYzMDc3MjhmNTU=
│   │   ├── =c2lnbmF0dXJlLWRiYmFjZWNjNDliMDg4NDU4NzgxYzE2ZjM3NzVmMmEyZWM3NTIxMDc5MDM0YTdiYTQ5OWM4YjBiYjdmODY4NzU=
│   │   └── manifest
│   ├── images.json
│   └── images.lock
├── overlay-layers
│   ├── layers.json
│   └── layers.lock
├── storage.lock
├── tmp
└── userns.lock

root@test:/var/lib/containers/storage# mount |grep overlay
overlay on /var/lib/containers/storage/overlay/b8648c033100732de66cf583971c08cd862aff24086e92a8ae4ff7fdbebe7bbf/merged type overlay (rw,nodev,relatime,lowerdir=/var/lib/container
s/storage/overlay/l/57MIHXCXZI4TMYKQRHKJAAIU2Q,upperdir=/var/lib/containers/storage/overlay/b8648c033100732de66cf583971c08cd862aff24086e92a8ae4ff7fdbebe7bbf/diff,workdir=/var/lib
/containers/storage/overlay/b8648c033100732de66cf583971c08cd862aff24086e92a8ae4ff7fdbebe7bbf/work,xino=off,metacopy=on)

images.json 和 layers.json

mages.json 文件存储了每个镜像的元数据,它是一个 JSON 对象,包含以下信息:

1. Image id (镜像 ID:每个镜像都有一个唯一的 ID,用于标识该镜像)
2. Name (名称:镜像的名称,通常是镜像仓库中的标识符)
3. Top most Layer id (最顶层的层 ID:镜像的最顶层的 ID。)
4. Creation time(创建时间:记录镜像的创建时间。)

layers.json文件存储了存储库中每个层的元数据,它也是一个 JSON 对象,包含以下信息:

1. Layer id(每个层都有一个唯一的 ID,用于标识该层。)
2. Creation time(创建时间:记录层的创建时间。)
3. Compressed digest(压缩摘要:对层进行压缩后的摘要信息。)
4. Compressed size(压缩大小:压缩后的层的大小。)
5. Uncompressed digest(解压缩摘要:层解压缩后的摘要信息。)
6. Uncompressed size(解压缩大小:解压缩后的层的大小。)
7. Parent layer(父层:每个层都可以有一个或多个父层,存储了层之间的继承关系。)

附加存储(additional storage)

镜像被存储在 /var/lib/containers/storage 目录中,这是 podman 的唯一可读写的镜像存储区。但是,可以存在多个只读的镜像存储区,称为附加存储。要使用附加存储区,可以将其绝对路径添加到 podman 的配置文件中,该文件位于 /etc/containers/storage.conf。
包含 Ubuntu 镜像的附加存储区的结构如下所示:

/path/to/additional/store
├── overlay
│ ├── 2ce3c1
│ │ ├── diff
│ │ ├── link
│ ├── 4cc8a6
│ │ ├── diff
│ │ ├── link
│ │ ├── lower
│ ├── 720c33
│ │ ├── diff
│ │ ├── link
│ │ ├── lower
│ ├── da0180
│ │ ├── diff
│ │ ├── link
│ │ ├── lower
│ └── l
│ │ ├── DCA3F65L7EAFZTJ55MTGPQMDSM -> ../4cc8a6/diff
│ │ ├── JJG53NDRFWK4UKBVGWDMYNB3L3 -> ../da0180/diff
│ │ ├── NQZ7WILONDZ22PEBUGEZ3MOFID -> ../720c33/diff
│ │ └── VAEMYZJDRVJZDW6ZIJO76NEVCQ -> ../2ce3c1/diff
├── overlay-images
│ ├── 4e2eef
│ │ ├── =bWFY3YTU=
│ │ ├── =bWFZTU=
│ │ ├── =c2hM2M=
│ │ ├── =c2lZTU=
│ │ └── manifest
│ ├── images.json
│ └── images.lock
├── overlay-layers
│ ├── layers.json
│ └── layers.lock

与可读写存储区相比,上述结构有以下变化:
在 overlay// 目录中,我们不再有 upper、merge 和 work 目录。这是因为它是一个只读存储区,因此无法将它们与叠加层(overlay)一起使用。相反,podman 将这些层挂载到可读写存储区的 overlay 目录中。
附加存储区是 podman 的一个强大功能。想象一下,设置了一个只读的镜像存储区,并通过 NFS 使用它,在不将镜像拉取到本地系统的情况下运行容器。 2020 年度 Google Summer of Code(GSoC)项目中,在 CVMFS 存储库中创建了一个附加存储区,可以在其中运行容器而无需将其拉取到本地系统。(这个功能不太熟悉)

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值