IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> HDFS之MapReduce(特别篇) -> 正文阅读

[大数据]HDFS之MapReduce(特别篇)

HDFS之MapReduce(特别篇)

1、MapReduce概述

1、MapReduce定义

? MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。

? MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整分布式运算程序,并发运行在一个Hadoop集群上。

2、MapReduce优点

1、MapReduce易于编程

? 它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。也就是说编写一个分布式程序,跟写一个简单的串行程序一模一样的。就是因为这个特点使得MapReduce编程变得非常流行。

2、良好的扩展性

? 当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。

3、高容错性

? MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而且完全是Hadoop内部完成的。

4、适合PB级以上海量数据的离线处理

? 可以实现上千台服务器集群并发工作,提供数据处理能力。

3、MapReduce缺点

1、不擅长实时计算

? MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。

2、不擅长流式计算

? 流式计算的输入数据是动态的,二MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce自身的设计特点决定了数据源必须是静态的。

3、不擅长DAG(有向图)计算

? 多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这个情况下,MapReduce并不是能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。

4、MapReduce核心思想

? 在这里插入图片描述

? (1) 分布式的运算程序往往需要分成2个阶段。

? (2) 第一个阶段的MapTask并发实例,完全并行运行,互不相干。

? (3) 第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。

? (4) MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,穿行运行。

5、MapReduce进程

? 一个完整的MapReduce程序在分布式运行时有三类实例进程:

? (1) MrAppMaster:负责整个程序的过程调度及状态协调。

? (2) MapTask:负责Map阶段的整个数据处理流程。

? (3) ReduceTask:负责Reduce阶段的整个数据处理流程。

6、常用数据序列化类型

Java类型Hadoop writable类型
booleanBooleanWritable
byteByteWritable
intIntWritable
floatFloatWritable
longLongWritable
doubleDoubleWritable
StringText
mapMapWritable
arrayArrayWritable

7、MapReduce编程规范

? 用户编程的程序分成三个部分:Mapper、Reducer和Driver。

1、Mapper阶段

? (1) 用户自定义的Mapper要继承自己的父类

? (2) Mapper的输入数据是KV对的形式(KV的类型可自定义)

? (3) Mapper中的业务逻辑写在map()方法中

? (4) Mapper的输出数据是KV对的形式(KV的类型可自定义)

? (5) map()方法(MapTask进程)对每一个<K,V>调用一次

2、Reduce阶段

? (1) 用户自定义的Reduce要继承自己的父类

? (2) Reduce的输入数据类型对应Mapper的输出数据类型,也是KV

? (3) Reducer的业务逻辑写在reduce()方法中

? (4) Reduce Task进程对每一组相同的<k,v>组调用一次reduce()方法

3、Driver阶段

? 相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装Map Reduce程序相关运行参数的job对象。

8、Word Count案例

? 1、首先创建工程项目

? 2、添加相关依赖

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>

? 3、编写Mapper类

package com.lyinl.mapreduce;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class WordcountMapper extends Mapper<LongWritable, 
Text, Text, IntWritable>{
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context 
context) throws IOException, InterruptedException {
// 1 获取一行
String line = value.toString();
// 2 切割
String[] words = line.split(" ");
// 3 输出
for (String word : words) {
k.set(word);
context.write(k, v);
} } }

? 4、编写Reducer类

package com.lyinl.mapreduce.wordcount;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class WordcountReducer extends Reducer<Text, 
IntWritable, Text, IntWritable>{
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> 
values,Context context) throws IOException, 
InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable count : values) {
sum += count.get();
}
// 2 输出
 v.set(sum);
context.write(key,v);
} }

? 5、编写Driver驱动类

package com.lyinl.mapreduce.wordcount;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import 
org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import 
org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordcountDriver {
public static void main(String[] args) throws IOException, 
ClassNotFoundException, InterruptedException {
// 1 获取配置信息以及封装任务
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 设置 jar 加载路径
job.setJarByClass(WordcountDriver.class);
// 3 设置 map 和 reduce 类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
// 4 设置 map 输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new 
Path(args[1]));
// 7 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
} }

? 6、集群上测试

? 用maven打jar包,需要添加的打包插件依赖

? 注意:mainClass类需要替换为自己工程主类

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin </artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-withdependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.lyinl.mr.WordcountDriver</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

