最新【Spark深入学习 -14】Spark应用经验与程序调优,2024年最新2024大厂大数据开发高级面试题及答案

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

主要涉及到的是jar包上传的优化,这里面分为2种情况:

1)程序依赖的jar,这种通常是spark lib目录下的所有jar包,有好几百兆,spark程序会上传这些,为了提升效率,可以提前上传好,

2)程序自身的jar包,如果程序不经常变动,也可以提前上传到HDFS上。

指标二:观察WebUI产生的系统监控数据

这里面有很多指标,罗列如下

1)观察job监控参数,产生了多少个job,一个action对应一个job,如果action之间没有依赖关系,资源有富余,可以让job并行执行。

2)观察stage监控参数,一个job分解成了结果stage,每个stage执行了多少时间,输入了多少数据量,shuffle read了多少数据,shuffle write 了多少数据。

3)观察executor监控参数,driver在哪里,executor在哪里,每个executor启动了几个CPU核数,起了多少内存,输入多少数据(可以查看数据是否有倾斜),shuffle了多少数据,内存放了多少数据,GC执行了多少时间。还可以查看stderr,查看日志。

4)观察每个task监控参数,task执行的时间,GC执行的时间,读入的数据总大小和记录数,shuffle的大小,task执行时的本地行,举个执行的任务例子,如下所示

可以看出来,一共有2个stage,1个stage包含8个task,一个包含2个task,先跑8个的,再跑2个的,一个14秒,一个0.1秒。再看看executor,发现只有2个executor,一个executor只有1个core,也就是一个executor只能处理一个task,集群也就是最多跑2个,10个task要跑5轮

再点击stage的链接进去,观察每个task跑多长时间

好了,按照前面罗列的监控指标点,可以看出如下监控参数:

集群资源:内存3个节点18G,VCORE:3个节点24VCORE

1)Job监控参数:只有一个Job

2)Stage监控参数:有2个Stage

·  stage1:8个task,这个先跑,执行了14秒

·  stage2:2个task,这个后跑,执行了0.1秒,是collect方法,数据汇聚到driver

3)executor:2个executor,1个executor 1G内存

4)task:本地性node_local,GC毫秒级,shuffle也是不足Kb

分析消耗的资源:

1)从stage的执行时间分析,stage1执行时间长,可以考虑优化stage1

2)executor分析:只有2个executor,内存也只有1G,启动的都是默认参数,和集群资源相比,有很作资源没有利用起来,有点浪费,可以调整executor个数,增大并发度,或者如果数据量大,也有必要,增大内存。

3)task:本地行还可以,node级别,gc也可以,shuffle也少

总结可能优化的点:

优化stage1,Job中的stage1有8个task,2个executor,需要跑4轮才能跑完第一轮的所有task,调整为8个executor(原来同时跑2个task,现在可以同时跑8个task了),那么只需要一轮就而已跑完所有的task。内存1G也够用了,因为输入数据就不足1G,如果输入数据很大的话,也可以增大内存。

2.2 设置合适的资源

设置合适的资源,要明确集群的资源总量,然后观察监控指标,检查是否充分使用了集群中的资源

资源量关联度比较大的一个是内存一个是CPU的使用率,当然还有其他,但这两个指标是比较重点的,这两个指标对应spark程序主要是Executor的内存和core。所以计算资源的设置单位是Executor

增加Executor个数:–num-executors 4

增加每个Executor同时云心的task数目:–executor-cores 2

除了Executor消耗资源,还有driver和shuffle等也都是消耗资源大户,我把几个常用的和资源使用相关的参数含义及参考值总结如下:

num-executors

参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。

参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。

executor-memory

参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。

参数调优建议:每个Executor进程的内存设置4G8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,就代表了你的Spark作业申请到的总内存量(也就是所有Executor进程的内存总和),这个量是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的总内存量最好不要超过资源队列最大总内存的1/31/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同学的作业无法运行。

executor-cores

参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。

