Docker快速入门

1. 什么是docker

Docker是一种开源的容器化平台,它通过使用容器来简化应用程序的交付和部署。容器是一种轻量级的虚拟化技术,它允许将应用程序及其依赖项打包在一起,以便在不同的环境中运行。Docker提供了一个可移植、可扩展和可靠的环境,使开发人员能够更轻松地构建、发布和管理应用程序。

简单的一句话说,Docker让部署更容易,实现了程序、环境和用户数据之间的完全分离。而对于软件开发或者运维而言,充分的解耦无疑是带来了巨大的维护优势。

这里为了快速上手做铺垫,如果对docker只闻其名,未曾用过的话。可以把以下几个经常出现的概念这么理解:

  • 镜像(Image): 这里的镜像,可以等于任何描述的包含所有内容的文件,包括命令行参数、环境变量等等。简单来说,如果用程序exe文件正在运行的进程之间的关系做比较的话,镜像就是相当于程序的exe文件。只是区别开来的是,镜像不止是一个程序,而是一个正在运行的虚拟系统,只是很精简,里面有特定的必须得程序。

  • 容器(Container):容器按照上面解释,其实就是某一个镜像正在运行起来的那个虚拟的环境。

如果熟悉程序和进程的差异,那么马上可以联想到这样一件事,对的,一个镜像可以生成多个容器,就好像一个程序可以启动多个,从而获取多个一样程序的进程一样。例如打开2个QQ同时登陆2个QQ号。知道了以上两个重要的概念,理解后面的东西就容易多了。

2. docker能做什么

Docker提供了一种简化和加速应用程序开发和交付过程的方法。它具有以下特点和优势:

a. 隔离性与可移植性: Docker容器提供了强大的隔离性,使应用程序能够在不同的环境中以一致的方式运行,而无需担心环境差异带来的问题。这种可移植性使得开发人员可以在本地开发、测试,然后将应用程序轻松地部署到生产环境中。

b. 资源效率: 与传统的虚拟机相比,Docker容器非常轻量级,启动和停止速度快,并且占用更少的系统资源。容器共享主机操作系统的内核,因此多个容器可以在同一台主机上高效运行。

c. 快速部署和扩展: Docker容器可以在几秒钟内启动,使得应用程序的部署变得非常快速和可靠。此外,可以根据应用程序的需求快速扩展容器,以满足高负载和流量的要求。

d. 简化依赖项管理: Docker允许开发人员将应用程序和其依赖项打包在一个容器中,以便简化依赖项管理。这意味着开发人员可以确保在不同的环境中使用相同的依赖项版本,从而避免了因环境差异引起的问题。

3. docker跨平台咋样

Docker在跨平台方面表现出色。它可以运行在不同的操作系统上,包括Linux、Windows和macOS等。Docker利用了操作系统级虚拟化技术,如Linux的命名空间和控制组,使得容器可以在不同的操作系统上运行,而无需进行修改。这使得开发人员可以在不同的开发环境中共享和交付容器,并确保应用程序在各个平台上一致运行。

这里需要额外说明一下,Docker的跨平台特性是需要额外操作才能实现的,默认docker直接的跨平台还需要保证宿主机架构和镜像架构一致,即x86_x64的系统只能运行x86_x64的镜像,ARM64的系统,可以运行armv5,armeabi-v7a和arm64-v8a的镜像。这种跨平台,在熟悉交叉编译的人眼中,属于跨了平台,但没有完全跨

对于嵌入式工程师而言,利用docker跨平台的特性,我们可以在没有板子的条件下,依旧方便的运行板子中的程序。

对于运维而言而言,docker镜像可以让你免去不同组件依赖的环境不同而需要繁琐部署,甚至产生组件和组件之间冲突而无法部署的问题。

3.1 如何实现真的跨平台

这里以如何在x86平台下运行ARM的Docker镜像作为介绍。

虽然Docker最初是为x86架构设计的,但借助跨平台的技术,也可以在x86平台上运行ARM架构的Docker镜像。这种情况下,需要使用一种称为"多架构镜像"的特性来实现。

以下是在x86平台上运行ARM镜像的基本步骤:

3.1.1 确认平台支持

首先,确保你的x86平台支持在其中运行ARM架构的容器。这通常需要在BIOS或UEFI设置中启用相应的虚拟化扩展(例如,Intel VT-x或AMD-V)。

3.1.2 安装QEMU和binfmt_misc

在x86平台上运行ARM镜像需要使用QEMU模拟器来实现。确保已在主机上安装QEMU模拟器和binfmt_misc内核模块。

3.1.3 配置binfmt_misc

