mongodb分片集群搭建踩坑记录

一、搭建流程

  1. 网络部署架构

  1. 目前采用的是同一个台机上,搭建两个分片副本集(每个副本集一主一从一仲裁节点),然后两个配置服务,一个路由节点,详细配置端口如下表。

  1. 分类

  1. 名称

  1. 角色

  1. 端口

目录

  1. 副本集1

  1. testrs

  1. 27017

master

  1. 副本集1

  1. testrs

  1. 27018

slave

  1. 副本集1

  1. testrs

  1. 仲裁

  1. 27019

arbiter

  1. 副本集2

  1. testrs2

  1. 27027

master2

  1. 副本集2

  1. testrs2

  1. 27028

slave2

  1. 副本集2

  1. testrs2

  1. 仲裁

  1. 27029

arbiter2

  1. 配置服务

  1. 1

  1. 配置

  1. 27030

confserv

  1. 配置服务

  1. 2

  1. 配置

  1. 27031

confserv2

  1. 路由服务

  1. 1

  1. 路由

  1. 30000

route

  1. 搭建副本集

  1. 主节点配置

dbpath=/home/mongodb/master/data
#指定MongoDB日志文件
logpath=/home/mongodb/master/mongodb.log
# 使用追加的方式写日志
logappend=true
directoryperdb=true
replSet=testrs
#端口号
port=27017
#方便外网访问,外网所有ip都可以访问,不要写成固定的linux的ip
bind_ip=0.0.0.0
fork=true # 以守护进程的方式运行MongoDB,创建服务器进程
#auth=true #启用用户验证
#bind_ip=0.0.0.0 #绑定服务IP,若绑定127.0.0.1,则只能本机访问,不指定则默认本地所有IP

启动命令: ./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongod --shardsvr --replSet testrs -f ./master/conf/master.conf

2. 从节点配置

dbpath=/home/mongodb/slave/data
#指定MongoDB日志文件
logpath=/home/mongodb/slave/mongodb.log
# 使用追加的方式写日志
logappend=true
replSet=testrs
#端口号
port=27018
#方便外网访问,外网所有ip都可以访问,不要写成固定的linux的ip
bind_ip=0.0.0.0
fork=true # 以守护进程的方式运行MongoDB,创建服务器进程
#auth=true #启用用户验证
#bind_ip=0.0.0.0 #绑定服务IP,若绑定127.0.0.1,则只能本机访问,不指定则默认本地所有IP

可以看到从节点和主节点配置除了端口和数据,以及日志目录不一样,其它完全一样,相当于是一个普通的数据节点,在后期初始化时动态设置主从角色。

启动命令: ./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongod --shardsvr --replSet testrs -f ./slave/conf/slave.conf

3. 仲裁节点配置

dbpath=/home/mongodb/arbiter/data
#指定MongoDB日志文件
logpath=/home/mongodb/arbiter/mongodb.log
# 使用追加的方式写日志
logappend=true
replSet=testrs
#端口号
port=27019
#方便外网访问,外网所有ip都可以访问,不要写成固定的linux的ip
bind_ip=0.0.0.0
fork=true # 以守护进程的方式运行MongoDB,创建服务器进程
#auth=true #启用用户验证
#bind_ip=0.0.0.0 #绑定服务IP,若绑定127.0.0.1,则只能本机访问,不指定则默认本地所有IP

也是跟上面一样。

启动命令: ./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongod --shardsvr --replSet testrs -f ./arbiter/conf/arbiter.conf

4. 初始化副本集集群

使用mongosh登录其中的一个数据节点,使用集群初始化命令进行初始化(注意要切换到admin数据库)。

./mongosh-1.8.0-linux-x64/bin/mongosh --port 27018
use admin
cfg={ _id:"testrs", members:[ {_id:0,host:'10.6.17.112:27017',priority:2}, {_id:1,host:'10.6.17.112:27018',priority:1}, 
{_id:2,host:'10.6.17.112:27019',arbiterOnly:true}] };
>rs.initiate(cfg)             #使配置生效
rs.status()

上面的priority优先级值越大,越是主节点,小的为从节点,arbiterOnly为TRUE的为仲裁节点。

