我們用了兩天介紹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
是一個UUID
、ts
是一個時間戳記、type
和level
都是一個枚舉。
我接著用官方文件提到的定義來解釋一些不正確的設計。
從範例中我們最直覺的想法應該是選擇type
作為片鍵,因為我們在搜尋log時總是需要指定type
來限制範圍。
但是,如果我們直接使用type
作為片鍵,我們會面臨熱點(Hot Spot)問題。熱點問題指的是某一個資料分片的尺寸明顯大過其他的分片,舉例來說,有三個分片分別儲存app
、web
和admin
的log,在這樣的情境下,app
的數量會比其他種類的log還要多數倍。
而且因為分片與type
綁定了,所以無法重新平衡。因此,選擇小基數的片鍵,那資料分片的尺寸就會不平衡。
既然type
不能作為片鍵,那ts
呢?我們在搜尋log時不只會用type
也會透過ts
來限縮log的範圍,而且ts
是完全均勻分佈,看起來是個適合的欄位。
事實上,不是。
當片鍵是一個遞增的欄位,那在一開始的確可以運作得不錯,儘管如此,他終究會導致效能障礙。
原因在於ts
只會一直遞增,因此新資料總是會寫在最後一個分片,而最後一個分片就會因為一直變大而頻繁進行重新平衡。更糟的是,根據我們的查詢模式,我們也會最常讀取最後一個分片,也就是說,大部分查詢都會落在重新平衡期間,效能進一步惡化。
根據之前的解釋,我們知道無論type
、level
和ts
都不是好的片鍵。那麼,如果使用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片鍵需要考慮的幾個因素,包含:熱點問題、頻繁重新平衡、隨機存取等。在資料庫中也許沒有一個粗粒度的遞增欄位可以選,但當掌握了設計片鍵的重點後,我相信大家都可找出適合的片鍵。