Flume日志收集系统详解----硬核解析

一、Flume简介

Apache Flume是一个 分布式的、可靠的、可用的 数据收集系统 ,它可以有效地收集、聚合和移动大量的 日志数据 ,这些数据可以从许多不同的来源转移到一个集中的数据存储中。
Apache Flume不仅仅限于日志数据聚合。由于数据源是可定制的,所以Flume可用于传输大量事件数据,包括但不限于网络流量数据、社交媒体生成的数据、电子邮件消息以及几乎所有可能的数据源。

1.1 Flume特点

1.1.1 可靠性

Flume 的核心是把 数据从数据源收集过来,再送到目的地 。为了保证输送一定成功,在送到目的地之前,会 先缓存数据 待数据真正到达目的地后,删除自己缓存的数据 。Flume 使用 事务性的方式保证传送 Event整个过程的可靠性 。 Sink 必须在Event 被存入 Channel 后,或者,已经被传达到下一站 Agent里,又或者,已经被存入外部数据目的地之后,才能把 Event 从 Channel 中 remove 掉。这样数据流里的 Event 无论是在一个 agent 里还是多个 agent 之间流转,都能保证可靠,因为以上的事务保证了 Event 会被成功存储起来。比如 Flume支持在本地保存一份文件 Channel 作为备份,而 Memory Channel 将 Event存在内存队列里,速度快,但丢失的话无法恢复。

1.1.2 可恢复性

Events在 通道中执行,由该通道管理从失败中恢复 。 Flume支持由本地文件系统支持的持久文件通道。还有一个内存通道,它只是简单地将事件存储在内存队列中,速度更快,但是当代理进程死亡时,仍然留在内存通道中的任何事件都无法恢复。

1.2 Flume架构

Flume架构包括三部分: ClientAgentEvent

①Event: 事件,是数据传输的基本单元 。 它具有字节有效载荷和一组可选的字符串属性 ,通常对应一行数据。 实际包含一个 Map结构的 headers和一个 byte[]类型的 body属性。 Event是数据流的数据对象,而 Flume数据流( Data Flow) 描述了数据从产生、传输、处理并最终写入目标的一条路径。

②Agent: 代理,是一个独立的JVM进程,包含三个组件(Source、Channel、Sink),事件通过组件从外部源流向下一个目标。

③Client: 客户端,数据产生的地方,如Web服务器。

注: Flume以一个或多个Agent部署运行,Flume数据流模型(架构图)如下图所示:

在这里插入图片描述

二、Flume原理

2.1 主要组件

Agent组件包括:包括: Source、 SourceRunner、 Interceptor、 Channel、ChannelSelector、ChannelProcessor、 Sink SinkRunner、 SinkProcessor、 SinkSelector,其中 SourceChannelSink为核心组件,各组件作用如下:

  • Source : 是负责接收数据到 Flume Agent的 组件,用来获取 Event 并写入 Channel。

  • SourceRunner : 负责启动 Source,一个 SourceRunner包含一个 Source对象。

  • Interceptor: 拦截器,是简单的插件式组件,设置在 Source和 Channel之间。Source接收到的事件 Event,在写入Channel之前,拦截器都可以进行转换或者删除这些事件。每个拦截器只处理同一个 Source接收到的事件,可以自定义拦截器。

  • Channel: 位于 Source和 Sink之间的缓冲区, 中转 Event 的一个临时存储,保存有 Source 组件传递过来的 Event,可以认为是一个队列。 Channel允许 Source和 Sink运作在不同的速率上。 Channel是线程安全的,可以同时处理几个 Source的写入操作和几个 Sink的读取操作。

  • ChannelSelector: 选择器,作用是为 Source选择下游的 Channel。有两种选择方式, 复制和多路复用 。复制 是把 Source中传递过来的 Event复制给所有对应的下游的Channel。多路复用 是把 Source传递过来的 Event按照不同的属性传递到不同的下游 Channel中去。

  • ChannelProcessor: 通过 ChannelSelector获取到 Channels后,如何发送 Event到
    Channel。 一个 Source对象包含一个 ChannelProcessor对象,一个 ChannelProcessor对象包含多个 Interceptor对象和一个 ChannelSelector对象。

  • Sink: 从 Channel 中读取并移除 Event,将 Event 传递到 Flow Pipeline 中的下一个 Agent 或者其他存储系统。 Sink不断地轮询 Channel中的事件且批量地移除它们 ,并将这些事件批量写入到存储或索引系统、或者被发送到另一个 Flume Agent。Sink是完全事务 性 的。 在从 Channel批量删除数据之前,每个 Sink用Channel启动一个事务。批量 事件 一旦成功写出到存储系统或下一个Flume Agent Sink就利用 Channel提交事务。事务 一旦 被提交,该 Channel从 自己 的内部缓冲区删除事件。

  • SinkRunner: 负责启动 Sink。在 Agent启动时,会同时启动 Channel SourceRunner
    SinkRunner

  • SinkProcessor: Flume提供 FailoverSinkProcessor和 LoadBalancingSinkProcesso,一个是失效备援,一个是负载均衡,那么 SinkProcessor不同子类的存在就是为了实现不同的分配操作和策略,而 sink的 start()通常是启动线程去执行消费操作。

  • SinkSelector: LoadBalancingSinkProcessor包含 SinkSelector,会根据 SinkSelector在 SinkGroup(逻辑上的一组 Sink)中选择 Sink并启动。

