Flink学习(1)——基础入门与架构了解

目录

1. 简单介绍

2. 简单开发词频统计demo

3. flink整体架构简介

3.1 API&Libraries层

3.2 Runtime层

3.3 物理运行环境层

4. Flink运行时架构

4.1 运行时核心组件

4.2 job提交流程

4.3 任务调度原理

4.4 任务执行图优化


1. 简单介绍

Apache Flink是一个流式数据处理的框架和分布式计算引擎,支持无界和有界的流式数据处理计算,同时支持批式数据处理计算,是一个。

flink是以事件驱动的,每当数据流中传递进来一个数据后,就会触发flink中的计算逻辑。使用flink最大的优势就是其可以保证低延迟、高吞吐以及计算结果的正确性。

 

2. 简单开发词频统计demo

1. 首先创建maven项目,然后引入jar包依赖

    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>1.10.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java_2.12</artifactId>
            <version>1.10.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.10</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka-0.11_2.12</artifactId>
            <version>1.10.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.bahir</groupId>
            <artifactId>flink-connector-redis_2.11</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.44</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-statebackend-rocksdb_2.12</artifactId>
            <version>1.10.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner_2.12</artifactId>
            <version>1.10.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner-blink_2.12</artifactId>
            <version>1.10.1</version>
        </dependency>
    </dependencies>

2. 简单入门开发demo——词频统计

(1)批处理模式的flink开发:

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.AggregateOperator;
import org.apache.flink.api.java.operators.DataSource;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.Collector;

/**
 * @ClassName WordCount
 * @Description: 批处理模式下的wordcount实例,在flink中,批处理一般常用于针对离线数据计算
 * @Author dinggang
 * @Date 2020/12/3 21:53
 * @Modified By:
 * @Version V1.0
 */
public class WordCount {

    public static void main(String[] args) throws Exception {
        //首先,创建flink批处理执行环境
        ExecutionEnvironment environment = ExecutionEnvironment.getExecutionEnvironment();
        //从文本文件中按行读取数据。这里是创建数据源操作
        DataSource<String> stringDataSource = environment.readTextFile("D:\\idea项目库\\flink_java_study\\src\\main\\resources\\word.txt");
        //对数据集进行处理,按空格分隔,并转换为(word,1)形式的二元组。flatMap方法就表示将数据打散并转化为想要的中间数据格式
        AggregateOperator<Tuple2<String, Integer>> sum = stringDataSource.flatMap(new MyFlatMapper())
                /*
                调用groupBy方法,对每个word进行分组,该操作相当于将同一个word分为一组
                参数0表示取二元组Tuple2中的第几个数据,也就是数据下标
                 */
                .groupBy(0)
                /*
                分组后求和,最终计算步骤,统计求和,这里的参数表示是针对二元组中的那个数据进行计算
                参数含义同上
                 */
                .sum(1);

        /*
        sum就是最后的计算结果,在这里输出查看
         */
        sum.print();
    }

    /*
    自定义FlatMapFunction,泛型中的第一个类表示输入数据类型,第二个类表示将输入数据打散后输出的数据类型
     */
    public static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String, Integer>> {

        //参数s是输入的数据,collector用来存放所有map运算后的结果数据
        public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
            //按空格分词
            String[] s1 = s.split(" ");
            //遍历所有单词,变成二元组输出
            for (String word:s1) {
                collector.collect(new Tuple2<String, Integer>(word, 1));
            }
        }
    }
}

(2)流处理模式下的flink流式运算

/**
 * @ClassName StreamWordCount
 * @Description: flink流式处理,常用于实时计算
 * @Author dinggang
 * @Date 2020/12/3 22:52
 * @Modified By:
 * @Version V1.0
 */
