搜尋老魚筆摘(本網誌及所屬協作平台)

2010-07-05

[雲端計算] Cassandra 應用實例[轉譯]

本篇同樣是篇老魚轉譯而來的相關系列文選, 但老魚除了校譯和排版外, 加上了個人實作過程中的理解說明混於其中, 建議您閱讀本文前, 應當俱備的知識範圍:
  1. 曾玩過 Twitter 介面操作的經驗.
  2. 對 Cassandra 的基本管理與操作能力, 已對其 Data Model 有著初級理解, 當然相關的組態配置檔的位置和編輯能力是要俱備地.
  3. 對關聯型資料庫系統開發與 E-R Model 初步的應用經驗.
  4. 對程式設計能力有熱愛的研究精神.

如果您有輿趣, 可別錯過了本篇原文網址的討論串與後續原作者的新文章. 老魚希望有心的您可以好好的閱讀它, 當然如果能配合實裝 Cassandra 與裝載本實例的教材, 您將更加瞭解它. 世上沒有完美的IT系統, 但它可使您精進學習IT甚至突破創新!老魚一慣的習性用“紅筆字”, 劃上了自己認為的閱讀重點, 供各位參考, 看完本篇如果您有興趣深入, 可以再閱讀老魚先前 Blog 所整理的相關文章(按學習順序排列):

原文: http://www.rackspacecloud.com/blog/2010/05/12/cassandra-by-example/# 
原作者:Eric Evan, http://blog.sym-link.com/
原文發佈日期:May 12, 2010
簡體中文譯者:王旭
http://wangxu.me/blog/ , @gnawux), http://wangxu.me/blog/?p=383
翻譯時間:2010年5月15,25,26日
正體中文校譯:郭朝益, http://oss-tw.blogspot.com/

[雲端計算] Cassandra 應用實例[轉譯]

近來 Cassandra 備受矚目,很多人正在評估是否可以應用 Cassandra。由於這群人有著更積極的求知速度,相對的,我們的開發專案所提供的說明文檔就顯的過於粗淺了。在這些文檔中,最不足的是為有關聯型資料庫基礎的人解釋有關 Cassandra 資料模型的部份。

Cassandra 資料模型實際和傳統的資料庫差異非常大,足夠讓人眩暈,而且很多誤解都需要修正。有些人把這個資料模型描述成存放 map 的 map,或對於 super column 的應用場景,視為是存放 map 的 map 的 map。這些解釋經常用類似 JSON 標記的視覺輔助展示方法來進行佐證。其他人則把列族(column family, cf)看做是係數表,還有人把 column family 看作是存放列物件的集合容器。甚至有人有時把列(column)看成是三元組。我覺得所有這些解釋都不夠好。

問題在於很難去用推類比擬的手法來確切解釋一個新的東西,而且如果比較的不準確的話常常把人搞糊塗。我仍然期望有人能解釋清楚這個資料模型,但同時我覺得確切的例子可能更容易說明白一些。

Twitter


儘管 Twitter 本身就是 Cassandra 的一個實際的應用場景,它仍然是一個不錯的教學實例,因為它眾所周知而且易於抽象。在例子中,和很多網站一樣,每個使用者都有一份使用者資料(顯示名稱、密碼、email等),這些資訊鏈接到朋友(譯註:使用者 follow 的人)和 follower(譯註:follow使用者的人)。此外,如果沒有那些短 tweets 的話也就不是 twitter 了,tweet 每條 140 個字符,它們都關聯著諸如時間戳記和唯一的 id 等諸如此的元資料,這個 id 我們可以從 URL 裡看到。

現在我們在一個關聯資料庫裡來直接進行建模,我們首先需要一個表來存放使用者。
CREATE TABLE user (
    id INTEGER PRIMARY KEY,
    username VARCHAR(64),
    password VARCHAR(64)
);

