首页 > 试题

谢衣

更新时间:2023-01-31 04:10:13 阅读: 评论:0

初三寒假课程规划-细亚俊秀


2023年1月31日发(作者:如花的日子)

《深⼊理解Spark:核⼼思想与源码分析》(前⾔及第1章)

⾃⼰牺牲了7个⽉的周末和下班空闲时间,通过研究Spark源码和原理,总结整理的《深⼊理解Spark:核⼼思想与源码分析》⼀书现在已经正式出版上市,⽬前亚马逊、京

东、当当、天猫等⽹站均有销售,欢迎感兴趣的同学购买。我开始研究源码时的Spark版本是1.2.0,经过7个多⽉的研究和出版社近4个⽉的流程,Spark⾃⾝的版本迭代也很

快,如今最新已经是1.6.0。⽬前市⾯上另外2本源码研究的Spark书籍的版本分别是0.9.0版本和1.2.0版本,看来这些书的作者都与我⼀样,遇到了这种问题。由于研究和出版都

需要时间,所以不能及时跟上Spark的脚步,还请⼤家见谅。但是Spark核⼼部分的变化相对还是很少的,如果对版本不是过于追求,依然可以选择本书。

为了让⼤家对本书有个⼤致了解,这⾥将本书的前⾔及第⼀章的内容附上:

前⾔

为什么写这本书

要回答这个问题,需要从我个⼈的经历说起。说来惭愧,我第⼀次接触计算机是在⾼三。当时跟⼤家⼀起去⽹吧玩CS,跟⾝边的同学学怎么“玩”。正是通过这种“玩”的过

程,让我了解到计算机并没有那么神秘,它也只是台机器,⽤起来似乎并不⽐打开电视机费劲多少。⾼考填志愿的时候,凭着直觉“糊⾥糊涂”就选择了计算机专业。等到真正学

习计算机课程的时候却⼜发现,它其实很难!

早在2004年,还在学校的我跟很多同学⼀样,喜欢看Flash,也喜欢谈论Flash甚⾄做Flash。感觉Flash正如它的名字那样“闪光”。那些年,在学校⾥,知道Flash的⼈可要

⽐知道Java的⼈多得多,这说明当时的Flash⼗分⽕热。此外Oracle也成为关系型数据库⾥的领军⼈物,很多⼈甚⾄觉得懂Oracle要⽐懂Flash、Java及其它数据库要厉害得多!

2007年,笔者刚刚参加⼯作不久。那时Struts1、Spring、Hibernate⼏乎可以称为那些⽤Java作为开发语⾔的软件公司的三驾马车。很快随着Struts2的诞⽣,很快替代了

Struts1的地位,让我第⼀次意识到IT领域的技术更新竟然如此之快!随着很多传统软件公司向互联⽹公司转型,更让⼈吃惊的是,当初那个勇于技术更新的年轻⼈GavinKing,

也许很难想象他创造的Hibernate也难以确保其地位,iBATIS诞⽣了!

2010年,有关Hadoop的技术图书涌⼊中国,当时很多公司⽤它只是为了数据统计、数据挖掘或者搜索。⼀开始,⼈们对于Hadoop的认识和使⽤可能相对有限。⼤约2011

年的时候,关于云计算的概念在⽹上吵得⽕热,当时依然在做互联⽹开发的我,对其只是“道听途说”。后来跟同事借了⼀本有关云计算的书,回家挑着看了⼀些内容,之后什么

也没有弄到⼿,怅然若失!上世纪60年代,美国的军⽤⽹络作为互联⽹的雏形,很多内容已经与云计算中的某些说法相类似。到上世纪80年代,互联⽹就已经开启了云计算,为

什么如今⼜要重提这样的概念?这个问题笔者可能回答不了,还是交给历史吧。

2012年,国内⼜呈现出⼤数据热的态势。从国家到媒体、教育、IT等⼏乎所有领域,⼈⼈都在谈⼤数据。我的亲戚朋友中,⽆论⽼师、销售还是⼯程师们都可以对⼤数据谈

谈⾃⼰的看法。我也找来⼀些Hadoop的书籍进⾏学习,希望能在其中探索到⼤数据的味道。

有幸在⼯作过程中接触到阿⾥的开放数据处理服务(OpenDataProcessingService,简称ODPS),并且基于ODPS与其他⼩伙伴⼀起构建阿⾥的⼤数据商业解决⽅案

