Spark Core知识点总结

一、RDD概述

RDD (Resilient Distributed Dataset):弹性分布式数据集,是Spark中最基本的数据抽象

1.1 RDD的属性

一组分区(partition),即数据集的基本组成单位;

一个计算每个分区的函数;

RDD之间的依赖关系

一个Partitioner,即RDD的分片函数

一个列表,存储存取每个Partition的优先位置(preferred location)

1.2 RDD的特点

  1. 分区

    RDD和MapReduce都要支持分区是因为它们处理的是非常大的数据集

  2. 只读

    由一个RDD转换到另一个RDD,可以通过算子实现

    RDD的操作算子包括两类:

    • transformations:转换算子,将RDD进行转换,构建RDD的血缘关系
    • actions:动作算子(立即执行,返回都是结果),用来触发RDD的计算,得到RDD的相关计算结果/将RDD保存到文件系统中
  3. 依赖

    RDD通过操作算子进行转换,所以之间存在依赖

    RDD依赖包括两种:

    • 窄依赖:RDDs之间分区是一一对应的
    • 宽依赖:下游RDD的每个分区与上游RDD(父RDD)的每个分区都有关(多对一的关系)
  4. 缓存

    cachepersist

    内存中缓存,内部的优化机制;当RDD重复被使用了,不需要再重新计算,直接从内存中获取使用,加速后期重用

  5. 容错

    spark的容错有两种:

    • Lineage:血缘关系,根据血缘关系重新计算,进行容错
    • CheckPoint:设置检查点,一般都是文件系统,磁盘IO

二、RDD编程

2.1 编程模型

在Spark中,RDD被表示为对象,通过对象上的方法调用来对RDD进行转换。

经过一系列的transformations定义RDD之后,就可以调用actions触发RDD的计算,action可以是向应用程序返回结果(count,collect等),或是向存储系统保存数据。

在Spark中,只有遇到action,才会执行RDD的计算(即延迟计算)。

RDD的并行度

一个RDD可以有多个分片,一个分片对应一个task,分片的个数决定并行度;并行度并不是越高越好,还要考虑资源的情况。

2.2 RDD的创建

  1. 通过本地集合创建RDD

    两种函数:parallelize()和makeRDD()

  2. 通过外部数据创建RDD

    textfile(" "):传入的是读取路径;hdfs://… 、file://…

    /…/…:这种方式分为在集群中执行和在本地执行;如果在集群中,读的是HDFS,本地读的是文件系统

    假如传入的path是hdfs://…,分区是由HDFS中文件的block决定的

  3. 通过其他RDD衍生新的RDD

    通过算子操作,RDD是不可变的

2.3 RDD的转化

RDD整体上分为value类型和key-value类型

2.3.1 Value类型
算子类型作用介绍
map返回一个新的RDD,该RDD由每一个输入元素经过函数转换后组成
mapPartitions类似于map,但独立地在RDD地每一个分片上运行,因此在类型上为T的RDD上运行时,函数类型必须是Iteraror[T] => Iterator[U]
mapPatitionsWithIndex类似于mapPartitions,但函数带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,函数类型必须是(Int, Iterator[T]) => Iterator[U]
flatMap类似于map,但是每一个输入元素可以被映射为0个过多个输出元素,所以函数应该返回一个序列,而不是单一元素
glom将每一个分区形成一个数组,形成新的RDD类型是RDD[Array[T]]
groupBy分组,按照传入函数的返回值进行分组,将相同的key对应的值放入一个迭代器
filter过滤,返回一个新的RDD,该RDD由经过函数计算后返回值为true的输入元素组成
sample以指定的随机种子随机抽样出数量为fraction的数据
distinct([numTasks])对原RDD进行去重后返回一个新的RDD,默认情况下,只有8个并行任务来操作,但是可以传入一个可选的numTasks参数改变它
coalesce(numTasks)缩减分区数,用于大数据集过滤后,提高小数据集的执行效率
repatition(numPartitions)根据分区数,重新通过网络随机洗牌所有数据
sortBy(func, [ascending], [numTasks])使用func先对数据进行处理,按照处理后的数据比较结果排序,默认正序
pipe(command, [envVars])管道,针对每个分区,都执行一个shell脚本,返回输出的RDD;注意:脚本需要放在Worker节点可以访问到的位置
  1. map()和mapPartitions()的区别?

    map():每次处理一条数据

    mapPartitions():每次处理一个分区的数据,这个分区的数据处理完后,原RDD分区的数据才能释放,可能导致OOM(内存溢出)

    当内存空间较大的时候建议使用mapPartitions(),以提高处理效率

  2. coalesce和repartition的区别?

    coalesce:重新分区,可以选择是否进行shuffle过程,由参数shuffle: Boolean = false/true决定

    reparation:实际上是调用coalesce,默认是进行shuffle的

