Home Database สรุปภาพรวมเรื่อง MongoDB Data Replication

สรุปภาพรวมเรื่อง MongoDB Data Replication

by khomkrit

MongoDB Data Replication คือหัวใจสำคัญหนึ่งของการบริหารจัดการ Database โดยเฉพาะหากเรายังต้องการให้ production database ของเราถูกเข้าถึงได้อยู่เสมอแม้ว่าระบบจัดการฐานข้อมูลจะมีปัญหา เข้าถึงไม่ได้ หรือ system failure ขึ้นมาก็ตาม การทำ Replication จะช่วยเพิ่ม hight availability และทำ disaster recovery ให้ database ของเราได้

บทความนี้ครอบคลุมเรื่องดังต่อไปนี้

  • ความเข้าในในหลักการและแนวคิดเบื้องต้นของ MongoDB Replication
  • การใช้งาน replica set จากฝั่ง application driver
  • การจัดการ replaca set เบื้องต้นและ automatic failover
  • การเขียนข้อมูลอย่างมั่นใจด้วย write concerns
  • optimize การอ่านด้วย read prefernece
  • จัดการ replica set ที่ซับซ้อนด้วยการติด tag

เนื้อหาในบทความนี้ส่วนใหญ่อิงมาจาก MongoDB 3.x แต่ concept โดยรวมยังสามารถนำมาใช้กับ MongoDB เวอร์ชั่นที่สูงกว่านี้ได้

ภาพรวม

Replication คือการกระจายข้อมูลท่ามกลาง MongoDB servers หลายๆ ตัว (หรือ หลายๆ node) โดย MongoDB สามารถกระจายข้อมูลไปยัง 1 หรือมากกว่านั้นและข้อมูลจะ sync กันตลอดเวลาเมื่อมีข้อมูลเปลี่ยนแปลง ซึ่ง replication แบบนี้จะทำงานผ่านสิ่งที่เรียกว่า replica set โดยที่ replica sets ก็คือกลุ่มของ nodes ที่ถูกตั้งค่าให้ sync กันอัตโนมัติ และนอกจากการ sync ข้อมูลแล้ว หาก node หลัก (primary node) เสียหายหรือไม่สามารถเข้าถึงได้ มันก็ยังทำ automatic failover ให้เราอีกด้วย

Replication ถูกออกแบบมาโดยมีจุดมุ่งหมายหลักเรื่อง redundancy และรับประกันว่า replicated nodes จะต้อง sync กับ primary node เสมอ ซึ่งแต่ละ node สามารถอยู่ที่ data center เดียวกัน หรือแยกกันอยู่กับ primary node ก็ได้ และอีกอย่างคือ replication นั้น asynchronous ซึ่งก็แปลว่า network latency หรือ partition ระหว่าง node นั้นไม่ได้กระทบกับ performance ของ primary เลย นั่นก็แปลว่า replicated node นั้นสามารถ delay ได้นั่นเอง

นอกจากเรื่อง redundancy และ failover แล้ว เรายังสามารถรัน expensive operations บน node ใดๆ ก็ได้นอกจาก primary node ยกตัวอย่างเช่น เราสามารถรัน backup บน secondary node เพื่อลดโหลดบน primary node หรือการทำ index (การ build index ถือว่าเป็น expensive operation อย่างหนึ่ง) ที่เราสามารถเลือกที่จะ build บน secondary node ก่อน จากนั้นสลับไปเป็น primary ก็ได้แล้ว build ใหม่สลับกันไป

สิ่งสำคัญอีกอย่างก็คือ เราควรทำความเข้าใจเกี่ยวกับ pitfalls ต่างๆ ของ replication โดยเฉพาะอย่างยิ่งเรื่องเกี่ยวกับ rollback ที่ replica set นั้น data จะไม่ถูกพิจารณาว่า commit ไปแล้ว จนกว่ามันจะถูกเขียนเข้าไปที่ majority (เครื่องส่วนใหญ่) ของสมาชิกใน replica set โดยคำว่า majority นี้มีความหมายความว่าข้อมูลได้ถูกเขียนไปแล้วมากกว่า 50% ของ node ใน replica set นั่นเอง ดังนั้นหากใน replica set เรามีแค่ 2 node ข้อมูลจะไม่มีวันถูก commit หากข้อมูลยังไม่ถูก replicate ไปครบแล้วทั้ง 2 node เป็นต้น อย่างไรก็ตามเนื้อหาในรายละเอียดต่างๆ จะกล่าวต่อไปในบทความนี้

การทำ Replication เป็นสิ่งจำเป็น

Replication รับประกันข้อมูลให้เราในกรณีที่ database failure แล้วอะไรบ้างที่เรียกว่า failure? ก็เช่น

  • ไม่สามารถเชื่อมต่อไปยัง database ได้
  • แผนการ down server เพื่อบำรุงรักษา
  • การ reboot
  • ไฟดับ
  • hard drive พัง

เมื่อเราทำ replication failure ที่ยกตัวอย่างมาจะน่ากลัวน้อยลงมาก โดยเฉพาะเมื่อเรารัน database โดยไม่ได้ enable journaling ไว้ mongodb data file ยิ่งไม่ถูกรับประกันได้เลยว่าข้อมูลจะยังคงอยู่หรือไม่หลังจากเกิดความเสียหาย แต่ replication รับประกันเรื่องข้อมูลให้เราได้

จะกล่าวถึงเรื่อง journaling ในบทความอื่น เนื่องจากไม่เกี่ยวข้องกับเนื้อหาเรื่อง replication

แม้ว่าการทำ data replication นั้นจะช่วยเราในเรื่องของการรับประกันความเสียหายของข้อมูล แต่ journaling นั้นก็ยังจำเป็นอยู่ดี เนื่องจากยังไงซะเราก็ยังต้องการในเรื่องของ high availability และ fast failover ในกรณีที่เราต้องการนำ node กลับมาออนไลน์ได้แบบไวๆ เราก็แค่ replay journal เท่านั้น และการทำแบบนี้นั้นเร็วกว่าการรอให้ node resync กันเองใน replica set

อีกอย่างที่สำคัญ แม้ว่ามันจะ redendant กัน แต่ replicas ก็ไม่ได้เอามาแทน backup เพราะว่า backup นั้นคือการ snapshot ของ data ณ เวลาหนึ่งๆ ในอดีต ในขณะที่ replica นั้นจะ update ตลอดเวลา หรืออีกนัยหนึ่ง backup นั้นเอามาใช้ในกรณีที่ logical failure เช่นเผลอพลาดลบข้อมูลทิ้งไป หรือข้อมูลเสียหาย

ดังนั้นจึงจำเป็นอย่างยิ่งที่เราต้องรัน production MongoDB instance โดยทำ replication และ journaling ไปพร้อมๆ กัน ยกเว้นแต่ว่าเราจะอยากเสีย data ไปสักวันหนึ่งข้างหน้า หรือต้องการ deployment ระบบแบบต้องลุ้นวัดดวงอยู่ตลอดเวลา

ทำความรู้จัก Replica Sets

Replica sets เป็น strategy ที่ทาง MongoDB แนะนำให้เราต้องทำเสมอ เราจะเริ่มโดยการ config replica set อย่างง่ายๆ ดูก่อน เพื่อทำความเข้าใจว่ามันทำงานอย่างไร เพราะว่าความรู้เรื่องนี้มีความจำเป็นต่อเรามากในกรณีที่เราต้องค้นหาสาเหตุเวลามีปัญหาใน production จากนั้นเราค่อยดูมาดูกันต่อเรื่องการตั้งค่าต่างๆ, failover, recovery และ best deployment practices กันอีกที

ในแต่ละ replica set จะประกอบไปด้วย primary node และ secondary node และอาจมี arbiter node อยู่ใน set เดียวกัน