public class StreamWordCount {
    public static void main(String[] args) throws Exception {
        //流式运算的运行环境对象和批处理不同
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        /*
        默认flink执行会采用多线程执行,也就是并行执行,并行度默认为cpu核心数,可以手动设置执行时并行度
        在env上设置的并行度是整个计算任务的并行度,但在局部上可以setParallelism修改
         */
//        env.setParallelism(1);

        //从文本文件中按行读取数据
        DataStreamSource<String> streamSource = env.readTextFile("D:\\idea项目库\\flink_java_study\\src\\main\\resources\\word.txt");
        //对数据集进行处理,按空格分隔,并转换为(word,1)形式的二元组
        SingleOutputStreamOperator<Tuple2<String, Integer>> sum = streamSource.flatMap(new WordCount.MyFlatMapper())
                /*
                对每个word进行分组,该操作相当于将同一个word分为一组
                参数0表示取二元组Tuple2中的第几个数据,也就是数据下标
                 */
                .keyBy(0)
                /*
                最终计算步骤,统计求和
                参数含义同上
                 */
                .sum(1);//.setParallelism(2);//通过setParallelism方法每一个计算步骤都可以设置并行度

        /*
        打印输出
         */
        sum.print();
//        sum.print().setParallelism(1);//每一个计算步骤都可以设置并行度
        env.execute();
    }

    public static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String, Integer>> {

        public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
            //按空格分词
            String[] s1 = s.split(" ");
            //遍历所有单词,变成二元组输出
            for (String word:s1) {
                collector.collect(new Tuple2<String, Integer>(word, 1));
            }
        }
    }
}

直接通过main方法启动,即可看到输出结果。

3. flink整体架构简介

整体架构图如下所示,实际上可以分为三层,分别是API&Libraries层、Runtime核心层以及物理部署环境层(运行环境层)。

3.1 API&Libraries层

作为分布式数据处理框架,Flink同时提供了支撑流计算和批计算的接口,同时在此基础之上抽象出不同的应用类型的组件库,如基于流处理的CEP(复杂事件处理库)、SQL&Table库和基于批处理的FlinkML(机器学习库)等、Gelly(图处理库)等。API层包括构建流计算应用的DataStream API和批计算应用的DataSet API,两者都提供给用户丰富的数据处理高级API,例如Map、FlatMap操作等,同时也提供比较低级的Process Function API,用户可以直接操作状态和时间等底层数据。

3.2 Runtime层

该层主要负责对上层不同接口提供基础服务,也是Flink分布式计算框架的核心实现层,支持分布式Stream作业的执行、JobGraph到ExecutionGraph的映射转换、任务调度等。将DataSteam和DataSet转成统一的可执行的Task Operator,达到在流式引擎下同时处理批量计算和流式计算的目的。

3.3 物理运行环境层

f该层主要涉及Flink的部署模式,目前Flink支持多种部署模式:本地、集群(Standalone/YARN)、云(GCE/EC2)、Kubenetes。Flink能够通过该层能够支持不同平台的部署,用户可以根据需要选择使用对应的部署模式。

 

4. Flink运行时架构

Runtime架构如下图所示,同时也是一个job提交的完整流程图

其中,Master 部分又包含了三个组件,即 Dispatcher、ResourceManager 和 JobManager。

(1)其中,Dispatcher 负责接收用户提供的作业,并且负责为这个 新提交的作业拉起一个新的 JobManager 组件。

(2)ResourceManager 负责资源的 管理,在整个 Flink 集群中只有一个 ResourceManager。

(3)JobManager 负责管理 作业的执行,在一个 Flink 集群中可能有多个作业同时执行,每个作业都有自己的 JobManager 组件。

这三个组件都包含在 AppMaster 进程中。

(1)当用户提交作业的时候,提交脚本会首先启动一个 Client 进程 负责作业的编译与提交。它首先将用户编写的代码编译为一个 JobGraph,在这个过程,它还会进行一些检查或优化等工作,例如判断哪些 Operator 可以 Chain 到同一 个 Task 中。然后,Client 将产生的 JobGraph 提交到集群中执行。此时有两种情 况,一种是类似于 Standalone 这种 Session 模式,AM 会预先启动,此时 Client 直接与 Dispatcher 建立连接并提交作业即可。另一种是 Per-Job 模式,AM 不会 预先启动,此时 Client 将首先向资源管理系统 (如 Yarn、K8S)申请资源来启动 AM,然后再向 AM 中的 Dispatcher 提交作业。

