設計 NoSQL 資料模型的特點

介紹

設計 NoSQL 資料模型的特點 「你必須盡可能快地跑才能留在原地,
為了到達某個地方,你必須以至少兩倍的速度奔跑!”
(c) 愛麗絲夢遊仙境

前陣子我被邀請去演講 分析師 我們公司的主題是設計資料模型,因為長時間(有時長達幾年)坐在專案上,我們忽略了 IT 技術世界中周圍正在發生的事情。 在我們公司(碰巧)很多專案不使用NoSQL資料庫(至少目前如此),所以在我的演講中我使用HBase的例子分別對它們進行了一些關注,並試圖將材料的呈現面向那些從未使用過它們的人已經起作用了。 特別是,我使用幾年前讀過的一個範例來說明資料模型設計的一些特徵 在 Amandeep Khurana 的文章「HB ase 架構設計簡介」中。 在分析例子時,我比較了解決相同問題的幾種方案,以便更好地向觀眾傳達主要思想。

最近,我「無事可做」地問自己一個問題(隔離期間的五月長週末尤其有利於這一點),理論計算與實踐的對應程度有多大? 事實上,這篇文章的想法就是這樣誕生的。 已經使用 NoSQL 幾天的開發人員可能不會從中學到任何新東西(因此可能會立即跳過一半的文章)。 但對於 分析師對於那些還沒有密切接觸過 NoSQL 的人來說,我認為這對於基本了解 HBase 設計資料模型的功能很有用。

實例分析

在我看來,在開始使用NoSQL資料庫之前,你需要仔細思考並權衡利弊。 通常,這個問題很可能可以使用傳統的關係 DBMS 來解決。 因此,沒有重大原因最好不要使用NoSQL。 如果您仍然決定使用 NoSQL 資料庫,那麼您應該考慮到這裡的設計方法有些不同。 特別是其中一些對於那些以前只處理過關係型 DBMS 的人來說可能是不尋常的(根據我的觀察)。 因此,在「關係」世界中,我們通常會先對問題域進行建模,然後在必要時對模型進行非規範化。 在NoSQL中我們 應立即考慮處理資料的預期場景 並首先對資料進行非規範化。 此外,還有許多其他差異,將在下面討論。

讓我們考慮以下「綜合」問題,我們將繼續研究這個問題:

有必要為一些抽象社交網路的使用者的好友清單設計一個儲存結構。 為了簡單起見,我們假設我們所有的聯繫都是定向的(就像在 Instagram 上,而不是 Linkedin 上一樣)。 該結構應該允許您有效地:

  • 回答用戶A是否閱讀用戶B的問題(閱讀模式)
  • 允許在用戶 B 訂閱/取消訂閱用戶 A 的情況下新增/刪除連線(資料變更範本)

當然,解決問題的方法有很多。 在常規關係資料庫中,我們很可能會簡單地製作一個關係表(例如,如果我們需要儲存一個使用者群組:家庭、工作等,其中包括這個“朋友”,則可能是典型的),並優化存取速度會增加索引/分區。 決賽桌很可能是這樣的:

USER_ID
朋友ID

Vasya
彼佳

Vasya
奧麗雅

在下文中,為了清楚和更好地理解,我將標明名稱而不是 ID

就 HBase 而言,我們知道:

  • 可以實現不導致全表掃描的高效搜索 僅透過密鑰
    • 事實上,這就是為什麼向此類資料庫編寫許多人熟悉的 SQL 查詢是一個壞主意; 從技術上講,當然,您可以從同一個 Impala 向 HBase 發送帶有 Joins 和其他邏輯的 SQL 查詢,但效果如何...

因此,我們被迫使用使用者ID作為金鑰。 我對「在哪裡以及如何儲存朋友的 ID?」這個主題的第一個想法也許是將它們儲存在列中的想法。 這個最明顯和“天真的”選項看起來像這樣(我們稱之為 選項 1(預設)以供進一步參考):

行鍵
擴音器

Vasya
1:彼佳
2:奧利亞
3:達莎

彼佳
1:瑪莎
2:瓦夏