——御膳房。去杭州出差的过程中,有幸认识和仲,跟他学习了阿⾥的实时多维分析平台——Garuda和实时计算平台——Galaxy的部分知识。和仲推荐我阅读Spark的源码,这

样会对实时计算及流式计算有更深⼊的了解。2015年春节期间,⾃⼰初次上⽹查阅Spark的相关资料学习,开始研究Spark源码。还记得那时只是出于对⼤数据的热爱,想使⾃⼰

在这⽅⾯的技术能⼒有所提升。

从阅读Hibernate源码开始,到后来阅读Tomcat、Spring的源码,随着挖掘源码,从学习源码的过程中成长,我对源码阅读也越来越感兴趣。随着对Spark源码阅读的深⼊,

发现很多内容从⽹上找不到答案,只能⾃⼰硬啃了。随着⾃⼰的积累越来越多,突然有天发现,我所总结的这些内容好像可以写成⼀本书了!从闪光(Flash)到⽕花

(Spark),⾜⾜有11个年头了。⽆论是Flash、Java,还是Spring、iBATIS我⼀直扮演着⼀个追随者,我接受这些书籍的洗礼,从未给予。如今我也是Spark的追随者,不同的

是,我不再只想简单的攫取,还要给予。

最后还想说下2016年是我从事IT⼯作的第⼗个年头,此书特别作为送给⾃⼰的⼗周年礼物。

本书的主要特⾊

按照源码分析的习惯设计,从脚本分析到初始化再到核⼼内容,最后介绍Spark的扩展内容。整个过程遵循由浅⼊深,由深到⼴的基本思路。

本书涉及的所有内容都有相应的例⼦,以便于对源码的深⼊研究能有更好的理解。

本书尽可能的⽤图来展⽰原理,加速读者对内容的掌握。

本书讲解的很多实现及原理都值得借鉴,能帮助读者提升架构设计、程序设计等⽅⾯的能⼒。

本书尽可能保留较多的源码,以便于初学者能够在脱离办公环境的地⽅(如地铁、公交),也能轻松阅读。

本书⾯向的读者

源码阅读是⼀项苦差事,⼈⼒和时间成本都很⾼,尤其是对于Spark陌⽣或者刚刚开始学习的⼈来说,难度可想⽽知。本书尽可能保留源码,使得分析过程不⾄于产⽣跳跃

感,⽬的是降低⼤多数⼈的学习门槛。如果你是从事IT⼯作1~3年的新⼈或者希望开始学习Spark核⼼知识的⼈来说,本书⾮常适合你。如果你已经对Spark有所了解或者已经使

⽤它,还想进⼀步提⾼⾃⼰,那么本书更适合你。

如果你是⼀个开发新⼿,对Java、Linux等基础知识不是很了解的话,本书可能不太适合你。如果你已经对Spark有深⼊的研究,本书也许可以作为你的参考资料。总体说

来,本书适合以下⼈群:

想要使⽤Spark,但对Spark实现原理不了解,不知道怎么学习的⼈;

⼤数据技术爱好者,以及想深⼊了解Spark技术内部实现细节的⼈;

有⼀定Spark使⽤基础,但是不了解Spark技术内部实现细节的⼈;

对性能优化和部署⽅案感兴趣的⼤型互联⽹⼯程师和架构师;

开源代码爱好者,喜欢研究源码的同学可以从本书学到⼀些阅读源码的⽅式⽅法。

本书不会教你如何开发Spark应⽤程序,只是拿⼀些经典例⼦演⽰。本书会简单介绍HadoopMapReduce、HadoopYARN、Mesos、Tachyon、ZooKeeper、HDFS、

AmazonS3,但不会过多介绍这些等框架的使⽤,因为市场上已经有丰富的这类书籍供读者挑选。本书也不会过多介绍Scala、Java、Shell的语法,读者可以在市场上选择适合

⾃⼰的书籍阅读。本书实际适合那些想要破解⼀个个潘多拉魔盒的⼈!

如何阅读本书

本书分为三⼤部分(不包括附录):

第⼀部分为准备篇(第1~2章),简单介绍了Spark的环境搭建和基本原理,帮助读者了解⼀些背景知识。

第⼆部分为核⼼设计篇(第3~7章),着重讲解SparkContext的初始化、存储体系、任务提交与执⾏、计算引擎及部署模式的原理和源码分析。