(2)当作业到 Dispatcher 后,Dispatcher 会首先启动一个 JobManager 组件,然 后 JobManager 会向 ResourceManager 申请资源来启动作业中具体的任务。这时根据 Session 和 Per-Job 模式的区别, TaskExecutor 可能已经启动或者尚未启动。如果是前者,此时 ResourceManager 中已有记录了 TaskExecutor 注册的资 源,可以直接选取空闲资源进行分配。否则,ResourceManager 也需要首先向外 部资源管理系统申请资源来启动 TaskExecutor,然后等待 TaskExecutor 注册相 应资源后再继续选择空闲资源进程分配。

(3)目前 Flink 中 TaskExecutor 的资源是通过 Slot 来描述的,一个 Slot 一般可以执行一个具体的 Task,但在一些情况下也可以执 行多个相关联的 Task,这部分内容将在下文进行详述。ResourceManager 选择到 空闲的 Slot 之后,就会通知相应的 TM “将该 Slot 分配分 JobManager XX ”,然 后 TaskExecutor 进行相应的记录后,会向 JobManager 进行注册。JobManager 收到 TaskExecutor 注册上来的 Slot 后,就可以实际提交 Task 了。

(4)TaskExecutor 收到 JobManager 提交的 Task 之后,会启动一个新的线程来 执行该 Task。Task 启动后就会开始进行预先指定的计算,并通过数据 Shuffle 模块 互相交换数据。

以上就是 Flink Runtime 层执行作业的基本流程。可以看出,Flink 支持两种不 同的模式,即 Per-job 模式与 Session 模式。Per-job 模式下整个 Flink 集群只执行单个作业,即每个作业会独享 Dispatcher 和 ResourceManager 组件。此外,Per-job 模式下 AppMaster 和 TaskExecutor 都是按需申请的。因 此,Per-job 模式更适合运行执行时间较长的大作业,这些作业对稳定性要求较 高,并且对申请资源的时间不敏感。与之对应,在 Session 模式下,Flink 预先启动 AppMaster 以及一组 TaskExecutor,然后在整个集群的生命周期中会执行多个作 业。可以看出,Session 模式更适合规模小,执行时间短的作业。

4.1 运行时核心组件

1. JobManager(作业管理器):

(1)控制每一个Flink执行任务的主进程,相当于Master节点。

(2)JobManager会最先接收到Client上传的Flink任务,其中包括了作业图(JobGraph)、逻辑数据流图(logical dataflow graph)、以及打包的代码jar包和其他资源文件。

(3)JobManager会把JobGraph转换为物理层面的数据流图,叫做执行图(ExecutionGraph),包含了所有并发执行的任务。

(4)JobManager会向资源管理器ResourceManager请求执行任务所需要的资源,这个资源是TaskManager中所管理的TaskSlot,一旦获取到了足够的TaskSlot,就会将执行图中的任务分发到TaskManager上执行

(5)任务下发后,JobManager就会开始负责各个TaskManager之间的协调调度,Flink JobManager会触发Checkpoints操作,每个TaskManager节点收到Checkpoint触发指令后,完成Checkpoint操作,所有的Checkpoint协调过程都是在Flink JobManager中完成。

(6)当任务完成后,Flink会将任务执行的信息反馈给客户端,并且释放掉TaskManager中的资源以供下一次提交任务使用。

2.TaskManager(任务管理器):

(1)TaskManager负责管理任务资源TaskSlot(内存、CPU等),相当于整个集群的Slave节点,一个Flink集群中会有多个TaskManager,每个TaskManager下都管理了一定数量的TaskSlot,TaskSlot的数量决定了一个TaskManager的执行任务数。

