Mongoose writeconcern實作筆記

陳柏仁/

2024-02-27

/
--- views
分享在mongoose上加入writeconcern的實作心得

緣起

為提供客戶更好的使用體驗,最近公司將現有系統架構新增設備容錯備援機制-HA(High Availability)架構 ,在程式操作面上希望加入 mongoDB 的 writeconcern 機制避免多資料庫間資料不同步的狀況

MongoDB writeconcern 介紹

因為專案使用 node.js 寫入資料庫,因此此篇文章以熱門的 npm 套件 mongoose 實作 writeconcern 應用

Mongoose writeconcern 參數

writeconcern 主要有幾個參數可以設定

  {
    w: 'majority', // 指定確認寫入數量,設置為majority時會確認過半(1加投票成員數量的一半,無條件捨去)的資料庫寫入之後,才作回應
    j: true, // boolean,設置為true會確認已寫入所使用的儲存引擎(EX:WiredTiger)的日誌當中,以供資料庫查找並資料恢復
    wtimeout: 1000 // 單位為毫秒,設置writeconcern超時限制,超過會跳出writeconcern錯誤,避免無限期阻塞
  }

Mongoose writeconcern 設置層級

在 mongoose 裡,可以在以下幾個地方設置 writeconcern,其優先順序依序為 Transaction level > Operator level > Schema level > global level

Transaction level

當使用 transaction 時,必須在 session.startTransaction()設置 writeconcern,並在 session.commitTransaction() catch 該錯誤

const session = await mongoDB.startSession()
session.startTransaction({
  writeConcern: {
    w: {
      w: "majority",
      j: true,
      wtimeout: 1000,
    },
  },
})
// Do some operators...
await session.commitTransaction().catch(errorHandler)
await session.endSession()

Operator level

如果想針對對單一操作,可以在此設置

值得注意的是,insertMany()無法套用 schema level 的設置,必須在此(Operator level) 設置

await Movies.insertMany(
  [{ name: "Star Wars" }, { name: "The Empire Strikes Back" }],
  {
    writeConcern: {
      w: "majority",
      j: true,
      wtimeout: 1000,
    },
  },
)

Schema level

可以在此設置各 collection 的 writeconcern,

const schema = new Schema(
  { name: String },
  {
    writeConcern: {
      w: "majority",
      j: true,
      wtimeout: 1000,
    },
  },
)

實測當操作 updateOne()save()deleteMany()deleteOne()等操作時皆會自動套用

Global level

可以在 mongosh 使用 db.adminCommand()設置 writeconcern,如果在其他層級沒有設定,則會預設使用此設置

db.adminCommand({
  setDefaultRWConcern: 1,
  defaultWriteConcern: {
    w: "majority",
  },
})

writeconcern 設置與錯誤處理實作

公司將 mongoDB 的副本機制設置為 Primary with a Secondary and an Arbiter (PSA)

當設置 w 為 majority 時,依規定必須要有超過 1 加投票成員數量的一半成功寫入,因此在此應用中,只要有一個資料庫節點掛掉導致延時,便會出現 writeconcern error 的狀況,但是該資料仍可以成功寫入 Primary 資料庫裡,因此為了使 user 操作不被中斷,我們必須接住 writeconcern error 狀況,讓程式能順利完成操作

writeconcern 設置

  1. 我們先在 schema 層設置 writeConcern
const schema = new Schema(
  { name: String },
  {
    writeConcern: {
      w: "majority",
      j: true,
      wtimeout: 1000,
    },
  },
)
  1. 在 Operator 層設置 writeConcern,

    • insertMany()必須設置在 Operator 層
    • 部分操作為客戶端資料庫輪詢同步資料,無同步必要,考量回應速度,設置為 w: '1'
await Movies.insertMany(
  [{ name: "Star Wars" }, { name: "The Empire Strikes Back" }],
  {
    writeConcern: {
      w: "majority",
      j: true,
      wtimeout: 1000,
    },
  },
)
await Movies.updateOne(
  { _id },
  { name: "Star Wars" },
  {
    writeConcern: {
      w: "1",
      j: true,
      wtimeout: 1000,
    },
  },
)
  1. 在所有 Operator 新增共用 function 做錯誤處理 * mongoDB 的 writeconcern error 代碼為 64(WriteConcernFailed),catch error 並確認錯誤代碼為 64 時 return 使程式可繼續運行,其他錯誤則 throw error,供外層錯誤處理

    當 operator 是 insertMany()時,可以抓到 error.insertedDocs 並 return 供程式繼續使用

const errorHandler = error => {
  const { result } = error

  const writeError = result?.writeError
  const writeErrors = result?.result?.writeErrors
  const writeConcernError = result?.writeConcernError
  const writeConcernErrors = result?.result?.writeConcernErrors

  const hasOtherError = writeError && writeError.code !== 64
  const hasOtherErrors = writeErrors?.some(err => err.code !== 64)

  const isSingleWCE = writeConcernError?.code === 64
  const isBulkWCE =
    writeConcernErrors?.length > 0 &&
    writeConcernErrors.every(err => err.code === 64)

  // 若僅為 WriteConcern Error (64) 且無其他錯誤,視為成功
  if (!hasOtherError && !hasOtherErrors && (isSingleWCE || isBulkWCE)) {
    return error.insertedDocs
  }

  // 若需拋出錯誤,先過濾掉 code 64 的警告,避免混淆
  if (writeConcernErrors) {
    error.result.result.writeConcernErrors = writeConcernErrors.filter(
      err => err.code !== 64,
    )
  }
  if (writeConcernError?.code === 64) {
    error.result.writeConcernError = {}
  }

  throw error
}

// catch the error
await Movies.updateOne(
  { _id },
  { name: "Star Wars" },
  {
    writeConcern: {
      w: "1",
      j: true,
      wtimeout: 1000,
    },
  },
).catch(errorHandler)

// other operators...

參考資料