2.3.2 双Value类型交互
算子类型作用介绍
union(otherDataSet)并集;对原RDD和参数RDD求并集后返回一个新的RDD
subtract(otherDataSet)差集;计算差的一种函数,去除两个RDD中相同的元素,不同的RDD将保留下来
intersection(otherDataSet)交集;对原RDD和参数RDD求交集后返回一个新的RDD
cartesian(otherDataSet)笛卡尔积(尽量避免使用)
zip(otherDataSet)将两个RDD组合成key/value形成的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常
2.3.3 Key-Value类型
算子类型作用介绍
partitionBy对pairRDD进行分区操作,如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区,否则会生成shuffleRDD,即会产生shuffle过程
groupByKeygroupByKey也是对每个key进行操作,但只生成一个sequence
reduceByKey(func, [numTasks])在一个(k, v)的RDD上调用,返回一个(k, v)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,reduce任务的个数可以通过第二个可选参数来设置
aggregateByKey在kv对的RDD中,按key将value进行分组合并,合并时,将每个value和初始值作为seq函数的参数,进行计算,返回的结果作为一个新的kv对,然后再将结果按照key进行合并,最后将每个分组的value传递给combine函数进行计算(先将前两个value进行计算,将返回结果和下一个value传给combine函数,以此类推),将key与计算结果作为一个新的kv对输出
foldByKeyaggregateByKey的简化操作,seqop和combop相同
combineByKey[c]对相同k,把v合并成一个集合
sortByKey([ascending], [numTasks])在一个(k, v)的RDD上调用,k必须实现Ordered接口,返回一个按照key进行排序的(k, v)的RDD
mapValues针对于(k, v)形式的参数类型只对v进行操作
join(otherDataset, [numTasks])在类型为(k, v)和(k, w)的RDD上调用,返回一个相同的key对应的所有元素对在一起的(k, (v, w))的RDD
cogroup(otherDataset, [numTasks])在类型为(k, v)和(k, w)的RDD上调用,返回一个(k, (Iterable< v>, Iterable< w>))类型的RDD
  1. reduceByKey和groupByKey的区别?

    reduceByKey:按照key进行聚合,在shuffle之前由combine(预聚合)操作,返回结果是RDD[k, v]

    groupByKey:按照key进行分组,直接进行shuffle

    reduceByKey相比于GroupByKey更建议使用,但需要注意是否会影响业务逻辑

  2. 使用什么方法可以代替join?

    广播变量 + map + filter

2.4 Action

算子类型作用介绍
reduce通过函数聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据
collect在驱动程序中,以数组的形式返回数据集的所有元素
count返回RDD中元素的个数
first返回RDD中的第一个元素
take(n)返回一个由RDD的前n个元素组成的数组
takeOrdered(n)返回该RDD排序后的前n个元素组成的数组
aggregate将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作;这个函数最终返回的类型不需要和RDD中元素类型一致
fold(num)折叠操作,aggregate的简化操作,seqop和combop一样
countByKey针对(k, v)类型的RDD,返回一个(k, Int)的map,表示每一个key对应的元素个数
foreach在数据集的每一个元素上,运行函数进行更新
saveAsTextFile(path)将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本
saveAsSequenceFile(path)将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统
saveAsObjectFile(path)用于将RDD中的元素序列化成对象,存储到文件中

宽依赖算子

所有的ByKey算子;repartition,coalesce算子;部分join算子

2.5 RDD的依赖关系

2.5.1 Lineage

RDD只支持粗粒度转换,即在大量记录上执行的单个操作;将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区;RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

Lineage:血缘关系,根据血缘关系从新计算,进行容错。

注意:RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)

2.5.2 DAG(Directed Acyclic Graph) 有向无环图

原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage;

对于窄依赖,partition的转换处理在Stage中完成计算;

对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算;

因此宽依赖是划分Stage的依据

2.5.3 任务划分