testrs [direct: secondary] test> rs.status();
{
  set: 'testrs',
  date: ISODate("2023-03-18T05:46:26.905Z"),
  myState: 2,
  term: Long("4"),
  syncSourceHost: '10.6.17.112:27017',
  syncSourceId: 0,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 3,
  writableVotingMembersCount: 2,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1679118380, i: 1 }), t: Long("4") },
    lastCommittedWallTime: ISODate("2023-03-18T05:46:20.129Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1679118380, i: 1 }), t: Long("4") },
    appliedOpTime: { ts: Timestamp({ t: 1679118380, i: 1 }), t: Long("4") },
    durableOpTime: { ts: Timestamp({ t: 1679118380, i: 1 }), t: Long("4") },
    lastAppliedWallTime: ISODate("2023-03-18T05:46:20.129Z"),
    lastDurableWallTime: ISODate("2023-03-18T05:46:20.129Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1679118380, i: 1 }),
  electionParticipantMetrics: {
    votedForCandidate: true,
    electionTerm: Long("4"),
    lastVoteDate: ISODate("2023-03-17T08:39:58.054Z"),
    electionCandidateMemberId: 0,
    voteReason: '',
    lastAppliedOpTimeAtElection: { ts: Timestamp({ t: 1679042359, i: 1 }), t: Long("3") },
    maxAppliedOpTimeInSet: { ts: Timestamp({ t: 1679042359, i: 1 }), t: Long("3") },
    priorityAtElection: 1,
    newTermStartDate: ISODate("2023-03-17T08:39:58.059Z"),
    newTermAppliedDate: ISODate("2023-03-17T08:39:58.468Z")
  },
  members: [
    {
      _id: 0,
      name: '10.6.17.112:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 75991,
      optime: { ts: Timestamp({ t: 1679118380, i: 1 }), t: Long("4") },
      optimeDurable: { ts: Timestamp({ t: 1679118380, i: 1 }), t: Long("4") },
      optimeDate: ISODate("2023-03-18T05:46:20.000Z"),
      optimeDurableDate: ISODate("2023-03-18T05:46:20.000Z"),
      lastAppliedWallTime: ISODate("2023-03-18T05:46:20.129Z"),
      lastDurableWallTime: ISODate("2023-03-18T05:46:20.129Z"),
      lastHeartbeat: ISODate("2023-03-18T05:46:26.732Z"),
      lastHeartbeatRecv: ISODate("2023-03-18T05:46:26.218Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1679042398, i: 1 }),
      electionDate: ISODate("2023-03-17T08:39:58.000Z"),
      configVersion: 1,
      configTerm: 4
    },
    {
      _id: 1,
      name: '10.6.17.112:27018',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 75992,
      optime: { ts: Timestamp({ t: 1679118380, i: 1 }), t: Long("4") },
      optimeDate: ISODate("2023-03-18T05:46:20.000Z"),
      lastAppliedWallTime: ISODate("2023-03-18T05:46:20.129Z"),
      lastDurableWallTime: ISODate("2023-03-18T05:46:20.129Z"),
      syncSourceHost: '10.6.17.112:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 4,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 2,
      name: '10.6.17.112:27019',
      health: 1,
      state: 7,
      stateStr: 'ARBITER',
      uptime: 75984,
      lastHeartbeat: ISODate("2023-03-18T05:46:26.730Z"),
      lastHeartbeatRecv: ISODate("2023-03-18T05:46:26.031Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      configVersion: 1,
      configTerm: 4
    }
  ],
  ok: 1,
  lastCommittedOpTime: Timestamp({ t: 1679118380, i: 1 }),
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1679118385, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1679118380, i: 1 })
}
5. 搭建副本集2

跟上面的步骤类似,只是替换了端口,和目录

//副本集2搭建
./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongod --shardsvr --replSet testrs2 -f ./master2/conf/master2.conf 

./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongod --shardsvr --replSet testrs2 -f ./slave2/conf/slave2.conf

./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongod --shardsvr --replSet testrs2 -f ./arbiter2/conf/arbiter2.conf

./mongosh-1.8.0-linux-x64/bin/mongosh --port 27028

use admin
cfg2={ _id:"testrs2", members:[ {_id:0,host:'10.6.17.112:27027',priority:2}, {_id:1,host:'10.6.17.112:27028',priority:1}, 
{_id:2,host:'10.6.17.112:27029',arbiterOnly:true}] };
>rs.initiate(cfg2)             #使配置生效
rs.status()

3. 搭建配置服务

1.节点配置和启动

基本上跟上面搭建副本集一样,只是端口不一样,然后启动时带的命令不相同。副本集启动时带的是--shardsvr --replSet ,配置服务启动时带的是--configsvr

replSet=rsconf
dbpath=/home/mongodb/confserv/data

#指定MongoDB日志文件
logpath=/home/mongodb/confserv/mongodb.log
# 使用追加的方式写日志
logappend=true
#端口号
port=27030
#方便外网访问,外网所有ip都可以访问,不要写成固定的linux的ip
bind_ip=0.0.0.0
fork=true # 以守护进程的方式运行MongoDB,创建服务器进程
#auth=true #启用用户验证
#bind_ip=0.0.0.0 #绑定服务IP,若绑定127.0.0.1,则只能本机访问,不指定则默认本地所有IP

./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongod --configsvr -f ./confserv/confserv.conf

replSet=rsconf
dbpath=/home/mongodb/confserv2/data

#指定MongoDB日志文件
logpath=/home/mongodb/confserv2/mongodb.log
# 使用追加的方式写日志
logappend=true
#端口号
port=27031
#方便外网访问,外网所有ip都可以访问,不要写成固定的linux的ip
bind_ip=0.0.0.0
fork=true # 以守护进程的方式运行MongoDB,创建服务器进程
#auth=true #启用用户验证
#bind_ip=0.0.0.0 #绑定服务IP,若绑定127.0.0.1,则只能本机访问,不指定则默认本地所有IP

./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongod --configsvr -f ./confserv2/confserv2.conf

2.配置集群初始化

./mongosh-1.8.0-linux-x64/bin/mongosh --port 27030

cfga={ _id:"rsconf", members:[ {_id:0,host:'10.6.17.112:27030'}, {_id:1,host:'10.6.17.112:27031'}] };

>rs.initiate(cfga) #使配置生效

rsconf [direct: primary] test> rs.status();
{
  set: 'rsconf',
  date: ISODate("2023-03-18T05:55:16.216Z"),
  myState: 1,
  term: Long("1"),
  syncSourceHost: '',
  syncSourceId: -1,
  configsvr: true,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 2,
  writableVotingMembersCount: 2,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1679118916, i: 1 }), t: Long("1") },
    lastCommittedWallTime: ISODate("2023-03-18T05:55:16.052Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1679118916, i: 1 }), t: Long("1") },
    appliedOpTime: { ts: Timestamp({ t: 1679118916, i: 1 }), t: Long("1") },
    durableOpTime: { ts: Timestamp({ t: 1679118916, i: 1 }), t: Long("1") },
    lastAppliedWallTime: ISODate("2023-03-18T05:55:16.052Z"),
    lastDurableWallTime: ISODate("2023-03-18T05:55:16.052Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1679118888, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate("2023-03-17T07:46:25.337Z"),
    electionTerm: Long("1"),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1679039174, i: 1 }), t: Long("-1") },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1679039174, i: 1 }), t: Long("-1") },
    numVotesNeeded: 2,
    priorityAtElection: 2,
    electionTimeoutMillis: Long("10000"),
    numCatchUpOps: Long("0"),
    newTermStartDate: ISODate("2023-03-17T07:46:25.361Z"),
    wMajorityWriteAvailabilityDate: ISODate("2023-03-17T07:46:26.749Z")
  },
  members: [
    {
      _id: 0,
      name: '10.6.17.112:27030',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 79869,
      optime: { ts: Timestamp({ t: 1679118916, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-03-18T05:55:16.000Z"),
      lastAppliedWallTime: ISODate("2023-03-18T05:55:16.052Z"),
      lastDurableWallTime: ISODate("2023-03-18T05:55:16.052Z"),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1679039185, i: 1 }),
      electionDate: ISODate("2023-03-17T07:46:25.000Z"),
      configVersion: 1,
      configTerm: 1,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 1,
      name: '10.6.17.112:27031',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 79742,
      optime: { ts: Timestamp({ t: 1679118914, i: 1 }), t: Long("1") },
      optimeDurable: { ts: Timestamp({ t: 1679118914, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-03-18T05:55:14.000Z"),
      optimeDurableDate: ISODate("2023-03-18T05:55:14.000Z"),
      lastAppliedWallTime: ISODate("2023-03-18T05:55:16.052Z"),
      lastDurableWallTime: ISODate("2023-03-18T05:55:16.052Z"),
      lastHeartbeat: ISODate("2023-03-18T05:55:15.492Z"),
      lastHeartbeatRecv: ISODate("2023-03-18T05:55:15.018Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: '10.6.17.112:27030',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    }
  ],
  ok: 1,
  lastCommittedOpTime: Timestamp({ t: 1679118916, i: 1 }),
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1679118916, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1679118916, i: 1 })
}

4.搭建路由服务

1.只需启动如下命令就行,需要指定配置服务。

./mongodb-linux-x86_64-rhel70-6.0.4/bin/mongos --configdb rsconf/10.6.17.112:27030,10.6.17.112:27031 --bind_ip 0.0.0.0 --port 30000 --fork --logpath ./route/route.log

  1. 路由集群初始化
  • 进入命令行

./mongosh-1.8.0-linux-x64/bin/mongosh --port 30000

  • 2.要在分片路由服务中设置默认写并发关注点数,大于1,否则添加分片失败。

db.adminCommand({

"setDefaultRWConcern" : 1,

"defaultWriteConcern" : {"w" : 2}

});

  • 3.添加分片

sh.addShard("testrs/10.6.17.112:27017,10.6.17.112:27018,10.6.17.112:27019");

sh.status();

sh.addShard("testrs2/10.6.17.112:27027,10.6.17.112:27028,10.6.17.112:27029");

  1. 查看集群状态

sh.status();

看到下面有两个副本集分片,shards:testrs,testrs2

shardingVersion
{
  _id: 1,
  minCompatibleVersion: 5,
  currentVersion: 6,
  clusterId: ObjectId("64141ad1a2fcc6238dc1d249")
}
---
shards
[
  {
    _id: 'testrs',
    host: 'testrs/10.6.17.112:27017,10.6.17.112:27018',
    state: 1,
    topologyTime: Timestamp({ t: 1679044288, i: 6 })
  },
  {
    _id: 'testrs2',
    host: 'testrs2/10.6.17.112:27027,10.6.17.112:27028',
    state: 1,
    topologyTime: Timestamp({ t: 1679046407, i: 5 })
  }
]
---
active mongoses
[ { '6.0.4': 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': 'No recent migrations'
}
---
]

二、测试分片集群

  1. 将数据库开启分片

要对一个集合分片,首先你要对这个集合的数据库启用分片

sh.enableSharding("books");

  1. 片键建索引

片键是集合的一个键,MongoDB根据这个键拆分数据。例如:username 。在启用分片之前,先在希望作为片键的键上创建索引:db.users5.createIndex({'username':"hashed"});

  1. 开启集合分片

db.runCommand({"shardcollection":"books.users5","key":{"name":"hashed"}})

4.测试数据

向集合插入10条数据。

for(var i=0;i<10;i++){

db.users5.insert({"name":"test"+i,"age":i});

}

5.查看集群状态

databases
[
  {
    database: {
      _id: 'books',
      primary: 'testrs2',
      partitioned: false,
      version: {
        uuid: new UUID("1e0a78ef-1430-4a4a-8d78-cd2fb2013c12"),
        timestamp: Timestamp({ t: 1679046964, i: 1 }),
        lastMod: 1
      }
    },
    collections: {
      'books.users': {
        shardKey: { name: 1 },
        unique: false,
        balancing: true,
        chunkMetadata: [ { shard: 'testrs2', nChunks: 1 } ],
        chunks: [
          { min: { name: MinKey() }, max: { name: MaxKey() }, 'on shard': 'testrs2', 'last modified': Timestamp({ t: 1, i: 0 }) }
        ],
        tags: []
      },
      'books.users5': {
        shardKey: { name: 'hashed' },
        unique: false,
        balancing: true,
        chunkMetadata: [
          { shard: 'testrs', nChunks: 2 },
          { shard: 'testrs2', nChunks: 2 }
        ],
        chunks: [
          { min: { name: MinKey() }, max: { name: Long("-4611686018427387902") }, 'on shard': 'testrs', 'last modified': Timestamp({ t: 1, i: 0 }) },
          { min: { name: Long("-4611686018427387902") }, max: { name: Long("0") }, 'on shard': 'testrs', 'last modified': Timestamp({ t: 1, i: 1 }) },
          { min: { name: Long("0") }, max: { name: Long("4611686018427387902") }, 'on shard': 'testrs2', 'last modified': Timestamp({ t: 1, i: 2 }) },
          { min: { name: Long("4611686018427387902") }, max: { name: MaxKey() }, 'on shard': 'testrs2', 'last modified': Timestamp({ t: 1, i: 3 }) }
        ],
        tags: []
      }
    }
  },
  {
    database: { _id: 'config', primary: 'config', partitioned: true },
    collections: {
      'config.system.sessions': {
        shardKey: { _id: 1 },
        unique: false,
        balancing: true,
        chunkMetadata: [ { shard: 'testrs', nChunks: 1024 } ],
        chunks: [
          'too many chunks to print, use verbose if you want to force print'
        ],
        tags: []
      }
    }
  },
  {
    database: {
      _id: 'order',
      primary: 'testrs',
      partitioned: false,
      version: {
        uuid: new UUID("3e8b3afe-8e40-4524-af33-a540a46fc54d"),
        timestamp: Timestamp({ t: 1679108219, i: 2 }),
        lastMod: 1
      }
    },
    collections: {}
  }

6.查看集合的分片分布

我们查看books.users5集合的分片状态

db.users5.getShardDistribution();

看到了两个副本集都有5个文档,总共有10条记录。

Shard testrs2 at testrs2/10.6.17.112:27027,10.6.17.112:27028
{
  data: '235B',
  docs: 5,
  chunks: 2,
  'estimated data per chunk': '117B',
  'estimated docs per chunk': 2
}
---
Shard testrs at testrs/10.6.17.112:27017,10.6.17.112:27018
{
  data: '235B',
  docs: 5,
  chunks: 2,
  'estimated data per chunk': '117B',
  'estimated docs per chunk': 2
}
---
Totals
{
  data: '470B',
  docs: 10,
  chunks: 4,
  'Shard testrs2': [ '50 % data', '50 % docs in cluster', '47B avg obj size on shard' ],
  'Shard testrs': [ '50 % data', '50 % docs in cluster', '47B avg obj size on shard' ]
}

7.集合查询分析

db.users5.find().explain();

因为是查询全表数据,所以分片策略为SHARD_MERGE

{
  queryPlanner: {
    mongosPlannerVersion: 1,
    winningPlan: {
      stage: 'SHARD_MERGE',
      shards: [
        {
          shardName: 'testrs',
          connectionString: 'testrs/10.6.17.112:27017,10.6.17.112:27018',
          serverInfo: {
            host: 'localhost.localdomain',
            port: 27017,
            version: '6.0.4',
            gitVersion: '44ff59461c1353638a71e710f385a566bcd2f547'
          },
          namespace: 'books.users5',
          indexFilterSet: false,
          parsedQuery: {},
          queryHash: '17830885',
          planCacheKey: '17830885',
          maxIndexedOrSolutionsReached: false,
          maxIndexedAndSolutionsReached: false,
          maxScansToExplodeReached: false,
          winningPlan: {
            stage: 'SHARDING_FILTER',
            inputStage: { stage: 'COLLSCAN', direction: 'forward' }
          },
          rejectedPlans: []
        },
        {
          shardName: 'testrs2',
          connectionString: 'testrs2/10.6.17.112:27027,10.6.17.112:27028',
          serverInfo: {
            host: 'localhost.localdomain',
            port: 27027,
            version: '6.0.4',
            gitVersion: '44ff59461c1353638a71e710f385a566bcd2f547'
          },
          namespace: 'books.users5',
          indexFilterSet: false,
          parsedQuery: {},
          queryHash: '17830885',
          planCacheKey: '17830885',
          maxIndexedOrSolutionsReached: false,
          maxIndexedAndSolutionsReached: false,
          maxScansToExplodeReached: false,
          winningPlan: {
            stage: 'SHARDING_FILTER',
            inputStage: { stage: 'COLLSCAN', direction: 'forward' }
          },
          rejectedPlans: []
        }
      ]
    }
  },
  serverInfo: {
    host: 'localhost.localdomain',
    port: 30000,
    version: '6.0.4',
    gitVersion: '44ff59461c1353638a71e710f385a566bcd2f547'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600
  },
  command: {
    find: 'users5',
    filter: {},
    lsid: { id: new UUID("0a03908f-f355-447c-9844-1273a79c1de8") },
    '$clusterTime': {
      clusterTime: Timestamp({ t: 1679120556, i: 1 }),
      signature: {
        hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
        keyId: Long("0")
      }
    },
    '$db': 'books'
  },
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1679120774, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1679120772, i: 1 })
}

单条记录查询分析

db.users5.find({"name":"test1"}).explain();

因为是单条记录,只会在一个分片的副本集上,所以分片策略SINGLE_SHARD

[direct: mongos] books> db.users5.find({"name":"test1"}).explain();
{
  queryPlanner: {
    mongosPlannerVersion: 1,
    winningPlan: {
      stage: 'SINGLE_SHARD',
      shards: [
        {
          shardName: 'testrs2',
          connectionString: 'testrs2/10.6.17.112:27027,10.6.17.112:27028',
          serverInfo: {
            host: 'localhost.localdomain',
            port: 27027,
            version: '6.0.4',
            gitVersion: '44ff59461c1353638a71e710f385a566bcd2f547'
          },
          namespace: 'books.users5',
          indexFilterSet: false,
          parsedQuery: { name: { '$eq': 'test1' } },
          queryHash: '64908032',
          planCacheKey: 'A6C0273F',
          maxIndexedOrSolutionsReached: false,
          maxIndexedAndSolutionsReached: false,
          maxScansToExplodeReached: false,
          winningPlan: {
            stage: 'FETCH',
            filter: { name: { '$eq': 'test1' } },
            inputStage: {
              stage: 'IXSCAN',
              keyPattern: { name: 'hashed' },
              indexName: 'name_hashed',
              isMultiKey: false,
              isUnique: false,
              isSparse: false,
              isPartial: false,
              indexVersion: 2,
              direction: 'forward',
              indexBounds: {
                name: [ '[8413400434652939183, 8413400434652939183]' ]
              }
            }
          },
          rejectedPlans: []
        }
      ]
    }
  },
  serverInfo: {
    host: 'localhost.localdomain',
    port: 30000,
    version: '6.0.4',
    gitVersion: '44ff59461c1353638a71e710f385a566bcd2f547'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600
  },
  command: {
    find: 'users5',
    filter: { name: 'test1' },
    lsid: { id: new UUID("0a03908f-f355-447c-9844-1273a79c1de8") },
    '$clusterTime': {
      clusterTime: Timestamp({ t: 1679120774, i: 1 }),
      signature: {
        hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
        keyId: Long("0")
      }
    },
    '$db': 'books'
  },
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1679120864, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1679120862, i: 1 })
}

