Basic Rules of Cassandra Data Modeling
原文地址:http://www.datastax.com/dev/blog/basic-rules-of-cassandra-data-modeling
選擇一個正確的數據模型是Cassandra使用中最難的部分(譯者也這么認為)。如果你有關系型數據庫開發經驗,你會覺得CQL看起來都很相似(和MySQL等),但是你使用它的方式會非常的不同。這篇文章的目的就是解釋當你在設計一個Cassandra數據庫的時候需要牢記在心里的一些基本規則。如果你遵守這些規則,你會得到很好的拿來就能用的性能提升。更好的是,你的性能將會隨着你的集群的節點增加而線性增長。
非 - 目標
來自有關系型數據庫背景的開發經常會嘗試把他們在關系型數據庫的設計規則經驗放到Cassandra來用。為了避免浪費時間在那些不適用Cassandra的規則上,我需要指出一下非目標:
盡量減少寫入次數
在Cassandra中寫入不是免費的,但是非常廉價。Cassandra在寫入吞吐量方面做了高度的優化,幾乎所有的寫入性能都是平等的(計數器、輕量事務和在list中插入數據除外)。如果你額外的寫入可以讓你改進讀的性能,這一般是一種好的設計。讀取是一種更昂貴並且更困難的東西。
最小化數據重復(譯注:范式設計)
反范式設計和冗余數據是Cassandra的設計核心。不要擔心這個問題。相比CPU,內存,磁盤IO,網絡IO,磁盤的儲存空間是非常廉價的,而Cassandra正是遵循這個思路來設計的。為了有更好的讀取性能,你往往需要重復數據。
另外Cassandra不提供JOIN ,你不會想在分布式系統中用這些特性。
真 - 目標
你的數據模型中有2個非常高優先級的目標
- 讓數據均勻分布在整個集群的每個節點中
- 最小化每次讀取使用的分區數量
還有一些小規則你需要記住,但是這兩個最重要。所以多數情況下,我會把這2個規則作為關注重點。當然你還有一些花哨的技巧可以使用,但是你最好知道如何評測它的效果。
規則1:讓數據分布在整個集群的每個節點中
你希望集群中的所有的節點都有大致差不多數量的數據。Cassandra做這個很容易,但不是全自動的。行基於分區主鍵被分布到集群的各個位置。所以你需要選擇一個好的主鍵,我將簡單解釋一下。
規則2:盡量減少分區讀取
分區是一組數據共享一個分區key。當你執行一個讀取的查詢,你希望讀取盡量少的分區來獲取你需要的數據。
為何如此重要?因為每個分區可能在不同的節點上。代理將會構建許多命令去不同的節點去執行以滿足你的請求。這增加了大量的前置操作並且導致各種延遲的情況。而且,即使在單個節點的情況下,由於數據的存儲方式導致讀取多個分區的數據也比單個分區要慢的多。
這是兩個沖突的規則嗎?
你需要盡量減少讀取的時候需要的分區數量,為什么不把數據放在一個大分區?你需要干掉規則1,它說要讓數據分布在整個集群。
事實是,這兩個規則確實經常沖突,所以你需要平衡他們。
設計你的查詢語句
最小話查詢分區的辦法就是讓你的數據結構適應你的查詢語句。不要依據關系來設計(譯注:沒錯,你的數據會更加抽象化,且不好理解)。基於查詢設計。下面是如何做:
第一步:決定要執行哪些查詢語句
盡量嘗試去確定你有哪些語句需要執行。這里可能包含大量的注意事項你可能一開始沒有考慮到,你需要考慮:
- 使用什么字段做分組
- 使用什么字段來排序
- 需要哪些過濾條件
- 結果的唯一性
- ...
任何一個查詢請求的改變都會影響到最優數據模型的設計。
第二步:嘗試創建一個表讓你可以大致的從一個分區里讀數據
事實上這意味着你可能需要為每個查詢建立一張表(譯注:也就是說你一開始會有很多表,他們數據會很重復)。如果你需要應付多個查詢,那么你需要更多的表。
換句話說,每個表需要高度匹配查詢語句需要的答案。如果你需要不同的答案,你需要不同的表。這是你如何優化讀操作。
記住,重復數據沒有問題。你的許多表可能重復了很多數據。
添加規則:例子
為了展示如何進行一個好的設計,我將帶你通過設計解決一個簡單的問題。
例1:用戶查詢
核心需求,我們有許多用戶,我想找到他們。
步驟1:
決定要執行哪些查詢。我們希望可以通過用戶名,或者用戶的email來查詢他們。任何一個查詢,我們都需要得到他們的所有信息。
步驟2:
嘗試創建一個表來滿足查詢需求,並且只需要用到1個分區。因為我們需要得到用戶的所有信息,這需要2張表。
CREATE TABLE users_by_username (
username text PRIMARY KEY,
email text,
age int
)
CREATE TABLE users_by_email (
email text PRIMARY KEY,
username text,
age int
)
現在我們來確認一下是否符合規則:
數據分布均勻?每個用戶使用它們自己的分區,所以是的。
最小化分區讀取?每個用戶我們只需要讀取一個分區,所以是的。
現在我們來嘗試對“非 - 目標”進行優化,然后又有了下面的設計方式
CREATE TABLE users (
id uuid PRIMARY KEY,
username text,
email text,
age int
)
CREATE TABLE users_by_username (
username text PRIMARY KEY,
id uuid
)
CREATE TABLE users_by_email (
email text PRIMARY KEY,
id uuid
)
這個數據模型同樣將數據分布到所有的節點,但是有個問題,我們需要讀取2個分區。一個是users_by_username/users_by_email 然后是 users表。所以讀取的成本大體上是之前的2倍。
例2: 用戶分組
核心需求:用戶被分到不同的組中,我們希望讀取分組的所有用戶。
步驟1:
決定要執行哪些查詢。我們希望獲取確切分組的用戶的所有信息,不關心排序。
步驟2:
嘗試創建一個表來滿足查詢需求,並且只需要用到1個分區。我們如何讓分組分不到不同的分區中?我們可以設計這樣的分區主鍵:
CREATE TABLE groups (
groupname text,
username text,
email text,
age int,
PRIMARY KEY (groupname, username)
)
注意到主鍵中有2個部分,groupname,這個是分區主鍵,username,被稱作clustering key (集群主鍵)。這會使得每個groupname在一個分區中。在一個特定的groupname中,行是按照username排序的。讀取分組數據非常簡單:
SELECT * FROM groups WHERE groupname = ?
這滿足了最小化查詢分區的要求,因為我們只需要讀取1個分區。但是它並沒有在數據均勻分布上做的很好。如果我們考慮有成千上萬的小分組,每個分組有幾百人,我們將得到一個相當於均勻分布的模型。但是如果有一個分組有1百萬用戶,所有的負擔都會被一個節點承擔。
如果我們希望負載均衡,有一些策略我們可以采用。最基本的是添加另一個字段到主鍵中做成一個復合分區主鍵,下面是例子:
CREATE TABLE groups (
groupname text,
username text,
email text,
age int,
hash_prefix int,
PRIMARY KEY ((groupname, hash_prefix), username)
)
新的HASH字段hash_prefix,保存了用戶名的hash的前綴。比如第一個字節對4取模。和groupname一起,這兩個字段組合成了一個復合分區主鍵。和單個分區不同,現在它分布到4個分區中了。我們的數據更加均勻,但是我們需要讀取4次的分區。這是一個規則沖突的例子。你需要在它們間找到一個合適的平衡。
如果你有很多的讀操作,而且分組不會太大,那么將取模的值從4改為2是不錯的選擇。如果你有較少的讀取,但是單個分組會增長到很大,將4改為10會更好。
還有一些其他方法可以分割分區,我將會在下面的例子中介紹。
在我繼續之前,讓我總結一下這個數據模型:我們多次重復了用戶信息,每個group一次,你可能希望像這樣重建模型來減少重復的數量:
CREATE TABLE users (
id uuid PRIMARY KEY,
username text,
email text,
age int
)
CREATE TABLE groups (
groupname text,
user_id uuid,
PRIMARY KEY (groupname, user_id)
)
顯然,這最小化了重復。但是我們需要讀取多少分區呢?如果分組是1000個用戶,我們需要讀取1001個分區。這和從一個分區讀取數據相比是100倍的差距。如果要求讀優先,那這正不是一個好的設計。另一方面,如果讀不頻繁,但是修改(update)是頻繁的,這個模型還是有道理的。當你在設計數據庫的時候,一定要確定你的讀/寫頻率。
例3:用戶加入分組的時間
假設我們繼續之前分組的例子,但是新增一個需求,讀取分組中新增的前X個用戶。
我們可以用和之前有點相似的表:
CREATE TABLE group_join_dates (
groupname text,
joined timeuuid,
username text,
email text,
age int,
PRIMARY KEY (groupname, joined)
)
這里我們使用timeuuid(和時間戳很像,但是不會沖突)作為clustering column 。在一個分組中,行將按照用戶加入的時間順序排序。這允許我們獲取最新的用戶信息,像下面這樣。
SELECT * FROM group_join_dates
WHERE groupname = ?
ORDER BY joined DESC
LIMIT ?
這是非常高效的,我們在同一個分區中順序查詢數據。為了避免總是要用到order by joined desc,這會讓查詢效率降低,我們可以修改clustering order:
CREATE TABLE group_join_dates (
groupname text,
joined timeuuid,
username text,
email text,
age int,
PRIMARY KEY (groupname, joined)
) WITH CLUSTERING ORDER BY (joined DESC)
現在我們可以執行更高效的查詢了
SELECT * FROM group_join_dates
WHERE groupname = ?
LIMIT ?
我們之前的例子中,我們有個問題就是如果有一個分組太大,如何把數據均勻的分配到所有節點中。在那個例子里我們隨機的分割了數據到同的分區(取模)。但是這個例子不同,我們可以利用我們對查詢模板的認識來分區:用時間分區。
比如我們用date來分區(date應該類似於:2016-05-19,所以每天有個分區)
CREATE TABLE group_join_dates (
groupname text,
joined timeuuid,
join_date text,
username text,
email text,
age int,
PRIMARY KEY ((groupname, join_date), joined)
) WITH CLUSTERING ORDER BY (joined DESC)
我們再次使用了復合分區主鍵,但是這次我們用了加入時間。每天都有一個新的分區。當查詢最近的X個用戶時,會先找今天的分區,然后昨天,前天,直到我們有X個用戶。我們可能會在得到limit個用戶之前,查找多個分區。
為了減少分區的查詢,我們需要為分區指定一個時間范圍,這樣你就只需要查詢1-2個分區。比如我們平均每天有新3個用戶大致上,然后我們按4天一分區,這樣,你就可以在1-2個分區查完10個最新用戶。
總結
這里提到的最基本的數據模型規則涵蓋了現有的所有版本的Cassandra,並且在將來的版本中應該也是一樣的。一些其他的小的數據模型問題,比如如何處理墓碑(刪除的數據),一樣是需要考慮的,但是這些可能在將來的Cassandra版本中可能會改變。
除了這里提到的基本策略,一些Cassandra華麗的功能,像集合,用戶自定義數據結構,靜態字段,一樣可以在讀的時候減少分區的使用。在設計的時候不要忘了考慮這些選擇。
希望我在你們處理不同的數據庫設計的時候已經給予了一些有用的工具。如果你想了解更多,我建議閱讀:Datastax’s free, self-paced online data modeling course (DS220) (譯者:這是一個英文視頻教程)
https://academy.datastax.com/courses/ds220-data-modeling?dxt=blogposting
一路順風!