第三部分为扩展篇(第8~11章),主要讲解基于Spark核⼼的各种扩展及应⽤,包括:SQL处理引擎、Hive处理、流式计算框架SparkStreaming、图计算框架GraphX、

机器学习库MLlib等内容。

本书最后还添加了⼏个附录,包括:附录A介绍的Spark中最常⽤的⼯具类Utils;附录B是Akka的简介与⼯具类AkkaUtils的介绍;附录C为Jetty的简介和⼯具类JettyUtils的

介绍;附录D为Metrics库的简介和测量容器MetricRegistry的介绍;附录E演⽰了Hadoop1.0版本中的wordcount例⼦;附录F介绍了⼯具类CommandUtils的常⽤⽅法;附录G是

关于Netty的简介和⼯具类NettyUtils的介绍;附录H列举了笔者编译Spark源码时遇到的问题及解决办法。

为了降低读者阅读理解Spark源码的门槛,本书尽可能保留源码实现,希望读者能够怀着⼀颗好奇的⼼,Spark当前很⽕热,其版本更新也很快,本书以Spark1.2.3版本为

主,有兴趣的读者也可按照本书的⽅式,阅读Spark的最新源码。

联系⽅式

致谢

感谢苍天,让我⽣活在这样⼀个时代接触互联⽹和⼤数据;感谢⽗母,这么多年来,在学习、⼯作及⽣活上的帮助与⽀持;感谢妻⼦在⽣活中的照顾和谦让。

感谢杨福川编辑和⾼婧雅编辑给予本书出版的⼤⼒⽀持与帮助。

感谢冰夷⽼⼤和王贲⽼⼤让我有幸加⼊阿⾥,接触⼤数据应⽤;感谢和仲对Galaxy和Garuda耐⼼细致的讲解以及对Spark的推荐;感谢张中在百忙之中给本书写评语;感谢

周亮、澄苍、民瞻、⽯申、清⽆、少侠、征宇、三步、谢⾐、晓五、法星、曦轩、九翎、峰阅、丁卯、阿末、紫丞、海炎、涵康、云飏、孟天、零⼀、六仙、⼤知、井凡、隆

君、太奇、晨炫、既望、宝升、都灵、⿁厉、归钟、梓撤、昊苍、⽔村、惜冰、惜陌、元乾等同学在⼯作上的⽀持和帮助。

耿嘉安

北京

第1章环境准备

“凡事豫则⽴,不豫则废;⾔前定,则不跲;事前定,则不困;”

——《礼记·中庸》

本章导读:

在深⼊了解⼀个系统的原理、实现细节之前,应当先准备好它的源码编译环境、运⾏环境。如果能在实际环境安装和运⾏Spark,显然能够提升读者对于Spark的⼀些感受,

对系统能有个⼤体的印象,有经验的技术⼈员甚⾄能够猜出⼀些Spark采⽤的编程模型、部署模式等。当你通过⼀些途径知道了系统的原理之后,难道不会问问⾃⼰?这是怎么

做到的。如果只是游⾛于系统使⽤、原理了解的层⾯,是永远不可能真正理解整个系统的。很多IDE本⾝带有调试的功能,每当你阅读源码,陷⼊重围时,调试能让我们更加理

解运⾏期的系统。如果没有调试功能,不敢想象阅读源码的困难。

本章的主要⽬的是帮助读者构建源码学习环境,主要包括以下内容:

1.在windows环境下搭建源码阅读环境;

2.在Linux搭建基本的执⾏环境;

的基本使⽤,如spark-shell。

1.1运⾏环境准备

考虑到⼤部分公司在开发和⽣成环境都采⽤Linux操作系统,所以笔者选⽤了64位的Linux。在正式安装Spark之前,先要找台好机器。为什么?因为笔者在安装、编译、调试

的过程中发现Spark⾮常耗费内存,如果机器配置太低,恐怕会跑不起来。Spark的开发语⾔是Scala,⽽Scala需要运⾏在JVM之上,因⽽搭建Spark的运⾏环境应该包括JDK和

Scala。

1.1.1安装JDK

使⽤命令getconfLONG_BIT查看linux机器是32位还是64位,然后下载相应版本的JDK并安装。

下载地址:

配置环境:

cd~

_profile

添加如下配置:

exportJAVA_HOME=/opt/java