三、踩坑记录

  1. 搭建路由集群时,添加分片失败,

  1. 提示不能添加非分片类型的副本集

原因分析:在启动副本集的三个节点时没有加上参数--shardsvr导致。

  1. 将副本集加 上--shardsvr参数后,还是提示添加分片失败

原因分析:

要在分片路由服务的命令行连接客户端中设置默认写并发关注点数,大于1,即写要同步至少两个节点,否则添加分片失败。

db.adminCommand({

"setDefaultRWConcern" : 1,

"defaultWriteConcern" : {"w" : 2}

});

3.将数据库和集合开启分片后,插入数据,最终只落在一个分片里,不能均匀分布到两个分片

users只有一个chunk,落在一个副本集,users5有四个chunk,分别落在两个副本集

[direct: mongos] books> db.users.getShardDistribution();
Shard testrs2 at testrs2/10.6.17.112:27027,10.6.17.112:27028
{
  data: '5.83MiB',
  docs: 120100,
  chunks: 1,
  'estimated data per chunk': '5.83MiB',
  'estimated docs per chunk': 120100
}
---
Totals
{
  data: '5.83MiB',
  docs: 120100,
  chunks: 1,
  'Shard testrs2': [
    '100 % data',
    '100 % docs in cluster',
    '50B avg obj size on shard'
  ]
}