(2)启动后,TaskManager会先向ResourceManager注册其持有的TaskSlot,当Client客户端提交一个Job并生成一个JobManager后,Job会从ResourceManager中申请TaskManager下的TaskSlot,然后将Job分发给TaskManager,有TaskManager发给具体的TaskSlot执行算子任务。

(3)在执行过程中,运行同一个Job的TaskManager会与其他TaskManager交换数据。

3.ResourceManager(资源管理器):

(1)主要负责管理TaskManager中的所有TaskSlot。

(2)当JobManager向ResourceManager申请资源时,ResourceManager将有空闲Slot资源的TaskManager分配给JobManager,如果ResourceManager中剩余的空闲TaskSlot数量不足,那么就会向资源提供平台发起会话,申请用来启动新的TaskManager的容器资源。比如如果是部署在yarn上,那么就会向yarn申请资源,如果是在K8S上,就向k8s申请资源。

4.Dispatcher(分发器):

(1)当一个job提交的时候,分发器就会启动,并将job移交给JobManager。

(2)Dispatcher实际上并不是绑定于某一个Job作业的,其实是公用的,它提供了Rest接口,会启动一个Web UI,方便用来客户端提交和展示Job执行信息。

(3)Dispatcher并不是必须的,因为job提交运行的方式有多种,但这种会比较方便。

4.2 job提交流程

这是简化的job提交流程图,完整的流程应该是上面的Runtime架构图

以yarn部署flink为例,能够更好的理解上面的Runtime架构图

4.3 任务调度原理

作业调度可以看做是对资源和任务进行匹配的过程。在 Flink 中,资源是通过 Slot 来表示的,每个 Slot 可以用来执行不同的 Task。而在另一端,任务即 Job 中 实际的 Task,它包含了待执行的用户逻辑。

调度的主要目的就是为了给 Task 找到 匹配的 Slot。逻辑上来说,每个 Slot 都应该有一个向量来描述它所能提供的各种资 源的量,每个 Task 也需要相应的说明它所需要的各种资源的量。但是实际上在 1.9 之前,Flink 是不支持细粒度的资源描述的,而是统一的认为每个 Slot 提供的资源和 Task 需要的资源都是相同的。从 1.9 开始,Flink 开始增加对细粒度的资源匹配的支持的实现。

作业调度的基础是首先提供对资源的管理。Flink 中的资源是由 Slot 来表示的。下图中,在 ResourceManager 中,有一个子组件叫做 SlotManager,它 维护了当前集群中所有 TaskManager 上的 Slot 的信息与状态,如该 Slot 在哪个 TaskManager 中,该 Slot 当前是否空闲等信息。

(1)当 JobManger 来为特定 Task 申请资源的时候,根据当前是 Per-job 还是 Session 模式,ResourceManager 可能会去申请资源来启动新的 TaskManager。

(2)当 TaskManager 启动之后,它会通过服务发现找到当前活跃的 ResourceManager 并进行注册。在注册信息中,会包含该 TaskManager 中所有 Slot 的信息。 ResourceManager 收到注册信息后,其中 的 SlotManager 就会记录下相应的 Slot 信息。

(3)当 JobManager 为某个 Task 来申 请资源时, SlotManager 就会从当前空闲的 Slot 中按一定规则选择一个空闲的 Slot 进行分配。

(4)当分配完成后,RM 会首先向 TaskManager 发送 RPC 要求将选定的 Slot 分配给特定的 JobManager。TaskManager 如果还没有执行过 该 JobManager 的 Task 的话,它需要首先向相应的 JobManager 建立连接,然 后发送提供 Slot 的 RPC 请求。

(5)在 JobManager 中,所有 Task 的请求会缓存到 SlotPool 中。当有 Slot 被提供之后,SlotPool 会从缓存的请求中选择相应的请求并结束相应的请求过程。

