MapReduce配置与优化

原创 2017年01月03日 21:42:53

1. 流程简介

MapReduce流程

参考MapReduce学习笔记之简介(一)

2. 配置详解

core-site.xml是全局配置,hdfs-site.xml和mapred-site.xml分别是hdfs和mapred的局部配置。

本文配置基于2.7.3版本。另外只列出了部分属性。

2.1 core-default.xml

选项 默认值 描述
hadoop.tmp.dir /tmp/hadoop-${user.name} 全局临时文件路径
io.seqfile.local.dir ${hadoop.tmp.dir}/io/local 合并序列化文件的中间文件存储路径,多个路径是用逗号隔开
fs.defaultFS file:/// namenode RPC交互端口,一般使用9000端口
io.file.buffer.size 4096 序列化文件的缓存大小,应该设置为硬件页面大小的倍数(x86是4096)
file.blocksize 67108864 块大小
ha.zookeeper.quorum (空) NameNode HA时,配置ZooKeeper节点,以逗号隔开
ha.zookeeper.session-timeout.ms 5000 ZKFC连接ZooKeeper的超时时间

2.2 hdfs-default.xml

选项 默认值 描述
dfs.replication 3 副本数
dfs.namenode.handler.count 10 设定 namenode server threads 的数量,这些 threads 會用 RPC 跟其他的 datanodes 沟通。
dfs.namenode.name.dir file://${hadoop.tmp.dir}/dfs/name namenode元数据存储路径,多个目录时,以逗号隔开
dfs.datanode.data.dir file://${hadoop.tmp.dir}/dfs/data datanode元数据存储路径,多个目录时,以逗号隔开
dfs.permissions.enabled true 开关权限验证,false时可以开启远程调试功能
dfs.namenode.secondary.http-address 0.0.0.0:50090 第二名字空间Http Server地址
dfs.namenode.secondary.https-address 0.0.0.0:50091 第二名字空间Https Server地址
dfs.datanode.address 0.0.0.0:50010 datanode地址
dfs.datanode.http.address 0.0.0.0:50075 datanode的http server地址
dfs.datanode.ipc.address 0.0.0.0:50020 datanode的ipc server地址
dfs.namenode.http-address 0.0.0.0:50070 dfs namenode的web ui地址
dfs.datanode.https.address 0.0.0.0:50475 datanode的https server地址
dfs.namenode.https-address 0.0.0.0:50470 namenode的https server地址
dfs.namenode.backup.address 0.0.0.0:50100 backupnode server地址
dfs.namenode.backup.http-address 0.0.0.0:50105 backupnode http server地址
dfs.blocksize 134217728 块大小,默认128m
dfs.namenode.checkpoint.dir file://${hadoop.tmp.dir}/dfs/namesecondary 第二名字空间存储用于合并的临时镜像的目录,可多个目录,用逗号隔开
dfs.nameservices (空) 用逗号分开的nameservice列表
dfs.ha.namenodes.xxx (空) xxx是nameservice名,该属性的值为其下的namenode列表,以逗号隔开
dfs.namenode.rpc-address.xxx.yyy (空) xxx是nameservice名,yyy是namennode名,该属性为对应RPC地址
dfs.namenode.http-address.xxx.yyy (空) xxx是nameservice名,yyy是namennode名,该属性为对应http server地址
dfs.namenode.shared.edits.dir (空) NameNode HA中,多个NameNode间共享数据的目录
dfs.ha.automatic-failover.enabled false 是否开启故障恢复
dfs.journalnode.rpc-address 0.0.0.0:8485 JournalNode RPC Server地址
dfs.journalnode.http-address 0.0.0.0:8480 JournalNode Http server地址
dfs.journalnode.https-address 0.0.0.0:8481 JournalNode https server地址

2.3 mapred-default.xml