Primary node เป็น node เดียวใน replica set ที่สามารถรับ write operation ได้เท่านั้น และหากไม่สามารถเข้าถึง primary node ได้ ระบบจะมีการเลือก primary node ขึ้นมาใหม่ โดยการโหวตจาก node อื่นๆ ที่ยังมีชีวิตอยู่ใน replica set เดียวกันโดยอัตโนมัติ แต่ถ้าการโหวตไม่สำเร็จจะทำให้ไม่มี primary node อยู่ใน replica set นั่นก็หมายความว่าา replica set นั้นๆ จะไม่สามารถ accept write ได้เลย ทำให้อยู่ในโหมดอ่านข้อมูล (read-only) ได้อย่างเดียวเท่านั้น

Secondary node เป็นเครื่องที่จะ replicate data มาเก็บไว้หลังจากที่ข้อมูลถูกเขียนสำเร็จแล้วใน primary node

Arbiter node จะเป็น lightweight mongod server node ที่มีไว้สำหรับโหวตเลือก primary node เท่านั้น และจะไม่มีการ replicate data ใดๆ มาเก็บไว้ที่นี่

สร้าง Replica Sets

Replica set หนึ่งๆ ควรมีอย่างน้อย 3 nodes เพราะว่าใน replica set ที่มี 2 node นั้นจะไม่สามารถทำให้เกิด majority (เสียงส่วนใหญ่) ในกรณีที่ primary server down ได้ แต่ถ้าเรามี 3 เครื่อง เราสามารถให้ทั้ง 3 เครื่องนั้นเก็บข้อมูล หรือเก็บข้อมูลลงไปแค่ 2 เครื่องก็ได้ ส่วนอีกเครื่องที่เหลือก็กำหนดให้เป็น arbiter ไป แต่ไม่ว่าอย่างไร ควรมีอย่างน้อย 3 node ดังที่กล่าวไป

MongoDB Data Replication with Arbiter
Simple MongoDB Replica Set with Arbiter

มาเริ่มสร้าง simple tree-node replica set ดังรูปด้านบน เพื่อศึกษาการทำงานของมันดู ปกติแล้วเราจะสร้างแต่ละ node บนแต่ละเครื่องแยกกัน แต่ในกรณีนี้เราจะลองสร้างไว้ในเครื่องเดียวกันดูก่อน โดยการสร้าง folder แยกกันดังนี้

mkdir ~/node1
mkdir ~/node2
mkdir ~/arbiter

จากนั้นรัน mongod แยกกัน 3 ตัว ใช้ --replSet เพื่อบอกว่าแต่ละตัวอยู่ใน replica set ชื่อ myapp

mongod --replSet myapp --dbpath ~/node1 --port 40000
mongod --replSet myapp --dbpath ~/node2 --port 40001
mongod --replSet myapp --dbpath ~/arbiter --port 40002

ต่อไปเราต้องเข้าไป config ว่าจะให้เครื่องไหนเป็น primary, secondary, arbiter โดยใช้ mongod เข้าไปที่ node1 ก่อนแล้วรันคำสั่ง rs.initiate()

> rs.initiate()
{
  "info2" : "no configuration specified. Using a default configuration for the set",
  "me" : "localhost:40000",
  "ok" : 1,
  "operationTime" : Timestamp(1563012969, 1),
  "$clusterTime" : {
          "clusterTime" : Timestamp(1563012969, 1),
          "signature" : {
                  "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                  "keyId" : NumberLong(0)
          }
  }
}

ในขั้นตอนการ init นี้หากใครทำแล้วไม่สำเร็จให้เข้าไปลบ bind_ip = 127.0.0.1 ใน /etc/mongod.conf หรือ /usr/local/etc/mongod.conf ก่อนแล้วลอง init ใหม่

ตอนนี้เรามี 1 member ใน replica set แล้วต่อไปก็คือการ add node อื่นๆ เข้ามาใน replica set โดยใช้ rs.add() และเพิ่ม Arbiter เข้ามาด้วย rs.addArb()

myapp:PRIMARY> rs.add('localhost:40001')
{
  "ok" : 1,
  "operationTime" : Timestamp(1563013513, 1),
  "$clusterTime" : {
          "clusterTime" : Timestamp(1563013513, 1),
          "signature" : {
                  "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                  "keyId" : NumberLong(0)
          }
  }
}
myapp:PRIMARY> rs.addArb('localhost:40002')
{
  "ok" : 1,
  "operationTime" : Timestamp(1563013604, 1),
  "$clusterTime" : {
          "clusterTime" : Timestamp(1563013604, 1),
          "signature" : {
                  "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                  "keyId" : NumberLong(0)
          }
  }
}

เราสามารถดูสรุป replica set ที่เราพึ่ง config ไปได้ด้วยคำสั่ง rs.isMaster() แล้วเราจะพบข้อมูลน่าสนใจคร่าวๆ เช่น hostsarbiterssetNameismasterprimaryme

myapp:PRIMARY> db.isMaster()
{
  "hosts" : [
          "localhost:40000",
          "localhost:40001"
  ],
  "arbiters" : [
          "localhost:40002"
  ],
  "setName" : "myapp",
  "setVersion" : 3,
  "ismaster" : true,
  "secondary" : false,
  "primary" : "localhost:40000",
  "me" : "localhost:40000",
  "electionId" : ObjectId("7fffffff0000000000000001"),
  "lastWrite" : {
          "opTime" : {
                  "ts" : Timestamp(1563013692, 1),
                  "t" : NumberLong(1)
          },
          "lastWriteDate" : ISODate("2019-07-13T10:28:12Z"),
          "majorityOpTime" : {
                  "ts" : Timestamp(1563013692, 1),
                  "t" : NumberLong(1)
          },
          "majorityWriteDate" : ISODate("2019-07-13T10:28:12Z")
  },
  "maxBsonObjectSize" : 16777216,
  "maxMessageSizeBytes" : 48000000,
  "maxWriteBatchSize" : 100000,
  "localTime" : ISODate("2019-07-13T10:28:13.560Z"),
  "logicalSessionTimeoutMinutes" : 30,
  "minWireVersion" : 0,
  "maxWireVersion" : 7,
  "readOnly" : false,
  "ok" : 1,
  "operationTime" : Timestamp(1563013692, 1),
  "$clusterTime" : {
          "clusterTime" : Timestamp(1563013692, 1),
          "signature" : {
                  "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                  "keyId" : NumberLong(0)
          }
  }
}

และหากต้องการดูรายละเอียดแยกเป็นราย node ใน replica set ให้ใช้คำสั่ง rs.status()

หลังจากสร้าง replica set เล็กๆ ขึ้นมาเสร็จแล้ว ต่อไปให้ลอง insert document ลงไปใน primary node เพื่อดูว่า document ที่เราพึ่ง insert ลงไปนั้นถูก replicate ไปยัง secondary node หรือไม่

myapp:PRIMARY> use bookstore
switched to db bookstore
myapp:PRIMARY> db.books.insert({ title: 'Oliver Twist'})

จากนั้นไปที่ secondary node mongo --port 40001 แล้วลอง show dbs ดูว่ามี bookstore เพิ่มเข้ามาหรือไม่

เราจะพบ error ไม่สามารถดูข้อมูลได้ โดยบอกเหตุผลว่า "errmsg" : "not master and slaveOk=false" ให้เราใช้คำสั่ง rs.slaveOk() ก่อนแล้วลองดูใหม่จะพบว่าข้อมูลถูก replicate มาจาก primary node แล้ว

สาเหตุที่เราต้องใช้คำสั่ง rs.slaveOk() ก็เนื่องจาก MongoDB พยายามปกป้องเราจากการ query ที่ secondary โดยไม่ได้ตั้งใจ เนื่องจากว่าจริงๆ แล้วการเขียนนั้นเกิดขึ้นที่ primary เท่านั้น ทำให้ผลลัพธ์ของการ query บน secondary นั้นอาจไม่ตรงกันกับบน primary เสมอไป เนื่องจากอาจมี delay ระหว่างการ replicate data เกิดขึ้น

ต่อไปให้ลอง shutdown primary node เพื่อดูการเปลี่ยนแปลง แล้วรอสักพักจน replica set รับรู้ว่า primary node ไม่สามารถ ping หาได้อีกต่อไปแล้ว secondary node จะถูกโหวตขึ้นมาทำหน้าที่ primarty node แทนทันที

