Docker 搭建叢集 MongoDB

邢闖洋發表於2020-09-08

前言

由於公司業務需要,我們打算自己搭建 MongoDB 的服務,因為 MongoDB 的雲資料庫好貴,我們這次採用副本集的方式來搭建叢集,三臺伺服器,一主、一副、一仲裁

基本概念

  1. Replica Set 副本集:一個副本集就是一組 MongoDB 例項組成的叢集,由一個主(Primary)伺服器和多個備份(Secondary)伺服器構成
    • 主節點(master):主節點接收所有寫入操作。主節點將對其資料集所做的所有更改記錄到其 oplog。
    • 副節點(secondary):複製主節點的 oplog 並將操作應用到其資料集,如果主節點不可用,一個合格的副節點將被選為新的主節點。
    • 仲裁節點(arbiter):負載選舉,當主節點不可用,它將從副節點中選一個作為主節點。
  2. Sharding 分片:
  3. Master-slave 主備
    • MongoDB 4.0 以上版本執行時提示:[main] Master/slave replication is no longer supported,也就是 MongoDB 4.0 後不在支援主從複製

一、環境準備

使用 CentOS 7.6 64bit 系統,安裝 Docker、Docker-compose、Docker-Swarm

二、生成 KeyFile

  • MongoDB 使用 KeyFile 認證,副本集中的每個 MongoDB 例項使用 KeyFile 內容作為認證其他成員的共享密碼。MongoDB 例項只有擁有正確的 KeyFile 才可以加入副本集。
  • keyFile 的內容必須是 61024 個字元的長度,且副本集所有成員的 KeyFile 內容必須相同。
  • 有一點要注意是的:在 UNIX 系統中,KeyFile 必須沒有組許可權或完全許可權(也就是許可權要設定成X00的形式)。Windows 系統中,keyFile 許可權沒有被檢查。
  • 可以使用任意方法生成 keyFile。例如,如下操作使用 openssl 生成複雜的隨機的 1024 個字串。然後使用 chmod 修改檔案許可權,只給檔案擁有者提供讀許可權。
  • 這是 MongoDB 官方推薦 keyFile 的生成方式:
    # 400許可權是要保證安全性,否則mongod啟動會報錯
    openssl rand -base64 756 > mongodb.key
    chmod 400 mongodb.key

二、建立跨主機網路

搭建叢集我們肯定是跨主機通訊,要搭建 Overlay Network 網路,我們就要用到 Docker Swarm 這個工具了。Docker Swarm 是 Docker 內建的叢集工具,它能夠幫助我們更輕鬆地將服務部署到 Docker daemon 的叢集之中。

Docker 搭建叢集 MongoDB
既然要將 Docker 加入到叢集,我們就必須先有一個叢集,我們在任意一個 Docker 例項上都可以透過 docker swarm init 來初始化叢集。

$ sudo docker swarm init

Swarm initialized: current node (t4ydh2o5mwp5io2netepcauyl) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-4dvxvx4n7magy5zh0g0de0xoues9azekw308jlv6hlvqwpriwy-cb43z26n5jbadk024tx0cqz5r 192.168.1.5:2377

在叢集初始化後,這個 Docker 例項就自動成為了叢集的管理節點,而其他 Docker 例項可以透過執行這裡所列印的 docker swarm join 命令來加入叢集。

加入到叢集的節點預設為普通節點,如果要以管理節點的身份加入到叢集中,我們可以透過 docker swarm join-token 命令來獲得管理節點的加入命令。

$ sudo docker swarm join-token manager
To add a manager to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-60am9y6axwot0angn1e5inxrpzrj5d6aa91gx72f8et94wztm1-7lz0dth35wywekjd1qn30jtes 192.168.1.5:2377

我們透過這些命令來建立用於我們服務開發的 Docker 叢集,並將相關開發同事的 Docker 加入到這個叢集裡,就完成了搭建跨主機網路的第一步。

建立跨主機網路

接下來,我們就透過 docker network create 命令來建立 Overlay 網路。

$ sudo docker network create --driver overlay --attachable mongodbs

在建立 Overlay 網路時,我們要加入 --attachable 選項以便不同機器上的 Docker 容器能夠正常使用到它。