(6)当 Task 结束之后,无论是正常结束还是异常结束,都会通知 JobManager 相 应的结束状态,然后在 TaskManager 端将 Slot 标记为已占用但未执行任务的状 态。JobManager 会首先将相应的 Slot 缓存到 SlotPool 中,但不会立即释放。这种方式避免了如果将 Slot 直接还给 ResourceManager,在任务异常结束之后需要重启时,需要立刻重新申请 Slot 的问题。通过延时释放,Failover 的 Task 可以 尽快调度回原来的 TaskManager,从而加快 Failover 的速度。

(7)当 SlotPool 中缓存的 Slot 超过指定的时间仍未使用时,SlotPool 就会发起释放该 Slot 的过程。与申请 Slot 的过程对应,SlotPool 会首先通知 TaskManager 来释放该 Slot,然后 TaskManager 通知 ResourceManager 该 Slot 已经被释放,从而最终完成释放的 逻辑。 除了正常的通信逻辑外,在 ResourceManager 和 TaskManager 之间还存在 定时的心跳消息来同步 Slot 的状态。在分布式系统中,消息的丢失、错乱不可避免, 这些问题会在分布式系统的组件中引入不一致状态,如果没有定时消息,那么组件无 法从这些不一致状态中恢复。此外,当组件之间长时间未收到对方的心跳时,就会认 为对应的组件已经失效,并进入到 Failover 的流程。

在 Slot 管理基础上,Flink 可以将 Task 调度到相应的 Slot 当中。如上文所 述,Flink 尚未完全引入细粒度的资源匹配,默认情况下,每个 Slot 可以分配给一个 Task。但是,这种方式在某些情况下会导致资源利用率不高。如图 5 所示,假如 A、 B、C 依次执行计算逻辑,那么给 A、B、C 分配分配单独的 Slot 就会导致资源利用 率不高。为了解决这一问题,Flink 提供了 Share Slot 的机制。如图 5 所示,基于 Share Slot,每个 Slot 中可以部署来自不同 JobVertex 的多个任务,但是不能部署 来自同一个 JobVertex 的 Task。如图 5 所示,每个 Slot 中最多可以部署同一个 A、 B 或 C 的 Task,但是可以同时部署 A、B 和 C 的各一个 Task。当单个 Task 占用 资源较少时,Share Slot 可以提高资源利用率。 此外,Share Slot 也提供了一种简 单的保持负载均衡的方式。

基于上述 Slot 管理和分配的逻辑,JobManager 负责维护作业中 Task 执 行的状态。如上文所述,Client 端会向 JobManager 提交一个 JobGraph,它 代表了作业的逻辑结构。JobManager 会根据 JobGraph 按并发展开,从而得到 JobManager 中关键的 ExecutionGraph。ExecutionGraph 的结构如下图 所示, 与 JobGraph 相比,ExecutionGraph 中对于每个 Task 与中间结果等均创建了对 应的对象,从而可以维护这些实体的信息与状态。

在一个 Flink Job 中是包含多个 Task 的,因此另一个关键的问题是在 Flink 中 按什么顺序来调度 Task。如下图所示,目前 Flink 提供了两种基本的调度逻辑,即 Eager 调度与 Lazy From Source。Eager 调度会在作业启动时,申请资源将所有的 Task 调度起来。这种调度算法主要用来调度可能没有终止的流作业。与之对应,Lazy From Source 则是从 Source 开始,按拓扑顺序来进行调度。 简单来说,Lazy From Source 会先调度没有上游任务的 Source 任务,当这些任 务执行完成时,它会将输出数据缓存到内存或者写入到磁盘中。然后,对于后续的任务,当它的前驱任务全部执行完成后,Flink 就会将这些任务调度起来。这些任务会从读取上游缓存的输出数据进行自己的计算。这一过程继续进行直到所有的任务完成计算。

4.4 任务执行图优化

问题:

1、JobManager在依据解析出的执行图,会为并行计算的任务申请多少个Slot?

2、一个流处理程序Job,解析后会包含多少个任务?