2.2 工作流程

在这里插入图片描述

三、flume创建实例

在flume安装的根目录/conf目录下创建文件job:,在该文件夹下存放自定义的flume配置文件

mkdir /opt/install/flume160/conf/job

3.1 Exec Source 类型

执行Linux指令,并按照指令返回结果,如 “tail-f”
示例: 在job目录下创建 exec.conf 文件,编辑内容如下:

a1.sources = s1
a1.sinks = sk1
a1.channels = c1

#设置source类型为exec
a1.sources.s1.type = exec
a1.sources.s1.command = tail -f /opt/dataFile/flume-0817/exectest.txt
#source和channel连接
a1.sources.s1.channels = c1
a1.channels.c1.type = memory

#指定sink
a1.sinks.sk1.type = logger

#sink和channel进行连接
a1.sinks.sk1.channel = c1

a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

在flume根目录执行:

./bin/flume-ng agent --name a1 --conf conf/ --conf-file conf/job/exec.conf -Dflume.root.logger=INFO,console

/opt/dataFile/flume-0817/exectest.txt 文件中写入数据,flume 即可开始运行,执行相应读写操作
注:exectest.txt尾部添加内容:

echo hahaha >> /opt/dataFile/flume-0817/exectest.txt

3.2 spooling directory source类型

从磁盘文件夹中获取文件数据,可避免重启或者发送失败后数据丢失,还可用于监控文件夹新文件

示例: 在job目录下创建 events-flume-logger.conf 文件,编辑内容如下:

events.sources = eventsSource
events.channels = eventsChannel
events.sinks = eventsSink

events.sinks.eventsSink.type = logger

events.sources.eventsSource.type = spooldir

#需先创建目录: /opt/dataFile/flumeFile/events   用于存放需要读取的.csv文件
events.sources.eventsSource.spoolDir = /opt/dataFile/flumeFile/events

events.sources.eventsSource.deserializer = LINE
events.sources.eventsSource.deserializer.maxLineLength = 32000

#正则匹配需要读的文件名
events.sources.eventsSource.includePattern = events_[0-9]{4}-[0-9]{2}-[0-9]{2}.csv

events.channels.eventsChannel.type = file

#需先创建目录:/optdataFile/flumeFile/checkpoint/events   设置检查点
events.channels.eventsChannel.checkpointDir = /optdataFile/flumeFile/checkpoint/events

#需先创建目录: /opt/dataFile/flumeFile/data/events  存放Channel数据
events.channels.eventsChannel.dataDirs = /opt/dataFile/flumeFile/data/events

events.sources.eventsSource.channels = eventsChannel
events.sinks.eventsSink.channel = eventsChannel

在flume根目录执行:

./bin/flume-ng agent --name events --conf conf/ --conf-file conf/job/events-flume-logger.conf -Dflume.root.logger=INFO,console

将要读取的数据 events.csv 拷贝至 /opt/dataFile/flumeFile/events 目录下,flume 即可开始运行,执行相应读写操作

拷贝数据,注意修改文件格式:

install events.csv /opt/dataFile/flumeFile/events/events_2020-08-17.csv

3.3 Taildir Source类型

Taildir Source监控指定的一些文件,并在检测到新的一行数据产生的时候实时地读取它们,如果新的一行数据还没写完, Taildir Source会等到这行写完后再读取。 Taildir Source可以从任意指定的位置开始读取文件,可以实现断点续读,如果发生宕机,会从宕机前记录的最后读取的位置开始读文件,而不是从首行重新开始读取。 默认情况下,它将从每个文件的第一行开始读取。
文件按照修改时间的顺序来读取。修改时间最早的文件将最先被读取(简单记成:先来先走)。 Taildir Source不重命名、删除或修改它监控的文件。当前不支持读 取二进制文件。只能逐行读取文本文件。注意:Taildir Source目前不能运行在 windows系统上。

示例: 在job目录下创建 tailDir.conf 文件,编辑内容如下:

a1.sources = s1
a1.sinks = sk1
a1.channels = c1

#设置source类型为TAILDIR
a1.sources.s1.type = TAILDIR
a1.sources.s1.filegroups = f1 f2

#配置filegroups的两个数据源 f1   f2
a1.sources.s1.filegroups.f1 = /opt/dataFile/flume-0817/tail_1/example.log

a1.sources.s1.filegroups.f2 = /opt/dataFile//flume-0817/tail_2/.*log.*

#指定position的位置, 读取文件会记录位置
a1.sources.s1.positionFile = /opt/dataFile//flume-0817/tail_position/taildir_position.json

#指定headers
a1.sources.s1.headers.f1.headerKey1 = value1
a1.sources.s1.headers.f2.headerKey1 = value2
a1.sources.s1.headers.f2.headerKey1 = value3

a1.sources.s1.fileHeader = true

#source和channel连接
a1.sources.s1.channels = c1
a1.channels.c1.type = memory

#指定sink
a1.sinks.sk1.type = logger

#sink和channel进行连接
a1.sinks.sk1.channel = c1

a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

在flume根目录执行:


./bin/flume-ng agent --name a1 --conf conf/ --conf-file conf/job/tailDir.conf -Dflume.root.logger=INFO,console

在f1、f2目录中创建一些数据,,flume 即可开始运行,执行相应读写操作

3.4 Netcat Source (TCP)类型

一个类似netcat的源,它监听给定的端口并将每行文本转换成一个事件。就像 nc -k -l [主机 ][端口 ]。换句话说,它打开指定的端口并侦听数据。期望提供的数据是换行分隔的文本 。每行文本被转换成一个 Flume事件并通过连接的通道发送。

示例: 在job目录下创建 netcat-flume-logger.conf 文件,编辑内容如下:

a1.sources = r1
a1.sinks = k1
a1.channels = c1

a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444

a1.sinks.k1.type = logger

a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1

在flume根目录执行:

./bin/flume-ng agent --name a1 --conf conf/ --conf-file conf/job/netcat-flume-logger.conf -Dflume.root.logger=INFO,console

测试: 需先下载一个网络工具netcat,Linux默认情况下是没有安装的,安装过程如下:

yum install -y nc

#列出telnet相关的安装包
yum list telnet*

#安装telnet服务
yum install telnet-server	

#安装telnet客户端
yum install telnet.*

输入端口号44444,测试连接:

telnet localhost 44444

3.4 将读取文件上传至hdfs上

示例: 读取文件user_friends.csv,上传至hdfs /data/userFriends目录下,在job目录下创建 file-flume-hdfs.conf 文件,编辑内容如下:

user_friends.sources = userFriendsSource
user_friends.channels = userFriendsChannel
user_friends.sinks = userFriendsSink

user_friends.sources.userFriendsSource.type = spooldir