通过配置binfmt_misc内核模块,将ARM架构的镜像关联到QEMU模拟器。这可以通过执行以下命令完成:

docker run --privileged linuxkit/binfmt:v0.8

这将加载binfmt_misc内核模块,并配置Docker以将ARM架构关联到QEMU模拟器。

3.1.4 拉取并运行ARM镜像

现在,可以拉取并运行ARM架构的Docker镜像了。可以使用常规的docker pull和docker run命令,指定所需的ARM镜像。Docker会自动使用QEMU模拟器来运行ARM镜像。

例如,要拉取并运行一个基于ARM的镜像,可以执行以下命令:

docker pull arm32v7/alpine
docker run --rm -it arm32v7/alpine /bin/sh

这将拉取Alpine Linux的ARM版本,并在x86平台上运行一个交互式容器。

需要注意的是,由于在x86平台上模拟ARM架构,性能可能会有所下降。因此,这种方法更适合于测试和开发目的,而不是在生产环境中广泛使用。通过使用QEMU模拟器和多架构镜像的特性,我们可以在x86平台上运行ARM架构的Docker镜像。这为开发人员提供了更大的灵活性,使他们能够在不同的硬件平台上测试和部署应用程序。

可以直接跨平台运行镜像的意义对嵌入式工程师说不言而喻,往往调试目标程序都需要一块目标板,系统测试或者集成测试的时候,测试工程师更是要借用很多目标板去实现测试上层平台的目的,但我们可以直接把嵌入式程序的版本制作成docker镜像交付给测试工程师,并把对应的log目录和sql目录映射到不同的地方,这样他们即便启动50,100台,也毫无压力。

4. 制作一个docker镜像

制作一个Docker镜像需要以下步骤:

4.1 编写Dockerfile

创建一个Dockerfile,并在其中定义构建镜像的步骤。这可能包括选择基础镜像、复制应用程序代码、安装依赖项、设置环境变量等。

4.1.1 如果觉得Dockfile上手比较复杂,想快速上手,请看这里

如果您不想使用Dockerfile来制作自定义镜像,还有其他方法可以创建自制镜像。以下是一种常见的方法:

4.1.1.1 创建一个容器

首先,您可以通过运行一个基础镜像的容器来开始创建自制镜像。选择与您要构建的应用程序或服务相关的基础镜像,例如Ubuntu、Alpine或CentOS。

运行容器的命令如下:

docker run -it --name mycontainer <基础镜像>

这将创建一个名为mycontainer的容器,并进入交互式模式。

4.1.1.2 在容器中进行配置和修改

进入容器后,您可以在其中进行配置和修改。安装所需的软件包、添加文件、更改配置等。您可以使用适用于容器内部的任何命令和工具来进行修改。

# 在容器内执行所需的配置命令
apt-get update
apt-get install -y <软件包>
4.1.1.3 提交容器为镜像

当您完成容器的配置和修改后,可以将其提交为一个新的镜像。

在宿主机上打开另一个终端窗口,运行以下命令:

docker commit mycontainer <自制镜像名称>

这将使用容器的当前状态创建一个新的镜像,并指定一个自定义的镜像名称。

4.1.1.4 测试和使用自制镜像

创建镜像后,您可以使用docker run命令来测试和运行该镜像:

docker run -it <自制镜像名称>

您可以验证自制镜像是否按预期工作,并根据需要进行进一步的修改和调整。

需要注意的是,这种方法可能不如使用Dockerfile来管理镜像构建过程灵活和可维护。Dockerfile提供了更结构化、可重复和可自动化的方法来构建镜像。但如果您只需创建简单的自制镜像,上述方法是一种替代方案。

总结而言,通过创建容器、进行配置和修改,并将其提交为新的镜像,您可以创建自制镜像而无需使用Dockerfile。这种方法对于一些简单的自定义需求可能是合适的,但对于更复杂的镜像构建和管理,建议使用Dockerfile来实现更好的可重复性和可维护性。

4.2 构建镜像

在Dockerfile所在的目录下,使用docker build命令来构建镜像。该命令将根据Dockerfile的定义,自动构建镜像并生成一个唯一的镜像标签。

4.3 推送镜像(可选)

如果需要将镜像分享给其他人或在不同的环境中使用,可以将镜像推送到Docker镜像仓库。可以使用docker push命令将镜像上传到Docker Hub或其他私有仓库。
当然如果不想推送到公网,你可以使用Docker save和Docker import来生成tar包镜像,进行离线交付传输

制作Docker镜像可以让我们更轻松地共享和交付应用程序,并确保应用程序在不同环境中一致运行。