我們還需要兩張表來存儲一對多的 follow 關聯。
CREATE TABLE followers (
    user INTEGER REFERENCES user(id),
    follower INTEGER REFERENCES user(id)
);
 
CREATE TABLE following (
    user INTEGER REFERENCES user(id),
    followed INTEGER REFERENCES user(id)
);

顯然,我們還需要表來存儲 tweets。
CREATE TABLE tweets (
    id INTEGER,
    user INTEGER REFERENCES user(id),
    body VARCHAR(140),
    timestamp TIMESTAMP
);
由於僅僅是個例子,我已經極大簡化了情況,但僅僅是這個極度簡化的模型,也還有很多需要做的工作。例如,要以可行的方法達到資料歸一化就需要一個外部鍵(FK)值的約束限制,而因為我們需要從多張表 join 資訊,我們需要對任意值建索引,以保證其高效。

但是讓一個分散式系統正常工作相當有挑戰性,幾乎不可能不做任何折衷。對 Cassandra 來說也是如此,而且這也是為什麼上述資料模型對我們來說是無法工作的的原因。對於入門者,沒有可供參考的完整性,缺乏次索引使得 join 很難進行,所以,你必須反歸一化。另一方面,你被迫思考你要進行的查詢的方式和期望結果,因為這差不多就是資料模型看起來的樣子。

Twissandra


那麼如何把上述模型翻譯到 Cassandra 中呢?十分幸運,我們只需要看看 Twissandra,這是 Eric Florenzano 寫的一個 Twitter 的簡化版克隆版,來作為例子。那麼讓我們來使用 Twitter 和 Twissandra 作為例子來看看 Cassandra 的資料模型是如何地。

程式範例下載(Git, Python 要求)
git clone git://github.com/ericflo/twissandra.git

Schema (架構網要)

Cassandra 是一種無 schema 的資料存儲方式,但為你的應用做一些特定的組態配置還是必的。Twissandra 給出了一個可以工作的 Cassandra 組態配置,不過研究一下關於資料模型方面的組態配置還是物超所值的。
Keyspaces (鍵值空間)
Keyspaces 是 Cassandra 中最頂層的命名空間。在未來版本的 Cassandra 中,將可以動態創建 keyspace,正如在 RDBMS 中創建資料庫一樣,但是對於 0.6 和以前的版本,這些都在主要組態配置檔中定義,如:

 <Keyspaces>
  <Keyspace Name="Twissandra">
  ...
  Keyspace>
Keyspaces>




Column Families, CF (列族)
對於每個 keyspace,都可以有一個或多個列族(column family, cf)。column family 是用於關聯類型相近的記錄的命名空間。Cassandra 在寫入作業時,在一個 column family 內部允許有記錄(record)級的原子性,對它們進行查詢非常高效。這些特性十分重要,在進行你的資料建模前必須記牢,其它細節我們會在下面討論到。和 keyspace 類似,列族也在主要組態配置文件中定義,雖然在將來的版本中你將可以在系統運行期間就可創建列族,正像在RDBMS 中創建資料表一樣。

<Keyspaces>
  <Keyspace Name="Twissandra">
    <ColumnFamily CompareWith="UTF8Type"  Name="User"/>
    <ColumnFamily CompareWith="BytesType" Name="Username"/>
    <ColumnFamily CompareWith="BytesType" Name="Friends"/>
    <ColumnFamily CompareWith="BytesType" Name="Followers"/>
    <ColumnFamily CompareWith="UTF8Type"  Name="Tweet"/>
    <ColumnFamily CompareWith="LongType"  Name="Userline"/>
    <ColumnFamily CompareWith="LongType"  Name="Timeline"/>
  Keyspace>
Keyspaces>