? 然后将jar包拷贝到Hadoop集群中。启动Hadoop集群,执行WordCount程序

[lyinl@hadoop102 software]$ hadoop jar wc.jar
com.lyinl.wordcount.WordcountDriver /user/lyinl/input 
/user/lyinl/output

2、Hadoop序列化

1、序列化概述

1、定义:

? 序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。

? 反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。

2、序列化的意义:

? 一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。

? 但例外的是,Java的序列化是一个重量级序列化框架(Serializable),一个对象序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable)。

Hadoop序列化特点:

? (1) 紧凑:高效使用存储空间。

? (2) 快速:读写数据的额外开销小。

? (3) 可扩展:随着通信协议的升级而可升级

? (4) 互操作:支持多语言的交互

2、自定义bean对象实现序列化接口(Writable)

? 由于在实际开发中,往往常用的基本序列化类型不能满足所有需求,故此需要实现bean序列化对象:

1、具体步骤:

? (1) 必须实现Writable接口

? (2) 反序列化时,需要反射调用空参构造函数,所以必须有空参构造

public FlowBean() {
super();
}

? (3) 重写序列化方法

@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}

? (4) 重写反序列化方法

@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}

? 注意:反序列化的顺序和序列化的顺序完全一致

? (5) 要想把结果显示在文件中,需要重写 toString(),可用”\t”分开,方便后续用。

? (6) 如果需要将自定义的 bean 放在 key 中传输,则还需要实现 Comparable 接口,因为MapReduce 框中的 Shuffle 过程要求对 key 必须能排序.

@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
2、案例

? 统计每一个手机号耗费的总上行流量、下行流量、总流量

? (1) 编写流量统计的Bean对象

package com.lyinl.mapreduce.flowsum;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Writable;
// 1 实现 writable 接口
public class FlowBean implements Writable{
private long upFlow;
private long downFlow;
private long sumFlow;
//2 反序列化时,需要反射调用空参构造函数,所以必须有
public FlowBean() {
super();
}
public FlowBean(long upFlow, long downFlow) {
super();
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
//3 写序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
//4 反序列化方法
//5 反序列化方法读顺序必须和写序列化方法的写顺序必须一致
@Override
public void readFields(DataInput in) throws IOException {
this.upFlow = in.readLong();
this.downFlow = in.readLong();
this.sumFlow = in.readLong();
}
// 6 编写 toString 方法,方便后续打印到文本
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
    this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
} }

? (2) 编写Mapper类

package com.lyinl.mapreduce.flowsum;
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class FlowCountMapper extends Mapper<LongWritable, 
Text, Text, FlowBean>{
FlowBean v = new FlowBean();
Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Context 
context) throws IOException, InterruptedException {
// 1 获取一行
String line = value.toString();
// 2 切割字段
String[] fields = line.split("\t");
// 3 封装对象
// 取出手机号码
String phoneNum = fields[1];
// 取出上行流量和下行流量
long upFlow = Long.parseLong(fields[fields.length -
3]);
long downFlow = Long.parseLong(fields[fields.length -
2]);
k.set(phoneNum);
v.set(downFlow, upFlow);
// 4 写出
context.write(k, v);
} }

? (3) 编写Reducer类

package com.lyinl.mapreduce.flowsum;
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class FlowCountReducer extends Reducer<Text, 
FlowBean, Text, FlowBean> {
@Override
protected void reduce(Text key, Iterable<FlowBean> values, 
Context context)throws IOException, InterruptedException {
long sum_upFlow = 0;
long sum_downFlow = 0;
// 1 遍历所用 bean,将其中的上行流量,下行流量分别累加
for (FlowBean flowBean : values) {
sum_upFlow += flowBean.getUpFlow();
sum_downFlow += flowBean.getDownFlow();
}
// 2 封装对象
FlowBean resultBean = new FlowBean(sum_upFlow, 
sum_downFlow);
// 3 写出
context.write(key, resultBean);
} }

? (4) 编写Driver驱动类