参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。同样建议,如果是跟他人共享这个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同学的作业运行。

driver-memory

参数说明:该参数用于设置Driver进程的内存。

参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

spark.default.parallelism

参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

spark.storage.memoryFraction

参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。

参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

spark.shuffle.memoryFraction

参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

资源参数的调优,没有一个固定的值,需要同学们根据自己的实际情况(包括Spark作业中的shuffle操作数量、RDD持久化操作数量以及spark web ui中显示的作业gc情况),同时参考本篇文章中给出的原理以及调优建议,合理地设置上述参数。

参数设置demo

/bin/spark-submit \

–master yarn-cluster \

–num-executors 100 \

–executor-memory 6G \

–executor-cores 4 \

–driver-memory 1G \

–conf spark.default.parallelism=1000 \

–conf spark.storage.memoryFraction=0.5 \

–conf spark.shuffle.memoryFraction=0.3 \

优化后的,一共执行了11秒

每个task执行时间

再观察每个task花费时间,观察发现,每个task执行的时间比原来的4秒还长,原因是在资源不变的情况,任务数多了只有2个CPU,CPU变成瓶颈了,要频繁切换如果CPU很多,优化效果会非常好,CPU是瓶颈

2.3 设置合适的并发度

任务的并发度有几个级别:job级别的并发,stage级别的并发,task级别的并发。这里谈的并发优化是task级别的,主要就是map任务并行度和reduce任务的并行度,这方面的调优其实可以参考MapReduce的调优,原理都是一样一样的。Spark的任务数需要注意几点:

1)Map个数默认是和输入文件的blok数是一样的,如hdfs则和 blokc数目一致,hbase则和regio个数一致。

2)rdd之间的map个数如果不修改,后面的和前面个数一样

3)reduce默认个数也是和map个数一样

map设置方法:

单个设置: sc.textFile(“/input/data”,100); //指定100个blokc,那么就100个map

批量设置:将每个map处理数量调大,map数就少了,默认128M

2.4 修改存储格式

很多人并不明白为什么文件存储格式会影响文件的读取效率,我打个最简单的比方。我们知道linux 系统是单机版的操作系统,里面有ext3,ext4这样的文件,ext的职责就是对linux系统文件进行管理,HDFS是分布式的文件系统,也是对文件进行管理。好的文件系统就像一个勤快的媳妇,在你房子面积不变的情况下,勤快的媳妇会将物品放的井井有条,利用到房子里面的每个空间,你要拿什么东西,都能很快找到;而不好的文件系统就像是一个懒媳妇,房间里面堆满了东西,找起来很麻烦,房间利用率也非常糟糕,找东西困难,放东西进去也很难,东西越多越脏越乱。(媳妇没有好坏之分,只有适合不适合,还有看你怎么和媳妇相处了,互相了不了解,性格和脾气对不对路,文件系统也是如此)

文件存储格式和文件系统是一样的原理,文件系统管理的是文件,而文件储存格式管理的是文件内容(管理的是文件中每一行每一列的具体内容)。所以低效率的文件存储格式就像是一个赖媳妇,家里被管的一塌糊涂,东西越多越脏乱差,高效率的文件存储格式就是勤快且聪明的媳妇,一切都管的井然有序,取东西方便,放东西也容易,还会根据不同的物品特征进行摆放,完美,6666!!!

csv,txt,json等等都是懒媳妇,parquet,orc都是勤快媳妇,那为什么文本文件是懒媳妇,parquet是好媳妇,主要有以下几个原因:

文本文件为什么不好?

文本文件行存储,存储占用空间,而且读取数据的时候会读出很多不必要的数据出来,这就好像你叫懒媳妇给你拿一顶帽子,结果她把衣服,鞋子,袜子统统拿出来,然后再从里面挑出你要的帽子。

parquet为什么好,Spark使用parquet文件存储格式意义在哪里?

  1. 如果说HDFS 是大数据时代分布式文件系统首选标准,那么parquet则是整个大数据时代文件存储格式实时首选标准

  2. 速度更快:从使用spark sql操作普通文件CSV和parquet文件速度对比上看,绝大多数情况