在建立了這個網路之後,我們可以在任何一個加入到叢集的 Docker 例項上使用 docker network ls 檢視一下其下的網路列表。我們會發現這個網路定義已經同步到了所有叢集中的節點上。

$ sudo docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
## ......
y89bt74ld9l8        mongodbs                overlay             swarm
## ......

接下來我們要修改 Docker Compose 的定義,讓它使用這個我們已經定義好的網路,而不是再重新建立網路。

我們只需要在 Docker Compose 配置檔案的網路定義部分,將網路的 external 屬性設定為 true,就可以讓 Docker Compose 將其建立的容器都連線到這個不屬於 Docker Compose 的專案上了。

networks:
  mongodbs:
    external: true

透過這個實現,我們在開發中就使整個服務都處於一個可以使用別名對映網路中,避免了要對不同功能聯調時切換服務 IP 的煩瑣流程。在這種結構下,我們只需要讓我們開發的 Docker 退出和加入不同的叢集,就能馬上做到切換不同聯調專案。

二、編寫 docker-compose 檔案

  1. 主節點
    version: "3"
    services:  
     master:
       image: mongo:4.1
       container_name: master
       environment:
         MONGO_INITDB_ROOT_USERNAME: root
         MONGO_INITDB_ROOT_PASSWORD: 123456
         TZ: "Asia/Shanghai"
       volumes:
         # 掛載 MongoDB 資料目錄
         - "/data/docker/mongodb/data/mongo:/data/db:rw"
         # 掛載 KeyFile
         - "/data/docker/mongodb/data/mongodb.key:/data/mongodb.key"
       ports:
         - "27018:27017"
       networks:
         - mongodbs
       command:
         # 密碼
         --auth
         # 副本集名稱
         --replSet testSet 
         --oplogSize 128
         --keyFile /data/mongodb.key
    # Swarm 跨主機網路網路
    networks:
      mongodbs:
        external: true
  2. 副節點
    version: "3"
    services:  
    secondary:
     image: mongo:4.1
     container_name: secondary
     environment:
       MONGO_INITDB_ROOT_USERNAME: root
       MONGO_INITDB_ROOT_PASSWORD: 123456
       TZ: "Asia/Shanghai"
     volumes:
       - "/data/docker/mongodb/data/mongo:/data/db:rw"
       - "/data/docker/mongodb/data/mongodb.key:/data/mongodb.key"
     ports:
       - "27018:27017"
     networks:
       - mongodbs
     command:
       --auth
       --replSet testSet 
       --oplogSize 128
       --keyFile /data/mongodb.key
    networks:
    mongodbs:
     external: true
  3. 仲裁節點,因為仲裁節點不需要儲存資料,他只是用來當主節點掛掉後選舉新的主節點,所以不需要密碼、對映埠等操作
    version: "3"
    services:
    arbiter:
     image: mongo:4.1
     container_name: arbiter
     restart: always
     volumes:
       - "/data/docker/mongodb/data/mongo:/data/db:rw"
       - "/data/docker/mongodb/data/mongo_key:/mongo:rw"
     networks:
       - mongodbs
     command:
       mongod --replSet testSet --smallfiles --oplogSize 128
    networks:
    mongodbs:
     external: true

三、啟動容器

接下來我們分別在三臺伺服器中使用容器編排啟動容器

docker-compose up -d