原因分析:

db.users5.createIndex({'username':"1"});

db.runCommand({"shardcollection":"books.users5","key":{"name":"1"}})

在创建索引,和开启分片键时,未配置策略为hashed,即为默认的策略,导致数据不能分布到多个分片。

四、其它资料理论讲解

分片

1. 分片(sharding)是指将数据拆分,将其分散存放在不同的机器上的过程。有时也用分区(partitioning)来表示这个概念。将数据分散到不同的机器上,不需要功能强大的大型计算机就可以

存储更多的数据,处理更大的负载。

2. MongoDB支持自动分片(autosharding),可以使数据库架构对应用程序不可见,也可以简化系统管理。对应用程序而言,好像始终在使用一个单机的MongoDB服务器一样。另一方面,

mongoDB自动处理数据在分片上的分布,也更容易添加和删除分片技术。

3. 复制与分片的区别:复制时让多台服务器都拥有同样的数据副本,每一台服务器都是其他服务器的镜像,而每一个分片都和其他分片拥有不同的数据子集。

4. 路由服务器:为了对应用程序隐藏数据库架构的细节,在分片之前要先执行mongos进行一次路由过程。这个路由服务器维护这一个"内容列表",指明了每个分片包含什么数据内容。应用

程序只需要连接路由服务器,就可以像使用单机一样进行正常的请求了。

