当前位置:   article > 正文

Nebula环境构建、性能测试_运行nebulas测试节点

运行nebulas测试节点

Nebula 环境构建

安装机器基本信息查看

查看系统内核:uname -a
查看操作系统版本:cat /etc/redhat-release
  • 1
  • 2

安装 Docker

官网地址:https://docs.docker.com/engine/install/centos/

sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
 
sudo yum install docker-ce docker-ce-cli containerd.io
 
yum list docker-ce --showduplicates | sort -r
 
sudo yum install docker-ce-19.03.9 docker-ce-cli-19.03.9 containerd.io
 
sudo systemctl start docker
 
sudo docker run hello-world
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

安装 Docker Compose

官网地址:https://docs.docker.com/compose/install/
reference: link

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# github访问太慢用daocloud
sudo curl -L "https://get.daocloud.io/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
 
# 还是不行把文件下载到本地,然后传到服务器
https://github.com/docker/compose/releases
 
scp -r docker-compose-Linux-x86_64 weixiaochao1@perfsys02.bdg.shbt.qihoo.net:/home/xx
 
mv /home/xx/docker-compose-Linux-x86_64 /usr/local/bin/docker-compose
 
sudo chmod +x /usr/local/bin/docker-compose
# 添加软链
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
 
# 验证
docker-compose --version
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

安装 Nebula

这个过程比较曲折,对过程不感兴趣可以直接跳到「快捷安装」
官网地址:link

# 源码clone到本地,再上传到服务器
scp -r nebula-graph xxx02.bdg.shbt.qihoo.net:/home/xx
# cmake指令安装
https://www.cnblogs.com/6b7b5fc3/p/12715954.html
# 下载文件上传到服务器,解压后进入到bin目录,执行 ./cmake -version 验证。
# 添加软链(使用绝对路径):
sudo ln -s /home/xx/cmake-3.21.1-linux-x86_64/bin/cmake /usr/bin/cmake
#使用源码安装方式行不通,cmake会去github clone nebula-common,so so so 打算换rpm方式安装,先确认支持rpm,rpm --version
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这里插入图片描述
「快捷安装」

xxx,前面都是歧路啊,按下面这个来。。。
https://docs.nebula-graph.com.cn/2.0.1/2.quick-start/2.deploy-nebula-graph-with-docker-compose/
  • 1
  • 2

Nebula 使用

# 切换到root用户
sudo su 
# 启动:
docker-compose up -d
# 关闭:
docker-compose down
# 连接:
docker run --rm -ti --network nebula-docker-compose_nebula-net --entrypoint=/bin/sh vesoft/nebula-console:v2-nightly
nebula-console -u user -p password --address=graphd --port=9669
 
# 可以在Studio页面创建图空间,等价于命令创建
# 查看空间:
show spaces
# 查看标签:
show tags
# 查看边:
show edges
# 查看标签下点:
fetch prop on <tag name> <vid>
 
# refrence:
https://docs.nebula-graph.com.cn/2.0.1/2.quick-start/5.start-stop-service/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

安装Nebula Graph Studio

# 官网地址
https://docs.nebula-graph.com.cn/2.0.1/nebula-studio/deploy-connect/st-ug-deploy/
# 添加连接:
Host:perfsys02.bdg.shbt.qihoo.net:9669
# 用户名/密码:未设置,用默认的
# 访问地址:
http://host:7001/schema
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

安装Nebula Importer

# 先装go环境
https://github.com/vesoft-inc/nebula-importer/blob/release-v2.0.0-ga/docs/golang-install.md
# 再编译nebula importer源码
https://docs.nebula-graph.com.cn/2.0.1/nebula-importer/use-importer/#
  • 1
  • 2
  • 3
  • 4

准备数据

笔者手里有一份可用验证的完整图数据,只不过是json格式,Nebula导入需要csv格式,so 先转换一波。

在线转换数据格式

# 小文件可以在线直接转换
http://www.convertcsv.com/json-to-csv.htm
  • 1
  • 2

Python 工具包转换数据格式

# 大文件只能是在本地写程序转换了~
https://github.com/alingse/jsoncsv