四、配置副本集

  1. 進入主節點容器內部

    docker exec -it master mongo
  2. 在 mongo shell 裡執行:

    > rs.initiate()
    {
         "info2" : "no configuration specified. Using a default configuration for the set",
         "me" : "7abd89794aa7:27017",
         "ok" : 1
    }
  3. 繼續執行:

    testSet:SECONDARY> rs.add('secondary:27017')
    {
         "ok" : 1,
         "$clusterTime" : {
                 "clusterTime" : Timestamp(1599562800, 1),
                 "signature" : {
                         "hash" : BinData(0,"wrxMUIX/0bEyLgCVoQqdLvH59T0="),
                         "keyId" : NumberLong("6870069879538450434")
                 }
         },
         "operationTime" : Timestamp(1599562800, 1)
    }
  4. 繼續執行, 其中 true 表示這個節點是仲裁節點

    testSet:PRIMARY> rs.add('arbiter:27017',true)
    {
         "ok" : 1,
         "$clusterTime" : {
                 "clusterTime" : Timestamp(1599562838, 1),
                 "signature" : {
                         "hash" : BinData(0,"p9ub49lLD8ij8nkxpfu2l/AvRRY="),
                         "keyId" : NumberLong("6870069879538450434")
                 }
         },
         "operationTime" : Timestamp(1599562838, 1)
    }
  5. 檢視配置

    testSet:PRIMARY> rs.conf()
    {
         "_id" : "testSet",
         "version" : 3,
         "protocolVersion" : NumberLong(1),
         "writeConcernMajorityJournalDefault" : true,
         "members" : [
                 {
                         "_id" : 0,
                         "host" : "7abd89794aa7:27017",
                         "arbiterOnly" : false,
                         "buildIndexes" : true,
                         "hidden" : false,
                         "priority" : 1,
                         "tags" : {
    
                         },
                         "slaveDelay" : NumberLong(0),
                         "votes" : 1
                 },
                 {
                         "_id" : 1,
                         "host" : "secondary:27017",
                         "arbiterOnly" : false,
                         "buildIndexes" : true,
                         "hidden" : false,
                         "priority" : 1,
                         "tags" : {
    
                         },
                         "slaveDelay" : NumberLong(0),
                         "votes" : 1
                 },
                 {
                         "_id" : 2,
                         "host" : "arbiter:27017",
                         "arbiterOnly" : true,
                         "buildIndexes" : true,
                         "hidden" : false,
                         "priority" : 0,
                         "tags" : {
    
                         },
                         "slaveDelay" : NumberLong(0),
                         "votes" : 1
                 }
         ],
         "settings" : {
                 "chainingAllowed" : true,
                 "heartbeatIntervalMillis" : 2000,
                 "heartbeatTimeoutSecs" : 10,
                 "electionTimeoutMillis" : 10000,
                 "catchUpTimeoutMillis" : -1,
                 "catchUpTakeoverDelayMillis" : 30000,
                 "getLastErrorModes" : {
    
                 },
                 "getLastErrorDefaults" : {
                         "w" : 1,
                         "wtimeout" : 0
                 },
                 "replicaSetId" : ObjectId("5f576426fe90ef2dd8cd2700")
         }
    }
  6. 檢視狀態

    testSet:PRIMARY> rs.status()
    {
         "set" : "testSet",
         "date" : ISODate("2020-09-08T11:45:12.096Z"),
         "myState" : 1,
         "term" : NumberLong(1),
         "syncingTo" : "",
         "syncSourceHost" : "",
         "syncSourceId" : -1,
         "heartbeatIntervalMillis" : NumberLong(2000),
         "optimes" : {
                 "lastCommittedOpTime" : {
                         "ts" : Timestamp(1599565502, 1),
                         "t" : NumberLong(1)
                 },
                 "lastCommittedWallTime" : ISODate("2020-09-08T11:45:02.775Z"),
                 "readConcernMajorityOpTime" : {
                         "ts" : Timestamp(1599565502, 1),
                         "t" : NumberLong(1)
                 },
                 "readConcernMajorityWallTime" : ISODate("2020-09-08T11:45:02.775Z"),
                 "appliedOpTime" : {
                         "ts" : Timestamp(1599565502, 1),
                         "t" : NumberLong(1)
                 },
                 "durableOpTime" : {
                         "ts" : Timestamp(1599565502, 1),
                         "t" : NumberLong(1)
                 },
                 "lastAppliedWallTime" : ISODate("2020-09-08T11:45:02.775Z"),
                 "lastDurableWallTime" : ISODate("2020-09-08T11:45:02.775Z")
         },
         "lastStableRecoveryTimestamp" : Timestamp(1599565492, 1),
         "lastStableCheckpointTimestamp" : Timestamp(1599565492, 1),
         "members" : [
                 {
                         "_id" : 0,
                         "name" : "7abd89794aa7:27017",
                         "ip" : "10.0.1.41",
                         "health" : 1,
                         "state" : 1,
                         "stateStr" : "PRIMARY",
                         "uptime" : 2784,
                         "optime" : {
                                 "ts" : Timestamp(1599565502, 1),
                                 "t" : NumberLong(1)
                         },
                         "optimeDate" : ISODate("2020-09-08T11:45:02Z"),
                         "syncingTo" : "",
                         "syncSourceHost" : "",
                         "syncSourceId" : -1,
                         "infoMessage" : "",
                         "electionTime" : Timestamp(1599562790, 2),
                         "electionDate" : ISODate("2020-09-08T10:59:50Z"),
                         "configVersion" : 3,
                         "self" : true,
                         "lastHeartbeatMessage" : ""
                 },
                 {
                         "_id" : 1,
                         "name" : "secondary:27017",
                         "ip" : "10.0.1.233",
                         "health" : 1,
                         "state" : 2,
                         "stateStr" : "SECONDARY",
                         "uptime" : 2711,
                         "optime" : {
                                 "ts" : Timestamp(1599565502, 1),
                                 "t" : NumberLong(1)
                         },
                         "optimeDurable" : {
                                 "ts" : Timestamp(1599565502, 1),
                                 "t" : NumberLong(1)
                         },
                         "optimeDate" : ISODate("2020-09-08T11:45:02Z"),
                         "optimeDurableDate" : ISODate("2020-09-08T11:45:02Z"),
                         "lastHeartbeat" : ISODate("2020-09-08T11:45:11.494Z"),
                         "lastHeartbeatRecv" : ISODate("2020-09-08T11:45:11.475Z"),
                         "pingMs" : NumberLong(0),
                         "lastHeartbeatMessage" : "",
                         "syncingTo" : "7abd89794aa7:27017",
                         "syncSourceHost" : "7abd89794aa7:27017",
                         "syncSourceId" : 0,
                         "infoMessage" : "",
                         "configVersion" : 3
                 },
                 {
                         "_id" : 2,
                         "name" : "arbiter:27017",
                         "ip" : null,
                         "health" : 0,
                         "state" : 8,
                         "stateStr" : "(not reachable/healthy)",
                         "uptime" : 0,
                         "lastHeartbeat" : ISODate("2020-09-08T11:45:10.463Z"),
                         "lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"),
                         "pingMs" : NumberLong(0),
                         "lastHeartbeatMessage" : "Error connecting to arbiter:27017 :: caused by :: Could not find address for arbiter SocketException: Host not found (authoritative)",
                         "syncingTo" : "",
                         "syncSourceHost" : "",
                         "syncSourceId" : -1,
                         "infoMessage" : "",
                         "configVersion" : -1
                 }
         ],
         "ok" : 1,
         "$clusterTime" : {
                 "clusterTime" : Timestamp(1599565502, 1),
                 "signature" : {
                         "hash" : BinData(0,"7/ei+8UrhlpIny9zKeWuAFpn46c="),
                         "keyId" : NumberLong("6870069879538450434")
                 }
         },
         "operationTime" : Timestamp(1599565502, 1)
    }