這裡,每一行對應一個網路用戶。 列的名稱為:1, 2, ... - 根據好友的數量,好友的 ID 儲存在列中。 需要注意的是,每行都有不同數量的列。 在上圖的範例中,一行有三列(1、2和3),第二行只有兩列(1和2)-這裡我們自己使用了關聯式資料庫沒有的兩個HBase屬性:

  • 動態變更列的組成的能力(新增朋友 -> 新增列,刪除朋友 -> 刪除列)
  • 不同的行可能有不同的列組成

讓我們檢查一下我們的結構是否符合任務的要求:

  • 讀取數據:為了了解 Vasya 是否訂閱了 Olya,我們需要減去 整條線 透過鍵 RowKey = “Vasya” 並對列值進行排序,直到我們在其中「遇到」Olya。 或迭代所有列的值,「不滿足」Olya並返回答案False;
  • 編輯資料:新增好友:對於類似的任務,我們還需要減去 整條線 使用鍵 RowKey = “Vasya” 來計算他的朋友總數。 我們需要這個好友總數來決定需要在其中記下新好友 ID 的列號。
  • 更改資料:刪除好友:
    • 需要減去 整條線 透過RowKey = “Vasya”鍵,對列進行排序,找到要刪除的好友所在的列;
    • 接下來,刪除好友後,我們需要將所有資料「轉移」到一列中,以免編號出現「間隙」。

現在讓我們評估這些演算法的效率如何,我們需要在「條件應用程式」方面實現這些演算法,使用 O-象徵主義。 讓我們將假設的社交網路的規模表示為 n。 那麼一個使用者最多可以擁有的好友數量為(n-1)。 為了我們的目的,我們可以進一步忽略這個(-1),因為在 O 符號使用的框架內它並不重要。

  • 讀取數據:需要減去整行並迭代其限制內的所有列。 這意味著成本的上限估計約為 O(n)
  • 編輯資料:新增好友:要確定朋友的數量,需要迭代該行的所有列,然後插入新列 => O(n)
  • 更改資料:刪除好友:
    • 與新增類似 - 您需要遍歷限制中的所有欄位 => O(n)
    • 刪除列後,我們需要「移動」它們。 如果您「迎頭」實施此操作,那麼您最多需要 (n-1) 次操作。 但在這裡以及進一步的實際部分,我們將使用不同的方法,該方法將為固定數量的操作實現「偽移位」——也就是說,無論 n 是多少,都會花費恆定的時間。 與 O(n) 相比,這個常數時間(準確地說是 O(2))可以忽略不計。 方法如下圖所示:我們只需將資料從「最後」列複製到我們要刪除資料的列,然後刪除最後一列:
      設計 NoSQL 資料模型的特點

總的來說,在所有場景中,我們都獲得了 O(n) 的漸近計算複雜度。
您可能已經注意到,我們幾乎總是必須從資料庫中讀取整行,並且在三分之二的情況下,只是為了遍歷所有列併計算好友總數。 因此,作為最佳化的嘗試,可以新增一個「計數」列,該列儲存每個網路使用者的好友總數。 在這種情況下,我們不能讀取整行來計算好友總數,而只能讀取一個「count」欄位。 最重要的是在操作資料時不要忘記更新“count”。 那。 我們得到改善 選項 2(計數):

行鍵
擴音器

Vasya
1:彼佳
2:奧利亞
3:達莎
數量:3

彼佳
1:瑪莎
2:瓦夏

數量:2

與第一個選項相比:

  • 讀取數據:得到「Vasya讀Olya嗎?」這個問題的答案沒有任何改變 => O(n)
  • 編輯資料:新增好友:我們簡化了新朋友的插入,因為現在我們不需要讀取整行並迭代其列,而只能取得「count」列的值等。 立即確定插入新朋友的列號。 這導致計算複雜度降低至 O(1)
  • 更改資料:刪除好友:刪除好友時,我們也可以利用這一欄來減少資料向左「平移」一個儲存格時的I/O操作次數。 但仍然需要遍歷列才能找到需要刪除的列,所以 => O(n)
  • 另一方面,現在更新資料時,我們每次都需要更新「count」列,但這需要恆定的時間,在O符號的框架內可以忽略不計