exportPATH=$PATH:$JAVA_HOME/bin

exportCLASSPATH=.:$JAVA_HOME/lib/:$JAVA_HOME/lib/

由于笔者的机器上已经安装过openjdk,安装命令:

$su-c"yuminstalljava-1.7.0-openjdk"

安装完毕后,使⽤java–version命令查看,确认安装正常,如图1-1所⽰。

图1-1查看java安装是否正常

1.1.2安装Scala

选择最新的Scala版本下载,下载⽅法如下:

wget/scala/2.11.5/

移动到选好的安装⽬录,例如:

~/install/

进⼊安装⽬录,执⾏以下命令:

配置环境:

cd~

_profile

添加如下配置:

exportSCALA_HOME=$HOME/install/scala-2.11.5

exportPATH=$PATH:$SCALA_HOME/bin:$HOME/bin

安装完毕后键⼊scala,进⼊scala命令⾏,如图1-2所⽰。

图1-2进⼊Scala命令⾏

1.1.3安装Spark

选择最新的Spark版本下载,下载⽅法如下:

wget/dist/spark/spark-1.2.0/

移动到选好的安装⽬录,如:

~/install/

进⼊安装⽬录,执⾏以下命令:

配置环境:

cd~

_profile

添加如下配置:

exportSPARK_HOME=$HOME/install/spark-1.2.0-bin-hadoop1

1.2Spark初体验

本节通过Spark的基本使⽤,让读者对Spark能有初步的认识,便于引导读者逐步深⼊学习。

1.2.1运⾏spark-shell

要运⾏spark-shell,需要先对Spark进⾏配置。

进⼊Spark的conf⽂件夹:

cd~/install/spark-1.2.0-bin-hadoop1/conf

拷贝⼀份te,命名为,对它进⾏编辑,命令如下:

添加如下配置:

exportSPARK_MASTER_IP=127.0.0.1

exportSPARK_LOCAL_IP=127.0.0.1

启动spark-shell:

cd~/install/spark-1.2.0-bin-hadoop1/bin

./spark-shell

最后我们会看到spark启动的过程,如图1-3所⽰:

图1-3Spark启动过程

从以上启动⽇志中我们可以看到SparkEnv、MapOutputTracker、BlockManagerMaster、DiskBlockManager、MemoryStore、HttpFileServer、SparkUI等信息。它们是做什么

的?此处望⽂⽣义即可,具体内容将在后边的章节详细给出。

1.2.2执⾏wordcount

这⼀节,我们通过wordcount这个⽿熟能详的例⼦来感受下Spark任务的执⾏过程。启动spark-shell后,会打开Scala命令⾏,然后按照以下步骤输⼊脚本:

步骤1输⼊vallines=le("../",2),执⾏结果如图1-4所⽰。

图1-4步骤1执⾏结果

步骤2输⼊valwords=p(line=>("")),执⾏结果如图1-5所⽰。

图1-5步骤2执⾏结果

步骤3输⼊valones=(w=>(w,1)),执⾏结果如图1-6所⽰。

图1-6步骤3执⾏结果

步骤4输⼊valcounts=ByKey(_+_),执⾏结果如图1-7所⽰。

图1-7步骤4执⾏结果

步骤5输⼊h(println),任务执⾏过程如图1-8和图1-9所⽰。输出结果如图1-10所⽰。

图1-8步骤5执⾏过程部分

图1-9步骤5执⾏过程部分

图1-10步骤5输出结果

在这些输出⽇志中,我们先是看到Spark中任务的提交与执⾏过程,然后看到单词计数的输出结果,最后打印⼀些任务结束的⽇志信息。有关任务的执⾏分析,笔者将在第5章中

展开。

1.2.3剖析spark-shell

通过wordcount在spark-shell中执⾏的过程,我们想看看spark-shell做了什么?spark-shell中有以下⼀段脚本,见代码清单1-1。

代码清单1-1spark-shell

functionmain(){

if$cygwin;then

stty-icanonmin1-echo>/dev/null2>&1

exportSPARK_SUBMIT_OPTS="$SPARK_SUBMIT_al=unix"

"$FWDIR"/bin/"${SUBMISSION_OPTS[@]}"spark-shell"${APPLICATION_OPTS[@]}"

sttyicanonecho>/dev/null2>&1

el

exportSPARK_SUBMIT_OPTS

"$FWDIR"/bin/"${SUBMISSION_OPTS[@]}"spark-shell"${APPLICATION_OPTS[@]}"

fi

}