需要指出的是,上面的組態配置片段中,指定名字的時候同時指定了一個比較者類型。這凸顯了 Cassandra 和傳統資料庫的又一個重大不同,記錄按照設計的順序存儲,在之後不能輕易改變。
這些列族(CF)都是什麼?
一下子看到所有的七個 Twissandra 列族(column family, cf)是做何用途地可能不那麼直觀,所以,我們來逐個仔細看一下:
  • User
    • User 用於儲存使用者資訊,大致相當於本文最上面描述的使用者資料表。cf 中的每條記錄以UUID 為鍵(key)值,並且包含使用者名稱和密碼列。
  • Username
    • 在 User 列族中查詢一個使用者需要知道使用者的鍵值,但從使用者名稱怎麼找到這個 UUID 鍵值呢?在上面描述的 SQL 關聯式資料庫裡的話,我們就在 User 表裡來一個匹配使用者名的SELECT 語句(WHERE username = 'jericevans')就行了。但這對於 Cassandra 來說卻不可能。
    • 首先,關聯式資料庫可以順序地掃瞄全表來進行這樣一個 SELECT,但由於記錄是基於鍵值分佈在 Cassandra 叢集中的,這個匹配將可能會在多個節點上進行,可能是很多節點。而且,即使是資料就在一個節點上,仍然有一個原因會讓這一作業遠沒有關聯式資料庫效率高,因為關聯式資料庫可以對 username 列有索引(index)。前面提到過,Cassandra 在當前是不支持第二索引的。
    • 解決方案就是:建立一個我們自己的反向索引,進行使用者名稱到 UUID 鍵值的映射,這就是Username CF 的用途。
  • Friends
  • Followers
    • Friends 和 Follower CF 可以回答這些問題:使用者X follow 了哪些人? 誰 follow 了使用者X? 這兩個 CF 的鍵值都是這個唯一的使用者 ID,其中包含了哪些有 follow 關聯的使用者以及它們創建的時間。
  • Tweet
    • Tweet CF 用於存放所有的 tweets。這個列族以每個 tweet 的 UUID 為鍵值,還包含了使用者id,tweet 內容以及 tweet 時間等有關的列。
  • Userline
    • 這是屬於每個使用者的時間軸線。記錄的鍵值是使用者的 ID,其他的列中,包含有一個數字時間戳記到 Tweet CF 中的 tweet ID 的映射(map)。
  • Timeline
    • 最後,Timeline CF 類似於 Userline,只是這裡存儲著每個使用者的朋友的 tweet 的時間軸線視圖(View)。
有了上面這些 CF,現在我們可以來看一些常用的作業都是如何發生的。

把這些列族(CF)放在一起來試一下

添加一個新使用者
首先,新使用者需要一個方法來註冊一個帳戶,當他們註冊的時候,組要將他們增加到 Cassandra 資料庫中去。對於 Twissandra,我們來看看裡面的內容:(Client 實作以 Python - Pycassa 為例)

username = 'jericevans'
password = '**********'
useruuid = str(uuid())
 
columns = {'id': useruuid, 'username': username, 'password': password}
 
USER.insert(useruuid, columns)
USERNAME.insert(username, {'id': useruuid})

Twissandra 是用 Python 寫成的,使用 Pycassa 作為存取 Cassandra 的客戶端,上述大寫的 USER 和 USERNAME 是 pycassa.ColumnFamily 的實例,它們需要在使用之前的某個位置被分別初始化。

這裡說明一下,這不是從 Twissandra 裡原樣摘出來的。我讓他們更加簡單而且是自包含的。比如,在上面的例子中,如果沒有對使用者名和密碼的賦值的話,可能不那麼好理解,不過一個 web 應用只能從使用者註冊表單裡得到這些內容。

從這個例子中回來,有兩個不同的 Cassandra 寫入作業“insert( )”,第一個創建了一個使用者 CF,另一個更新了使用者名到使用者 UUID 鍵值的反向映射(map)表。在兩個例子中,參數都是用於查找記錄的鍵值,以及包含列名和值的 map。
Following 一個朋友
frienduuid = 'a4a70900-24e1-11df-8924-001ff3591711'
 