總的來說,選項2似乎更優化一些,但它更像是「進化而不是革命」。 為了進行一場“革命”,我們需要 選項 3(欄).
讓我們把一切「顛倒過來」:我們將任命 列名 使用者 ID! 列本身寫什麼對我們來說不再重要,讓它成為數字1(一般來說,有用的東西可以存放在那裡,例如,“家人/朋友/等”組)。 這種方法可能會讓沒有準備好的「外行人」感到驚訝,因為他們以前沒有使用 NoSQL 資料庫的經驗,但正是這種方法可以讓您在這項任務中更有效地利用 HBase 的潛力:

行鍵
擴音器

Vasya
彼佳:1
奧利亞:1
大傻:1

彼佳
瑪莎:1
瓦夏:1

在這裡,我們同時獲得了幾個優勢。 為了理解它們,讓我們分析新結構並估計計算複雜度:

  • 讀取數據:為了回答 Vasya 是否訂閱 Olya 的問題,只需讀取一列「Olya」即可:如果存在,則答案為 True,如果不存在 – False => O(1)
  • 編輯資料:新增好友:新增好友:只需新增一個新欄位「Friend ID」 => O(1)
  • 更改資料:刪除好友:只需刪除好友 ID 欄位 => O(1)

正如您所看到的,這種儲存模型的一個顯著優點是,在我們需要的所有場景中,我們只對一列進行操作,避免了從資料庫中讀取整行,而且還列舉了該行的所有列。 我們可以就此打住,但是…

你可能會感到困惑,並沿著優化效能和減少存取資料庫時 I/O 操作的道路走得更遠。 如果我們將完整的關係資訊直接儲存在行鍵本身會怎麼樣? 也就是說,將金鑰組合為 userID.friendID? 在這種情況下,我們甚至根本不需要讀取該行的列(選項4(行)):

行鍵
擴音器

瓦夏·佩蒂亞
彼佳:1

瓦夏·奧利亞
奧利亞:1

瓦夏·達莎
大傻:1

彼佳·瑪莎
瑪莎:1

彼佳·瓦夏
瓦夏:1

顯然,與先前的版本一樣,在這種結構中評估所有資料操作場景的複雜度將為 O(1)。 與選項 3 的差異僅在於資料庫中 I/O 操作的效率。

好吧,最後一個「鞠躬」。 很容易看出,在選項4中,行鍵將具有可變長度,這可能會影響效能(這裡我們記得HBase將資料儲存為一組位元組,表中的行按鍵排序)。 另外,我們有一個分隔符,在某些情況下可能需要處理。 為了消除這種影響,您可以使用 userID 和friendID 的雜湊值,並且由於這兩個雜湊值都具有恆定的長度,因此您可以簡單地將它們連接起來,而無需分隔符號。 那麼表中的數據將如下所示(選項5(哈希)):

行鍵
擴音器

dc084ef00e94aef49be885f9b01f51c01918fa783851db0dc1f72f83d33a5994
彼佳:1

dc084ef00e94aef49be885f9b01f51c0f06b7714b5ba522c3cf51328b66fe28a
奧利亞:1

dc084ef00e94aef49be885f9b01f51c00d2c2e5d69df6b238754f650d56c896a
大傻:1

1918fa783851db0dc1f72f83d33a59949ee3309645bd2c0775899fca14f311e1
瑪莎:1

1918fa783851db0dc1f72f83d33a5994dc084ef00e94aef49be885f9b01f51c0
瓦夏:1

顯然,在我們正在考慮的場景中使用這種結構的演算法複雜度將與選項 4 相同,即 O(1)。
總的來說,讓我們在一張表中總結我們對計算複雜性的所有估計:

新增好友
檢查朋友的狀況
刪除好友

選項 1(預設)
O(N)
O(N)
O(N)

選項2(計數)
O(1)
O(N)
O(N)

選項 3(欄)
O(1)
O(1)
O(1)

選項 4(行)
O(1)
O(1)
O(1)

選項 5(哈希)
O(1)
O(1)
O(1)