หากต้องการ kill mongod process เราสามารถหา process ID ได้จากไฟล์ mongod.lock ที่อยู่ใน ~/node1 ได้ จากนั้นสั่ง kill -3 <process id>

ให้ลองเข้าไปใน node2 แล้วรัน rs.isMaster() จะพบว่าตอนนี้ node2 เป็น primary แล้ว จากนั้นให้ลอง start node1 กลับขึ้นมาใหม่ เราจะพบว่า node1 ได้กลับเข้ามา join replica set เองโดยอัตโนมัติในฐานะ secondary node

Data Replication ทำงานอย่างไร?

หลังจากที่เราได้ทดลองทำไปในเนื้อหาก่อนหน้านี้แล้ว ต่อไปเราจะมาดูกันว่าจริงๆ แล้วมันมี process แบบไหนในการทำแบบนั้นทั้งในเรื่องของการ replicate data และ automatic failover

Replica sets นั้นทำงานโดยอิงจาก 2 สิ่ง ได้แก่ oplog และ heartbeat โดยที่ oplog นั้นทำให้ data replication ได้ ส่วน heartbeat นั้นคอยสอดส่องสุขภาพของ node และช่วย trigger failover ให้เรา

oplog

oplog เป็น capped collection ที่อยู่ใน local database ของทุกๆ replication node และบันทึกทุกการเปลี่ยนแปลงของข้อมูลที่เกิดขึ้น ถ้าหาก client เขียนข้อมูลลงไปที่ primary node แล้ว oplog ของ primary node ก็จะบันทึกการเขียนครั้งนี้ไว้ และเมื่อข้อมูลถูก replicate ไปยัง secondary node แล้ว oplog ของ secondary node ก็จะบันทึกการเขียนครั้งนี้ไว้เช่นกัน โดยแต่ละ oplog นั้น identified ด้วย BSON timestamp และ secondary node ทุกๆ ตัวจะใช้ timestamp นี้แหละคอยดูว่าข้อมูลตรงไหนเปลี่ยนแปลงไปแล้วบ้างเพื่อจะได้หาจุดที่เริ่ม sync ข้อมูลระหว่าง node ได้ถูกต้อง

Capped collections are fixed-size collections that support high-throughput operations that insert and retrieve documents based on insertion order. Capped collections work in a way similar to circular buffers: once a collection fills its allocated space, it makes room for new documents by overwriting the oldest documents in the collection.

เราสามารถเข้าไปดู oplog ได้ด้วยการลอง connect ไปที่ primary node แล้วเข้าไปดูใน database ชื่อ local และเปิด collection ชื่อ oplog.rs

database ชื่อ local จะเก็บข้อมูลรายละเอียดต่างๆ ของ database รวมถึง oplog ด้วย โดยปกติแล้ว local จะไม่ replicate ตัวเองไปยัง node อื่นๆ

ลอง query oplog.rs ดู จะพบข้อมูลเกี่ยวกับการเขียน document ที่ยกตัวอย่างไปแล้วตอนแรก

myapp:PRIMARY> db.oplog.rs.findOne({ op: 'i' })
{
  "ts" : Timestamp(1563014215, 2),
  "t" : NumberLong(1),
  "h" : NumberLong("5776078762678146549"),
  "v" : 2,
  "op" : "i",
  "ns" : "bookstore.books",
  "ui" : UUID("19ecbe3c-5e86-4825-8e54-762b6b9d807e"),
  "wall" : ISODate("2019-07-13T10:36:55.682Z"),
  "o" : {
          "_id" : ObjectId("5d29b4471055aebca9741095"),
          "title" : "Oliver Twist"
  }
}

ts คือ BSON timestamp, op คือ i = insert, ns คือ namespace และ o คือ object ที่ถูกเขียนลงไป

แต่ละ document จะมี oplog เป็นของตัวเอง แม้จะเป็นการ update หลายๆ object พร้อมๆ กันภายในคำสั่งเดียวกันก็ตาม

หากต้องการ query หาจาก timestamp เราจำเป็นต้องสร้างเป็น Timestamp Object ขึ้นมาเอง ดังนี้

db.oplog.rs.findOne({ ts: Timestamp(1382739381, 1) })

ถ้าหากเราต้องการดูสถานะของ oplog แบบเร็วๆ สามารถใช้คำสั่ง db.getReplicationInfo() ได้

myapp:PRIMARY> db.getReplicationInfo()
{
  "logSizeMB" : 192,
  "usedMB" : 0.05,
  "timeDiff" : 4525,
  "timeDiffHours" : 1.26,
  "tFirst" : "Sat Jul 13 2019 17:25:13 GMT+0700",
  "tLast" : "Sat Jul 13 2019 18:40:38 GMT+0700",
  "now" : "Sat Jul 13 2019 18:40:40 GMT+0700"
}

เราจะเห็น timestamp ของ document ตัวแรกสุด และตัวสุดท้ายใน oplog ซึ่งเราสามารถ query หาเองได้ง่ายๆ ด้วยวิธีนี้

myapp:PRIMARY> db.oplog.rs.find({ op: 'i'}).sort({$natural: 1}).limit(1).pretty()

เมื่อมีการเขียนข้อมูลลงใน primary node ของ replica set แล้ว การเขียนนั้นจะถูกบันทึกไว้ใน oplog ของ primary node ซึ่ง secondary แต่ละ node ก็จะมี oplog เป็นของตัวเองเช่นกัน เมื่อถึงเวลาหนึ่งที่ secondary node ต้องอัพเดทตัวเองแล้วจะทำ 3 อย่างตามลำดับต่อไปนี้

  1. ดูที่ timestamp ของ entry สุดท้ายใน oplog
  2. query จาก oplog ของ primary โดยหา entry ที่ oplog timestamp มากกว่าของตัวเอง
  3. เขียน data และบันทึก oplog

จาก 3 ขั้นตอนง่ายๆ ดังกล่าว ทำให้เกิดเหตุการณ์บางอย่างที่เราอาจเจอได้ ดังต่อไปนี้

Replication หยุดทำงาน

Replication จะหยุดทำงาน (halt) ถาวร กรณีที่ secondary ไม่เจอจุดที่จะ sync ข้อมูลได้ เมื่อเหตุการณ์นี้เกิดขึ้น เราจะพบข้อความประมาณนี้ใน log

repl: replication data to stale, halting

ที่เคยบอกไปแล้วว่า oplog นั้นเป็น capped collection ซึ่งนั่นหมายความว่า ถ้ามี operation เกิดขึ้นเป็นจำนวนมาก แล้ว secondary offine ไปนานมาก รวมถึง oplog นั้นไม่ได้ใหญ่พอที่จะเก็บการเปลี่ยนแปลงทุกอย่างไว้ได้ครบจนมันวนลูปมาทับ last operation ที่ secondary เคยใช้งานครั้งล่าสุด จนทำให้ secondary node ไม่สามารถหาจุดถัดไปที่จะเริ่ม replicate data ได้ และหากยังดึงดันจะทำต่อไปก็ไม่รับประกันว่าข้อมูลที่มาทั้งหมดนั้นเหมือนกับ primary node หรือไม่ ดังนั้น replication จึงต้องหยุดทำงาน

เมื่อเกิดเหตุนี้ขึ้นวิธีแก้ไขมีทางเดียวคือการ resync กับ primary node ใหม่ทั้งหมด ส่วนวิธีป้องกันไม่ให้เกิดเหตุการณ์นี้ก็คือการ monitor secondary delay และตั้งค่าให้ oplog นั้นมีขนาดใหญ่เพียงพอต่อการใช้งานใน application ของเรา

การกำหนดขนาดของ oplog

oplog เป็น capped collection ที่เราสามารถสร้างขึ้นมาแล้วเปลี่ยนขนาดได้ในภายหลังด้วยการ stop mongod จากนั้น start แบบ standalone ขึ้นมาแก้ไข oplog size แล้ว restart กลับเข้าไป join replica set ใหม่