5. 运行sh.status()可以看到集群的状态:分片摘要信心、数据库摘要信息、集合摘要信息。

6. 要对一个集合分片,首先你要对这个集合的数据库启用分片,执行如下命令:sh.enableSharding("test")

7. 片键:片键是集合的一个键,MongoDB根据这个键拆分数据。例如:username 。在启用分片之前,先在希望作为片键的键上创建索引:db.users.ensureIndex({"username":1})

8. 对集合分片:sh.shardCollection("test.users",{"username":1})

9. 集合被拆分为多个数据块,每个数据块都是集合的一个数据子集。这是按照片键的范围排列的({"username":minValue}-->>{"username":maxValue}指出了每个数据块的范围)。

10. 包含片键的查询能够直接被发送到目标分片或者是集群分片的一个子集。这样的查询叫做定向查询(targetd query)。有些查询必须被发送到所有分片,这样的查询叫做分散-聚合查询(

scatter-gather query);mongos将查询分散到所有的分片上,然后经各个分片的查询结果聚集起来。

11. cluster.stop() 关闭整个集群。

BSON类型

配置分片:

1. 何时进行分片:决定何时分片是一个值得权衡的问题。通常不必太早分片,因为分片不仅会增加部署的操作复杂度,还要求做出设计决策,而改决策以后很难再改。另外最好也不要在系统