正如您所看到的,選項 3-5 似乎是最可取的,理論上可以確保在恆定時間內執行所有必要的資料操作場景。 在我們的任務條件下,並沒有明確要求獲取用戶所有好友的列表,但在實際的專案活動中,作為優秀的分析師,“預測”可能會出現這樣的任務並進行預測是有好處的。 “鋪上一根稻草。” 因此,我同情選項3。但在實際專案中,這個請求很可能已經透過其他方式解決了,因此,在沒有對整個問題有整體了解的情況下,最好不要這樣做最終結論。

實驗準備

我想在實踐中檢驗上述理論論證——這是長週末產生的想法的目標。 為此,有必要評估我們的「條件應用程式」在所有描述的使用資料庫的場景中的運行速度,以及隨著社交網路(n)規模的增加而增加的時間。 我們感興趣並在實驗過程中測量的目標參數是「條件應用」執行一項「業務操作」所花費的時間。 「商業交易」指以下其中一項:

  • 新增一位新朋友
  • 檢查用戶A是否是用戶B的好友
  • 刪除一名好友

因此,考慮到最初聲明中概述的要求,驗證場景如下:

  • 數據記錄。 隨機產生大小為 n 的初始網路。 為了更接近“真實世界”,每個用戶擁有的好友數量也是一個隨機變數。 測量我們的「條件應用程式」將所有產生的資料寫入 HBase 的時間。 然後將所得時間除以添加好友的總數——這就是我們得到一次「業務操作」的平均時間的方法
  • 讀取數據。 為每個用戶建立一個“個性”列表,無論用戶是否訂閱這些“個性”,您都需要獲得答案。 列表的長度=大約用戶好友的數量,對於檢查的好友中一半的答案應該是“是”,而另一半則是“否”。 檢查的執行順序是交替回答「是」和「否」(也就是說,每隔一種情況,我們就必須檢查選項 1 和 2 行的所有欄位)。 然後將總篩選時間除以接受測試的朋友數量,以獲得每位受試者的平均篩選時間。
  • 刪除數據。 刪除該用戶的所有好友。 而且,刪除順序是隨機的(也就是說,我們「打亂」用來記錄資料的原始清單)。 然後將總檢查時間除以刪除的好友數量,以獲得每次檢查的平均時間。

需要針對 5 個資料模型選項中的每一個以及不同規模的社交網路運行這些場景,以了解時間如何隨著其成長而變化。 當然,在 5 個 n 內,所有 XNUMX 個選項的網路連線和要檢查的使用者清單必須相同。
為了更好地理解,以下是 n= 5 時產生資料的範例。編寫的「生成器」產生三個 ID 字典作為輸出:

  • 第一個用於插入
  • 第二個用於檢查
  • 第三——刪除

{0: [1], 1: [4, 5, 3, 2, 1], 2: [1, 2], 3: [2, 4, 1, 5, 3], 4: [2, 1]} # всего 15 друзей

{0: [1, 10800], 1: [5, 10800, 2, 10801, 4, 10802], 2: [1, 10800], 3: [3, 10800, 1, 10801, 5, 10802], 4: [2, 10800]} # всего 18 проверяемых субъектов

{0: [1], 1: [1, 3, 2, 5, 4], 2: [1, 2], 3: [4, 1, 2, 3, 5], 4: [1, 2]} # всего 15 друзей

可以看到,字典中所有大於 10 的 ID 都可以檢查,這正是那些肯定會給出答案 False 的 ID。 「好友」的插入、檢查和刪除完全按照字典中規定的順序進行。

該實驗是在一台運行 Windows 10 的筆記型電腦上進行的,其中 HBase 在一個 Docker 容器中運行,Python 和 Jupyter Notebook 在另一個容器中運行。 Docker 被分配了 2 個 CPU 核心和 2 GB RAM。 所有邏輯,例如“條件應用程式”的模擬以及用於產生測試資料和測量時間的“管道”,都是用 Python 編寫的。 該庫用於與 HBase 一起使用 快樂基地,計算選項 5 的雜湊值 (MD5) - hashlib