package com.lyinl.mapreduce.flowsum;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import 
org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import 
org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class FlowsumDriver {
public static void main(String[] args) throws 
IllegalArgumentException, IOException, 
ClassNotFoundException, InterruptedException {
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[] { "e:/input/inputflow", 
"e:/output1" };
// 1 获取配置信息,或者 job 对象实例
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 6 指定本程序的 jar 包所在的本地路径
job.setJarByClass(FlowsumDriver.class);
// 2 指定本业务 job 要使用的 mapper/Reducer 业务类
job.setMapperClass(FlowCountMapper.class);
job.setReducerClass(FlowCountReducer.class);
// 3 指定 mapper 输出数据的 kv 类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 4 指定最终输出的数据的 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 5 指定 job 的输入原始文件所在目录
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new 
Path(args[1]));
// 7 将 job 中配置的相关参数,以及 job 所用的 java 类所在的 jar
包, 提交给 yarn 去运行
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
} }

3、Map Reduce框架原理

1、Input Format数据输入

1、切片与MapTask并行度决定机制

? 首先,MapTask的并行度决定Map阶段的任务处理并行度,进而影响到整个Job的处理速度。

? 例如1G的数据,启动8个MapTask,可以提高集群的并发处理能力。那么1k的数据,也启动8个MapTask,会提高集群性能吗?MapTask并行任务是否越多越好呢?那影响Map Task并行度因素有哪些?

? (1) MapTask并行度决定机制

? 数据块:Block是HDFS物理上把数据分成一块一块。

? 数据切片:数据切片只是在逻辑上对输入进行分片,不会在磁盘上将其切分成片进行储存。

在这里插入图片描述

2、Job提交流程源码和切片源码详解

? (1) Job提交流程源码详解:

waitForCompletion()
submit();
// 1 建立连接
connect();
// 1)创建提交 Job 的代理
new Cluster(getConfiguration());
// (1)判断是本地 yarn 还是远程
initialize(jobTrackAddr, conf); 
// 2 提交 job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的 Stag 路径
Path jobStagingArea = 
JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取 jobid ,并创建 Job 路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝 jar 包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向 Stag 路径写 XML 配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交 Job,返回提交状态
status = submitClient.submitJob(jobId, 
submitJobDir.toString(), job.getCredentials());

在这里插入图片描述

? (2) FileInputFormat切片源码解析(input.getSplits(job))

? a、程序先找到数据存储的目录。

? b、开始遍历处理(规划切片)目录下的每一个文件

? c、遍历第一个文件ss.txt

? 1、获取文件大小fs.sizeOf(ss.txt)

? 2、计算切片大小

computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M

? 3、默认情况下,切片大小=block.size

? 4、开始切,形成第1个切片:ss.txt-0:128M第2个切片ss.txt-128:256M第3个切片ss.txt-256M:300M

? 每次切片时,都要判断切完剩下的部分是否大于的1.1倍,不大于1.1倍就划分一块切片

? 5、将切片信息写到一个切片规划文件中

? 6、整个切片的核心过程在getSplit()方法中完成

? 7、InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。

? d、提交切边规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数。

3、FileInputFormat切片机制

? 1、切片机制

? (1) 简单地按照文件的内容长度进行切片

? (2) 切片大小,默认等于Block大小

? (3) 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片

? 2、案例分析

在这里插入图片描述

? (1) 源码中计算切片大小的公式

Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
因此,默认情况下,切片大小=blocksize。

? (2) 切片大小设置

maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。

? (3) 获取切片信息API

// 获取切片的文件名称
String name = inputSplit.getPath().getName();
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
4、Combine TextInputFormat切片机制

? 框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率及其低下。

? 1、应用场景:

? CombinTextInputFormat用于下文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。

? 2、虚拟存储切片最大值设置

CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m

注意:虚拟存储切片最大设置最好根据实际的小文件大小情况来设置具体值

? 3、切片机制

? 生成切片过程包括:虚拟存储过程和切片过程两部分。

在这里插入图片描述

? (1) 虚拟存储过程:

? 将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块:当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。

? 例如setMaxInputSplitSize值为4M,输入文件大小8.02M,则先逻辑上分成一个4M。剩余的大小文件4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分(2.01M和2.01M)两个文件。

? (2) 切片过程:

? (a) 判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。

? (b) 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。

? 例如:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟储存之后形成6个文件块,大小分别为:1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M)最终会形成2个切片,大小分别为:(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2021-08-18 12:46:21  更:2021-08-18 12:46:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:02:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码