选项 默认值 描述
mapreduce.jobtracker.jobhistory.location (空) job历史文件保存路径,默认在logs的history文件夹下。
mapreduce.task.io.sort.factor 10 排序文件时用于合并的流数量,即打开的文件句柄数
mapreduce.task.io.sort.mb 100 排序文件时总的内存量(MB),默认每个合并流1MB
mapreduce.map.sort.spill.percent 0.80 Map阶段溢写文件的阈值(排序缓冲区大小的百分比)
mapreduce.jobtracker.http.address 0.0.0.0:50030 jobtracker的tracker页面服务监听地址
mapreduce.cluster.local.dir ${hadoop.tmp.dir}/mapred/local mapred做本地计算所使用的文件夹,可以配置多块硬盘,逗号分隔
mapreduce.jobtracker.system.dir ${hadoop.tmp.dir}/mapred/system mapred存放控制文件所使用的文件夹,可配置多块硬盘,逗号分隔。
mapreduce.jobtracker.staging.root.dir ${hadoop.tmp.dir}/mapred/staging 用来存放与每个job相关的数据
mapreduce.job.running.map.limit 0 单个任务并发的最大map数,0或负数没有限制
mapreduce.job.running.reduce.limit 0 单个任务并发的最大reduce数,0或负数没有限制
mapreduce.map.memory.mb 1024 每个Map Task需要的内存量
mapreduce.map.cpu.vcores 1 每个Map Task需要的虚拟CPU个数
mapreduce.reduce.memory.mb 1024 每个Reduce Task需要的内存量
mapreduce.reduce.cpu.vcores 1 每个Reduce Task需要的虚拟CPU个数
mapred.child.java.opts -Xmx200m vm启动的子线程可以使用的最大内存。建议值-XX:-UseGCOverheadLimit -Xms512m -Xmx2048m -verbose:gc -Xloggc:/tmp/@taskid@.gc
mapreduce.reduce.shuffle.merge.percent 0.66 超过shuffle最大内存的一定限度后,开始往磁盘刷
mapreduce.reduce.shuffle.input.buffer.percent 0.70 shuffile在reduce内存中的数据最多使用内存量
mapreduce.reduce.shuffle.memory.limit.percent 0.25 每个fetch取到的输出的大小能够占的内存比的大小,所以,如果我们想fetch不进磁盘的话,可以适当调大这个值。
mapreduce.map.speculative true 是否对Map Task启用推测执行机制
mapreduce.reduce.speculative true 是否对Reduce Task启用推测执行机制
mapreduce.job.queuename default 作业提交到的队列
mapreduce.reduce.shuffle.parallelcopies 5 Reduce Task启动的并发拷贝数据的线程数目
mapreduce.map.output.compress false map输出结果是否要压缩
mapreduce.map.output.compress.codec org.apache.hadoop.io.compress
.DefaultCodec
map输出的压缩算法
map.sort.class org.apache.hadoop.util.QuickSort 排序时使用的算法
mapreduce.shuffle.port 13562 ShuffleHandler运行的默认端口
mapreduce.jobhistory.address 0.0.0.0:10020 MapReduce JobHistory Server IPC地址
mapreduce.jobhistory.webapp.address 0.0.0.0:19888 MapReduce JobHistory Server Web UI地址
mapreduce.jobhistory.admin.address 0.0.0.0:10033 History Server的管理地址
mapreduce.input.fileinputformat.split.minsize 0 map任务输入数据块最小大小
yarn.app.mapreduce.am.command-opts -Xmx1024m MR App master的java选项

3. Map优化

3.1 输入过滤

见本人博客
输入过滤

3.2 小文件优化

见本人博客
Hadoop的“小文件”问题

3.3 连接

3.3.1 Map端连接

使用场景:待连接的数据集中有一个数据集小到可以完全放在缓存中。

job的main函数中设置缓存文件

Job job = Job.getInstance(conf, "MapJoinDemo");
job.setJarByClass(ProvinceMapJoinStatistics.class);

job.addCacheFile(new Path(args[1]).toUri());

Mapper的setup方法中读取缓存文件

private String provinceWithProduct = "";

/**
 * 加载缓存文件
 *
 * @param context 上下文
 *
 * @throws IOException
 * @throws InterruptedException
 */
