mongoDB高级篇-mongo分片初体验

如果说复制集是mongo为了备份数据,将一份数据存储在多台实例上的一种集群架构的话,那么当我们的数据存储过多,最好能将数据分开存储,这个时候就可以使用mongo的另一个多实例部署架构--数据分片

分片的概念就是将数据拆分,将其分散存储在不同机器上的过程。在很多中间件都有类似的概念,有些中间件中叫做'分区'概念,其实也是与分片类似的说法。基本上现代主流数据库都支持,用户手动管理,将数据存储在不同的集合中,连接彼此独立,但是手动维护带来的问题就是维护难度的增大,整体分片的调整,如增加节点,删除节点难度都变得很困难。而在MongoDB中,内部集成了自动分片功能,可以让分片操作对用户不可见,从而简化手动管理的成本。

分片初体验

想要开始搭建一个分片集群,我们先理解一下这个集群中的几个组件以及其作用,由于分片集群的目标一般是想要满足五台、十台甚至更多的实例,对外仅仅是和连接单机mongo一样,因此必须有其他组件帮忙隐藏其中的一些细节,因此整个mongo的分片集群架构就可以分成如下这样:

1621232061692.png
分片集群的三大组件

我们可以看到整个MongoDB Sharding集群中大体可以分为三个组件:

数据分片 -- shards

数据分片用于保存数据,保证了数据的高可用和一致性,这里每一个shard可以是独立的mongod实例,也可以是一个复制集,防止出现单点的故障问题。当然也可以选择把所有的shard的复制集放在一个服务器中启动多个mongod实例,在sharding中,每个node的database数据库可以选择分片也可以选择不分片,每一个db中都有一个独立的primary shard,在未分片的集合中就是存在于各自的primary shard中的

查询路由 -- mongos

我们可以看到,当我们想要连接分片集群,将数据分开存储,或者想要从分开存储的分片集群中,将我们想要的数据聚集查询出来,为了隐藏其中的细节,这个时候就需要使用查询路由--mongos了,客户端连接不是直接连接分片集群中,而是连接到mongos,通过mongos进行一次路由过程的操作分发,而mongos通过查询维护的一个“内容列表”,里面记录了每个分片中按照什么规则(分片键)存储数据,每个分片中包含了哪些内容等,而客户端的请求到达mongos以后,mongos会根据记录的内容,选择性将请求分发到对应的一个或者多个分片服务实例上,然后再将所有分片的响应结果进行合并再统一输出给客户端程序,从而实现了屏蔽分片的细节,流程大体如下:

1614761108966.png

配置服务器 -- Config Server

配置服务器就是用于存储分片集群的配置信息的元数据,其中包含了mongos需要的shard的路由规则,路由键等信息,在mongo 3.2版本开始config server可以配置成为replica set了,在3.4以后官方已经规定config server必须为replica set,并且为了保证生产环境的稳定,rs中至少要有三个副本集成员存在

分片集群上手初体验

现在我们开始搭建一个简单的分片架构,在mongo3.4以后的版本中,一个分片集群,shard节点的数量至少要是两个,而config节点和路由mongos节点则至少需要一个,因此我们这里来通过在一台服务器上启动多个节点的方式 模拟分片架构,搭建两个shards复制集作为两个shard节点,搭建一个config server复制集作为config节点,而mongos我们则使用单节点启动方式,快速搭建一个分片集群架构快速上手体验一下

配置shards
cd /etc;
#首先创建一下shard的log存放的目录
mkdir -p /var/log/mongodb_shard;
mkdir -p /var/log/mongo_shard1;
mkdir -p /var/log/mongo_shard2;
mkdir -p /var/log/mongodb_shard3;
#接着创建shard的数据库文件存放目录
mkdir -p /var/lib/mongo_shard;
mkdir -p /var/lib/mongo_shard1;
mkdir -p /var/lib/mongo_shard2;
mkdir -p /var/lib/mongo_shard3;
#创建shard启动配置文件存放目录
mkdir mongo_shard/;
mkdir mongo_shard1/;
mkdir mongo_shard2/;
mkdir mongo_shard3/;
#创建pidFilePath存放目录
mkdir -p /var/run/mongo_shard;
mkdir -p /var/run/mongo_shard1;
mkdir -p /var/run/mongo_shard2;
mkdir -p /var/run/mongo_shard3;
#将mongo的配置文件分别cp到这些目录中,这里我们cp的是上篇文章中我们配置的复制集的配置文件,否则配置文件中还要配置复制集
cp -r mongod1.conf mongo_shard/mongod.conf;
cp -r mongod1.conf mongo_shard1/mongod.conf;
cp -r mongod1.conf mongo_shard2/mongod.conf;
cp -r mongod1.conf mongo_shard3/mongod.conf;
#vim修改每一个shard的config启动参数,主要修改systemLog下的path,storage下的dbPath,processManagement下的pidFilePath,这里我们分别修改为上面创建的log目录,db存储目录,以及pidFilePath存放目录,net下的启动端口这里分别修改为30017、30018和30019、30020,除此之外,由于shard我们要创建成为两个rs,因此,replication下的replSetName参数,mongo_shard和mongo_shard1我们将其指定为shard,mongo_shard和mongo_shard1我们将其指定为shard2,代表几个shard组成的复制集名称分别叫shard和shard2
vim mongo_shard/mongod.conf;
vim mongo_shard1/mongod.conf;
vim mongo_shard2/mongod.conf;
vim mongo_shard3/mongod.conf;