运行太久之后再分片,因为在一个过载的系统上不停机进行分配是很困难的。

2. 分片的目的:增加可用的RAM,增加可用磁盘空间,减轻单台服务器的负载,处理单个mongod无法承受的吞吐量。

3. 一般情况下至少应该创建3个或者以上的分片。

4. 启动服务器:

1). 配置服务器:配置服务器相当于集群的大脑,保存着集群和分片的元数据,即各分片包含哪些数据的信息。因此,应该首先建立配置服务器,鉴于它所包含的的数据极端重要性,必须启用

其日志功能,并确保其数据保存在非易失性驱动器上。每个配置服务器都应该位于单独的物理机上,最好是分布在不同地址位置的机器上。

a. 启动配置服务器:mongod --configsvr --dbpath /var/lib/mongodb -f /var/lib/config/mognd.conf 。需要启动三台配置服务器,且都是可写的。

为什么是3台配置服务器?因为我们需要考虑不时之需。但是,也不需要过多的配置服务器,因为配置服务器上的确认操作比较耗时。另外,如果有服务器宕机了,集群源数据就会变成只读的。

--configsvr 选项指定mongod为新配置服务器。该选项并非必选项,因为它所做的不过是将mongod的默认监听端口改为27019,并大默认的数据目录改为/data/configdb而已(可以使用

--port 和 --dbpath 选项修改这两项配置)。但建议使用--configsvr选项,因为它比价直白地说明了这些配置服务器的用途。

配置服务器的1KB相当于200MB知识数据,它保存的真实数据的分布表。由于配置服务器并不需要太多的资源,因此可以将其部署在运行着其他程序的服务器上。

2). mongos进程:三个配置服务器均处于运行状态后,启动一个mongos进程供应用程序连接。mongos进程需要配置服务器的地址,所以必须使用--configdb选项启动mongos:

mongos --configdb config-1:27019,config-2:27019,config-3:27019 -f /var/lib/mongos.conf

默认情况下,mongos运行在27017端口。mongos本身不保存数据,它会在启动时从配置服务器加载集群数据。

可以启动任意数量的mongos进程。通常的设置时每个应用程序服务器使用一个mongos进程(与应用服务器运行在同一台机器上)

每个mongos进程必须按照列表排序,使用相同的配置服务器列表。

3). 将副本集转换为分片:有两种可能性:已经有一个副本集,或者从零开始建立集群。下例假设我们已经拥有了一个副本集。如果是从零开始的话,可先初始化一个空的副本集,然后按照本例操作。

a. 告知mongos副本集名称和副本集成员列表:sh.addShard("spock/server-1:27017,server-2:27017,server-4:27017") mongos能够自动检测到没有包含在副本集成员表中的成员。

b. 副本集作为分片添加到集群后,就可以将应用程序设置从连接到副本集改为连接到mongos。

c. 副本集名称spokc被作为分片名称。如果之后希望移除这个分片或者是向这个分片迁移数据,可以使用spock来标志这个分片。

d. 配置完分片后,必须将客户端设置为将所有请求发送到mongos,而不是副本集。同时配置防火墙规则,以确保客户端不能直接将请求发送到分片。

e. 有一个--shardsvr选项,与前面介绍的--configsvr选项类似,它也没什么实用性(只是将默认端口改为27018),但在操作中建议使用该选项。

f. 不建议创建单mongod服务器分片(而不是副本集分片),将单一服务器分片转换为副本集需要停机操作。

4). 增加集群容量:通过增加分片来增加集群容量。

5). 数据分片:除非明确指定规则,否则MongoDB不会自动对数据进行拆分。如果有必要,必须明确告知数据库和集合。加入对music数据库中的artists集合按照name进行分片,

db.enableSharding("music") 对数据库分片是对集合分片的先决条件

sh.shardCollection("music.artists",{"name":1}) 对集合分片,集合会按照name键进行分片。如果是对已存在的集合分片,那么name键上必须有索引,否则会返回错误。

shardCollection()命令会经集合拆分为多个数据块,这是MongoDB迁移数据的基本单元。命令执行后,MongoDB会均衡的将数据分散到集群的分片上。

5. MongoDB如何追踪集群数据

1). MongoDB将文档分组为块(chunk),每个块由给定片键特定范围内的文档组成。一个块只存在于一个分片上,所以MongoDB用一个比较小的表就能够维护跟分片的映射。

2). 当一个块增长到特定大小时,MongoDB会自动将其拆分为两个较小的块。

3). 一个常见的误解释同一个块内的数据保存在磁盘的同一片区域。这是不正确的,块并不影响mongod保存集合数据的方式。

4). 块信息保存在config.chunks集合中。左闭右开。

5). 可以使用复合片键,工作方式与使用复合索引进行排序一样。