default oplog size ของแต่ละระบบที่รัน mongod นั้นแตกต่างกันไป เช่น 32-bit system จะมีขนาด 50MB, 64-bit system มีขนาดได้มากกว่า หรือ 5% ของ free disk space อย่างไรก็ตาม รายละเอียดให้ตรวจสอบอีกทีในเอกสารอย่างเป็นทางการของ MongoDB

ที่ยกเรื่อง default oplog size ขึ้นมาเพราะจะอธิบายว่า default size ที่ถูกกำหนดมาให้แต่แรกนั้นอาจไม่ได้เหมาะสมกับ application ที่เรากำลังใช้งานอยู่ หากเรารู้ว่า application ของเรานั้นมีการเขียนที่เยอะมากๆ เราอาจต้องลองทดสอบกันก่อนที่จะ deploy ระบบโดยการลองเขียนดูสักพัก แล้วลองใช้คำสั่ง db.getReplicationInfo() เพื่อให้เราพบปริมาณคร่าวๆ ที่ oplog ต้องการใช้งานได้

ถ้าเราต้องการกำหนด default oplog size เองตั้งแต่ตอนแรกเราสามารถใช้ option --oplogSize ได้ โดยค่าที่กำหนดมีหน่วยเป็น MB ยกตัวอย่างเช่นเราต้องการกำหนด oplog size เป็น 1GB สามารทำได้ดังนี้

mongod --replSet myapp --oplogSize 1024

Heartbeat and failover

ปกติแล้วแต่ละ node ใน replica set จะ ping หา node อื่นๆ ทุก 2 วินาที ซึ่งถ้าเราใช้คำสั่ง rs.status() เราจะเห็นความถี่ในการ ping จาก attribute "heartbeatIntervalMillis" : NumberLong(2000) และ "myState" : 1 ที่ 1 แปลว่าปกติและ 0 แปลว่าไม่ปกติ ซึ่งตราบใดที่ทุก node มีสถานะเป็น 1 ก็แปลว่าทุกอย่างปกติดี ping หากันเจอหมด

MongoDB Data Replication with Heartbeat and automatic failover
Heartbeat and failover

ทุกๆ replica set ต้องการเพียงแค่ความมั่นใจว่า ณ เวลาหนึ่งๆ จะมี primary node อยู่เสมอ ซึ่งจะเกิดขึ้นได้ก็ต่อเมื่อใน replica set นั้นมี majority of nodes ยกตัวอย่างจาก replica set ที่ได้สร้างมาตอนต้น (1 primary, 1 secondary, 1 arbiter) ถ้าเราทำลาย secondary node, majority ก็ยังคงอยู่ ดังนั้น replica set ก็ไม่มีการเปลี่ยนแปลงอะไร ทำได้แค่รอให้ secondary node กลับมาเท่านั้น แต่หากเราทำลาย primary node, majority ก็ยังคงอยู่ (เพราะเหลือ 2 node จาก 3 node) แต่ไม่มี primary node แล้ว secondary ก็จะถูกเลือกให้มาทำหน้าที่เป็น primary node แทน

แต่ถ้าทั้ง secondary และ arbiter ถูกทำลายลงไป แม้จะยังมี primary node อยู่ แต่ไม่มี majority แล้ว (เพราะเหลือแค่ 1 node จาก 3 node) กรณีนี้เราจะเห็นข้อความประมาณนี้ใน log ของ primary node

[rsMgr] can't see a majority of the set, relinquishing primary
[rsMgr] replSet relinguishing primary state
[rsMgr] replSet SECONDARY
[rsMgr] replSet closing client sockets after relinquishing primary

มีความหมายว่า primary node ได้เปลี่ยนให้ตัวเองกลายเป็น secondary node ที่ทำแบบนี้ก็เพราะว่า primary node นั้นไม่แน่ใจแล้วว่าตัวเองยังสมควรเป็น primary node จริงๆ อยู่หรือไม่ เพราะถ้าเกิด network มีปัญหาจนทำให้ ping หากันไม่เจอ ทั้งๆ ที่ node ที่ ping หากันไม่เจอนั้นที่จริงแล้วยังออนไลน์อยู่ เช่น primary node ถูกตัดขาดจาก secondary, arbiter และ secondary กับ arbiter ยัง ping หากันเจออยู่ จะทำให้ primary เดิมยังคงเป็น primary และ secondary จะถูกเลือกให้กลายเป็น primary ซึ่งนั่นทำให้เรามี 2 primary node ใน replica set เดียวกัน!

ถ้า client ยังคงใช้งาน replica set อยู่จะเกิดอะไรขึ้น? อ่านข้อมูลจาก primary ที่ต่างกัน, เขียนข้อมูลลง primary ที่ต่างกัน ซึ่งนั่นทำให้ข้อมูลเละเทะแน่นอน นี่คือเหตุผลที่ว่าเมื่อใดก็ตามที่ primary node ไม่พบ majority ใน replica set แล้วจะต้อง step down ตัวเองให้กลายเป็น secondary ไปก่อนเพื่อความปลอดภัย

Commit and rollback

เมื่อมีการเขียนไปที่ primary node แล้ว MongoDB จะยังไม่นับว่า commit operation ใดๆ จนกว่าจะถูก replicate ไปยัง majority ของ replica set

ก่อนอื่นต้องเข้าใจก่อนว่า operation ใดๆ ที่กระทำกับ single document นั้นจะ atomic เสมอ แต่สำหรับ operation ที่ทำกับหลายๆ document นั้นไม่ได้รับประกันว่าทุกๆ document จะ atomic

สมมติว่าเราสั่งเขียนหลาย document ลงไปที่ primary แล้วยังไม่ทันจะได้ replicate ไปยัง secondary ด้วยเหตุบางอย่าง เช่น network มีปัญหา, server down หรือ delay อยู่ แล้วทันใดนั้น secondary node ก็ถูก promote ให้เป็น primary ขึ้นมาแทนตัวที่มีปัญหา แล้วหลังจากนั้น primary ตัวเก่าก็ฟื้นขึ้นมาได้อีกครั้งแล้วก็พยายาม replicate จาก primary ตัวใหม่ แต่ก็ไม่สามารถทำได้แล้ว เพราะว่า primary ตัวเก่ามีข้อมูลที่ไม่มีอยู่ใน oplog ของ primary ตัวใหม่ สถานการณ์แบบนี้จะทำให้เกิดการ rollback

การ rollback ที่เกิดขึ้นก็คือการนำข้อมูลการเขียนที่ไม่ถูก replicate ไปยัง majority นั้นออกไป ซึ่งนั่นหมายความว่าข้อมูลจะถูกนำออกจาก oplog ของ secondary node และเอาออกจาก collection ไปจริงๆ ด้วย แต่ในทางตรงกันข้าม กรณีที่ข้อมูลถูกลบออกไปจาก secondary ก็จะนำข้อมูลกลับมาใหม่ ซึ่งไม่ใช่แค่การเขียนและการลบ document เท่านั้น แต่ยังรวมถึงการ drop collection และ update document อีกด้วย

การ rollback write ก็คือการเอาอะไรที่เคยเขียนไปแล้วออกไป ซึ่งสิ่งที่ถูกเอาออกไปนี้จะถูกเก็บไว้ใน rollback directory เป็นไฟล์ BSON เราสามารถเปิดไฟล์พวกนี้อ่านได้จาก bsondump และ restore กลับเข้าไปเองด้วย mongorestore ได้ในภายหลัง

ถ้าหากว่า application ของเรานั้นไม่จำเป็นต้องใช้งานข้อมูลแบบ realtime ในลักษณะที่เขียนแล้วต้องใช้ข้อมูลนั้นๆ เลยทันทีเราสามารถใช้ write concerns เข้ามารับประกันให้เราได้ว่าสิ่งที่เราทำไปนั้นถูก replicate ไปยัง majority แล้วแน่นอน

Administration

Replication Configuration

ก่อนหน้านี้เราได้ทำความรู้จักกับการสร้าง init replica set ด้วยคำสั่ง rs.initiate() และการเพิ่ม node เข้ามาใน replica set ด้วยคำสั่ง rs.add() ไปแล้ว ซึ่งทั้ง 2 คำสั่งดังกล่าวจะทำงานโดยใช้ default configuration ที่ MongoDB เตรียมไว้ให้ ต่อไปนี้เรามาดูกันว่าเราสามารถเขียน configuration เองได้อย่างไรบ้าง