#需先创建目录: /opt/dataFile/flumeFile/userFriends 用于存放需要读取的.csv文件
user_friends.sources.userFriendsSource.spoolDir = /opt/dataFile/flumeFile/user_friends

user_friends.sources.userFriendsSource.deserializer = LINE
user_friends.sources.userFriendsSource.deserializer.maxLineLength = 600000

#正则匹配需要读的文件名
user_friends.sources.userFriendsSource.includePattern = userfriends_[0-9]{4}-[0-9]{2}-[0-9]{2}.csv

user_friends.channels.userFriendsChannel.type = file

#需先创建目录:/optdataFile/flumeFile/checkpoint/userFriends  设置检查点
user_friends.channels.userFriendsChannel.checkpointDir = /opt/dataFile/flumeFile/checkpoint/userFriends

#需先创建目录: /opt/dataFile/flumeFile/data/userFriends 存放Channel数据
user_friends.channels.userFriendsChannel.dataDirs = /opt/dataFile/flumeFile/data/userFriends

user_friends.sinks.userFriendsSink.type = hdfs
user_friends.sinks.userFriendsSink.hdfs.fileType = DataStream
user_friends.sinks.userFriendsSink.hdfs.filePrefilx = userFriends
user_friends.sinks.userFriendsSink.hdfs.filePrefilx = .csv
user_friends.sinks.userFriendsSink.hdfs.path = hdfs://192.168.206.129:9000/data/userFriends/%Y-%m-%d
user_friends.sinks.userFriendsSink.hdfs.useLocalTimeStamp = true
user_friends.sinks.userFriendsSink.hdfs.batchSize = 640
user_friends.sinks.userFriendsSink.hdfs.rollCount = 0
user_friends.sinks.userFriendsSink.hdfs.rollSize = 6400000
user_friends.sinks.userFriendsSink.hdfs.rollInterval = 30

user_friends.sources.userFriendsSource.channels = userFriendsChannel
user_friends.sinks.userFriendsSink.channel = userFriendsChannel

在flume根目录执行:

./bin/flume-ng agent --name user_friends --conf conf/ --conf-file conf/job/file-flume-hdfs.conf -Dflume.root.logger=INFO,console

将要读取的数据 user_friends.csv 拷贝至 /opt/dataFile/flumeFile/userFriends 目录下,flume 即可开始运行,执行相应读写操作

拷贝数据,注意修改文件格式:

install events.csv /opt/dataFile/flumeFile/userFriends/userfriends_2020-08-17.csv

3.5 Java自定义拦截器,将读取数据上传至hdfs

自定义拦截器实现功能: 读取每行数据,数据以 “spark” 开头时,将文件上传至 hdfs: /data/sparkDemo目录下,否则上传至 hdfs: /data/tmpDemo 目录下

①: IDEA中需先导入Maven依赖:

<dependency>
      <groupId>org.apache.flume</groupId>
      <artifactId>flume-ng-core</artifactId>
      <version>1.6.0</version>
</dependency>

IDEA中创建java文件 InterceptorDemo
自定义拦截器代码如下:

package cn.com;

import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class InterceptorDemo implements Interceptor {   //继承拦截器接口
    private List<Event> addHeaderEvents;
    @Override
    public void initialize() {
        addHeaderEvents = new ArrayList<>();
    }

    @Override
    public Event intercept(Event event) {
        Map<String,String> headers = event.getHeaders();
        String body = new String(event.getBody());
        if(body.startsWith("spark")){
            headers.put("type","spark");
        }else{
            headers.put("type","tmp");
        }
        return event;
    }

    @Override
    public List<Event> intercept(List<Event> list) {
        addHeaderEvents.clear();
        for(Event event:list){
            addHeaderEvents.add(intercept(event));
        }
        return addHeaderEvents;
    }

    @Override
    public void close() {

    }

	//创建一个静态内部类,同过静态内部类加载  InterceptorDemo  对象
    public static class Builder implements Interceptor.Builder{
        @Override
        public Interceptor build() {
            return new InterceptorDemo();
        }

        @Override
        public void configure(Context context) {

        }
    }
}