6). 拆分块:mongos会记录在每个块中插入了多少数据,一旦达到某个阈值,就会检查是否需要对块进行拆分。mongos就会在配置服务器更新这个块的源信息。块拆分中只需要改变块源数据即可,

而无需进行数据移动。进行拆分时,配置服务器会创建新的块文档,同时修改旧的块范围,拆分完成以后,mongos会重置对原始块的追踪器,同时为新的块创建新的追踪器。

7). 分片有时可能会找不到任何可用的拆分点,因为合法拆分块方法有限。具有相同片键的文档必须保存在相同的块中。

8). 如果mongos试图进行拆分时有一个服务器挂了,那么mongos就无法更新源数据。mongos不断重复发起拆分请求却无法进行拆分的过程,叫做拆分风暴。防止拆分风暴的唯一方法是尽可能保证

配置服务器的可用和健康。也可以重启mongos,重置引入计数器,这样他就不会再处于拆分阈值点了。

9). 如果mongos进程不断重启,它们的计数器可能永远也不会到达阈值点,因此块的增加不存在最大值,也就无法到达阈值点。

10). 防止无法拆分的两种方法:一是减少mongos进程的波动,二是使块的大小比实际预期小一些,这样就更容易达到拆分阈值点。

11). 可以在mongos启动时指定--nosplit选项,从而关闭块的拆分。

6. 均衡器:均衡器负责数据的迁移。它会周期性地检查分片间是否存在不均衡,如果存在,则会开始块的迁移。虽然均衡器通常被看成单一的实体,但每个mongos有时也会扮演均衡器的角色。

每隔几秒,mongos就会尝试变身均衡器。如果没有其他可用的均衡器,mongos就会对整个集群加锁,以防止配置服务器对整个集群进行修改,然后做一次均衡。

mongos成为均衡器后,就会检查每个集合的分块表,从而查看是否有分片达到了均衡阈值。

选择片键

1. 对集合进行分片时,要选择一或两个字段用于拆分数据,这个键就叫做片键。

2. 拆分数据最常用的数据分发方式有三种:升序片键、随机分发的片键和基于位置的片键。

1). 升序片键:升序片键通常有点类似于"date"字段或者是ObjectId,是一种随着时间稳定增长的字段。缺点:例如ObjectId可能会导致接下来的所有的写入操作都在同一块分片上。

2). 随机分发的片键:随机分发的片键可以是用户名,邮件地址,UDID,MD5散列值或者数据集中其他一些没有规律的键。缺点:MongoDB在随机访问超出RAM大小的数据时效率不高。

3). 基于位置的片键:基于位置的片键可以是用户的IP、经纬度、或者地址。这里的"位置"比较抽象,不必与实际的物理位置字段相关。

如果希望特定范围内的块出现在特定的分片中,可以为分片添加tag,然后为块指定相应的tag

3. 片键策略:

1). 散列片键:如果追求的是数据加载速度的极致,那么散列片键是最佳选择。散列片键可使其他任何键随机分发,因此,如果打算在大量查询中使用使用升序键,但同时又希望写入数据随机分发的话,

散列片键会是一个非常好的选择。缺点:无法使用散列片键做指定目标的范围查找。

创建步骤: db.users.ensureIndex({"username":"hashed"}) , sh.shardCollection("app.users",{"username":"hashed"})

2). GridFS的散列片键

3). 流水策略:如果有一些服务器比其他服务器更强大,我们可能希望让这些强大的服务器处理更多的负载。比如说:加入有一个使用SSD的分片能够处理10倍于其他机器的负载。我们可以强制将所有新数据

插入到SSD,然后让均衡器将旧的块移动到其他分片上。

a. 为SSD指定一个标签:sh.addShardTag("shard-name","ssd")

b. 将升序键的当前值一直到正无穷范围的块都指定分布在SSD分片上:sh.addTagRange("dbName.collName",{"_id":ObjectId()},...{"_id":MaxKey},"ssd")

所有插入请求均会路由到这个块上,这个块始终位于标签的ssd的分片上。

c. 除非修改标签范围,否则从升序键的当前值一直到正无穷都被固定在这个分片上。可以创建一个定时任务每天更新一次标签范围:

use config

var tag =db.tags.findOne({"ns":"dbName.collName",..."max":{"shardKey":MaxKey}})

tag.min.shardKey = ObjectId()

db.tags.save(tag)

这样前一天的数据就会被移动到其他分片上了。

此策略的另一个缺点:需要修改才能进行扩展。如果写请求超出了SSD的处理能力,无法进行负载均衡。

4). 多热点:写请求分布在集群中时,分片是最高效的。这种技术会创建多个热点(最好在每个分片上都创建几个热点),写请求于是会均衡地分布在集群内,而在单个分片上则是以升序分布的。

为了实现这种方式,需使用复合片键。复合片键中的第一个值只是比较粗略的随机值,势也比较低。

4. 片键规则和指导方针:

1). 片键限制:片键不可以是数组。文档一旦插入,其片键就无法修改了。要修改文档的片键值,就必须先删除文档。

2). 片键的势:选择一个值会变化的的键非常重要,即值很多,随着数据量的增大可以分出更多的片键。分片在势比较高的字段上性能更佳。

5. 控制数据分发

1). 对多个数据库和集合使用一个集群:通过tag标记,将重要的数据放到性能更好的服务器上,将不重要的数据放在性能一般的服务器上。

2). 手动分片:如果不希望数据被自动分发,可以关闭均衡器,使用moveChunk命令手动对数据进行迁移。