我们看到脚本spark-shell⾥执⾏了spark-submit脚本,那么打开spark-submit脚本,发现其中包含以下脚本。

exec"$SPARK_HOME"/bin/ubmit"${ORIG_ARGS[@]}"

脚本spark-submit在执⾏spark-class脚本时,给它增加了参数SparkSubmit。打开spark-class脚本,其中包含以下脚本,见代码清单1-2。

代码清单1-2spark-class

if[-n"${JAVA_HOME}"];then

RUNNER="${JAVA_HOME}/bin/java"

el

if[`command-vjava`];then

RUNNER="java"

el

echo"JAVA_HOMEisnott">&2

exit1

fi

fi

exec"$RUNNER"-cp"$CLASSPATH"$JAVA_OPTS"$@"

读到这,应该知道Spark启动了以SparkSubmit为主类的jvm进程。

为便于在本地能够对Spark进程使⽤远程监控,给spark-class脚本增加追加以下jmx配置:

JAVA_OPTS="-XX:MaxPermSize=128m$OUR_JAVA_=ticate==

在本地打开jvisualvm,添加远程主机,如图1-11所⽰:

图1-11添加远程主机

右键单击已添加的远程主机,添加JMX连接,如图1-12:

图1-12添加JMX连接

选择右侧的“线程”选项卡,选择main线程,然后点击“线程Dump”按钮,如图1-13。

图1-13查看Spark线程

从dump的内容中找到线程main的信息如代码清单1-3所⽰。

代码清单1-3main线程dump信息

"main"-Threadt@1

:RUNNABLE

0(NativeMethod)

(:210)

aracter(:152)

rtualKey(:125)

rtualKey(:933)

nding(:1136)

ne(:1218)

ne(:1170)

eLine(:80)

ctiveReader$ne(:43)

ne(:25)

eLine$1(:619)

oop$1(:636)

(:641)

Loop$$anonfun$process$$mcZ$sp(:968)

Loop$$anonfun$process$(:916)

Loop$$anonfun$process$(:916)

lassLoader$.savingContextLoader(:135)

s(:916)

s(:1011)

$.main(:31)

()

0(NativeMethod)

(:57)

(:43)

(:606)

ubmit$.launch(:358)

ubmit$.main(:75)

()

从main线程的栈信息中看出程序的调⽤顺序:→→s。s⽅法中会调⽤initializeSpark⽅法,initializeSpark的实现

见代码清单1-4。

代码清单1-4initializeSpark的实现

definitializeSpark(){

tDuring{

command("""

@transientvalsc={

val_sc=SparkContext()

println("Sparkcontextavailableassc.")

_sc

}

""")

command("ontext._")

}

}

我们看到initializeSpark调⽤了createSparkContext⽅法,createSparkContext的实现,见代码清单1-5。

代码清单1-5createSparkContext的实现

defcreateSparkContext():SparkContext={

valexecUri=("SPARK_EXECUTOR_URI")

valjars=edJars

valconf=newSparkConf()

.tMaster(getMaster())

.tAppName("Sparkshell")

.tJars(jars)

.t("",)

if(execUri!=null){

("",execUri)

}

sparkContext=newSparkContext(conf)

logInfo("Createdsparkcontext..")

sparkContext

}

这⾥最终使⽤SparkConf和SparkContext来完成初始化,具体内容将在“第3章SparkContext的初始化”讲解。代码分析中涉及的repl主要⽤于与Spark实时交互。

1.3阅读环境准备

准备Spark阅读环境,同样需要⼀台好机器。笔者调试源码的机器的内存是8GB。源码阅读的前提是⾸先在IDE环境中打包、编译通过。常⽤的IDE有IntelliJIDEA、Eclip,笔

者选择⽤Eclip编译Spark,原因有⼆:⼀是由于使⽤多年对它⽐较熟悉,⼆是社区中使⽤Eclip编译Spark的资料太少,在这⾥可以做个补充。笔者在windows系统编译Spark

源码,除了安装JDK外,还需要安装以下⼯具。

(1)安装Scala

下载完毕,安装。

(2)安装SBT

(3)安装GitBash

(4)安装EclipScalaIDE插件

图1-14EclipScalaIDE插件安装地址