# 安装工具
# 先安装python运行环境,再安装jsoncsv。
pip install jsoncsv
 
cat accounts_filter.csv | jsoncsv | mkexcel > real_accounts_filter.csv
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Java 代码转换数据格式

上述两种方式转化后格式不符,最终还是Java手写额~

public class JsonToCsv {
    private static String[] tagHeaderArray = new String[]{":VID(string)", "accounts.record_date:string", "accounts.record_time:string", "accounts.acct_id:string", "accounts.dsply_nm:string",
            "accounts.type:string", "accounts.acct_stat:string", "accounts.acct_rptng_crncy:string", "accounts.prior_sar_count:string", "accounts.branch_id:string", "accounts.open_dt:string",
            "accounts.close_dt:string", "accounts.initial_deposit:string", "accounts.tx_behavior_id:string", "accounts.bank_id:string", "accounts.country:string"};
    private static String[] edgeHeaderArray = new String[]{":SRC_VID(string)", ":DST_VID(string)", ":RANK",
            "transactions.record_date:string", "transactions.record_time:string", "transactions.tran_id:string",
            "transactions.orig_acct:string", "transactions.bene_acct:string", "transactions.tx_type:string",
            "transactions.base_amt:double", "transactions.tran_timestamp:string", "transactions.is_sar:string",
            "transactions.alert_id:string"};
 
    public static void main(String[] args) {
        System.out.println("=============== start ===============");
        // 转换tag数据 accounts_filter.csv共计 1000000 条数据
//        String readDataPath = "D:\\data_tmp\\account.csv";
//        String writeDataPath = "D:\\data_tmp\\real_account.csv";
//        String readDataPath = "D:\\Downloads\\datas\\accounts_filter.csv";
//        String writeDataPath = "D:\\Downloads\\datas\\real_accounts.csv";
//        readJsonWriteCsv(readDataPath, writeDataPath, tagHeaderArray, "tag");
 
        // 转换edge数据 transactions_filter.csv共计 103080302 条数据
//        String readDataPath = "D:\\data_tmp\\edge.csv";
//        String writeDataPath = "D:\\data_tmp\\real_edge.csv";
        String readDataPath = "D:\\Downloads\\datas\\transactions_filter.csv";
        String writeDataPath = "D:\\Downloads\\datas\\real_transactions.csv";
        readJsonWriteCsv(readDataPath, writeDataPath, edgeHeaderArray, "edge");
 
        System.out.println("=============== end ===============");
    }
 
    private static void writeRow(List<String> row, BufferedWriter csvWriter) throws IOException {
        StringBuilder rowStr = new StringBuilder();
        for (int i=0; i<row.size(); i++) {
            if (i == row.size() -1 ) {
                rowStr.append(row.get(i));
            } else {
                rowStr.append(row.get(i)).append(",");
            }
        }
        csvWriter.write(rowStr.toString());
        csvWriter.newLine();
    }
 
    public static void readJsonWriteCsv(String readDataPath, String writeDataPath, String[] headerArray, String type) {
        List<String> headerList = Arrays.asList(headerArray);
        BufferedWriter csvWriter = null;
        try {
            // 创建输出csv文件
            File csvFile = new File(writeDataPath);
            csvFile.createNewFile();
            // GB2312使正确读取分隔符","
            csvWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(
                    csvFile), "GB2312"), 1024);
            // 写入文件头
            writeRow(headerList, csvWriter);
            // 读json文件
            FileReader fr = new FileReader(readDataPath);
            Scanner scanner = new Scanner(fr);
            int rowNum = 0;
            while (scanner.hasNextLine()) {
                String oneLineJson = scanner.nextLine();
//                System.out.println(oneLineJson);
                JSONObject jsonObj = JSON.parseObject(oneLineJson);
                List<String> rowDataList;
                if ("tag".equals(type)) {
                    rowDataList = getTagRowData(jsonObj);
                } else if ("edge".equals(type)){
                    rowDataList = getEdgeRowData(jsonObj, rowNum);
                } else {
                    rowDataList = new ArrayList<>();
                }
                // 写入文件内容
                writeRow(rowDataList, csvWriter);
                System.out.println("================ 转换条数:" + (rowNum + 1));
                rowNum++;
            }
            System.out.println("================ rowNum 总条数: " + rowNum);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                csvWriter.flush();
                csvWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    private static List<String> getTagRowData(JSONObject jsonObj) {
        List<String> result = new ArrayList<>();
        result.add(jsonObj.getString("acct_id"));
        result.add(jsonObj.getString("record_date"));
        result.add(jsonObj.getString("record_time"));
        result.add(jsonObj.getString("acct_id"));
        result.add(jsonObj.getString("dsply_nm"));
        result.add(jsonObj.getString("type"));
        result.add(jsonObj.getString("acct_stat"));
        result.add(jsonObj.getString("acct_rptng_crncy"));
        result.add(jsonObj.getString("prior_sar_count"));
        result.add(jsonObj.getString("branch_id"));
        result.add(jsonObj.getString("open_dt"));
        result.add(jsonObj.getString("close_dt"));
        result.add(jsonObj.getString("initial_deposit"));
        result.add(jsonObj.getString("tx_behavior_id"));
        result.add(jsonObj.getString("bank_id"));
        result.add(jsonObj.getString("country"));
        return result;
    }
 