会比使用csv等普通文件速度提升10倍左右,在一些普通文件系统无法在spark上成功运行的情况

下,使用parquet很多时候可以成功运行

  1. parquet的压缩技术非常稳定出色,在spark sql中对压缩技术的处理可能无法正常的完成工作

(例如会导致lost task,lost executor)但是此时如果使用parquet就可以正常的完成

  1. 极大的减少磁盘I/o,通常情况下能够减少75%的存储空间,由此可以极大的减少spark sql处理

数据的时候的数据输入内容,尤其是在spark1.6x中有个下推过滤器在一些情况下可以极大的

减少磁盘的IO和内存的占用,(下推过滤器)

  1. spark 1.6x parquet方式极大的提升了扫描的吞吐量,极大提高了数据的查找速度spark1.6和spark1.5x相比而言,提升了大约1倍的速度,在spark1.6X中,操作parquet时候cpu也进行了极大的优化,有效的降低了cpu

  2. 采用parquet可以极大的优化spark的调度和执行。我们测试spark如果用parquet可以有效的减少stage的执行消耗,同时可以优化执行路径

3.Spark调优经验

3.1 Spark原理及调优工具

· Spark Web UI界面

· jstack、jstat、jprofile

· history server:当Spark应用退出后,仍可以获得历史Spark应用的stages和tasks执行信息,便于分析程序不明原因挂掉的情况,Spark的history server依赖mr的history server。

3.2 运行环境优化

3.2.1 防止不必要的分发

每个Application都会上传一个spark-assembly-x.x.x-SNAPSHOT-hadoopx.x.x-cdhx.x.x.jar的jar包,影响HDFS的性能以及占用HDFS的空间.对于用户的jar包,有时候体积也非常庞大,我们同样的方式上传hdfs上,然后直接使用。

  1. 依赖ja包重复上传

执行spark任务有大量jar包上传HDFS,将系统jar包上传到hdfs上,直接使用hdfs上的文件,具体下:

1)修改conf/spark-default.conf添加以下配置

spark.yarn.jar hdfs://master:9000/system/spark/jars/spark-assembly-1.6.0-hadoop2.6.0.jar

2)再次执行SparkPi,提交脚本发生了变化,如下:

bin/spark-submit  --class  org.apache.spark.examples.SparkPi \ --master yarn-cluster \ --num-executors 3 \ --driver-memory 1g \ --executor-memory 1g \ --executor-cores 1 \ lib/spark-examples*.jar  10

  1. 用户jar包重复上传,避免重复分发

bin/spark-submit  --class  org.apache.spark.examples.SparkPi \ --master yarn-cluster \ --num-executors 3 \ --driver-memory 1g \ --executor-memory 1g \ --executor-cores 1 \ hdfs://master:9000/user/spark/jars/spark-examples-1.6.0-hadoop2.6.0.jar 10

3.2.2 提高数据本地性

分布式数据并行环境下,保持数据的本地性是非常重要的内容,事关分布式系统性能高下,涉及到数据本地性的概念有block、partition、worker、rack。

Spark中的数据本地性有三种:

  • PROCESS_LOCAL是指读取缓存在本地节点的数据
  • NODE_LOCAL是指读取本地节点
  • RACK_LOCAL是指读非本机架的节点数据

yarn和hfs尽可能的在一个节点上很多rack local,说明本地性很差,可以通过增加副本数来提升本地新。

3.2.3 存储格式选择

BAT等公司80%都是采用列式存储结构 ,相同的列存储在一起,只读取所需的列,io减少,相同的列存在一起,压缩比会非常高。大概是行存储的1/22个apache顶级项目ORC:源自于hive,建表最好都搞成orc,hive常用parquet,rdd读取效率低,spark sql高效率读取

列式存储和行式存储相比有哪些优势呢?