总结而言,Docker是一个强大的容器化平台,可以简化应用程序的交付和部署过程。它具有跨平台能力、高效的资源利用、快速部署和扩展的优势,并且能够简化依赖项管理。通过使用Docker,开发人员可以更加高效地开发、测试和部署应用程序,从而提高开发团队的生产力和应用程序的可靠性。

5. 这里结合前一篇博客的WEB服务后台部署进行介绍如何Docker化

这里之前使用Nginx + supervisor + gunicorn + Flask + Redis + Postgresql 搭建过一个简单的Web服务器。Web数据流的一生——从前端界面到后端数据,以及万级QPS的后台服务搭建。对于这个服务器的部署迁移,使用docker技术就帮上了大忙。

5.1 不用docker之前需要做什么

不使用docker的时候,一般需要先去vps上找一个合适的系统,然后在系统里装上依赖环境,依次需要安装Nginx + supervisor + gunicorn + Flask + Redis + Postgresql,比较麻烦的是Postgresql还要进行一定的配置,设置账号,设置权限等等。对于Flask和gunicorn,一般还需要找到合适的版本和对应的环境正确,大多数时候,总是免不了安装miniconda等Python环境快速转换轮子。

以上步骤弄好之后,还一定要试运行,确保万无一失,往往经验丰富的运维会弄一个专门的部署脚本,但是由于vps的依赖源更新,总是不可避免的会有部分翻车现象。

5.2 使用docker后可以如何简化

使用docker之后,在vps中安装了docker,然后把开发环境的自制镜像拉去过来即可(如果你推送到了dockerHub,没推送的话,把镜像tar包复制到vps一样可以import进来)。

接下来不用担心环境问题,依次把需要的几个组件起来就好,这里给一个样例启动脚本。(由于项目不复杂就没整Docker Compose了,而实际上Docker Compose只是把下面的脚本用了一个Dokcer自制的语言体现出来,逻辑没区别。)

先看启动脚本:

docker run  -p 8080:8080 -p 80:80  -p 8081:81 -p  8088:5000 \
 --name flaskapp  --restart always \
 -v /Users/adamxiao/Documents/DDJN/src/nginx.conf:/etc/nginx/nginx.conf -v /Users/adamxiao/Documents/DDJN/web/:/home/DDJN/web -v /Users/adamxiao/Documents/DDJN/app/:/home/DDJN/app \
 -d nginx

docker run  -v /Users/adamxiao/Documents/DDJN/:/home/  -v /Users/adamxiao/Documents/DDJN/src/conf/supervisor_app.conf:/etc/supervisor/conf.d/supervisor_app.conf  -v /Users/adamxiao/Documents/DDJN/src/conf/supervisor_ver.conf:/etc/supervisor/conf.d/supervisor_ver.conf \
 --name flaskcore   
 --network=container:flaskapp \
 -d  aiyanzielf/flask:0.2  sh /home/src/flask.sh


docker run  --name flaskredis --restart always --network=container:flaskapp \
-d redis

docker run -d \
  --name flasksql \
  -e POSTGRES_USER=aiyanzielf \
  -e POSTGRES_PASSWORD=************ \
  -e POSTGRES_DB=ddjn \
  -v /Users/adamxiao/Documents/DDJN/postgres:/var/lib/postgresql/data \
  -v /Users/adamxiao/Documents/DDJN/:/home/ \
  --network=container:flaskapp \
  --restart always \
  postgres 

对docker run命令不太熟的朋友可以先跳过去看一下,docker run

这里分别启动了4个镜像,nginx, flask, redis, postgresql。上部分中介绍的superviosrgunicorn我把官方的flask改了改直接丢了进去。这样整个服务其实就已经部署好了。

这里回到最前面介绍的,docker的作用是把程序和数据分离,通过配置也能够发现,nginx等服务的配置文件实际上并不在镜像中,而是通过挂载,把外面保存好的配置映射到容器内,从而实现了无论是什么配置,都无需修改镜像的目的。

这里还需要注意的一点是,如果在同一个环境中运行多个镜像,容器之间实际上是隔离的,那么这几个组件之间是怎么通信的呢?docker中新增了一个docker网络的概念,可以把所有的容器都添加到一个网络中,这样虽然他们之间是相互独立的,但在一个网络内通信就好像是在一个环境中一样, --network=container:flaskapp启动脚本中,除了nginx其他的服务均由这个指令,而nginx的容器名被设置为flaskapp,通过这样的手法,4个服务均在一个网络内,整个应用服务就能顺利运行起来了。

#这里给出启动脚本对应 docker-compose.yml
# 在同等目录下,直接运行docker-compose up可以直接前台批量启动
# docker-compose up -d可以直接后台批量启动
version: '3.8'