RDD 任务切分中间分为:Application、Job、Stage和Task

  1. Application:初始化一个SparkContext即生成一个Application

  2. Job:一个Action算子就会生成一个Job

  3. Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage

    DAG如何划分Stage?

    会把DAG划分为不同的阶段,划分依据就是看算子有没有shuffle,从最后的一个RDD开始往前面倒推,如果上一个RDD变成这个RDD没有发生shuffle,上一个RDD和这个RDD在一个stage里面,如果上一个RDD变成这个RDD发生了shuffle,那么上一个RDD就在一个新的stage里面,这个stage就结束了;然后新的stage里面,上一个RDD就是这个stage最后一个RDD,然后继续前面的操作,往前面追溯,直到把整个DAG全部追溯完,这个DAG就被划分为了多个stage

  4. Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task

    spark程序执行的最小单位,spark集群的worker里面运行的是一个个的task

    Task分为shuffleMapTask和resultTask

注意:Application -> Job -> Stage -> Task每一层都是1对n的关系

2.6 RDD的缓存

RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下persist()会把数据以序列化的形式缓存在JVM的堆空间中

但是并不是这两个方法被调用时立即缓存,而是触发后面的action 时,该RDD将会被缓存在计算节点的内存中,并供后面重用

存储级别StorageLevel

末尾加“_2”表示把持久化的数据存为两份

级别使用的空间CPU时间是否在内存中是否在磁盘上
NONE//
DISK_ONLY
MEMORY_ONLY
MEMORY_ONLY_SER
MEMORY_AND_DISK中等部分部分
MEMORY_AND_DISK_SER部分部分
OFF_HEAP//

spark持久化的选择?

如果数据在内存中放不下,则在内存中存放序列化数据,最后选择磁盘

2.7 RDD CheckPoint

设置检查点(本质是通过将RDD写入DISK做检查点)是为了通过Lineage做容错的辅助,Lineage过长会造成容错成本高,所以在中间阶段做检查点容错,这样会减少开销。

在CheckPoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除;对RDD进行CheckPoint操作并不会马上被执行,必须执行Action操作才能触发。

三、键值对RDD数据分区器

Spark目前支持Hash分区Range分区,用户也可以自定义分区

Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数。

注意

  1. 只有Key-Value类型的RDD才有分区器的,非Key-Value类型的RDD分区器的值是None
  2. 每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的

Rdd partition的个数由什么来决定的

  1. 默认的,两个
  2. 指定的,numPartitions
  3. 从hdfs读取数据,由块的个数决定
  4. 从kafka读取数据,由topic的partition个数决定

3.1 获取RDD分区

获取分区:.partitioner

重新分区:HashPartitioner()

3.2 Hash分区

HashPartitioner分区的原理:对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于0,则用余数 + 分区的个数(否则加0),最后返回的值就是这个key所属的分区ID

弊端:可能导致每个分区数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据

3.3 Ranger分区

将一定范围的数据映射到某一个分区内

RangePartitioner作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的

3.4 自定义分区

要实现自定义的分区器,需要继承org.apache.spark.Partitioner类,并实现下面三个方法

  1. numPartitions: Int:返回创建出来的分区数
  2. getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)
  3. equals():Java判断相等性的标准方法

四、数据读取与保存

Spark的数据读取,SparkCore连接MySQL及HBase的数据读取

五、RDD编程进阶

5.1 累加器

累加器用来对信息进行聚合,通常在向Spark传递函数时,如果想实现所有分片处理时更新共享变量的功能,那么累加器可以实现想要的效果

调用SparkContext.accumulator(initialValue)方法

声明累加器:val accu = new LongAccumulator

:工作节点上的任务不能访问累加器的值;从任务的角度来看,累加器是一个只写变量

对于要在行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次

因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,必须把它放在foreach()这样的行动操作中

转化操作中累加器可能会发生不止一次更新

累加器的特点

  1. 累加器在全局唯一的,只增不减,记录全局集群的唯一状态
  2. 在exe中修改它,在driver读取
  3. executor级别共享的,广播变量是task级别的共享
  4. 两个application不可以共享累加器,但是同一个app不同的job可以共享

5.2 自定义累加器

自定义累加器(含代码)

5.3 广播变量(调优策略)

广播变量用来高效分发较大的对象

向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用

在多个并行操作中使用同一个变量,但是Spark会为每个任务分别发送

将变量(num)广播出去:val bc: Broadcast[Int] = sc.broadcast(num)

  1. 在算子中使用广播变量代替直接引用集合,只会复制executor一样的数量
  2. 在使用广播之前,赋值map了task数量份
  3. 在使用广播以后,赋值次数和executor数量一致