·可以跳过不符合条件的数据,只读取需要的数据,降低IO数据量。

·压缩编码可以降低磁盘存储空间。由于同一列的数据类型是一样的,可以使用更高效的压缩编码(例如Run Length Encoding和Delta Encoding)进一步节约存储空间。

·只读取需要的列,支持向量运算,能够获取更好的扫描性能。

从上图可以很清楚地看到,行式存储下一张表的数据都是放在一起的,但列式存储下都被分开保存了。所以它们就有了如下这些优缺点:

通过字典表压缩数据。为了方面后面的讲解,这部分也顺带提一下了。

下面中才是那张表本来的样子。经过字典表进行数据压缩后,表中的字符串才都变成数字了。正因为每个字符串在字典表里只出现一次了,所以达到了压缩的目的(有点像规范化和非规范化Normalize和Denomalize)

下面这个图,通过一条查询的执行过程说明列式存储(以及数据压缩)的优点:

关键步骤如下:

1.去字典表里找到字符串对应数字(只进行一次字符串比较)。

  1. 用数字去列表里匹配,匹配上的位置设为1。

  2. 把不同列的匹配结果进行位运算得到符合所有条件的记录下标。

  3. 使用这个下标组装出最终的结果集。

3.2.4 选择高配机器

随着硬件的不断发展和企业的需求的不断变化, 大部分企业在集群搭建初期和中期的集群配置都不一样。而Spark是一个非常消耗内存的,因此对于初期一些配置较低,尤其内存较差的机器,是不适合跑spark任务的,更加适合硬件配置高一点的机器上跑。机器配置的参差不齐,应该如何有区别的调度

,yarn提供了很好的解决方案。

yarn支持标签,根据机器的配置,给机器打相应的标签,标签如何打不在讨论范围,目前只有capacity 调度算法支持标签给队列支持打标签,将标签和队列绑定在一起,将应用程序提交到指定标签的队列,执行的时候就会提交到到相应标签节点 。

很多时候是基础平台的修改,运维负责优化 yarn基于标签的调度,haodop从hadoop2.6.0开始提供基于标签的调度策略。

3.3 优化操作符

3.3.1 过滤操作导致多小任务

filter操作使用不当,很容易引发麻烦。假如一个任务有3个parition,经过filger过滤之后,可能导致部分剩下很少,有些剩余很多,剩余很多的在下一步计算量很大,会拖后腿,其他的作业很快就做完了,而剩余很多的要执行很长时间,整个任务都要延误,而其他很快执行完的作业早就释放资源了

造成资源还的浪费

对于这种场景有2种优化策略:

1)coalses:合并已有的partiion,性能非常高,但是很有可能还不是很均与,

大的依旧很大,小的进行了合并

2)repartion:根据数据量灯亮划分,每个partion尽可能均匀,会经过一次shuffle比较均匀

3.3.2 降低单条记录开销

做过Java连接数据库操作的人都知道,要尽量避免数据库链接的频繁建立和断开,方法很多,比如数据库连接池的发明。单机版本对数据库的连接操作比较容易管理和控制,但在分布式环境下,数据库的连接管理和控制很麻烦,数据的连接是不可序列化的,因此分布式环境下,统一管理数据库连接显然是不靠谱的。比如这段代码,如果写数据到数据库,就会频繁建立和断开连接,显然是低效率的。因为数据库连接的不可序列化,你也不可能把conn拿出来。

解决方法是:使用mapPartitions或者mapWith操作符

原因在于mapPartitions是map的调用的粒度不同,map的输入变换函数是应用于RDD中每个元素,而mapPartitions的输入函数是应用于每个分区。

假设一个rdd有10个元素,分成3个分区。如果使用map方法,map中的输入函数会被调用10次;而使用mapPartitions方法的话,其输入函数会只会被调用3次,每个分区调用1次。在大数据集情况下的资源初始化开销和批处理处理,尤其数据库链接操作,显得特别好用。