โครงสร้างหลักประกอบด้วย 2 attribute หลัก ได้แก่ _id และ members โดย _id เราจะกำหนดเป็นชื่อเดียวกันกับ --replSet เช่น

> config = { _id: 'myapp', members: [] }

และเราสามารถเพิ่ม member เข้าไปทีละตัวได้แบบนี้ ซึ่งมันก็คือการ push object ลงไปใน array ของ javascript ปกติ

> config.members.push({ _id: 0, host: 'localhost:40000' })
> config.members.push({ _id: 1, host: 'localhost:40001' })
> config.members.push({ _id: 2, host: 'localhost:40002', arbiterOnly: true })

และเราสามารถเพิ่ม config นี้ไปตอนที่เราเรียก rs.initiate() ได้เลย

ในทางเทคนิคแล้วมันคือ document ที่มี _id ที่เก็บชื่อของ replica set และมีสมาชิกจำนวนตั้งแต่ 3 node ถึง มากที่สุดได้ 50 node แต่ถึงแม้จะมีได้มากถึง 50 node ก็ตาม เราสามารถมี node สำหรับโหวต primary node ได้มากที่สุดที่ 7 node

เราสามารถเปิดดู configuration ที่กำลังใช้อยู่ได้จากคำสั่ง rs.conf()

หากเราเขียน config ขึ้นมาใหม่ และอยากนำไปแก้ไข config ตัวเก่าเราจะใช้คำสั่ง rs.reconfig() และทุกครั้งที่การตั้งค่า config ใหม่นั้นมีผลกระทบให้ replica set ต้องเลือก primary node ใหม่ MongoDB จะตัดการเชื่อมต่อกับ client ให้เราโดยอัตโนมัติเพื่อรับประกันว่า client จะไม่พยายามเขียนข้อมูลใดๆ ลงใน secondary node

Member options

มาดูกันว่า options ที่น่าสนใจต่างๆ ที่เรานำมาตั้งค่าให้กับแต่ละ member ใน replica set ว่ามีตัวไหนบ้าง

_id (required) ถือว่าเป็นหมายเลขประจำ node ใน replica set โดยจะมีค่าเริ่มต้นที่ 0 และเพิ่มไปทีละ 1 เรื่อยๆ ทุกครั้งที่เพิ่ม node ใหม่ๆ เข้ามา

host (required) คือ hostname และสามารถเขียนรวมถึง port ได้ด้วย หากไม่ได้กำหนด port จะใช้ default port ที่ 27017

arbiterOnly มีค่าเป็น true หรือ false หาก node ไหนเป็น Arbiter จะเก็บแค่ configuration ไว้เท่านั้น ไม่มีการ replicate data มาที่ node นี้ และมีไว้สำหรับโหวตเลือก primary node เท่านั้น

priority เป็นตัวเลขมีค่าได้ตั้งแต่ 0 ถึง 1000 โดยที่ตัวเลขนี้มีไว้ใช้ตอนที่เกิดการโหวตเลือก primary node โดยที่ node ไหนมีค่านี้มากที่สุดก็จะถูกเลือกเป็น primary ก่อน เราจะใช้เมื่อเรามีเครื่องที่มีประสิทธิภาพสูงกว่าและอยากให้เป็น primary ก่อนเมื่อมีการโหวต เราก็จะกำหนดค่านี้ให้สูงกว่าเครื่องที่มีประสิทธิภาพต่ำกว่า แต่หากเครื่องไหนที่เราไม่ต้องการให้ถูกเลือกเป็น primary node เลยเนื่องจากเราจะเก็บไว้ใช้เป็นเครื่อง DR (Disaster Recovery) เราจะกำหนดค่านี้เป็น 0

votes แต่ละ node ใน replica set จะมีสิทธิ์ออกเสียงได้ 1 เสียงโดย default ซึ่งการกำหนดค่าให้ option นี้จะทำให้ node นั้นๆ มีสิทธิ์ในการออกเสียงโหวตได้เท่ากับค่าที่เรากำหนดให้ แต่ค่านี้ไม่แนะนำให้เปลี่ยนเป็นค่าอื่นนอกจาก 1 เพราะอาจทำให้ majority นั้นผิดปกติทำให้เกิดเหตุการณ์บางอย่างที่ชวนปวดหัวได้ในภายหลัง

hidden เมื่อต้องการให้ node ใดๆ ซ่อนตัวจากคำสั่ง rs.isMaster() ให้กำหนดค่านี้เป็น true เหตุผลในการซ่อนก็คือ MongoDB driver นั้นอิงการรับรู้ถึง node ต่างๆ ใน replica set เพื่อเชื่อมต่อเข้ามาใช้งาน ดังนั้นหากเราไม่ต้องการให้ driver เข้าถึง node ใดๆ ก็สามารถซ่อนไว้ได้ด้วยวิธีนี้

buildIndexes default เป็น true เราจะใช้เมื่อไม่ต้องการให้ทำ index ที่ node นั้นๆ ซึ่งควรกำหนดให้กับ node ที่ไม่มีวันถูกเลือกขึ้นมาเป็น primary node (priority = 0) โดย option นี้มักถูกใช้สำหรับเครื่องที่เอาไว้ใช้ backup เท่านั้น

slaveDelay จำนวนวินาทีที่ข้อมูลใน secondary node นั้นตามหลัง primary node โดยที่ option นี้ควรถูกใช้กับเครื่องที่ไม่มีวันถูกโหวตเป็น primary node เท่านั้น ซึ่งแปลว่าเครื่องที่ถูกกำหนดให้ option นี้มีค่ามากกว่า 0 ก็แปลว่า priority ก็ต้องเท่ากับ 0 ด้วยเช่นกัน โดยเรามักใช้ option นี้เพื่อป้องกัน user error เช่น ถ้าเรากำหนดไว้ว่าให้ delay 30 นาที แล้ว admin เผลอ drop database ทิ้งไป ก็หมายความว่า admin มีเวลา 30 นาทีในการกู้ database คืนมาก่อนที่การ drop นั้นจะถูก replicate ไปยัง node อื่นๆ

tags มีค่าเป็น document ที่ข้างในบรรจุเป็นค่าต่างๆ ได้แก่ ตำแหน่งใน datacenter, rack เป็นต้น (แล้วแต่เราจะตั้งค่าหรือตั้งชื่อ)เช่น

{
  '_id': 0,
  'host': 'localhost:40000',
  'tags': {
    'datacenter': 'NY',
    'rack': 'B'
  }
}

ที่กล่าวมาก่อนหน้านี้คือ option ที่นำมาใช้กับแต่ละ node แยกกันแล้วแต่เราจะตัดสินใจว่า option ไหนจะถูกกำหนดให้ node ไหนอย่างไร แต่ยังมี option อีก 2 ตัวที่นำมาใช้กับทุกๆ node ใน replica set โดย config นี้จะอยู่ใน attribute settings ดังนี้

getLastErrorDefaults ถูกใช้เมื่อ client เรียกดู getLastError โดยไม่ใส่ argument ใดๆ มา ค่านี้จะถูกส่งกลับไป และเมื่อเราเปิด rs.conf() ดูจะพบเนื้อหาดังนี้

"getLastErrorDefaults" : {
  "w" : 2,
  "wtimeout" : 500
},

มีความหมายว่าทุกการเขียนถูก replicate ไปอย่างน้อย 2 node โดยมี timeout 500 ms ไม่อย่างนั้นแล้ว error จะถูกส่งกลับไปยัง driver

getLastErrorModes เป็นการกำหนด mode ให้กับคำสั่ง getLastError โดยฟีเจอร์นี้จะขึ้นอยู่กับ tag ที่เรากำหนดไว้อีกที ซึ่งเราจะกล่าวถึงในภายหลังอีกที

Replica set status