②: 将以上java文件,打jar包上传至 /opt/install/flume160/lib/ 目录

③: Linux中在job目录下创建 netcat-flume-logerhdfs.conf 文件,编辑内容如下:

a1.sources = r1
a1.channels = c1 c2
a1.sinks = k1 k2

a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444

#设置拦截器,注意设置拦截器所在jar包位置: cn.com.InterceptorDemo$Builder ,用$连接
a1.sources.r1.interceptors = i1
a1.sources.r1.interceptors.i1.type = cn.com.InterceptorDemo$Builder

#设置选择器
a1.sources.r1.selector.type = multiplexing
a1.sources.r1.selector.header = type
a1.sources.r1.selector.mapping.gree = c1
a1.sources.r1.selector.mapping.lijia = c2

a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

a1.channels.c2.type = memory
a1.channels.c2.capacity = 1000
a1.channels.c2.transactionCapacity = 100

a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.fileType = DataStream
a1.sinks.k1.hdfs.filePrefix = spark
a1.sinks.k1.hdfs.fileSuffix = .csv
a1.sinks.k1.hdfs.path = hdfs://192.168.206.129:9000/data/sparkdemo/%Y-%m-%d
a1.sinks.k1.hdfs.useLocalTimeStamp = true
a1.sinks.k1.hdfs.batchSize = 640
a1.sinks.k1.hdfs.rollCount = 0
a1.sinks.k1.hdfs.rollSize = 100
a1.sinks.k1.hdfs.rollInterval = 3

a1.sinks.k2.type = hdfs
a1.sinks.k2.hdfs.fileType = DataStream
a1.sinks.k2.hdfs.filePrefix = tmp
a1.sinks.k2.hdfs.fileSuffix = .csv
a1.sinks.k2.hdfs.path = hdfs://192.168.206.129:9000/data/tmpdemo/%Y-%m-%d
a1.sinks.k2.hdfs.useLocalTimeStamp = true
a1.sinks.k2.hdfs.batchSize = 640
a1.sinks.k2.hdfs.rollCount = 0
a1.sinks.k2.hdfs.rollSize = 100
a1.sinks.k2.hdfs.rollInterval = 3

a1.sources.r1.channels = c1 c2
a1.sinks.k1.channel = c1
a1.sinks.k2.channel = c2

④: 测试连接

telnet localhost 44444

分别输入数据

sparkabc
spark abc 123
abcdefg
123456
sparktest

可以在hdfs端口: http://192.168.206.129:50070/ 查看拦截器是否分类读取上传至 hdfs/data 目录下

3.7 flume读取数据至kafka

例① 将读取的数据直接保存至kafka中:
vi userfriends-flume-kafka.conf ,内容如下:

user_friends.sources = userFriendsSource
user_friends.channels = userFriendsChannel
user_friends.sinks = userFriendsSink

user_friends.sources.userFriendsSource.type = spooldir
user_friends.sources.userFriendsSource.spoolDir = /opt/dataFile/flumeFile/user_friends

user_friends.sources.userFriendsSource.deserializer = LINE
user_friends.sources.userFriendsSource.deserializer.maxLineLength = 60000
user_friends.sources.userFriendsSource.includePattern = userfriends_[0-9]{4}-[0-9]{2}-[0-9]{2}.csv

user_friends.channels.userFriendsChannel.type = file
user_friends.channels.userFriendsChannel.checkpointDir = /opt/dataFile/flumeFile/checkpoint/userFriends

user_friends.channels.userFriendsChannel.dataDir = /opt/dataFile/flumeFile/data/userFriends
 
user_friends.sinks.userFriendsSink.type = org.apache.flume.sink.kafka.KafkaSink
user_friends.sinks.userFriendsSink.batchSize = 640
user_friends.sinks.userFriendsSink.brokerList = 192.168.206.129:9092
user_friends.sinks.userFriendsSink.topic = user_friends_raw

user_friends.sources.userFriendsSource.channels = userFriendsChannel
user_friends.sinks.userFriendsSink.channel = userFriendsChannel