3.3.3 处理数据倾斜或者任务倾斜

spark任务中的数据倾斜可能导致某一台节点超负荷运转、内存不足,其他节点都处于空闲等待,加内存不能解决问题。应该找到出问题的shuffle操作,修正它。具体问题具体分析,有如下个方向,但不限于此。

1)改变数据结构,从源头调整数据的分布,例如分表,分区存放,横向或者纵向分表等。

2)修改并行度

· 改变并行度可以改善数据倾斜的原因是因为如果某个task有100个key并且数据巨大,那么有可能导致OOM或者任务运行缓慢;    ·此时如果把并行度变大,那么可以分解每个task的数据量,比如把该task分解给10个task, 那么每个task的数据量将变小,从而可以解决OOM或者任务执行慢.对应reduceByKey而言可以传入并行度参数也可以自定义partition.   · 增加并行度:改变计算资源并没有从根本上解决数据倾斜的问题,但是加快了任务运行的速度.   · 这是加入有倾斜的key, 加随机数前缀,reduceByKey聚合操作可以分而治之,产生的结果是代前缀的,因此需

3)提取聚集,预操作join,  把倾斜数据在上游进行操作.

4)局部聚合+全局聚合

5)尽量避免shuffle

6)启用推测执行,避免慢节点任务拖后腿,慢磁盘问题在hadoop集群运行了好几年之后非常明显,尤其是磁盘,hadoop以及很多监控工具并没有对慢磁盘进行监控,需要自己写脚本监控。

下面的例子就是通过对key进行增加随机数,然后进行局部聚合+全局聚合

3.3.4 复用RDD进行缓存

RDD是一系列的数据+计算,每一次计算利用上次计算结果都会重新计算,对于一些常用的计算结果可以缓存起来,避免重复计算

cache和persist的区别:cache只有一个默认的缓存级别MEMORY_ONLY ,而persist可以根据情况设置其它的缓存级别。

3.3.5 慎用耗资源操作符

选择 Operator 方案的主要目标是减少 shuffle 的次数以及被 shuffle 的文件的大小。因为 shuffle 是最耗资源的操作,所以有 shuffle 的数据都需要写到磁盘并且通过网络传递,repartition,join,cogroup,以及任何 *By 或者 *ByKey 的 transformation 都需要 shuffle 数据。不是所有这些 Operator 都是平等的,但是有些常见的性能陷阱是需要注意的。消耗资源的操作尽量少用,能用小砍刀办到的事情,何须屠龙刀,Spark中比较消耗资源的操作有

· 笛卡尔积操作

· 带shuffle的各种算子

如果可能,用treeReduce代替reduce,尽量用reduceByKey替代groupByKey,举个栗子

尽量避免差生shuffle,什么时候不发生 Shuffle 当前一个 transformation 已经用相同的 patitioner 把数据分 patition 了,Spark知道如何避免 shuffle

3.3.6 作业并行化

Job之间如果没有依赖关系,在资源允许的情况下,当然是能并行就更佳,能高并发的,高吞吐量的时候那还要等什么,除非你不希望活早点干完。Job之间并行注意2点

· 启动FAIR调度器:spark.scheduler.mode=fait

· 将action相关操作放到单独线程中

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

避免差生shuffle,什么时候不发生 Shuffle 当前一个 transformation 已经用相同的 patitioner 把数据分 patition 了,Spark知道如何避免 shuffle

3.3.6 作业并行化

Job之间如果没有依赖关系,在资源允许的情况下,当然是能并行就更佳,能高并发的,高吞吐量的时候那还要等什么,除非你不希望活早点干完。Job之间并行注意2点

· 启动FAIR调度器:spark.scheduler.mode=fait

· 将action相关操作放到单独线程中

[外链图片转存中…(img-52Hm61sw-1715815460327)]
[外链图片转存中…(img-GZEPIvWY-1715815460327)]
[外链图片转存中…(img-NeYMvtx9-1715815460327)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值