เราได้เห็น status ของแต่ละ node จากคำสั่ง rs.status() กันมาบ้างแล้ว ซึ่งก็คือ 1 สำหรับ primary, 2 สำหรับ secondary และ 7 สำหรับ Arbiter ซึ่งจริงๆ แล้วมีทั้งหมด 10 สถานะที่ควรรู้ได้แก่

StateState StringNotes
0STARTUPอยู่ในระหว่างการ ping หา node อื่นๆ และ share config กัน
1PRIMARYprimary node
2SECONDARYsecondary node ที่รอโอกาสขึ้นมาเป็น primary node แต่ต้องมี priority มากกว่า 0 และต้องไม่ถูกกำหนดให้ hidden
3RECOVERYเรามักเห็นสถานะนี้หลังจาก failover หรือหลังจากเพิ่ม node เข้าไปใน replica set ใหม่ๆ ซึ่งโหมดนี้จะทำให้เรายังอ่าน และเขียนไม่ได้
4FATALถ้า network นั้นปกติแต่ node นี้ไม่ตอบสองต่อการ ping หา ซึ่งมีโอกาสเป็นไปได้ว่าผิดปกติ
5STARTUP2เริ่มการ sync data กับ node อื่นๆ แล้ว
6UNKNOWNยังไม่ได้เชื่อมต่อ network
7ARBITERเป็น arbiter
8DOWNเคยเข้าถึง node นี้ได้ แล้วอยู่ดีๆ ก็ไม่ตอบสองต่อ heartbeat ping
9ROLLBACKกำลัง rollback อยู่
10REMOVEDเคยเป็นสมาชิกใน replica set แต่ตอนนี้ถูกเอาออกไปแล้ว

เราสามารถดูสถานะคร่าวๆ ว่า node ไหนใช้งานได้จากสถานะที่ 1, 2 และ 7

Failover and recovery

สมมติว่าเรามี replica set ที่มี 3 node แบบไม่มี node ใดเป็น arbiter แล้วอยู่ดีๆ primary node ก็พังไป แต่ secondary ทั้ง 2 ที่เหลือยังมองเห็นกันได้อยู่ กระบวนการ automatic failover ก็จะเริ่มทำงาน โดยที่ node ทั้ง 2 ที่มองเห็นกันนั้นจะโหวตเลือก primary กันโดยตัดสินใจจากว่าใครอัพเดท oplog ล่าสุดกว่ากัน และใครมี priority มากกว่ากัน node นั้นก็จะถูกเลือกเป็น primary node ต่อไป

Recovery คือกระบวนการในการ restore replica set ไปยังสถานะเดิมก่อนที่จะมีปัญหา การ failure นั้นโดยทั่วไปแบ่งได้เป็น 2 แบบ ได้แก่

Clean failure – ปกติถ้า node ไหนไม่สามารถติดต่อกับ node อื่นๆ ใน replica set ได้ เราก็แค่รอให้สามารถเชื่อมต่อกันได้อีกครั้งก็ได้แล้ว และถึงแม้ว่าถ้า mongod ถึงกับถูก terminate ไปไม่ว่าด้วยเหตุผลใดก็ตาม เมื่อเรา start ขึ้นมาใหม่ ก็ยังสามารถ join replica set เดิมและยัง sync ได้เป็นปกติ แบบนี้เราไม่ต้องทำอะไร

Categorical failure – เมื่อ data file เสียหาย หรือสูญหาย หรือ hard drive เสียหาย มีทางเดียวที่เราจะกู้ข้อมูลกลับมาได้ก็คือการ resync ข้อมูลท้งหมดใหม่ หรือ restore จาก backup เท่านั้น

การ resync นั้นทำได้โดยการรัน mongod เปล่าๆ ขึ้นมาแทน node ที่เสียหายไป แล้วเพิ่มเข้าไปใน replica set ใหม่จากนั้นก็รอให้มัน resync กันจนเสร็จ แต่ในกรณีที่เรามีการเปลี่ยน host และ port ใหม่เราจำเป็นต้อง reconfig ใหม่โดยการนำ config เดิมมาแก้ไขในลักษณะนี้ได้เลย

> config = rs.conf()
> config.members[1].host = 'localhost:40000'
> rs.reconfig(config)

การ restore แบบ resync กันใหม่หมดเรามีทางเลือก 2 ทาง คือแบบที่สร้าง node ขึ้นมาใหม่แล้วรอมัน sync กันเองตามปกติดังที่กล่าวมา หรือเราจะ restore จาก backup แบบ offline ไปเลยก็ได้ ซึ่งแบบนี้สามารถทำได้รวดเร็วกว่ากรณีที่ข้อมูลมีเยอะมากๆ อีกทั้งเรายังไม่จำเป็นต้องเสียเวลา build index ใหม่ทั้งหมดอีกด้วย แต่สิ่งทีต้องระวังก็คือ oplog snapshot จะต้องยังมีอยู่จริง ไม่ถูกบันทึกทับไปแล้ว และอ้างถึงได้จริงใน live oplog

Deployment strategies

โดยทั่วไป minimal replica set configuration ที่ควรทำก็คือต้องมีอย่างน้อย 3 node แบ่งเป็น 2 replica และ 1 arbiter ซึ่งใน production เราสามารถรัน arbiter ไว้ใน application server ได้เลยในขณะที่ node อื่นๆ ก็รันบนเครื่องชองใครของมันแยกต่างหาก ซึ่ง configuration นี้ถือว่าเพียงพอแล้วกับ application ทั่วๆ ไป

แต่ใน application ที่ uptime นั้น critical มากขึ้นเราอาจต้องใช้ replica node ทั้ง 3 node เลย (3 complete replica) แทนที่จะให้มี 1 arbiter เหมือนที่ยกตัวอย่างมาตอนแรก ที่ทำแบบนี้ก็เพื่อต่อให้ 1 node fail ไปแล้ว และเรากำลัง recovery node ที่เหลือ อย่างน้อยก็ยังเหลือ 2 node ที่ยังเก็บข้อมูลให้เราได้อยู่

อีกกรณีก็คือ บาง application อาจต้องการในเรื่องของ redundancy เราอาจออกแบบให้ทั้ง 3 node แยกกันอยู่คนละ data center ก็ได้ เช่น primary, secondary อยู่ที่ data center A และ secondary อีกตัวอยู่ data center B โดยที่ node ที่อยู่ที่ data center B เราต้องการใช้มันแค่เก็บข้อมูลให้เราเพียงอย่างเดียว เป็น DR server ซึ่งแน่นอนว่าเราต้องกำหนดให้ priority ของ node นี้มีค่าเป็น 0 (ไม่มีวันถูกเลือกให้เป็น primary)

จาก configuration ดังกล่าว primary node สามารถเป็นได้ทั้ง 2 node ใน data center A เราสามารถเสีย node ใดก็ได้ใน data center A แล้วระบบก็ยังทำงานได้ปกติ แต่ถ้าหากว่าเสียไปทั้ง 2 node ใน data center A ระบบจะหยุดทำงานทันที เราสามารถ recover ได้แบบเร็วๆ โดยการ shutdown node ใน data center B แล้ว start ขึ้นมาใหม่โดยไม่ใช้ flag --replSet หรืออีกทางหนึ่งเราสามารถสร้าง 2 node เพิ่มเข้ามาใน data center B แล้ว reconfig replica set ใหม่ แต่เราจะยังทำไม่ได้เนื่องจาก MongoDB ไม่อนุญาตให้ทำเพราะ replica set นี้ไม่มี majority เราจึงต้องบังคับ reconfig ในกรณีฉุกเฉินได้แบบนี้

> rs.reconfig(config, { force: true })

Driver Connection

ปกติแล้วเราจะไม่ได้ติดต่อกับ MongoDB server เองตรงๆ แต่จะติดต่อผ่าน driver ต่างๆ ที่ MongoDB สร้างมาให้เราอีกที ซึ่งเราสามารถกำหนดค่าบางอย่างให้ driver ใช้ในการติดต่อกับ database ได้ทั้งการอ่าน และการเขียนข้อมูล ในแบบที่เหมาะสมกับ application ของเรา