所有Flink程序都是由三部分组成,source、tansformation和sink,source负责读取数据,tansformation利用各个算子(map、keyby、window等)负责进行数据转换、数据计算,sink则负责将结果数据输出。

1.  算子任务的个数被称为算子并行度(比如下图中source、map任务的并行度就是2),而一个job的并行度则是指所有算子任务中的最大并行度。

每一个算子任务都是运行在TaskManager上的TaskSlot中,TaskManager实际上就是一个JVM进程,而Slot就是一个独立的线程,Slot之间是互相隔离的,所以,Job的并行能力同时也会受到slot数量的限制。

2.  flink为了节省资源,减少数据交互,提高资源利用率,重复利用申请的Slot,默认允许不同算子任务之间共享Slot,所以就会发生如下图所示的情况,同一个Job下的不同子任务可能会在同一个Slot中运行,甚至会出现一个Job的整个管道(或者说一条完整的从source到sink任务流水线)都会在同一个Slot上运行。

但是并行的同一个任务一定不会在同一个Slot运行,比如下图中的source任务的并行度为2,所以会有2个source算子任务,这两个算子任务不会运行在同一个Slot上,因为一个Slot是一个线程,一个线程同一时间只能运行一个算子任务,如果两个source任务安排在了同一个Slot,那就不是并行计算。

当然,我们可以在代码里指定每一个算子任务是否可以共享Slot,slotSharingGroup方法表示设置当前算子任务的Slot组,只有同一个slot组的算子任务才可共享slot,所以通过这个方法可以取消算子任务之间的Slot共享,不调用此方法设置共享组名称的话,默认都是default。

SingleOutputStreamOperator<Tuple2<String, Integer>> sum = streamSource.flatMap(new WordCount.MyFlatMapper()).slotSharingGroup("red")
                .keyBy(0)
                .sum(1).slotSharingGroup("blue");

所以,我们这里可以计算出一个Job任务提交后所需要的Slot数量计算方式为:各个Slot组内最大并行度之和。

 

3. 任务执行图的生成:Flink中的任务执行图可以分为4层,StreamGraph——>JobGraph——>ExecutionGraph——>物理执行图

(1)StreamGraph:根据用户通过API编写的代码生成的最初的执行图,用来表示程序的拓扑结构。

(2)JobGraph:将StreamGraph优化后生成JobGraph,然后交给JobManager的数据结构,主要进行的优化就是多任务节点合并

(3)ExecutionGraph:JobManager根据JobGraph生成ExecutionGraph,ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。

(4)物理执行图:JobManager根据ExecutionGraph对Job进行调度后在各个TaskManager上部署taks后形成的图,并不是一个数据结构。

4.  数据传输和任务链:上面已经说过,Flink会把多个任务放在一个Slot中执行,也就是共享Slot来提高效率,但是实际上还有一个优化就是合并任务,即将多个任务合并为一个任务执行。

(1)数据从source到sink会流经多个算子任务,算子之间的数据的传输形式主要有两种,一种是one-to-one,一种就是Redistributing,具体是哪一种形式,取决于算子的种类

One-to-one:stream维护着分区以及元素的顺序(比如source和map之间)。 这意味着map 算子的子任务看到的元素的个数以及顺序跟 source 算子的子任务 生产的元素的个数、顺序相同。比如source和map、fliter、flatMap等算子之间都是one-to-one 的对应关系。

Redistributing:stream的分区会发生改变。每一个算子的子任务依据所选择的 transformation发送数据到不同的目标任务。例如,keyBy 基于 hashCode 重 分区、而 broadcast 和 rebalance 会随机重新分区,这些算子都会引起 redistribute过程

(2)Flink 采用了一种称为任务链的优化技术,可以在特定条件下减少本地 通信的开销。为了满足任务链的要求,必须将两个或多个算子设为相同 的并行度,并通过本地转发(local forward)的方式进行连接,这样就可以减少算子任务之间的数据传输,提升性能。

任务链形成要求:必须是相同并行度的one-to-one算子操作

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值