    private static List<String> getEdgeRowData(JSONObject jsonObj, int rank) {
        List<String> result = new ArrayList<>();
        result.add(jsonObj.getString("orig_acct"));
        result.add(jsonObj.getString("bene_acct"));
        result.add(String.valueOf(rank));
        result.add(jsonObj.getString("record_date"));
        result.add(jsonObj.getString("record_time"));
        result.add(jsonObj.getString("tran_id"));
        result.add(jsonObj.getString("orig_acct"));
        result.add(jsonObj.getString("bene_acct"));
        result.add(jsonObj.getString("tx_type"));
        result.add(jsonObj.getString("base_amt"));
        result.add(jsonObj.getString("tran_timestamp"));
        result.add(jsonObj.getString("is_sar"));
        result.add(jsonObj.getString("alert_id"));
        return result;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127

导入数据

1.创建 space

可以使用命令创建,也可以在Studio页面创建。

2.创建 tag

CREATE TAG accounts(record_date string, record_time string, acct_id string, dsply_nm string, type string, acct_stat string, acct_rptng_crncy string, prior_sar_count string, branch_id string, open_dt string, close_dt string, initial_deposit string, tx_behavior_id string, bank_id string, country string);
  • 1

2.1上传文件

上传导入的配置文件nebula_import.yml,创建err文件夹(yml文件中使用),上传待导入的原始数据。yml中files配置信息,limit不要设置
在这里插入图片描述
nebula_import.yml 配置内容如下

# 连接的Nebula Graph版本,连接2.x时设置为v2。
version: v2
description: example
# 是否删除临时生成的日志和错误数据文件。
removeTempFiles: false
 
clientSettings:
  # nGQL语句执行失败的重试次数。
  retry: 3
  # Nebula Graph客户端并发数。
  concurrency: 10
  # 每个Nebula Graph客户端的缓存队列大小。
  channelBufferSize: 128
  # 指定数据要导入的Nebula Graph图空间。
  space: anti_money_laundering
  # 连接信息。
  connection:
    user: user
    password: password
    address: 127.0.0.1:9669
# 错误等日志信息输出的文件路径。
logPath: /home/weixiaochao1/anti_money_laundering/vertex/err/fail.log
# CSV文件相关设置。
files:
  # 数据文件的存放路径,如果使用相对路径,则会将路径和当前配置文件的目录拼接。本示例第一个数据文件为点的数据。
  - path: /home/weixiaochao1/anti_money_laundering/vertex/real_accounts.csv
    # 插入失败的数据文件存放路径,以便后面补写数据。
    failDataPath: /home/weixiaochao1/anti_money_laundering/vertex/err/fail_real_accounts.csv
    # 单批次插入数据的语句数量。
    batchSize: 100
    # 上传导入配置yml文件,创建err文件夹(yml文件中使用),上传待导入的原始数据。
    #limit: 10
    # 是否按顺序在文件中插入数据行。如果为false,可以避免数据倾斜导致的导入速率降低。
    inOrder: false
    # 文件类型,当前仅支持csv。
    type: csv
    csv:
      # 是否有表头。
      withHeader: true
      # 是否有LABEL。
      withLabel: false
      # 指定csv文件的分隔符。只支持一个字符的字符串分隔符。
      delimiter: ","
    schema:
      # Schema的类型,可选值为vertex和edge。
      type: vertex
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

2.2执行导入

# 进入到nebula安装目录
./nebula-importer --config /home/weixiaochao1/anti_money_laundering/vertex/accounts_import.yml
  • 1
  • 2

3.创建edge

CREATE EDGE transactions(record_date string, record_time string, tran_id string, orig_acct string, bene_acct string, tx_type string, base_amt double, tran_timestamp string, is_sar string, alert_id string);
  • 1

3.1

文件太大了,先压缩再上传,yml文件edge的配置和vertex不同,nebula_importer.yml配置内容如下

# 连接的Nebula Graph版本,连接2.x时设置为v2。
version: v2
description: example
# 是否删除临时生成的日志和错误数据文件。
removeTempFiles: false
 
clientSettings:
  # nGQL语句执行失败的重试次数。默认 3
  retry: 3
  # Nebula Graph客户端并发数。
  concurrency: 20
  # 每个Nebula Graph客户端的缓存队列大小。
  channelBufferSize: 128
  # 指定数据要导入的Nebula Graph图空间。
  space: anti_money_laundering
  # 连接信息。
  connection:
    user: user
    password: password
    address: 127.0.0.1:9669
# 错误等日志信息输出的文件路径。
logPath: /home/weixiaochao1/anti_money_laundering/edge/err/fail.log
# CSV文件相关设置。
files:
  # 数据文件的存放路径,如果使用相对路径,则会将路径和当前配置文件的目录拼接。本示例第一个数据文件为点的数据。
  - path: /home/weixiaochao1/anti_money_laundering/edge/real_transactions.csv
    # 插入失败的数据文件存放路径,以便后面补写数据。
    failDataPath: /home/weixiaochao1/anti_money_laundering/edge/err/fail_real_transactions.csv
    # 单批次插入数据的语句数量。
    batchSize: 200
    # 读取数据的行数限制。不要限制啊,要不只能导入10条数据
    # limit: 10
    # 是否按顺序在文件中插入数据行。如果为false,可以避免数据倾斜导致的导入速率降低。
    inOrder: true
    # 文件类型,当前仅支持csv。
    type: csv
    csv:
      # 是否有表头。
      withHeader: true
      # 是否有LABEL。
      withLabel: false
      # 指定csv文件的分隔符。只支持一个字符的字符串分隔符。
      delimiter: ","
    schema:
      # Schema的类型,可选值为vertex和edge。
      type: edge
      edge:
        # 边类型名称。
        name: transactions
        # 是否包含rank。
        withRanking: true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

3.2执行导入

# 配置:客户端并发数concurrency=20,单批次插入数据量batchSize=200   
# 效果:每秒导入7W条数据
./nebula-importer --config /home/weixiaochao1/anti_money_laundering/edge/transactions_import.yml
  • 1
  • 2
  • 3

4.执行结果

在这里插入图片描述

5.小插曲

边数据导入一半,疯狂报错,查看日志发现磁盘剩余空间不足,有点方啊~~

df -h
在这里插入图片描述

查看大文件目录
du -sh /* | sort -nr
du -sh /home/* | sort -nr

在这里插入图片描述

du -h | grep G
在这里插入图片描述

发现个 31G 的txt文件,删除后,空间依然不够用,没办法进行扩容吧~
在这里插入图片描述

5.1LVM逻辑卷扩容

fdisk -l    #查看磁盘分区
df -h       #查看磁盘容量
lsblk -f    #查看系统分区,磁盘挂载
lsblk       #查看各分区容量
lvdisplay   #查看已经存在的lv信息
 
#eg:
lvextend -L 120G /dev/mapper/centos-home     #增大至120G
lvextend -L +20G /dev/mapper/centos-home     #增加20G
lvreduce -L 50G /dev/mapper/centos-home      #减小至50G
lvreduce -L -8G /dev/mapper/centos-home      #减小8G
resize2fs /dev/mapper/centos-home            #执行调整
 
#实操命令:
lvextend -L +100G  /dev/VolGroup00/LogVol03
resize2fs /dev/VolGroup00/LogVol03 #不生效,执行下面两个命令
xfs_info /dev/VolGroup00/LogVol03
xfs_growfs /dev/VolGroup00/LogVol03
 
reference: https://www.cnblogs.com/liyy7520/p/11905979.html
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

Nebula 性能测试

环境信息

操作系统:CentOS Linux release 7.8.2003 (Core)
CPU:逻辑 48 核
内存:125G
磁盘:200G

数据准备

顶点数据量边数据量
1000000条/128M103080302条/12G

测试case

1.唯一标识查询顶点速度

#唯一标识查询顶点速度,执行50次作为样本
fetch prop on accounts "996997";
#耗时:0.6us ~ 0.8us
  • 1
  • 2
  • 3

2.唯一标识查询边速度

#唯一标识查询边速度,执行50次作为样本
fetch prop on transactions "344527" -> "328642"@743;
#耗时:0.7us ~ 0.8us
  • 1
  • 2
  • 3

3.go拓展节点速度

#go拓展节点速度,执行30次作为样本

GO FROM "344527" OVER transactions;
#一跳耗时:0.7us ~ 0.9us
 
GO 2 steps FROM "344527" OVER transactions;
#两跳耗时:0.7us ~ 0.9us
 
GO 3 steps FROM "344527" OVER transactions;
#三跳耗时:0.8us ~ 0.9us
 
GO 4 steps FROM "344527" OVER transactions;
#四跳耗时:0.8us ~ 1us
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

4.路径发现

准备工作

#创建索引后才能使用 Match 语句。
CREATE TAG INDEX i_accounts_acctId on accounts(acct_id(10));
 
#创建索引后不生效,执行索引重建。
REBUILD TAG INDEX i_accounts_acctId;
 
#查看索引状态。
SHOW TAG INDEX STATUS;
 
https://docs.nebula-graph.com.cn/2.0.1/3.ngql-guide/14.native-index-statements/4.rebuild-native-index/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
4.1普通顶点测试
MATCH p=(v:accounts{acct_id:"344527"})-->(v2) RETURN p;
#走一步,匹配 15 条路径,耗时:0.5us ~ 0.9us

MATCH p=(v:accounts{acct_id:"344527"})-->(v2)-->(v3) RETURN p;
#走两步,匹配 225 条路径,耗时:0.2us ~ 0.3us

MATCH p=(v:accounts{acct_id:"344527"})-->(v2)-->(v3)-->(v4) RETURN p;
#走三步,匹配 7200 条路径,耗时:0.05us ~ 0.1us

MATCH p=(v:accounts{acct_id:"344527"})-->(v2)-->(v3)-->(v4)-->(v5) RETURN p;
#走四步,匹配 65025 条路径,耗时:0.05us ~ 0.2us
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
4.2 超级顶点测试
MATCH p=(v:accounts{acct_id:"999978"})-->(v2) RETURN p;
#走一步,匹配 9829 条路径,耗时:0.06us ~ 0.1us

MATCH p=(v:accounts{acct_id:"999978"})-->(v2)-->(v3) RETURN p;
#未跑出结果
  • 1
  • 2
  • 3
  • 4
  • 5

5.超级顶点

超级顶点 999978 关联 9829 个点

GO FROM "999978" OVER transactions;
#一跳耗时:0.09us ~ 0.14us,获取行数:9829
 
GO 2 steps FROM "999978" OVER transactions;
#两跳耗时:0.2us ~ 0.3us,获取行数:9365
 
GO 3 steps FROM "999978" OVER transactions;
#三跳耗时:0.2us ~ 0.3us,获取行数:19925
 
GO 4 steps FROM "999978" OVER transactions;
#四跳耗时:0.1us ~ 0.2us,获取行数:85437
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/571460
推荐阅读
相关标签
  

闽ICP备14008679号