这里我们需要注意,由于当前的角色是shard,因此我们在给常规的配置文件参数修改后,还要添加sharding相关的配置,代表当前的角色为shard,如下:


1614847222095.png

添加如下配置即可:

sharding:
  # 分片角色
  clusterRole: shardsvr

下面是一个配置好的shard的mongod.conf的完整配置示例:

# mongod.conf
# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb_shard/mongod.log

# Where and how to store data.
storage:
  dbPath: /var/lib/mongo_shard
  journal:
    enabled: true
#  engine:

#  wiredTiger:

# how the process runs
processManagement:
  fork: true  #后台启动进程
  pidFilePath: /var/run/mongo_shard/mongod.pid  
  timeZoneInfo: /usr/share/zoneinfo

# network interfaces
net:
  port: 30017
  bindIp: 0.0.0.0  # Enter 0.0.0.0,:: to bind to all IPv4 and IPv6 addresses or, alternatively, use the net.bindIpAll setting.

#security:

#operationProfiling:

#replication:
replication:
 replSetName: shard
#sharding:
sharding:
  # 分片角色
  clusterRole: shardsvr

## Enterprise-Only Options

#auditLog:

#snmp:

将所有的shard的目录和文件配置准备完成,我们开始将其分别启动,组成rs复制集:

/usr/bin/mongod -f /etc/mongo_shard/mongod.conf;
/usr/bin/mongod -f /etc/mongo_shard1/mongod.conf;
/usr/bin/mongod -f /etc/mongo_shard2/mongod.conf;
/usr/bin/mongod -f /etc/mongo_shard3/mongod.conf;
#查看当前进程是否都存在
ps -ef|grep mongo
#可以看到 已经启动完成了
root     113935      1 13 02:35 ?        00:00:02 /usr/bin/mongod -f /etc/mongo_shard/mongod.conf
root     114005      1 26 02:35 ?        00:00:03 /usr/bin/mongod -f /etc/mongo_shard1/mongod.conf
root     114151      1 41 02:35 ?        00:00:03 /usr/bin/mongod -f /etc/mongo_shard2/mongod.conf
root     114252      1 41 02:35 ?        00:00:03 /usr/bin/mongod -f /etc/mongo_shard3/mongod.conf
root     114341   2433  0 02:35 pts/0    00:00:00 grep --color=auto mongo
#登录shard复制集的任意一个节点,初始化复制集
/usr/bin/mongo --port 30017;
#初始化复制集 
rs.initiate({ _id:"shard", members:[{ _id:0, host:"127.0.0.1:30017" },{ _id:1, host:"127.0.0.1:30018" }] });
#登录shard2复制集的任意一个节点,初始化复制集
/usr/bin/mongo --port 30019;
#初始化复制集 
rs.initiate({ _id:"shard2", members:[{ _id:0, host:"127.0.0.1:30019" },{ _id:1, host:"127.0.0.1:30020" }] });
配置config server

用于存储分片配置的Config Server我们这里仅仅配置成一个master-slave的rs即可:

