《深⼊理解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 条评论) |