五、驗證 MongoDB 可用性

  1. 先進入主節點伺服器新增一條資料
    docker exec -it master mongo
    use admin
    db.auth('root', '123456')
    use test
    db.test.insert({name:"muyang",age:20})
  2. 在來副節點伺服器檢視是否已經同步了這條資料
    [root@linux secondary] docker exec -it secondary mongo
    testSet:SECONDARY> use admin
    testSet:SECONDARY> db.auth('root', '123456')
    testSet:SECONDARY> use test
    testSet:SECONDARY> db.test.find()
    2020-09-08T19:03:02.295+0800 E  QUERY    [js] uncaught exception: Error: listCollections failed: {
         "operationTime" : Timestamp(1599562972, 1),
         "ok" : 0,
         "errmsg" : "not master and slaveOk=false",
         "code" : 13435,
         "codeName" : "NotMasterNoSlaveOk",
         "$clusterTime" : {
                 "clusterTime" : Timestamp(1599562972, 1),
                 "signature" : {
                         "hash" : BinData(0,"mhsrpGHRl7qZg2QOjyS3RbBb/Yc="),
                         "keyId" : NumberLong("6870069879538450434")
                 }
         }
    } :
    testSet:SECONDARY> rs.slaveOk()
    testSet:SECONDARY> db.users.find()
    { "_id" : ObjectId("5f5764b1f909544b783696c2"), "name" : "muyang", "age" : 20 }
    在 secondary 查詢時報如下錯誤:
    not master and slaveok=false
    這是正常的,因為 secondary 是不允許讀寫的,如果非要解決,方法如下:
    testSet:SECONDARY> rs.slaveOk()
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章