六、spark的提交流程

在这里插入图片描述

  1. 打包程序 xxx.jar,上传到某个节点上;

  2. 执行一个SparkSubmit,在SparkSubmit里会写各种配置信息,包括–master、需要的cpu、内存等;

  3. 以Client为例,会在提交的节点上启动一个Driver(就是Application)进程;

  4. 创建SparkContext对象,会在内部创建DAGScheduler和TaskScheduler;

  5. 在Driver代码中,如果遇到了Action算子,就会创建一个Job;

    在Spark中,有多少个Action,就会产生多少个Job

  6. DAGScheduler会接收Job,会为这个job生成DAG;

  7. 把DAG划分为Stage;

  8. 把Stage里面的Task切分出来,生成TaskSet;

  9. 接收TaskSet后,调度Task;

  10. TaskScheduler里会有一个后台程序,去专门连接Master,向Master注册(就是告诉Master是什么程序,需要什么资源(cpu、内存));

  11. Master接收到Dirver端的注册;

    结合需要的资源和本身空闲的资源,利用资源调度算法来决定在哪些Worker上运行这个Application算法,有两种策略:

    • 尽量打散:尽量让需要的资源平均的在不同的机器上启动
    • 尽量集中:尽量在某一台或某几台机器上启动
  12. Master通知Worker去启动Executor;

  13. Worker启动Executor(需要运行的cpu和内存);

  14. Executor反向注册Driver里面的TaskScheduler;

  15. TaskScheduler接收Executor的反向注册Task的分配算法,把TaskSet里面的Task分配给executor;

    接收到的是一个序列化的文件,先反序列化拷贝等,生成Task,Task里面由RDD的执行算子,一些方法需要的常量;

    Executor接收到很多Task,每接收到一个Task都会从线程池里面获取一个线程,用TaskRunner来执行Task

    Task分为两种:ShuffleMapTask和ResultTask,最后的一个Stage对应的Task是ResultTask,之前所有的Stage对应的Task都是ShuffleMapTask;如果Spark程序执行的是ShuffleMapTask,那么程序在执行完这个Stage之后,还需要继续执行下一个Stage

  16. Spark程序就是Stage被切分为很多Task,封装到TaskSet里面,提交给Executor执行,一个Stage一个Stage的执行,每个Task对应一个RDD的partition,这个Task执行的就是所写的算子操作,最后直到最后一个Stage执行完毕。

七、扩展

7.1 spark的优化?

  1. 避免创建重复的RDD

  2. 尽可能使用同一个RDD

  3. 对多次使用的RDD进行持久化

  4. 尽量避免使用shuffle类算子

  5. 使用map-side预聚合的shuffle操作

  6. 使用高性能的算子

  7. 广播大变量

  8. 使用kryo优化序列化性能

  9. 优化数据结构

    对象、字符串、集合都比较占用内存

    字符串代替对象,数组代替集合,使用原始类型(比如Int、Long)代替字符串

  10. 资源调优

  11. 数据倾斜调优

    map filter

7.2 excutor内存的分配?

内存会被分为几个部分:

  1. 第一块是让task执行自己编写的代码时使用,默认是占Executor总内存的20%

  2. 第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用,默认也是占Executor总内存的20%

    spark.shuffle.memoryFraction,用来调节executor中,进行数据shuffle所占用的内存大小默认是0.2

  3. 第三块是让RDD持久化时使用,默认占Executor总内存的60%
    spark.storage.memoryFraction,用来调节executor中,进行数据持久化所占用的内存大小,默认是0.6

7.3 节点和task 执行的关系?

  1. 每个节点可以起一个或多个Executor
  2. 每个Executor由若干core组成,每个Executor的每个core一次只能执行一个Task
  3. 每个Task执行的结果就是生成了目标RDD的一个partiton

注意

这里的core是虚拟的core而不是机器的物理CPU核,可以理解为就是Executor的一个工作线程

而Task被执行的并发度 = Executor数目 * 每个Executor核数