MongoDB Data Replication driver connection
Driver Connection

Single-node connections

เราสามารถเชื่อมต่อไปยัง node เดียวใน replica set ได้เสมอ ไม่ได้แตกต่างกับการเชื่อมต่อไปยัง node ที่ไม่ได้ทำ replica set เลย เมื่อเชื่อมต่อไปแล้ว driver จะเปิด TCP socket แล้วรันคำสั่ง isMaster ดังที่เคยกล่าวถึงไปก่อนหน้า ซึ่งสิ่งที่ driver สนใจที่สุดจากผลลัพธ์ของคำสั่งที่ได้ ก็คือ attribute 'isMaster' : true เพื่อที่จะเริ่ม CRUD ไปที่ node นี้ได้ตามปกติ

แต่ถ้า connect ไปยัง secondary node เดี่ยวๆ node เดียวใน replica set นั้นเราจำเป็นต้องบอก driver ด้วยว่าเราต้องการ connect เพื่ออ่านอย่างเดียวเท่านั้น (วิธีบอกแตกต่างกันไปตาม driver ที่ใช้) หากไม่กำหนดเราจะไม่มีวันรู้เลยว่าเราไม่สามารถอ่านได้แม้จะ connect ได้ จนกว่าเราจะส่งคำสั่งไป ที่ทำแบบนี้เพราะว่าปกติเราต้อง connect ไปยัง primary node เท่านั้น ดังนั้น MongoDB จึงต้องการตรวจสอบให้แน่ใจว่าเราเชื่อมต่อไปถูก node ที่ต้องการแล้วจริงๆ

Replica set connections

จากที่กล่าวมาเราสามารถ connect ไปยัง node ใดๆ แบบเดี่ยวๆ ใน replica set ก็ได้ แต่หากเรามี replica set แล้วเราก็ย่อมต้องใช้ replica set เราให้ได้คุ้มค่าสมกับที่เราสร้างมันขึ้นมา

driver ส่วนใหญ่เปิดทางให้เราสามารถใส่ list ของ seed node และ replica set name เข้าไปเป็น parameter ตอนเชื่อมต่อได้ โดยเมื่อเกิดการเชื่อมต่อขึ้น driver จะไปเรียก isMaster เพื่อดูว่าใน replica set นี้มีสมาชิกใดอยู่บ้าง จากนั้น client ก็จะรู้แล้วว่า node ไหนคือ primary แล้วก็ทำการเชื่อมต่อกับ node นั้นเป็นหลัก หลังจากนั้นหาก primary node fail ขึ้นมา driver ก็จะวิ่งหา node อื่นๆ เพื่อค้นหา primary ตัวต่อไปใข้งานใหม่

จากที่กล่าวมาจะเห็นได้ว่าจริงๆ แล้วเราไม่จำเป็นต้อง list ทุกๆ node ใน replica set ก็ได้เพราะว่ายังไง driver ก็ใช้คำสั่ง isMaster เพื่อรับรู้การมีอยู่ของทุกๆ node ใน replica set ให้อยู่แล้วเพื่อค้นหา primary node มาใช้งานอีกที แต่อย่างไรก็ตามหาก driver ไม่สามารถเชื่อมต่อกับ seed node ใดๆ ที่ list มาให้แล้ว connection จะถูกตัดทันที ดังนั้นเราควร list ทุกๆ node เท่าที่ทำได้ไปตั้งแต่ตอนแรกเพื่อความปลอดภัย อย่างน้อยๆ ควรเป็นรายชื่อของ node ที่เป็นตัวแทนของแต่ละ data center ในกรณีที่เรามีหลาย data center

Write concern

เราสามารถเขียนข้อมูลลงใน database โดยตรวจสอบ หรือไม่ตรวจสอบ error ที่เกิดขึ้นก็ได้โดยการกำหนดค่าของ write concern เมื่อมีการส่งคำสั่งไปยัง database หากค่านี้มีค่าตั้งแต่ 1 หรือมากกว่านั้น driver จะตรวจสอบทุกครั้งว่ามีการเขียนลงไปสำเร็จหรือไม่โดยเรียกคำสั่ง getLastError แต่ถ้า write concern มีค่าเป็น 0 แล้ว driver จะเขียน TCP socket ออกไปแล้วไม่สนใจเลยว่าเขียนสำเร็จหรือไม่

write concern จะถูกกำหนดค่าเป็น 1 โดย default ให้อยู่แล้ว ซึ่งนั่นหมายความว่าทุกการเขียนนั้นรับประกันว่าส่งไปถึง node หนึ่งๆ อย่างน้อย 1 node ใน replica set แน่ๆ

การกำหนดค่า write concert เป็น 1 นั้นเพียงพอแล้วสำหรับ application โดยทั่วไป ซึ่งถ้ามีค่ามากกว่านี้นั่นหมายความว่าเราต้องการลดเหตุการณ์ที่จะต้อง rollback ลงไป เนื่องจาก MongoDB จะตอบกลับ (acknowledgement) ไปยัง driver ก็ต่อเมื่อข้อมูลถูก replicate ไปยัง node ต่างๆ ใน replica set ได้เท่ากับจำนวนที่ถูกระบุไว้ใน write concern แล้วเท่านั้น

ในทางเทคนิคแล้วเราควบคุม write concern ผ่านค่าใน attribute getLastError โดยดูได้จากคำสั่ง rs.conf()

"getLastErrorDefaults" : {
  "w" : 1,
  "wtimeout" : 0
},

ซึ่ง w คือจำนวน server ที่การเขียนครั้งล่าสุดจะต้องถูก replicate ไป และ wtimeout คือ timeout มีหน่วยเป็น milliseconds ที่ทำให้ตอบกลับไปยัง application ว่า error ถ้าไม่สามารถ replicate ไปได้ครบตามจำนวน node ที่ระบุใน write concern

Read scaling

ถ้า server หลักของเราไม่สามารถจัดการกับการอ่านครั้งละเยอะๆ ได้ดี เราสามารถกำหนดให้ driver อ่านข้อมูลจากหลายๆ node ได้โดยไม่ต้องผ่าน primary node เสมอไปก็ได้ ผ่านการตั้งค่า read preference

MongoDB Data Replication read preference
Read Preference

โดยทั่วไปเราสามารถกำหนด read preference ได้ตั้งแต่ตอน initialize driver เลย โดย MongoDB driver ทั่วไปเราสามารถกำหนด read preference ได้ดังนี้

primary – ค่านี้เป็น default setting ที่ทำให้ทุกๆ การอ่านจะอ่านจาก primary node เท่านั้น ถ้า primary node มีปัญหา และไม่มี secondary node เหลืออยู่ การอ่านนั้นก็จะ error

primaryPreferred – driver จะอ่านจาก primary node เป็นหลัก แต่ถ้าหากไม่สามารถเข้าถึง primary node ได้ การอ่านนั้นจะถูกย้ายไปอ่านที่ secondary node แทน ซึ่งนั่นแปลว่าการอ่านนี้ไม่รับประกัน consistency

secondary – driver จะอ่านข้อมูลจาก secondary node เสมอ การตั้งค่าแบบนี้รับประกันให้เราได้ว่าการอ่านข้อมูลใดๆ จะไม่กระทบกับประสิทธิภาพของ primary node แน่นอน และถ้าไม่มี secondary node เหลืออยู่ใน replica set การอ่านครั้งนั้นก็จะ error

secondaryPreferred – driver อ่านข้อมูลจาก secondary node เป็นหลัก และถ้าเข้าถึง secondary node ได้ก็จะย้ายไปอ่านข้อมูลที่ primary node

nearest – driver จะอ่านข้อมูลจาก primary node หรือ secondary node ที่อยู่ใกล้ที่สุดใน replica set โดยวัดความใกล้จาก network latency เรามักตั้งค่าแบบนี้หากต้องการการอ่านที่เร็วมากๆ

จาก options ต่างๆ ดังกล่าวจะเห็นได้ว่ามีแค่ primary read preference เท่านั้นที่รับประกัน consistency