cd /etc;
#首先创建一下config_server的log存放的目录
mkdir -p /var/log/mongo/config_master;
mkdir -p /var/log/mongo/config_slave;
#接着创建config_server的数据库文件存放目录
mkdir -p /var/lib/mongo/config_master;
mkdir -p /var/lib/mongo/config_slave;
#创建config_server启动配置文件存放目录
mkdir mongo_config_master/;
mkdir mongo_config_slave/;
#创建pidFilePath存放目录
mkdir -p /var/run/mongo_config_master;
mkdir -p /var/run/mongo_config_slave;
#将mongo的配置文件分别cp到这些目录中,这里我们cp的是上篇文章中我们配置的复制集的配置文件,否则配置文件中还要配置复制集
cp -r mongod1.conf mongo_config_master/mongod.conf;
cp -r mongod1.conf mongo_config_slave/mongod.conf;
#vim修改每一个config_server的config启动参数,主要修改systemLog下的path,storage下的dbPath,processManagement下的pidFilePath,这里我们分别修改为上面创建的log目录,db存储目录,以及pidFilePath存放目录,net下的启动端口这里分别修改为37017和37018,除此之外,由于这里的节点我们要创建成为一个主-从的rs,因此,replication下的replSetName参数我们将其定义成config,代表几个节点组成的复制集名称叫config
vim mongo_config_master/mongod.conf;
vim mongo_config_slave/mongod.conf;

与上面shard的处理方式几乎一样,当然这里唯一的区别就是,分片角色的配置,上面是shard,这里需要配置为config,如下:

#sharding:
sharding:
  # 分片角色
  clusterRole: configsvr

一切配置准备好以后,我们开始启动config server复制集,完成初始化操作:

/usr/bin/mongod -f /etc/mongo_config_master/mongod.conf;
/usr/bin/mongod -f /etc/mongo_config_slave/mongod.conf;
#登录第一个复制集
/usr/bin/mongo --port 37017;
#初始化复制集 
rs.initiate({ _id:"config", members:[{ _id:0, host:"127.0.0.1:37017" },{ _id:1, host:"127.0.0.1:37018" }] });
启动查询路由--mongos

这里我们只配置一个mongos实例,不去做集群启动了,因此我们需要和前面一样,创建四个目录,分别存放log,db文件和pid目录以及config配置文件的目录

cd /etc;
#首先创建一下shard的log存放的目录
mkdir -p /var/log/mongos_db;
#接着创建mongos的数据库文件存放目录
mkdir -p /var/lib/mongos;
#创建mongos启动配置文件存放目录
mkdir mongos/;
#创建pidFilePath存放目录
mkdir -p /var/run/mongos;
#复制 配置文件到目录下
cp -r mongod1.conf mongos/mongod.conf;

#修改配置文件
vim mongos/mongod.conf;
#修改 path: /var/log/mongos_db/mongod1.log
#修改 dbPath: /var/lib/mongos/mongo1
#修改 pidFilePath: /var/run/mongos/mongod1.pid
#修改  port: 27050
#修改 replSetName: mongos
#配置文件中添加如下配置,指定config集群的信息
sharding:
  configDB: config/127.0.0.1:37017,127.0.0.1:37018
  
#需要注意的是,使用mongos的话,是无法识别storage.dbPath配置的,因此要注释当前的配置
#storage:
  #dbPath: /var/lib/mongos/mongo1  
  #journal:
  #  enabled: true
#除此之外,还要删除replication相关的配置
#replication:
# replSetName: mongos
#启动mongos,注意这里用的是mongos启动
/usr/bin/mongos -f /etc/mongos/mongod.conf;
#连接mongos
mongo --port=27050;

这个时候我们连接进去,可以看到,当前的角色已经变了,成为了mongos,需要注意的是我们要做分片操作,首先需要给需要分片的数据库启动分片,开启命令如下:

sh.enableSharding("db_name");

现在整个db的集合都可以进行分片了,不过需要注意的是,我们要对某个集合做分片,必须要选择一个片键,片键是集合文档内的一个键,Mongo会根据策略和这个片键进行拆分数据,我们要给集合启用分片之前,需要先在片键上创建索引:

db.collection.ensureIndex({"key":1});

然后我们就可以设置根据这个key来作为片键进行分片操作了:

sh.shardCollection("db.collection",{id:"key"});

配置完成后,我们来将shard信息添加进来,进行分片数据插入测试:

use admin;
#添加shard信息
mongos> sh.addShard("shard/127.0.0.1:30017,127.0.0.1:30018");
mongos> sh.addShard("shard2/127.0.0.1:30019,127.0.0.1:30020");
#设置test库为允许sharding
sh.enableSharding("test");
#设置test中的tes集合的id按照hash的方式进行sharding
sh.shardCollection("test.tes",{id:"hashed"});
#插入几条数据测试一下
for(i=1;i<=1000;i++){db.tes.insert({id:i,name:"Leo"})}

这里我们插入数据完成以后,分别进入两个shard查看一下当前的数据,看是否生效