在Eclip中选择“Help”菜单,然后选择“InstallNewSoftware…”选项,打开Install对话框,如图1-15所⽰:

图1-15安装ScalaIDE插件

点击“Add…”按钮,打开“AddRepository”对话框,输⼊插件地址,如1-16图所⽰:

图1-16添加ScalaIDE插件地址

全选插件的内容,完成安装,如图1-17所⽰:

图1-17安装ScalaIDE插件

1.4Spark源码编译与调试

1.下载Spark源码

图1-18Spark官⽹

点击“DownloadSpark”按钮,在下⼀个页⾯找到git地址,如图1-19所⽰:

图1-19Spark官⽅git地址

打开GitBash⼯具,输⼊gitclonegit:///apache/命令将源码下载到本地,如1-20图所⽰:

图1-20下载Spark源码

2.构建Scala应⽤

使⽤cmd命令⾏进到Spark根⽬录,执⾏sbt命令。会下载和解析很多jar包,要等很长的时间,笔者⼤概花费了⼀个多⼩时,才执⾏完。

3.使⽤sbt⽣成eclip⼯程⽂件

等sbt提升符>出现后,输⼊eclip命令,开始⽣成eclip⼯程⽂件,也需要花费很长的时间,笔者本地⼤致花费了40分钟。完成时的状况,如图1-21所⽰:

图1-21sbt编译过程

现在我们查看Spark下的⼦⽂件夹,发现其中都⽣成了.project和.classpath⽂件。⽐如mllib项⽬下就⽣成了.project和.classpath⽂件,如图1-22所⽰:

图1-22sbt⽣成的项⽬⽂件

4.编译Spark源码

由于Spark使⽤Maven作为项⽬管理⼯具,所以需要将Spark项⽬作为Maven项⽬导⼊到Eclip中,如1-23图所⽰:

图1-23导⼊Maven项⽬

点击Next按钮进⼊下⼀个对话框,如图1-24所⽰:

图1-24选择Maven项⽬

全选所有项⽬,点击finish按钮。这样就完成了导⼊,如图1-25所⽰:

图1-25导⼊完成的项⽬

导⼊完成后,需要设置每个⼦项⽬的buildpath。右键单击每个项⽬,选择“BuildPath”→“ConfigureBuildPath…”,打开BuildPath对话框,如图1-26:

图1-26Java编译⽬录

点击“AddExternalJARs…”按钮,将Spark项⽬下的lib_managed⽂件夹的⼦⽂件夹bundles和jars内的jar包添加进来。

注意:lib_managed/jars⽂件夹下有很多打好的spark的包,⽐如:spark-catalyst_。这些jar包有可能与你下载的Spark源码的版本不⼀致,导致你在调

试源码时,发⽣jar包冲突。所以请将它们排除出去。

Eclip在对项⽬编译时,笔者本地出现了很多错误,有关这些错误的解决见附录H。所有错误解决后运⾏mvncleaninstall,如图1-27所⽰:

图1-27编译成功

5.调试Spark源码

以Spark源码⾃带的JavaWordCount为例,介绍如何调试Spark源码。右键单击,选择“DebugAs”→“JavaApplication”即可。如果想修改配置参数,右

键单击,选择“DebugAs”→“DebugConfigurations…”,从打开的对话框中选择JavaWordCount,在右侧标签可以修改Java执⾏参数、JRE、classpath、环

境变量等配置,如图1-28所⽰:

图1-28源码调试

读者也可以在Spark源码中设置断点,进⾏跟踪调试。

1.5⼩结

本章通过引导⼤家在Linux操作系统下搭建基本的执⾏环境,并且介绍spark-shell等脚本的执⾏,⽬的⽆法是为了帮助读者由浅⼊深的进⾏Spark源码的学习。由于⽬前多数

开发⼯作都在Windows系统下,并且Eclip有最⼴⼤的⽤户群,即便是⼀些开始使⽤IntelliJ的⽤户对Eclip也不陌⽣,所以在Windows环境下搭建源码阅读环境时,选择这些

最常⽤的⼯具,希望能降低读者的学习门槛,并且替⼤家节省时间。

本文发布于:2023-01-31 04:10:13,感谢您对本站的认可!

本文链接:http://www.wtabcd.cn/fanwen/fan/88/165687.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

上一篇:金山公园
标签:谢衣
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图