FRIENDS.insert(useruuid, {frienduuid: time.time()})
FOLLOWERS.insert(frienduuid, {useruuid: time.time()})

這裡我們再來兩個不同的 insert() 作業,這次是加入一個使用者到我們的朋友列表,並加入反向關聯:給被 follow 使用者增加一個 follower。
發出 Tweet
tweetuuid = str(uuid())
body = '@ericflo thanks for Twissandra, it helps!'
timestamp = long(time.time() * 1e6)
 
columns = {'id': tweetuuid, 'user_id': useruuid, 'body': body, '_ts': timestamp}
TWEET.insert(tweetuuid, columns)
 
columns = {struct.pack('>d'), timestamp: tweetuuid}
USERLINE.insert(useruuid, columns)
 
TIMELINE.insert(useruuid, columns)
for otheruuid in FOLLOWERS.get(useruuid, 5000):
    TIMELINE.insert(otheruuid, columns)

要存儲一條新的 tweet,我們需要使用一個新的 UUID 作為鍵值,在 Tweet 列族創建一個記錄,其中的列包含作者的使用者 ID,創建的時間,當然還有 tweet 的文本內容本身。

此外,使用者的 Userline 中也要加入 tweet 的時間和它的 id。如果這是使用者的第一條 tweet 的話,這個insert() 會產生一條新的紀錄,後面的只是為這條記錄增加新列。

最後要給發出 tweet 的使用者和其他 follower 的 timeline 列族添加這條 tweet 的 ID 和時間。

值得注意的一件事是,這裡,時間戳使用的是 64位元長整型變數,而當它成為一個列的名字的時候,它會被包裝為網絡位元組序的二進制值。這是因為 Userline 和 Timeline CF 使用了一個 LongType Comparator,允許我們使用數值區間指定查找指定範圍,所以它們被按照數值來存放起來。
接收一個使用者的 tweets
timeline = USERLINE.get(useruuid, column_reversed=True)
tweets = TWEET.multiget(timeline.values())

接收一個使用者的 tweet,首先從 Userline 獲取tweet ID 的一個列表,然後從 Tweet CF 通過 multiget()方法來讀取這些 tweet。得到的結果將是通過著數值表示的時間戳記逆序排列的,因為 Userline 使用了LongTyper comparator,並且 reversed 設置為了 True。
獲取一個使用者的時間線
start = request.GET.get('start')
limit = NUM_PER_PAGE
 
timeline = TIMELINE.get(useruuid, column_start=start,
column_count=limit, column_reversed=True)
tweets = TWEET.multiget(timeline.values())

和上一個例子類似,這次是從 Timeline 讀取 tweet ID,不過這次我們還使用了 start 和 limit 來控制讀取列的範圍。這樣有助於輸出結果的分頁。

那麼,下一步呢?

希望這篇足夠提供給你一個大致的概念。重複一下,我從程式碼中提取了一些例子,為了簡明起見,在撰寫本文時略去了一些作業,所以現在你可能是 check out 出 Twissandra 的原始碼並進行下一步深入研究的好時機了。有很多功能,諸如 retweet 和 lists,都還空著沒有實作,你可以作為一個練習的起點。如果你已經熟悉 Python 和 Django 的話,那你可以考慮實作一下這些方法。

Cassandra 的 wiki 包含了大量的資訊,而且還在不斷增多,還包括一個即時更新的眾人貢獻的 文章與投影片 的列表。

如果你喜歡 IRC 的話,你可以加入 irc.freenode.net 的 #cassandra 頻道,來和來自全球對 Cassandra 社群者們聊聯天,他們總是熱衷於提供協助和回答詢問的問題。如果你更青睞 email 的話,cassandra-user 郵件列表上也有很多可以提供協助的人。

沒有留言:

張貼留言

熱門文章

大智若魚::人生處處是道場-站內SEO參考標籤雲