iT邦幫忙

2022 iThome 鐵人賽

DAY 22
2
Software Development

軟體架構師的自我修養系列 第 22

[Day 22] MongoDB的片鍵該如何選擇?

  • 分享至 

  • xImage
  •  

我們用了兩天介紹MySQL在處理交易隔離的一些手段和方法,對於MySQL來說,能夠駕馭交易就能夠很好上手了,因為MySQL背後的概念相對單純,比起分散式的環境,MySQL的適用場域更偏向小型應用程式。

而MySQL最廣為人知的限制就是,他在儲存的資料量體巨大時效能會跳水。

而這也是NoSQL興起的主要原因,今天要講的是MongoDB,這應該算是除MySQL外最常被使用到的資料庫了,而MongoDB也很好的解決了MySQL的痛點:資料量。

MongoDB如何解決資料量的問題呢?在第三天我們提過,透過資料分片(Sharding)。那麼,選擇一個好的片鍵就至關重要。

雖然在官方文件中講解了許多挑選片鍵的重點,但文件中並沒有提供一個公式,導致在選擇片鍵時我們依然很容易陷入選擇困難。

這篇文章就是要告訴你那個公式,並且解釋公式背後的理由,那麼話不多說,先給公式:

{粗粒遞增欄位: 1, 搜尋欄位: 1}

接著讓我們詳細解釋原因。

使用情境

為了更好解釋這個公式,我會用一個實際的使用場景作為例子。

有一個用來儲存應用程式log的集合,每一個log的格式如下。

{
    "id": "4df16cf0-2699-410f-a07e-ca0bc3d3e153",
    "type": "app",
    "level": "high",
    "ts": 1635132899,
    "msg": "Database crash"
}

每一筆log都有相同的格式,而id是一個UUIDts是一個時間戳記、typelevel都是一個枚舉。

我接著用官方文件提到的定義來解釋一些不正確的設計。

小基數(Low Cardinality)片鍵

從範例中我們最直覺的想法應該是選擇type作為片鍵,因為我們在搜尋log時總是需要指定type來限制範圍。

但是,如果我們直接使用type作為片鍵,我們會面臨熱點(Hot Spot)問題。熱點問題指的是某一個資料分片的尺寸明顯大過其他的分片,舉例來說,有三個分片分別儲存appwebadmin的log,在這樣的情境下,app的數量會比其他種類的log還要多數倍。

而且因為分片與type綁定了,所以無法重新平衡。因此,選擇小基數的片鍵,那資料分片的尺寸就會不平衡。

遞增(Ascending)片鍵

既然type不能作為片鍵,那ts呢?我們在搜尋log時不只會用type也會透過ts來限縮log的範圍,而且ts是完全均勻分佈,看起來是個適合的欄位。

事實上,不是。

當片鍵是一個遞增的欄位,那在一開始的確可以運作得不錯,儘管如此,他終究會導致效能障礙。

原因在於ts只會一直遞增,因此新資料總是會寫在最後一個分片,而最後一個分片就會因為一直變大而頻繁進行重新平衡。更糟的是,根據我們的查詢模式,我們也會最常讀取最後一個分片,也就是說,大部分查詢都會落在重新平衡期間,效能進一步惡化。

隨機(Random)片鍵

根據之前的解釋,我們知道無論typelevelts都不是好的片鍵。那麼,如果使用id作為片鍵,我們既可以確保分布均勻,又不會一直疊加在最後一個分片,這樣如何?

如果資料集的量不大,那這樣的解法還算能接受。但若是資料量不斷成長,在做重新平衡的成本會非常高!

因為資料是隨機分佈的,而MongoDB就必須在重新平衡時使用隨機存取的方式抓取資料。若是資料是遞增的,那麼重新平衡就可以依靠循序存取,這比隨機存取高效不少。

解法

綜合上述,我們發現,無論選擇哪一個欄位作為片鍵都有其問題,因此好的片鍵通常會是一個複合鍵。也就是我們開頭提供的公式:

{粗粒遞增欄位: 1, 搜尋欄位: 1}

為了避免隨機存取,我們需要有個遞增欄位,但也要避免頻繁的進行重新平衡,所以必須要選用粗粒度的遞增欄位。

除此之外,我們還必須要確保所有常用查詢都可以盡量落在同一個分片,因此還必須考量搜尋的常用模式,也就是片鍵的後面那個欄位。

在我們應用程式log的例子中要怎麼改進比較適合呢?

ts雖然可以標註log的產生時間,但是他是一個細粒度的遞增欄位,不適合作為片鍵。因此我們新建立一個欄位,month,作為查詢的依據和粗粒度遞增欄位。

整個文件會長成下面這樣。

{
    "id": "4df16cf0-2699-410f-a07e-ca0bc3d3e153",
    "type": "app",
    "level": "high",
    "ts": 1635132899,
    "msg": "Database crash",
    "month": new Date(2021, 10) // only month
}

而片鍵則會是{month: 1, type: 1}

新增加月份的欄位看起來好像是為了解決片鍵的設計問題,但實際上他也為查詢帶來許多方便性。

例如我們想要查詢近一個月的log,那麼若是用ts來做,就必須進行秒和日期的轉換,但若是使用month很容易就可以透過getMonth這個內建功能達成,整個搜尋就會變得更簡單。

var d = new Date();
d.setMonth(d.getMonth() - 1); //1 month ago
db.data.find({month:{$gte: d}});

總結一下,這篇文章介紹了設計MongoDB片鍵需要考慮的幾個因素,包含:熱點問題、頻繁重新平衡、隨機存取等。在資料庫中也許沒有一個粗粒度的遞增欄位可以選,但當掌握了設計片鍵的重點後,我相信大家都可找出適合的片鍵。


上一篇
[Day 21] 解決MySQL的幻讀
下一篇
[Day 23] MongoDB索引的奧秘
系列文
軟體架構師的自我修養31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言