ถึงแม้ว่าเราไม่ได้ใช้ nearest ก็ตาม ถ้า MongoDB driver จะต้องอ่านข้อมูลจาก secondary node มันก็ยังพยายามอ่านจาก node ที่อยู่ใกล้ที่สุดอยู่ดี เนื่องจาก selection strategy ของ driver จะทำการเรียงลำดับ node ตาม network latency จากนั้นหา node ที่มี network latency น้อยที่สุดมา แล้ว exclude node ที่มี network latency มากกว่า node ที่มี latency น้อยที่สุดเป็นเวลา 15ms ออกไป สุดท้ายจะสุ่มมา 1 node เพื่อใข้งาน โดยที่ 15ms ที่กล่าวถึงนี้ driver บางตัวเปิดทางให้เราตั้งค่าให้ต่างออกไปได้ตั้งแต่ตอน initialize driver

การตั้งค่าเป็น nearest ก็ใช้ selection strategy นี้เช่นเดียวกันเพียงแต่จะนำ primary node เข้ามาคิดร่วมด้วย

Tagging

ถ้าหากว่าเรามี 5-node replica set ที่แต่ละ node กระจายกันอยู่ใน 2 data center สมมติว่าอยู่ที่ BKK 3 nodes และที่นี่เป็น primary data center และที่เหลืออีก 2 node อยู่ที่ SG และเราต้องการกำหนดให้ MongoDB response กลับไปก็ต่อเมื่อมีการเขียนเกิดขึ้นที่ SG แล้วเท่านั้น

จากสิ่งที่เรารู้มาก่อนหน้านี้ทั้งเรื่อง write concern และ read scaling นั้นไม่มีอะไรมาช่วยเราได้ในเรื่องนี้ได้แบบตรงๆ เลย เราไม่สามารถกำหนด w ให้มีค่าเป็น 3 ได้ เนื่องจากมีโอกาสที่ทั้ง 3 node ใน primary data center ที่ BKK จะถูกเขียนครบ 3 ก่อนที่ข้อมูลจะถูก replicate มาที่ SG ก็แปลว่าทำแบบนี้ไม่รับประกันว่า SG จะถูกเขียนอยู่ดี หรือจะแก้ให้ w เป็น 4 ก็ยังไม่ค่อยจะดีนัก หากเรามี node ที่ใช้ไม่ได้ data center ละ 1 node เพราะจะเขียนได้มากที่สุดแค่ 3 เท่านั้น

Tagging เข้ามาช่วยเราในเรื่องนี้โดยการทำให้เราสามารถสร้าง mode ให้ write concern ได้ ซึ่งก่อนหน้านี้เรากำหนดได้แค่จำนวน node แต่ตอนนี้เราจะกำหนดเป็นโหมดได้แล้ว สมมติว่าเราลองดู rs.conf() แล้วพบ config ดังนี้

{
  "_id" : "myapp",
  "version" : 1,
  "members" : [
    {
      "_id" : 0,
      "host" : "bkk1.myapp.com:30000",
      "tags": { "dc": "BKK", "rackBKK": "A" }
    },

    {
      "_id" : 1,
      "host" : "bkk2.myapp.com:30000",
      "tags": { "dc": "BKK", "rackBKK": "A" }
    },
    {
      "_id" : 2,
      "host" : "bkk3.myapp.com:30000",
      "tags": { "dc": "BKK", "rackBKK": "B" }
    },
    {
      "_id" : 3,
      "host" : "sg1.myapp.com:30000",
      "tags": { "dc": "SG", "rackSG": "A" }
    },
    {
      "_id" : 4,
      "host" : "sg2.myapp.com:30000",
      "tags": { "dc": "SG", "rackSG": "B" }
    }
  ],
  settings: {
    getLastErrorModes: {
      multiDC: { dc: 2 } },
      multiRack: { rackBKK: 2 } },
    }
  }
}

จาก configuration ด้านบนจะพบว่าแต่ละ member จะมี tags ติดอยู่ คือ dc เอาไว้เก็บชื่อของ data center และ rackSGrackBKK เอาไว้เก็บว่าอยู่ rack ไหนใน data center นั้นๆ และมี getLastErrorMode เก็บ write concern mode อยู่ 2 ค่า คือ multiDC และ multiRack

ซึ่งทั้งชื่อ tag dcrackBKKrackSG รวมถึงชื่อ mode multiDCmultiRack นั้นเราสามารถตั้งเป็นชื่ออะไรก็ได้อย่างอิสระตามที่เราเข้าใจ

โหมด multiDC อธิบายว่า write จะต้องถูก replicate ไปยัง node ที่มี tag dc ต่างกัน 2 ค่า ({ dc: 2 }) และโหมด multiRack อธิบายว่า write จะต้องถูก replicate ไปยัง node ที่มี tag rackBKK ต่างกัน 2 ค่า ({ rackBKK: 2 })

จากการตั้งค่าแบบนี้ทำให้เรารับประกันได้ว่า write จะถูก replicate ไปยัง data center 2 ที่แน่นอนก่อนที่จะ response กลับไปยัง application และ rack ที่ NY จะถูกเขียนลงไปบน node ที่อยู่ 2 rack ที่แตกต่างกันแน่นอน แล้วแต่โหมดที่เราเลือกตอนส่งคำสั่ง write มายัง primary node โดยแทนที่เราจะกำหนดเป็นจำนวน node ที่จะถูก replicate ไปก็ให้กำหนดเป็นชื่อของ mode ที่เราตั้งไว้ใน getLastErrorModes แทน

นอกจาก write แล้ว เรายังสามารถใช้ mode เหล่านี้ในการ read ได้อีกด้วย แต่ tag จะไม่มีประโยชน์กับ primary read preference เพราะ preference นี้จะอ่านเขียนได้จากแค่ node เดียวเท่านั้นดังที่อธิบายไว้ก่อนหน้านี้

สรุป MongoDB Data Replication

จากที่กล่าวมาทั้งหมดในบทความนี้ MongoDB Data Replication น่าจะทำให้เราเข้าใจถึงความสำคัญของการทำ replication มากยิ่งขึ้น ซึ่งการทำ replication นั้นดูเหมือนง่ายเพราะทุกอย่างทำงานเบื้องหลังโดยอัตโนมัติแทบทั้งสิ้น แต่เมื่อมีปัญหาความเข้าใจอย่างถ่องแท้และประสบการณ์เท่านั้นที่จะช่วยนำทางเราให้แก้ไขปัญหาได้

  • ทุก production ควรทำ Data Replication และต้องมีอย่างน้อย 3 node ในแต่ละ replica set ซึ่ง 1 ในนั้นอาจเป็น arbiter หรือไม่ก็ได้
  • ข้อมูลจะไม่ถูกนับว่า commit หากยังไม่ถูก replicate ไปยัง majority
  • rollback จะเกิดขึ้นเมื่อข้อมูลไม่สามารถ commit ได้ และข้อมูลที่ต้อง rollback จะเก็บไว้ใน rollback directory หลังจากนั้นเราต้องทำต่อเองว่าจะเอายังไงกับข้อมูลที่เขียนไปแล้วแต่ไม่ commit
  • ถ้า secondary node เข้าถึงไม่ได้นานเกินไปจนเมื่อกลับมาออนไลน์แล้วไม่สามารถ sync กับ oplog ได้ จำเป็นต้อง resync ใหม่ หากต้องการลดการ resync ใหม่ก็ควรปรับปรุงขนาดของ oplog ให้เหมาะสมต่อการใช้งาน
  • write concern จะกำหนดจำนวน node ที่ถูก replicate data ให้สำเร็จก่อนจะตอบกลับไปยัง driver แต่ถ้าหากต้องการหลีกเลี่ยงการ rollback ก็ให้กำหนด write concern ให้เป็น majority แต่นั่นก็ต้องแลกมากับ performance ที่ลดลง
  • read preference และ taggling ทำให้เราควบคุม read, write ได้เฉพาะเจาะจงมากขึ้น ดังนั้นควรใช้ 2 อย่างนี้โดยเฉพาะอย่างยิ่งหากเรามี database รันอยู่บนต่าง data center กัน

External Link

You may also like