考慮到特定筆記型電腦的計算能力,透過實驗選擇了 n = 10, 30, … 的啟動。 170 – 當整個測試週期的總操作時間(所有 n 的所有選項的所有場景)或多或少合理並且適合一次茶會期間(平均 15 分鐘)。

這裡有必要指出的是,在這個實驗中我們主要不是評估絕對的性能數據。 即使是不同兩個選項的相對比較也可能不完全正確。 現在我們感興趣的是時間隨 n 變化的性質,因為考慮到「測試台」的上述配置,很難獲得「清除」隨機和其他因素影響的時間估計(並且沒有設定這樣的任務)。

實驗結果

第一個測驗是填寫好友清單所花費的時間如何變化。 結果如下圖所示。
設計 NoSQL 資料模型的特點
正如預期的那樣,選項 3-5 顯示了幾乎恆定的「業務交易」時間,該時間不依賴網路規模的成長和無法區分的效能差異。
選項 2 也顯示出穩定但稍差的性能,幾乎是選項 2-3 的 5 倍。 這不能不讓人高興,因為它與理論相關——在這個版本中,HBase 的 I/O 操作數量恰好是原來的 2 倍。 這可以作為我們的測試台原則上提供良好準確性的間接證據。
如預期的那樣,選項 1 也是最慢的,並且表示將另一個選項新增至網路大小所花費的時間呈線性增加。
現在讓我們來看看第二次測試的結果。
設計 NoSQL 資料模型的特點
選項 3-5 再次如預期運作 - 恆定時間,與網路大小無關。 選項 1 和 2 展示了隨著網路規模的增加而線性增加的時間以及類似的效能。 此外,選項 2 被證明有點慢 - 顯然是因為需要校對和處理額外的「計數」列,隨著 n 的增長,這一列變得更加明顯。 但我仍然不會下任何結論,因為這種比較的準確性相對較低。 此外,這些比率(選項 1 或 2 更快)在每次運行中都會發生變化(同時保持依賴性和「並駕齊驅」的性質)。

好吧,最後一張圖是去除測試的結果。

設計 NoSQL 資料模型的特點

同樣,這裡沒有什麼驚喜。 選項 3-5 在恆定時間內執行刪除。
此外,有趣的是,與前面的場景不同,選項 4 和 5 的效能明顯比選項 3 稍差。顯然,行刪除操作比列刪除操作更昂貴,這通常是合乎邏輯的。

如預期的那樣,選項 1 和 2 表現出時間的線性增加。 同時,由於「維護」計數列需要額外的 I/O 操作,因此選項 2 始終比選項 1 慢。

實驗的一般結論:

  • 選項 3-5 展示了更高的效率,因為它們利用了 HBase; 此外,它們的效能彼此之間存在常數差異,並且不依賴網路的大小。
  • 沒有記錄選項 4 和 5 之間的差異。 但這並不意味著不應該使用選項5。 考慮到測試台的效能特徵,所使用的實驗場景很可能不允許它被偵測到。
  • 利用數據執行「業務操作」所需時間增加的性質總體上證實了先前獲得的所有選項的理論計算。

尾聲

進行的粗略實驗不應被視為絕對真理。 有許多因素沒有考慮在內,導致結果失真(這些波動在網路規模較小的圖中尤其明顯)。 例如happybase使用的thrift的速度,我用Python寫的邏輯的實現量和方法(我不能說程式碼是最優寫的,有效利用了所有元件的能力),也許HBase 快取的功能、筆記型電腦上Windows 10的後台活動等。 一般來說,我們可以假設所有理論計算都已透過實驗證明了其有效性。 好吧,或者至少是無法用這樣的「正面攻擊」來反駁他們。

總之,給剛開始在 HBase 中設計資料模型的每個人的建議:從以前使用關聯式資料庫的經驗中抽像出來並記住「命令」:

  • 在設計時,我們從資料操作的任務和模式出發,而不是從領域模型出發
  • 高效存取(無需全表掃描)-僅透過鍵
  • 非規範化
  • 不同的行可以包含不同的列
  • 揚聲器的動態組成

來源: www.habr.com

添加評論