@Override
protected void setup(Context context) throws IOException, InterruptedException {

    URI[] uri = context.getCacheFiles();
    if (uri == null || uri.length == 0) {
        return;
    }
    for (URI p : uri) {
        if (p.toString().endsWith("part-r-00000")) {
            // 读缓存文件
            try {
                provinceWithProduct = HdfsUtil.read(new Configuration(), p.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Mapper的map方法中实现连接

public void map(LongWritable key, Text value, Context context)
        throws IOException, InterruptedException {

    if (!provinceWithProduct.contains(value.toString()
            .substring(0, 2))) {
        context.write(value, NullWritable.get());
    }
}

3.3.2 半连接

待连接的数据集中有一个数据集非常大,但同时这个数据集可以被过滤成小到可以放在内存中。

job的main函数中设置缓存文件(即过滤条件)

# 同map端连接

Mapper的setup方法中读取缓存文件

# 同map端连接
# 生成过滤集合 joinKeySet

Mapper的map方法中实现数据过滤

@Override  
protected void map(Object key, Text value, Context context)  
        throws IOException, InterruptedException {  
    // 获得文件输入路径  
    String pathName = ((FileSplit) context.getInputSplit()).getPath()  
            .toString();  
    // 数据来自tb_dim_city.dat文件,标志即为"0"  
    if (pathName.endsWith("tb_dim_city.dat")) {  
        String[] valueItems = value.toString().split("\\|");  
        // 过滤格式错误的记录  
        if (valueItems.length != 5) {  
            return;  
        }  
        // 过滤掉不需要参加join的记录  
        if (joinKeySet.contains(valueItems[0])) {  
            flag.set("0");  
            joinKey.set(valueItems[0]);  
            secondPart.set(valueItems[1] + "\t" + valueItems[2] + "\t"  
                    + valueItems[3] + "\t" + valueItems[4]);  
            combineValues.setFlag(flag);  
            combineValues.setJoinKey(joinKey);  
            combineValues.setSecondPart(secondPart);  
            context.write(combineValues.getJoinKey(), combineValues);  
        } else {  
            return;  
        }  
    }
    // 数据来自于tb_user_profiles.dat,标志即为"1"  
    else if (pathName.endsWith("tb_user_profiles.dat")) {  
        String[] valueItems = value.toString().split("\\|");  
        // 过滤格式错误的记录  
        if (valueItems.length != 4) {  
            return;  
        }  
        // 过滤掉不需要参加join的记录  
        if (joinKeySet.contains(valueItems[3])) {  
            flag.set("1");  
            joinKey.set(valueItems[3]);  
            secondPart.set(valueItems[0] + "\t" + valueItems[1] + "\t"  
                    + valueItems[2]);  
            combineValues.setFlag(flag);  
            combineValues.setJoinKey(joinKey);  
            combineValues.setSecondPart(secondPart);  
            context.write(combineValues.getJoinKey(), combineValues);  
        } else {  
            return;  
        }  
    }  
}

Reducer的reduce方法实现连接

public static class SemiJoinReducer extends  
        Reducer<Text, CombineValues, Text, Text> {  
    // 存储一个分组中的左表信息  
    private ArrayList<Text> leftTable = new ArrayList<Text>();  
    // 存储一个分组中的右表信息  
    private ArrayList<Text> rightTable = new ArrayList<Text>();  
    private Text secondPar = null;  
    private Text output = new Text();  

    /** 
     * 一个分组调用一次reduce函数 
     */  
    @Override  
    protected void reduce(Text key, Iterable<CombineValues> value,  
            Context context) throws IOException, InterruptedException {  
        leftTable.clear();  
        rightTable.clear();  
        /** 
         * 将分组中的元素按照文件分别进行存放 这种方法要注意的问题: 如果一个分组内的元素太多的话,可能会导致在reduce阶段出现OOM, 
         * 在处理分布式问题之前最好先了解数据的分布情况,根据不同的分布采取最 
         * 适当的处理方法,这样可以有效的防止导致OOM和数据过度倾斜问题。 
         */  
        for (CombineValues cv : value) {  
            secondPar = new Text(cv.getSecondPart().toString());  
            // 左表tb_dim_city  
            if ("0".equals(cv.getFlag().toString().trim())) {  
                leftTable.add(secondPar);  
            }  
            // 右表tb_user_profiles  
            else if ("1".equals(cv.getFlag().toString().trim())) {  
                rightTable.add(secondPar);  
            }  
        }  
        logger.info("tb_dim_city:" + leftTable.toString());  
        logger.info("tb_user_profiles:" + rightTable.toString());  
        for (Text leftPart : leftTable) {  
            for (Text rightPart : rightTable) {  
                output.set(leftPart + "\t" + rightPart);  
                context.write(key, output);  
            }  
        }  
    }  
}

4. Shuffle优化

4.1 中间输出结果的排序与溢出

中间输出结果的排序与溢出

4.2 本地Reducer和Combiner

本地Reducer和Combiner

4.3 Map侧输出

获取中间输出结果(Map侧)

5. Reduce优化

5.1 Reduce任务数

见本人博客Reduce任务

5.2 获取中间输出结果(Reduce侧)

见本人博客获取中间输出结果(Reduce侧)

5.3 中间输出结果的合并与溢出

见本人博客中间输出结果的合并与溢出

5.4 Reduce端连接

使用场景:连接两个或多个大型数据集。

package com.mr.reduceSideJoin;  

import java.io.DataInput;  
import java.io.DataOutput;  
import java.io.IOException;  

import org.apache.hadoop.io.Text;  
import org.apache.hadoop.io.WritableComparable;  

public class CombineValues implements WritableComparable<CombineValues> {  
    private Text joinKey;// 链接关键字  
    private Text flag;// 文件来源标志  
    private Text secondPart;// 除了链接键外的其他部分  

    public void setJoinKey(Text joinKey) {  
        this.joinKey = joinKey;  
    }  

    public void setFlag(Text flag) {  
        this.flag = flag;  
    }  

    public void setSecondPart(Text secondPart) {  
        this.secondPart = secondPart;  
    }  

    public Text getFlag() {  
        return flag;  
    }  

    public Text getSecondPart() {  
        return secondPart;  
    }  

    public Text getJoinKey() {  
        return joinKey;  
    }  

    public CombineValues() {  
        this.joinKey = new Text();  
        this.flag = new Text();  
        this.secondPart = new Text();  
    }  

    @Override  
    public void write(DataOutput out) throws IOException {  
        this.joinKey.write(out);  
        this.flag.write(out);  
        this.secondPart.write(out);  
    }  

    @Override  
    public void readFields(DataInput in) throws IOException {  
        this.joinKey.readFields(in);  
        this.flag.readFields(in);  
        this.secondPart.readFields(in);  
    }  

    @Override  
    public int compareTo(CombineValues o) {  
        return this.joinKey.compareTo(o.getJoinKey());  
    }  

    @Override  
    public String toString() {  
        // TODO Auto-generated method stub  
        return "[flag=" + this.flag.toString() + ",joinKey="  
                + this.joinKey.toString() + ",secondPart="  
                + this.secondPart.toString() + "]";  
    }  
}
package com.mr.reduceSideJoin;  

import java.io.IOException;  
import java.util.ArrayList;  

import org.apache.hadoop.conf.Configuration;  
import org.apache.hadoop.conf.Configured;  
import org.apache.hadoop.fs.Path;  
import org.apache.hadoop.io.Text;  
import org.apache.hadoop.mapreduce.Job;  
import org.apache.hadoop.mapreduce.Mapper;  
import org.apache.hadoop.mapreduce.Reducer;  
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;  
import org.apache.hadoop.mapreduce.lib.input.FileSplit;  
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;  
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;  
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;  
import org.apache.hadoop.util.Tool;  
import org.apache.hadoop.util.ToolRunner;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  

public class ReduceSideJoin_LeftOuterJoin extends Configured implements Tool {  
    private static final Logger logger = LoggerFactory  
            .getLogger(ReduceSideJoin_LeftOuterJoin.class);  

    public static class LeftOutJoinMapper extends  
            Mapper<Object, Text, Text, CombineValues> {  
        private CombineValues combineValues = new CombineValues();  
        private Text flag = new Text();  
        private Text joinKey = new Text();  
        private Text secondPart = new Text();  

        @Override  
        protected void map(Object key, Text value, Context context)  
                throws IOException, InterruptedException {  
            // 获得文件输入路径  
            String pathName = ((FileSplit) context.getInputSplit()).getPath()  
                    .toString();  
            // 数据来自tb_dim_city.dat文件,标志即为"0"  
            if (pathName.endsWith("tb_dim_city.dat")) {  
                String[] valueItems = value.toString().split("\\|");  
                // 过滤格式错误的记录  
                if (valueItems.length != 5) {  
                    return;  
                }  
                flag.set("0");  
                joinKey.set(valueItems[0]);  
                secondPart.set(valueItems[1] + "\t" + valueItems[2] + "\t"  
                        + valueItems[3] + "\t" + valueItems[4]);  
                combineValues.setFlag(flag);  
                combineValues.setJoinKey(joinKey);  
                combineValues.setSecondPart(secondPart);  
                context.write(combineValues.getJoinKey(), combineValues);  

            }// 数据来自于tb_user_profiles.dat,标志即为"1"  
            else if (pathName.endsWith("tb_user_profiles.dat")) {  
                String[] valueItems = value.toString().split("\\|");  
                // 过滤格式错误的记录  
                if (valueItems.length != 4) {  
                    return;  
                }  
                flag.set("1");  
                joinKey.set(valueItems[3]);  
                secondPart.set(valueItems[0] + "\t" + valueItems[1] + "\t"  
                        + valueItems[2]);  
                combineValues.setFlag(flag);  
                combineValues.setJoinKey(joinKey);  
                combineValues.setSecondPart(secondPart);  
                context.write(combineValues.getJoinKey(), combineValues);  
            }  
        }  
    }  

    public static class LeftOutJoinReducer extends  
            Reducer<Text, CombineValues, Text, Text> {  
        // 存储一个分组中的左表信息  
        private ArrayList<Text> leftTable = new ArrayList<Text>();  
        // 存储一个分组中的右表信息  
        private ArrayList<Text> rightTable = new ArrayList<Text>();  
        private Text secondPar = null;  
        private Text output = new Text();  

        /** 
         * 一个分组调用一次reduce函数;相同key的数据进了同一个reduce,这样就实现了join。 
         */  
        @Override  
        protected void reduce(Text key, Iterable<CombineValues> value,  
                Context context) throws IOException, InterruptedException {  
            leftTable.clear();  
            rightTable.clear();  
            /** 
             * 将分组中的元素按照文件分别进行存放 这种方法要注意的问题: 如果一个分组内的元素太多的话,可能会导致在reduce阶段出现OOM, 
             * 在处理分布式问题之前最好先了解数据的分布情况,根据不同的分布采取最 
             * 适当的处理方法,这样可以有效的防止导致OOM和数据过度倾斜问题。 
             */  
            for (CombineValues cv : value) {  
                secondPar = new Text(cv.getSecondPart().toString());  
                // 左表tb_dim_city  
                if ("0".equals(cv.getFlag().toString().trim())) {  
                    leftTable.add(secondPar);  
                }  
                // 右表tb_user_profiles  
                else if ("1".equals(cv.getFlag().toString().trim())) {  
                    rightTable.add(secondPar);  
                }  
            }  
            logger.info("tb_dim_city:" + leftTable.toString());  
            logger.info("tb_user_profiles:" + rightTable.toString());  
            // 这里体现了左连接  
            for (Text leftPart : leftTable) {  
                for (Text rightPart : rightTable) {  
                    output.set(leftPart + "\t" + rightPart);  
                    // leftTable中有数据 rightTable中没有数据 就无法进到这一步  
                    // rightTable中有数据 leftTable中没有数据 外面的循环就进不去  
                    context.write(key, output);  
                }  
            }  
        }  
    }  

    @Override  
    public int run(String[] args) throws Exception {  
        Configuration conf = getConf(); // 获得配置文件对象  
        Job job = new Job(conf, "LeftOutJoinMR");  
        job.setJarByClass(ReduceSideJoin_LeftOuterJoin.class);  

        FileInputFormat.addInputPath(job, new Path(args[0])); // 设置map输入文件路径  
        FileOutputFormat.setOutputPath(job, new Path(args[1])); // 设置reduce输出文件路径  

        job.setMapperClass(LeftOutJoinMapper.class);  
        job.setReducerClass(LeftOutJoinReducer.class);  

        job.setInputFormatClass(TextInputFormat.class); // 设置文件输入格式  
        job.setOutputFormatClass(TextOutputFormat.class);// 使用默认的output格式  

        // 设置map的输出key和value类型  
        job.setMapOutputKeyClass(Text.class);  
        job.setMapOutputValueClass(CombineValues.class);  

        // 设置reduce的输出key和value类型  
        job.setOutputKeyClass(Text.class);  
        job.setOutputValueClass(Text.class);  
        job.waitForCompletion(true);  
        return job.isSuccessful() ? 0 : 1;  
    }  

    public static void main(String[] args) throws IOException,  
            ClassNotFoundException, InterruptedException {  
        try {  
            Tool rdf = new ReduceSideJoin_LeftOuterJoin();  
            int returnCode = ToolRunner.run(rdf, args);  
            System.exit(returnCode);  
        } catch (Exception e) {  
            System.out.println(e.getMessage());  
        }  
    }  
}

6. 其他

6.1 作业JVM堆大小设置优化

示例演示限制客户修改堆大小

<!--
mapred.task.java.opts是自定义的。
final被设置为true,不让客户修改
后面的-Xmx1000m会覆盖前面的
-->
<property>
  <name>mapred.task.java.opts</name>
  <value>-Xmx2000m</value>
</property>

<property>
  <name>mapred.child.java.opts</name>
  <value>${mapred.task.java.opts} -Xmx1000m</value>
  <final>true</final>
</property>

<!-- 分别配置作业的Map和Reduce阶段的heap的大小 -->
<property>
  <name>mapred.map.child.java.opts</name>
  <value>-Xmx512M</value>
</property>

<property>
  <name>mapred.reduce.child.java.opts</name>
  <value>-Xmx1024M</value>
</property>

通过管理员配置限制

<property>
  <name>mapreduce.admin.map.child.java.opts</name>
  <value>-Xmx1000M</value>
</property>

<property>
  <name>mapreduce.admin.reduce.child.java.opts</name>
  <value>-Xmx1000M</value>
</property>

# 修改源码
private static String getChildJavaOpts(JobConf jobConf, boolean isMapTask) {
    // 略

    // old: return adminClasspath + " " + userClasspath;
    // 修改为
    return userClasspath + " " + adminClasspath;
}

7. 参考

Hadoop中两表JOIN的处理方法

hadoop MapReduce 三种连接

hadoop核心逻辑shuffle代码分析-reduce端

hadoop作业调优参数整理及原理

MapReduce任务参数调优

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Hadoop 设置任务执行的队列以及优先级

Hadoop 设置任务执行的队列以及优先级
  • wisgood
  • wisgood
  • 2014年09月05日 08:07
  • 23538

Hadoop 设置队列计算能力调度器应用和配置

http://www.cnblogs.com/ggjucheng/archive/2012/07/25/2608817.html 需求 公司里有两个部门,一个叫hive,一个叫pig,...

Hadoop 设置任务执行的队列以及优先级

http://blog.csdn.net/wisgood/article/details/39075883 作业提交到的队列:mapreduce.job.queuename ...

hadoop队列管理(指定queue跑程序)

Hadoop 队列管理: 配置Queue相关信息 •配置属性在mapre-site.xml 配置Queue的相关信息  mapred.acls.enabled  true  mapred.que...

CDH集群调优:内存、Vcores和DRF

原文地址:http://blog.selfup.cn/1631.html?utm_source=tuicool&utm_medium=referral 吐槽 最近“闲”来无事,通过...

Yarn资源分配性能调优

日志: Container [pid=134663,containerID=container_1430287094897_0049_02_067966] is running beyond phy...

yarn & mapreduce 配置参数总结

配置 设置nodemanager 总内存大小为32G,在yarn-site.xml 增加如下内容: yarn.nodemanager.resource.memory-mb 32768 con...

MapReduce配置

MapReduce程序可以以以下三种模式运行 Local(Standalone) Mode:只有一个 Java 虚拟机在跑,完全没有分布式的成分。且不使用HDFS文件系统,而是使用本机的Linux文...

hadoop cdh4.5 mapreduce yarn 配置文件 (接上文)

yarn-site.xml:

hadoop2的配置参数

name value description mapreduce.jobtracker.jobhistory.location   If job tracker is static ...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:MapReduce配置与优化
举报原因:
原因补充:

(最多只允许输入30个字)