services:
  nginx:
    build: .
    image: nginx:latest 
    container_name: flaskapp
    restart: always
    ports:
      - 8080:8080
      - 80:80
      - 8081:81
      - 8088:55555
    volumes:
      - /Users/adamxiao/Documents/DDJN/src/nginx.conf:/etc/nginx/nginx.conf
      - /Users/adamxiao/Documents/DDJN/web/:/home/DDJN/web
      - /Users/adamxiao/Documents/DDJN/app/:/home/DDJN/app
    networks:
      - mynetwork

  flask:
    build: .
    image: ddbackbone:latest 
    container_name: flaskcore
    restart: always
    volumes:
      - /Users/adamxiao/Documents/DDJN/apk/:/home/DDJN/apk
    environment:
      - DEBUG=True
    network_mode: "service:nginx"

  redis:
    build: .
    image: redis:latest
    container_name: flaskredis
    restart: always
    network_mode: "service:nginx"

  db:
    build: .
    image: postgres:latest
    container_name: flasksql
    restart: always
    volumes:
      - /Users/adamxiao/Documents/DDJN/postgres:/var/lib/postgresql/data
      - /Users/adamxiao/Documents/DDJN/:/home/
    environment:
      - POSTGRES_USER=aiyanzielf 
      - POSTGRES_PASSWORD=******
      - POSTGRES_DB=ddjn 
    network_mode: "service:nginx"

networks:
  mynetwork:
    driver: bridge
    driver_opts:
      com.docker.network.container_mode: 1

docker-compose的好处是可以在前台方便的批量看到每个容器的实时输出,好像你在一个系统中一起启动这些服务一样,这里贴出我的demo服务器启动的打印:

[+] Building 0.0s (0/0)                                                                                                                                                                                  
[+] Running 4/0
 ✔ Container flaskapp    Created                                                                                                                                                                    0.0s 
 ✔ Container flasksql    Recreated                                                                                                                                                                  0.0s 
 ✔ Container flaskcore   Recreated                                                                                                                                                                  0.0s 
 ✔ Container flaskredis  Recreated                                                                                                                                                                  0.0s 