7.4 spark的序列化?

  1. Java序列化

    在默认情况下,Spark采用Java的ObjectOutputStream序列化一个对象,该方式适用于所有实现了java.io.Serializable的类。

    通过继承 java.io.Externalizable,能进一步控制序列化的性能。

    Java序列化非常灵活,但是速度较慢,在某些情况下序列化的结果也比较大。

  2. Kryo序列化

    Spark也能使用Kryo(版本2)序列化对象。

    Kryo不但速度极快,而且产生的结果更为紧凑(通常能提高10倍)。

    Kryo的缺点是不支持所有类型,为了更好的性能,需要提前注册程序中所使用的类(class)。

  3. 序列化的作用

    将对象或者其他数据结构转换成二进制流,便于传输,后续再使用反序列化将其还原;因为二进制流是最便于网络传输的数据格式。

    序列化可以减少数据的体积,减少存储空间,高效存储和传输数据;不好的是使用的时候要反序列化,非常消耗CPU。

7.5 Spark中数据倾斜引发原因?

  1. key本身分布不均衡
  2. 计算方式有误
  3. 过多的数据在一个task里面
  4. shuffle并行度不够

7.6 持久化和容错的应用场景?

  1. RDD数据持久化的应用场景

    某个RDD数据被多次使用,即重复RDD

    某个RDD数据来之不易(经过复杂的处理得到的RDD),使用超过1次

  2. RDD数据通常选择的持久化策略

    MEMORY_ONLY_2

    MEMORY_AND_DISK_SER_2

  3. 容错的应用场景(为什么需要容错)

    节点挂点,数据丢失的时候需要容错机制,恢复分区,找回丢失的数据

7.7 hadoop和spark的shuffle过程?

hadoop:map端保存分片数据,通过网络收集到reduce端。

spark:spark的shuffle是在DAGSchedular划分Stage的时候产生的,TaskSchedule要分发Stage到各个worker的executor。

减少shuffle可以提高性能。

Spark Shuffle的优化

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spark-Core文档是本人经三年总结笔记汇总而来,对于自我学习Spark核心基础知识非常方便,资料中例举完善,内容丰富。具体目录如下: 目录 第一章 Spark简介与计算模型 3 1 What is Spark 3 2 Spark简介 3 3 Spark历史 4 4 BDAS生态系统 4 5 Spark与Hadoop的差异 5 6 Spark的适用场景 6 7 Spark成功案例 6 第二章 Spark开发环境搭建 8 1 Spark运行模式 8 2 Spark环境搭建 8 2.1Scala的安装 8 2.2Spark的单节点配置 9 2.3Spark-Standalone集群配置 9 2.4Spark-on-Yarn模式配置 12 2.5Spark-on-Mesos模式配置 13 2.6Hive-on-Spark配置 13 第三章 Spark计算模型 15 1 RDD编程 15 1.1弹性分布式数据集RDD 15 1.2构建RDD对象 15 2RDD操作 15 2.1将函数传递给Spark 16 2.2了解闭包 16 2.3Pair RDD模型 17 2.4Spark常见转换操作 18 2.5Spark常见行动操作 20 2.6RDD持久化操作 21 2.7注意事项 23 2.7并行度调优 24 2.8分区方式 25 3Examle:PageRank 27 第四章 Spark编程进阶 29 1共享变量 29 1.1累加器 30 1.2广播变量 31 2基于分区进行操作 32 3与外部程序间的管道 33 4数值RDD的操作 34 5 Spark Shuffle机制 34 第五章 Spark调优与调试 39 1开发调优: 40 1.1调优概述 40 1.2原则一:避免创建重复的RDD 40 1.3原则二:尽可能复用同一个RDD 41 1.4原则三:对多次使用的RDD进行持久化 42 1.5原则四:尽量避免使用shuffle类算子 43 1.6原则五:使用map-side预聚合的shuffle操作 44 1.7原则六:使用高性能的算子 45 1.8原则七:广播大变量 46 1.9原则八:使用Kryo优化序列化性能 47 1.10原则九:优化数据结构 48 2资源调优 48 2.1调优概述 48 2.2 Spark作业基本运行原理 49 2.3资源参数调优 50 第六章 Spark架构和工作机制 52 1 Spark架构 52 1.1 Spark架构组件简介 52 1.2 Spark架构图 54 2 Spark工作机制 54 2.1 Spark作业基本概念 54 2.2 Spark程序与作业概念映射 55 2.3 Spark作业运行流程 55 3 Spark工作原理 55 3.1 作业调度简介 55 3.2 Application调度 56 3.3 Job调度 56 3.4 Tasks延时调度 56 第七章 Spark运行原理 57 1 Spark运行基本流程 57 2 Spark在不同集群中的运行架构 58 2.1 Spark on Standalone运行过程 59 2.2 Spark on YARN运行过程 60

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值