#登录第一个shard
/usr/bin/mongo --port 30017;
use test;
db.tes.find();
#结果如下:
shard:PRIMARY> db.tes.find()
{ "_id" : ObjectId("60a3a08cd9ea857697bcf0cd"), "id" : 3, "name" : "Leo3" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d0"), "id" : 6, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d2"), "id" : 8, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d5"), "id" : 11, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d6"), "id" : 12, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0de"), "id" : 20, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0df"), "id" : 21, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0e4"), "id" : 26, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0e5"), "id" : 27, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0e8"), "id" : 30, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0ea"), "id" : 32, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0eb"), "id" : 33, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0f0"), "id" : 38, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0f2"), "id" : 40, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0f3"), "id" : 41, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0f4"), "id" : 42, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0f6"), "id" : 44, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0fa"), "id" : 48, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0fc"), "id" : 50, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0fe"), "id" : 52, "name" : "Leo" }

#登录第二个shard
/usr/bin/mongo --port 30019;
use test;
db.tes.find();
#结果如下:
shard2:PRIMARY> db.tes.find()
{ "_id" : ObjectId("60a3a07bd9ea857697bcf0ca"), "id" : 0, "name" : "Leo0" }
{ "_id" : ObjectId("60a3a081d9ea857697bcf0cb"), "id" : 1, "name" : "Leo1" }
{ "_id" : ObjectId("60a3a086d9ea857697bcf0cc"), "id" : 2, "name" : "Leo2" }
{ "_id" : ObjectId("60a3a091d9ea857697bcf0ce"), "id" : 4, "name" : "Leo4" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0cf"), "id" : 5, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d1"), "id" : 7, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d3"), "id" : 9, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d4"), "id" : 10, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d7"), "id" : 13, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d8"), "id" : 14, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0d9"), "id" : 15, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0da"), "id" : 16, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0db"), "id" : 17, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0dc"), "id" : 18, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0dd"), "id" : 19, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0e0"), "id" : 22, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0e1"), "id" : 23, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0e2"), "id" : 24, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0e3"), "id" : 25, "name" : "Leo" }
{ "_id" : ObjectId("60a3a0d3d9ea857697bcf0e6"), "id" : 28, "name" : "Leo" }

可见当前我们的分片整个是已经搭建完成了,这个时候我们可以查看当前的分片集群的一个状态,通过如下命令:

mongos> sh.status();
--- Sharding Status --- 
  sharding version: {
        "_id" : 1,
        "minCompatibleVersion" : 5,
        "currentVersion" : 6,
        "clusterId" : ObjectId("60a39c69f5d7a342f9cdfb1d")
  }
  shards:
        {  "_id" : "shard",  "host" : "shard/127.0.0.1:30017,127.0.0.1:30018",  "state" : 1 }
        {  "_id" : "shard2",  "host" : "shard2/127.0.0.1:30019,127.0.0.1:30020",  "state" : 1 }
  active mongoses:
        "4.2.8" : 1
  autosplit:
        Currently enabled: yes
  balancer:
        Currently enabled:  yes
        Currently running:  no
        Failed balancer rounds in last 5 attempts:  0
        Migration Results for the last 24 hours: 
                512 : Success
  databases:
        {  "_id" : "config",  "primary" : "config",  "partitioned" : true }
                config.system.sessions
                        shard key: { "_id" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                shard   512
                                shard2  512
                        too many chunks to print, use verbose if you want to force print
        {  "_id" : "test",  "primary" : "shard2",  "partitioned" : true,  "version" : {  "uuid" : UUID("72bb131e-1e88-4267-adc2-9f97e1e8d663"),  "lastMod" : 1 } }
                test.tes
                        shard key: { "id" : "hashed" }
                        unique: false
                        balancing: true
                        chunks:
                                shard   2
                                shard2  2
                        { "id" : { "$minKey" : 1 } } -->> { "id" : NumberLong("-4611686018427387902") } on : shard Timestamp(1, 0) 
                        { "id" : NumberLong("-4611686018427387902") } -->> { "id" : NumberLong(0) } on : shard Timestamp(1, 1) 
                        { "id" : NumberLong(0) } -->> { "id" : NumberLong("4611686018427387902") } on : shard2 Timestamp(1, 2) 
                        { "id" : NumberLong("4611686018427387902") } -->> { "id" : { "$maxKey" : 1 } } on : shard2 Timestamp(1, 3) 

sh命令和前面的rs命令很像,不过这里是用来代替分片集群管理的,而rs则是一个全局变量,除了可以管理以外,还内置了很多辅助函数,我们也可以选择使用sh.help();查看可以使用的辅助函数有哪些

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容