例② 使用flume自带的拦截器过滤掉首行,如下图:
想要过滤掉首行字段:

在这里插入图片描述

vi users-flume-kafka.conf ,内容如下:

users.sources = usersSource
users.channels = usersChannel
users.sinks = usersSink

users.sources.usersSource.type = spooldir
users.sources.usersSource.spoolDir = /opt/dataFile/flumeFile/users
users.sources.usersSource.deserializer = LINE
users.sources.usersSource.deserializer.maxLineLength = 3000
users.sources.usersSource.includePattern = users_[0-9]{4}-[0-9]{2}-[0-9]{2}.csv

#设置自带的拦截器
users.sources.usersSource.interceptors = head_filter
users.sources.usersSource.interceptors.head_filter.type = regex_filter
users.sources.usersSource.interceptors.head_filter.regex = ^user_id*
users.sources.usersSource.interceptors.head_filter.excludeEvents = true

users.channels.usersChannel.type = file
users.channels.usersChannel.checkpointDir = /opt/dataFile/flumeFile/checkpoint/users
users.channels.usersChannel.dataDir = /opt/dataFile/flumeFile/data/users

users.sinks.usersSink.type = org.apache.flume.sink.kafka.KafkaSink
users.sinks.usersSink.batchSize = 640
users.sinks.usersSink.brokerList = 192.168.206.129:9092
users.sinks.usersSink.topic = users 

users.sources.usersSource.channels = usersChannel
users.sinks.usersSink.channel = usersChannel

例③ 将读取数据同时保存到 kafkahdfs 中:
vi train-flume-hdfs_kafka.conf ,内容如下:

train.sources = trainSource
train.channels = kafkaChannel hdfsChannel
train.sinks = kafkaSink hdfsSink

train.sources.trainSource.type = spooldir
train.sources.trainSource.spoolDir = /opt/dataFile/flumeFile/train
train.sources.trainSource.deserializer = LINE
train.sources.trainSource.deserializer.maxLineLength = 3000
train.sources.trainSource.trainSource.includePattern = train_[0-9]{4}-[0-9]{2}-[0-9]{2}.csv

#使用自带拦截器过滤首行
train.sources.trainSource.interceptors = head_filter
train.sources.trainSource.interceptors.head_filter.type = regex_filter
train.sources.trainSource.interceptors.head_filter.regex = ^user*
train.sources.trainSource.interceptors.head_filter.excludeEvents = true

train.channels.kafkaChannel.type = file
train.channels.kafkaChannel.checkpointDir =  /opt/dataFile/flumeFile/checkpoint/train
train.channels.kafkaChannel.dataDir = 
/opt/dataFile/flumeFile/data/train

train.channels.hdfsChannel.type = memory
train.channels.hdfsChannel.capacity = 64000
train.channels.hdfsChannel.transactionCapacity = 16000

train.sinks.kafkaSink.type = org.apache.flume.sink.kafka.KafkaSink
train.sinks.kafkaSink.batchSize = 640
train.sinks.kafkaSink.brokerList = 192.168.206.129:9092
train.sinks.kafkaSink.topic = train

train.sinks.hdfsSink.type = hdfs
train.sinks.hdfsSink.hdfs.fileType = DataStream
train.sinks.hdfsSink.hdfs.filePrefix = train
train.sinks.hdfsSink.hdfs.fileSuffix = .csv
train.sinks.hdfsSink.hdfs.path = hdfs://192.168.206.129:9000/data/train/%Y-%m-%d
train.sinks.hdfsSink.hdfs.useLocalTimeStamp = true
train.sinks.hdfsSink.hdfs.batchSize = 6400
train.sinks.hdfsSink.hdfs.rollCount = 0
train.sinks.hdfsSink.hdfs.rollSize = 64000000
train.sinks.hdfsSink.hdfs.rollInterval = 10

train.sources.trainSource.channels = hdfsChannel kafkaChannel
train.sinks.hdfsSink.channel = hdfsChannel
train.sinks.kafkaSink.channel = kafkaChannel
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值