Attaching to flaskapp, flaskcore, flaskredis, flasksql
flaskapp    | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
flaskapp    | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
flaskapp    | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
flaskapp    | 10-listen-on-ipv6-by-default.sh: info: IPv6 listen already enabled
flaskapp    | /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
flaskapp    | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
flaskapp    | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
flaskapp    | /docker-entrypoint.sh: Configuration complete; ready for start up
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: using the "epoll" event method
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: nginx/1.25.1
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14) 
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: OS: Linux 5.15.49-linuxkit-pr
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: start worker processes
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: start worker process 22
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: start worker process 23
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: start worker process 24
flaskapp    | 2023/07/03 16:38:30 [notice] 1#1: start worker process 25
flaskredis  | 1:C 03 Jul 2023 16:38:30.209 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
flaskredis  | 1:C 03 Jul 2023 16:38:30.209 # Redis version=7.0.11, bits=64, commit=00000000, modified=0, pid=1, just started
flaskredis  | 1:C 03 Jul 2023 16:38:30.209 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
flaskredis  | 1:M 03 Jul 2023 16:38:30.209 * monotonic clock: POSIX clock_gettime
flaskredis  | 1:M 03 Jul 2023 16:38:30.210 * Running mode=standalone, port=6379.
flaskredis  | 1:M 03 Jul 2023 16:38:30.210 # Server initialized
flaskredis  | 1:M 03 Jul 2023 16:38:30.214 * Loading RDB produced by version 7.0.11
flaskredis  | 1:M 03 Jul 2023 16:38:30.214 * RDB age 52 seconds
flaskredis  | 1:M 03 Jul 2023 16:38:30.214 * RDB memory usage when created 0.85 Mb
flaskredis  | 1:M 03 Jul 2023 16:38:30.214 * Done loading RDB, keys loaded: 0, keys expired: 0.
flaskredis  | 1:M 03 Jul 2023 16:38:30.214 * DB loaded from disk: 0.000 seconds
flaskredis  | 1:M 03 Jul 2023 16:38:30.214 * Ready to accept connections
flaskcore   | /usr/lib/python3/dist-packages/supervisor/options.py:474: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security.
flaskcore   |   self.warnings.warn(
flaskcore   | No config updates to processes
flasksql    | 
flasksql    | PostgreSQL Database directory appears to contain a database; Skipping initialization
flasksql    | 
flasksql    | 2023-07-03 16:38:30.979 UTC [1] LOG:  starting PostgreSQL 15.3 (Debian 15.3-1.pgdg120+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
flasksql    | 2023-07-03 16:38:30.979 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
flasksql    | 2023-07-03 16:38:30.979 UTC [1] LOG:  listening on IPv6 address "::", port 5432
flasksql    | 2023-07-03 16:38:30.982 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
flasksql    | 2023-07-03 16:38:30.992 UTC [30] LOG:  database system was shut down at 2023-07-03 16:37:38 UTC
flasksql    | 2023-07-03 16:38:31.011 UTC [1] LOG:  database system is ready to accept connections
flaskcore   | app: stopped
flaskcore   | app: started
flaskcore   | verd: stopped
flaskcore   | verd: started
^CGracefully stopping... (press Ctrl+C again to force)
Aborting on container exit...
[+] Stopping 4/4
 ✔ Container flasksql    Stopped                                                                                                                                                                    0.1s 
 ✔ Container flaskcore   Stopped                                                                                                                                                                   10.2s 
 ✔ Container flaskredis  Stopped                                                                                                                                                                    0.1s 
 ✔ Container flaskapp    Stopped                                                                                                                                                                    0.1s 
canceled

Appendix Docker 常用指令

1. 镜像操作指令

docker pull <镜像名称>:从镜像仓库中拉取指定的镜像。
docker build -t <镜像名称> <Dockerfile路径>:根据Dockerfile构建一个镜像。
docker push <镜像名称>:将本地的镜像推送到镜像仓库中。
docker images:列出本地的镜像列表。
docker rmi <镜像ID>:删除指定的镜像。

2. 容器操作指令

docker run <镜像名称>:创建并运行一个容器。
docker start <容器ID>:启动一个已停止的容器。
docker stop <容器ID>:停止一个正在运行的容器。
docker restart <容器ID>:重启一个容器。
docker rm <容器ID>:删除一个容器。
docker ps:列出正在运行的容器列表。
docker ps -a:列出所有容器,包括已停止的容器。

2.1 这里针对最常用的docker run进行扩展

docker run是一个常用的Docker命令,用于创建并运行容器。除了基本的用法之外,还有一些常见的扩展指令可以用于更精细地控制容器的行为和配置。下面是一些常见的docker run扩展指令的使用和解释:

2.1.1 端口映射

docker run -p <主机端口>:<容器端口> <镜像名称>

此命令用于将容器内部的端口映射到主机上的指定端口。例如,docker run -p 8080:80 nginx将运行一个Nginx容器,并将容器内部的80端口映射到主机的8080端口。

2.1.2 环境变量设置

docker run -e <变量名>=<变量值> <镜像名称>

通过此命令可以设置容器的环境变量。可以多次使用-e选项来设置多个环境变量。例如,docker run -e MYSQL_ROOT_PASSWORD=password mysql将在MySQL容器中设置一个名为MYSQL_ROOT_PASSWORD的环境变量,并将其值设置为password。

2.1.3 挂载数据卷

docker run -v <主机路径>:<容器路径> <镜像名称>

使用此命令可以将主机上的目录或文件挂载到容器内部的指定路径。例如,docker run -v /host/data:/container/data nginx将主机上的/host/data目录挂载到容器内的/container/data路径。

2.1.4 后台运行

docker run -d <镜像名称>

通过添加-d选项,容器将在后台以守护进程方式运行。这样可以将容器放入后台运行,而不会阻塞当前终端。

2.1.5 容器命名

docker run --name <容器名称> <镜像名称>

使用此命令可以为容器指定一个自定义的名称,而不是由Docker自动生成一个随机名称。

2.1.6 交互式模式

docker run -it <镜像名称> <命令>

通过添加-it选项,容器将以交互式模式运行,并且终端会连接到容器的标准输入和输出。这常用于进入容器的Shell或执行交互式命令。

3. 容器日志和执行指令

docker logs <容器ID>:查看容器的日志输出。
docker exec -it <容器ID> <命令>:在运行中的容器中执行指定的命令。
docker attach <容器ID>:附加到正在运行的容器,查看其实时输出。

4. 网络和数据卷指令

docker network create <网络名称>:创建一个用户定义的网络。
docker network ls:列出所有网络。
docker volume create <卷名称>:创建一个数据卷。
docker volume ls:列出所有数据卷。

5. 其他指令

docker-compose up:使用Docker Compose启动多个容器。
docker-compose down:停止并删除Docker Compose定义的所有容器。

这些指令只是Docker功能的一小部分。Docker提供了更多的命令和选项,用于管理和操作容器、镜像、网络和卷等。您可以通过运行docker --help或查阅Docker官方文档来获取更详细的信息和指导。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值