分片管理

1. 检查集群状态:

1). 使用sh.status查看集群摘要信息: 块的数量比较多时,sh.status()命令会概述块的状态,而非打印出每个块的相关信息。如需查看所有的块,可使用sh.status(true)命令。

sh.status()显示的所有信息都来自config数据库。运行sh.status()命令,使用MapReduce获取这一数据。因此,如果启动数据库时指定--noscripting选项,则无法运行sh.status()命令。

2). 检查配置信息:

a. 集群相关的所有配置信息都保存在配置服务器上config数据库的集合中。可以直接访问该数据库,不过shell提供了一些辅助函数。

b. 永远不要直接连接到配置服务器,以防止配置服务器数据被不小心修改或删除。应该先连接到mongos,然后通过config数据库来查询相关信息:use config

如果通过mongos操作配置数据,mongos会保证将修改同步到所有配置服务器,也会防止危险的操作发生,如意外删除config数据库等。

c. 总的来说,不应直接修改config数据库中的任何数据。如果确实修改了某些数据,通常需要重启所有的mongos服务器,才能看到效果。

d. config中几个关键集合:

shards : 跟踪记录集群中所有分片的信息。

databases: 跟踪记录集群中所有数据库的信息,不管数据库有没有分片。

collections: 跟踪记录所有分片集合的信息(非分片集合信息除外)

chunks: 记录集合中所有块的信息。

changelog: 跟踪记录集群的操作,因为该集合会记录所有拆分和迁移的操作。

tags: 该集合的创建是在为系统配置分片标签时发生的。每个标签都与一个块范围相关联。

settings: 该集合含有当前的均衡器设置和块大小的文档信息。通过修改该集合的文档,可开启和关闭均衡器,也可以修改块的大小。注意,应总是连接到mongos修改该集合的值。

2. 查看网络连接:

1). 查看连接统计:可以使用connPoolStats命令,查看mongos和mongod之间的连接信息:db.adminCommand({"connPoolStats":1})

在一个分片上执行connPoolStats,输出信息中可以看到该分片与其他分片间的连接,包括连接到其他分片做数据迁移的连接。

2). 限制连接数量: 可在mongos的命令行配置中使用maxConns选项,这样可以限制mongos能够创建的连接数量。可以使用下面公式计算分片能够处理的来自单一mongos连接数量:

maxConns = 20000 - (mongos进程的数量 * 3 ) - (每个副本集的成员数量 * 3 ) - (其他/mongos进程的数量)

MongoDB如果没有安全退出,那些已经打开的套接字很可能没有被关闭。

在出现大量重新连接时,除了重启进程,没有其他特殊有效的方法。

3. 服务器管理

1). 添加服务器:使用addShard命令,向集群中添加新的分片

2). 修改分片的服务器:要修改分片的成员,需直接连接到分片的主服务器上,然后对副本集进行重新配置。集群配置会自动检测更改,并将其更新到config.shards上。

3). 通常来说,不应从集群中删除分片。执行removeShard命令排除数据和查看排出进度。

4). 修改配置服务器:修改配置服务器非常困难,而且有风险,通常还需要停机。注意,修改配置服务器前,应做好备份。

首先必须关闭所有mongos进程,然后使用新的--configdb参数重启所有mongos进程。

4. 数据均衡:

1). 均衡器:均衡器只使用块的数量,而非数据大小,作为衡量分片间是否均衡的指标。自动均衡总是根据数据集的当前状态来决定数据迁移,而不考虑数据集历史状态。我们可以手动均衡数据集块的数量。

2). 修改块的大小:块的大小默认为64M,这个大小的块既易于迁移,又不至于导致过多的流失。使用shell连接到mongos,修改config.setting集合,从而完成块大小的修改。

该设置的有效范围是整个集群:它会影响所有集合的数据库。因此,如需对一个集合使用较小的块,而对另一个集合使用较大的块,比较好的解决方式是取一个折中值(或者将这两个值放到不同的集合中)。

如果MongoDB频繁进行数据迁移或文档增大,则可能需要增加块的大小。

3). 迁移块:同一块内的所有数据都位于同一分片上。如该分片的块数量比其他分片多,则MongoDB会将其中的一部分块迁移到其他块数量较少的分片上。移动快的过程叫迁移,MongoDB就是这样在集群中

实现数据均衡的。可在shell中使用moveChunk辅助函数,手动移动块。

如果某个块的大小超出了系统指定的最大值,mongos则会拒绝移动这个块。移动之前必须先手动拆分这个块,可以使用splitAt命令对块进行拆分。特大块,无法被拆分。

4). 特大块:某些片键,值比较少,例如:日期等。可能会形成超出设置的最大块大小的块,这种块成为特大块.

出现特大块的表现之一是,某个分片的大小增长速度要比其他分片块的多。也可使用sh.status()来检查是否出现了特大块;特大块会存在一个jumbo属性。

a. 分发特大块,一个复杂的过程

b. 防止特大块的出现:修改片键,细化片键的粒度

5). mongos有时无法从配置服务器正确更新配置。如果发现配置有误,mongos的配置过旧或无法找到应有的数据,可以使用flushRouterConfig命令手动刷新所有缓存:db.adminCommand({"flushRouterConfig":1})

如flushRouterConfig命令没能解决问题,则应重启所有的mongos或者mongod进程,以便清除所有可能的缓存。

https://www.mongodb.com/docs/manual/reference/command/setDefaultRWConcern/

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值