2009年8月10日 星期一

減輕資料查詢、更新的負擔

在 Google App Engine 上開發應用程式,很多人會面臨到使用額度(Quota)的問題,所以開發者在將應用程式上線後,必須不斷觀察應用程式的存取狀況,以便隨時調整應用程式,避免某一項使用額度過份衝高。

這篇文章要介紹的是--儘可能地減少資料查詢、更新的動作,因為 Datastore API 的呼叫次數有限制,而且一個 request 的處理時間也有 30 秒的限制,如果在操作資料時沒有注意到一些細節,可能就會碰到問題。

平行讀取、更新或刪除 data entities


在「避免資料寫入衝突」這篇文章中,提到可以使用 App Engine 所提供的 memcache API 來作資料快取,這樣也是一種減少呼叫 Datastore API 的策略。

除此之外,善加利用每個 data entity 的 key 也可以達到減少呼叫 Datastore API 的目標。例如以下這段程式碼:
...
# 修改許多 data entity
for product in products:
product.price = product.price * 1.1
product.put()
...

在這個例子中,products 中有多少個 entities,就會呼叫多少次 put(),但若是將上述的程式碼修改為:
...
from google.appengine.ext import db
# 修改許多 data entity
for product in products:
product.price = product.price * 1.1
db.put(products)

由於 db.put() 函式支援 list 資料型態的參數,所以這樣的作法僅會算作一次 Datastore API 的呼叫,比起上述的方式大大節省了 Datastore API 的呼叫次數。

讀取資料時也是以此類推,若原本的程式為:
...
products = []
# keys 為一個 key list
for key in keys:
products.append(db.get(key))

也可以改寫成:
...
from google.appengine.ext import db
# keys 為一個 key list
products = db.get(keys)

同理可證,若要同時刪除許多 entities,也可以使用 db.delete

有效率地使用 GQL


另外,若是要使用 GqlQuery 作資料查詢時,若是只需要取出 entities 的 key 值,在 GQL 查詢語句中僅需取出 __key__ 欄位即可:
...
from google.appengine.ext import db

product_keys = db.GqlQuery('SELECT __key__ FROM Product WHERE title = :title', title='...')
# 或是 product_keys = Product.gql('WHERE title = :title', title='...', keys_only=True)

如此一來,讀取的時間就會比讀取全部的欄位還要快許多(如果資料量夠多的話...)

另外,若是查詢語句會重複使用,可以將該語句建立成一個 GqlQuery 物件後,再利用 bind() 方法重新利用該查詢語句。例如本來的程式碼可能是:
...
from google.appengine.ext import db

conditions = [['x', 'y'], ['1', '2'], .....]
for cond in conditions:
query = db.GqlQuery('SELECT * FROM Foo WHERE first = :first, second = :second', first=cond[0], second=cond[1])
....

如此一來,迴圈每執行一次就會建立一個 GqlQuery 物件,資料查詢會變得很沒有效率,如果碰到這樣的情況,程式碼應該改寫為:
...
from google.appengine.ext import db

conditions = [['x', 'y'], ['1', '2'], .....]
prepared_query = db.GqlQuery('SELECT * FROM Foo WHERE first = :first, second = :second')
for cond in conditions:
query = prepared_query.bind(first=cond[0], second=cond[1])
....

如此便能重複利用 GqlQuery 這個物件了。

2009年8月5日 星期三

使用 Eclipse + PyDev 開發 Google App Engine 專案

在 Windows 上開發應用程式,大多數的開發者會藉著使用 IDE 來輔助開發,簡化一些設定、啟動或測試等步驟。而在 Windows 上若要開發 Google App Engine 的應用程式,使用 Eclipse 作為 IDE,並且搭配 PyDev 這個 Eclipse 的 plugin,會簡化許多開發的設定,尤其是最新的 PyDev 甚至還直接支援了 Google App Engine 專案呢!以下就為各位做個簡單的介紹。

安裝及設定


在開始之前,先確定您的 Windows 環境已經安裝了 Java SDK (JDK) 以及 Python (目前 Google App Engine 僅支援 Python 2.5.x) 。Java 是為了執行 Eclipse,而 Python 當然就是為了用來執行 Google App Engine 的專案囉。

首先,到 Eclipse 的官方網站下載 Eclipse Classic 3.5.0



下載回來後,將 zip 檔案解壓縮,執行 eclipse 目錄中的 eclipse.exe 便可以開啟 Eclipse:



開啟 Eclipse 之後,首先將環境的設定作點修改,從功能表列的 Window » Preferences 進入設定畫面,首先設定用空白取代 tab 字元,因為 Python 對於程式碼的縮排有嚴格的一致性,所以為了避免不必要的煩惱,在 General » Editor » Text Editors 的設定中,將 tab 取代為 4 個空白字元



除此之外,也將由 Eclipse 所建立的專案,調整成預設使用 UTF-8 作為字元編碼,以及使用 UNIX 換行字元




為了讓 Eclipse 能夠作為開發 Python 專案的 IDE,此時還需要安裝 PyDev 這個 plugin,可以從PyDev 的官方網站上看到安裝 URL(如:http://nightly.aptana.com/pydev/site.xml),將這個 URL 複製下來,回到 Eclipse,到功能表列的 Help » Install New Software...,將剛才複製的 URL 貼在 Work with: 的文字框中,並按下 Add 按鈕:



接著就是勾選 PyDev 然後將它安裝完成,安裝完畢後,Eclipse 便會請你重新啟動或是套用變更將 plugin 完成整合到 Eclipse 中。



安裝完 PyDev 之後,別忘了先設定 PyDev,讓它瞭解 Python 被安裝在哪裡:



建立 Google App Engine 專案


設定完 Python 之後,建立新專案時,就有 PyDev Google App Engine Project 可以選擇了:



輸入專案名稱,以及別忘了選擇正確的 Python 版本:



接著選擇 Google App Engine 的安裝位置:



最後就是填入你的 application ID 及專案的範本:



如此一來專案就建立完成了。

執行及部署


當你的程式寫完,想要啟動開發用伺服器來作測試時,在專案視窗中的 src 目錄上按下右鍵,選擇 Run as... 就有 Run: Google App 可以選擇了:



如果要將應用程式部署至 Google App Engine 上,則一樣在 src 目錄上按下右鍵,選擇 PyDev: Google App Engine 就有 UploadManage 可以使用。



這樣是不是簡單多了呢?祝各位開發愉快 :-)

2009年7月22日 星期三

避免資料寫入衝突

在使用 App Engine 時,常常會建立 model 來作 sequence(如:手動配置資料的 id)、counter(計數器)之類的工作,以前面文章提到的 counter 為例,如果 Counter 在 datastore 中的 data entity 只有一個,每次新增資料時,程式都會到同一個地方作資料寫入(update 也是一種 write)的動作,如果新增資料的頻率很高,那就很容易發生寫入衝突(write contention)而使得程式的效能低落。

試想你在作一個類似 Twitter 的網站,使用者增加訊息的速度是非常驚人的,如果只使用單一個 data entity 來儲存、更新計數器,那每則訊息的更新時間就會變長,因為都在等待更新計數器的動作。如果你曾經學習過用在磁碟機上的 RAID 技術,那便可以借鏡 RAID 5 規格中分散資料擺放的方式,使得各個讀寫資料的動作可以同時進行。

以計數器為例,可以試著把計數器拆開成好幾個碎片(shard),在更新計數器時,可以只要隨便選一個碎片來更新就可以了,如果要取得完整的數值,只要把各碎片的值加總就得到答案了。

這樣的概念,程式要怎麼寫呢?首先把 Counter 的定義修改成這樣:
from google.appengine.ext import db

class Counter(db.Model):
name = db.StringProperty(required=True)
count = db.IntegerProperty(required=True, default=0)

增加了一個 name 欄位,這是為了標記碎片是屬於哪一個計數器,也就是 name 欄位相同的碎片就是屬於同一個計數器。有了這樣的 model 之後,更新計數器的程式碼可以改寫成這樣:
...
import random

shard_name = 'product_counter_%d' % random.randint(0, 3) # 假設我分成4個碎片
counter = Counter.get_by_key_name(shard_name)
if counter is None:
counter = Counter(key_name=shard_name, name='product_counter')
counter.count += 1
counter.put()

上面的例子是我預計將計數器切成 4 個碎片,然後每個「碎片 entity」的 key_name 分別為 product_counter_0, ..., product_counter_3,於是便能夠直接根據 key_name 將碎片的 entity 取出,若是碎片不存在就立刻產生一個新的 entity,而新的 entity 除了要設定 key_name 之外,最重要的就是要記得標上 name 欄位的值,因為當我們需要加總計數器時,就是靠著這個欄位來取出各個碎片。最後則是將 count 欄位的值加 1 後儲存,完成更新計數器的動作。

接下來要處理的就是加總的動作囉,因為可以藉著 name 欄位來查詢,所以程式也變得很簡單:
...
counters = Counter.all().filter('name =', 'product_counter')
total = 0
for counter in counters:
total += counter.count

所以 total 的值就是計數器正確的數值了。

改善效能


上面的例子雖然改善了寫入衝突的問題,但光是這樣作,除了每次讀取計數器數值時都要做一次查詢(耗費 Datastore API 呼叫次數)以外,碎片如果太多也會增加計算時間,為了改善這個問題,可以為計數器作 cache。

Google App Engine 有提供 Memcache 的服務,我們可以直接拿它來作為計數器的 cache,首先從計數器的加總下手,一旦數值被計算出來後就放進 cache,如此一來,讀取計數器數值時就可以先從 cache 讀取,不在 cache 裡才去作資料查詢:
# 讀取計數器總數時,使用 cache
from google.appengine.api import memcache

counter_name = 'product_counter'
total = memcache.get(counter_name) # 這裡用 counter_name 作為 cache key
if total is None:
total = 0
counters = Counter.all().filter('name =', counter_name)
for counter in counters:
total += counter.count
memcache.set(counter_name, total)


那當計數器被更新的時候怎麼辦?除了更新 entity 之外,若是計數器的 cache 還存在,可以直接把 cache 裡的值加 1,這樣計數器的總數就不必重新計算了:
# 更新計數器時,別忘了更新 cache
from google.appengine.api import memcache
import random

counter_name = 'product_counter'
shard_name = 'product_counter_%d' % random.randint(0, 3) # 假設我分成4個碎片
counter = Counter.get_by_key_name(shard_name)
if counter is None:
counter = Counter(key_name=shard_name, name=counter_name)
counter.count += 1
counter.put()

# 更新 cache
total = memcache.get(counter_name)
if total:
memcache.incr(counter_name) # 使用 incr 函式直接對數值資料加 1

這樣就不必擔心更新了計數器,卻忘記更新快取了。

2009年7月6日 星期一

支援交易運算的計數器

支援交易運算


在許多資料庫運算中,為了保持相關的資料庫運算結果能夠不被分割地將同時送進資料庫(也就是一旦有一個操作發生失敗,則整串操作都會同時取消),所以提供了「交易」(transaction)的運算。

Google App Engine 上的 datastore 當然也支援「交易」運算,在上述的例子中,共有兩個資料庫運算--Product entity 的寫入以及 Counter entity 的更新,試著想像這兩個操作若各自(或同時)發生錯誤時會有什麼問題,若是 Product entity 在寫入時發生錯誤,程式就中斷了,那就沒什麼太大的問題,若是 Product entity 成功寫入 datastore,但 Counter entity 在更新時發生錯誤,這時就會造成計數器的統計數量不一致,這時候就適合使用交易運算了。

交易運算的限制


在 Google App Engine 中,交易運算只能操作相同 entity group 的資料,如果我們要將 ProductCounter 的新增動作在同一個交易運算中完成的話,就必須讓它們成為同一個 entity group。

要成為同一個 entity group,不管是 Product 還是 Counter 都需要一個共同的 parent,因此需要一個額外的 data model 來作為這個 entity group 的 root,這裡我提供一個 Index 的 model,順便用來作為每一筆 Product 資料的「序號產生器」 :p
class Index(db.Model):
max_index = db.IntegerProperty(required=True, default=0)

有了這個 model 後,新增 Product 及更新 Counter 的動作就可以改寫為:
# 新增 product 的程式片段...
...
# 從 Index 中取出 Product 的 index
ind = Index.get_by_key_name('product')
if ind is None:
ind = Index()
ind.max_index += 1
ind.put()

# 新增 product entity 並設定 parent 為 ind
p = Product(parent=ind, key_name="product_%d" % ind.max_index,........)
p.put()

# 根據 key_name 取得 counter,並且指定 parent
counter = Counter.get_by_key_name('product_counter', parent=ind.key())
if counter is None:
# 如果 counter 不存在,則建立一個新的,別忘了指定 key_name 及 parent
counter = Counter(parent=ind, key_name='product_counter')

counter.count += 1
counter.put()

在新增 ProductCounter 時,都加上指定 parent 的參數,以此將這些資料建構成一個 entity group,然而,一旦資料在 entity group 中,在取出時也要加上 parent 參數。

作成交易運算的函式


因此,我們可以把上面的程式碼包成一個函式,比方說是 create_product,這樣就可以利用 Datastore API 中的 run_in_transaction 函式來作成交易運算了。

....
def create_product():
# 放入上述的程式碼

....
# 新增資料時...
from google.appengine.ext import db
db.run_in_transaction(create_product)


如此一來,只有當 Index, ProductCounter 的資料操作都成功時,更新的資料才會進入 datastore 中。

2009年6月18日 星期四

資料計數器

Datastore API 的限制


在使用 Datastore API 時有個限制,就是每作一次 query ,最多能存取到的資料數量為 1000 筆(註1),也因為這個限制,所以也不能直接使用 Query 物件下的 count() 方法來計算資料數量。

舉例來說,如果要設計一個網路商店,每一件商品是一個 Product entity,假如網站中的商品數量超過 1000 筆,那這樣的程式碼:
from google.appengine.ext import db

class Product(db.Model):
...
# product 各欄位

...
query = Product.all()
number_of_products = query.count()

並不會幫你統計出究竟有多少 Product entity,因為 query 的結果最多就是 1000 筆資料。

手動記錄


為了解決這樣的問題,其實可以自己定義一個「計數器」的資料,用來統計究竟有多少資料,於是立刻可以寫出這樣的 data model:
from google.appengine.ext import db

class Counter(db.Model):
count = db.IntegerProperty(required=True, default=0)

有了這樣的資料模型,便能夠用來統計網站中任何資料的數量。我們可以經由不同的 key_name 來區分出不同的計數器,根據上述的例子,便可以在新增 Product 時,在計數器上加1以便統計。
# 新增 product 的程式片段...
...
p = Product(........) # 新增 product entity
p.put()

# 根據 key_name 取得 counter
counter = Counter.get_by_key_name('product_counter')
if counter is None:
# 如果 counter 不存在,則建立一個新的,別忘了指定 key_name
counter = Counter(key_name='product_counter')

counter.count += 1
counter.put()

以此類推,當要刪除一個或多個 Product entities 時,也要同步更新對應的計數器資料,以保持資料數量的一致性(consistency)。



  1. 之後會介紹如何讀出超過1000筆的資料

屬性中的 indexed=False

如果你的程式中,有個 data model 的 property 很多,那資料在存取時會隨著 property 數量的成長而降低效能(尤其是發生在呼叫 put() 來儲存資料時),這是因為在儲存或更新資料時,datastore 也會針對這些 property 作一些 index 的動作,使得整個操作的時間變長。

如果確定不會用某個 property 來作資料查詢 filter 的條件,那麼可以在定義 property 時加上 indexed=False 參數,提示 datastore 這個 property 不要做 index,範例程式如下:
from google.appengine.ext import db

class Foo(db.Model):
....
bar = db.StringProperty(indexed=False)

2009年6月15日 星期一

為儲存的內容加上標籤(簡單版)

現在很多網站都會為儲存的內容加上標籤,像 Flickr 的每一張相片都可以設定標籤:



或是像 delicious 的書籤也可以設定標籤:



在 App Engine 上開發應用程式,如果要為儲存的內容加上標籤的支援,那麼在資料模型(Data Model)的定義時,可以加上一個 property:
from google.appengine.ext import db
class MyContent(db.Model):
....
tags = db.StringListProperty()

如此一來,tags就可以儲存由字串所組成的 list資料,也就是可以儲存 MyContent 物件的標籤。

假設使用者透過表單送出標籤的資料,而標籤的資料是一個以逗號(,)隔開的字串,那麼要儲存標籤的作法就是:
....
tags_string = self.request.get('tags')
content = MyContent()
....
content.tags = map(lambda x: x.strip(), tags_string.split(','))
content.put()
....

雖然 tags_string.split(',') 已經產生一個由字串所組成的 list 了,但是使用者輸入的字串,可能會在逗號的前後留下空白,所以用了 map 函數將 list 中的每個元素空白消掉(strip 函數)

如果要取出含有某個標籤值(如:food)的內容,則可以直接使用 GQL 中特殊的語法來取出:
....
query = MyContent.gql('WHERE tags = :1', 'food')
for content in query:
....

因為 GQL 的設計,雖然 tags 欄位是一個 list,但是只要比對的元素有出現在 list 中,則 = 運算的結果就會是 True,所以就能夠輕易地根據 list 中的元素來查詢資料。

本部落格的宗旨

筆者因為深感 Google App Engine 的中文討論資源不足,所以打算把自己的研究整理成專門的 blog 來發表,順便與同好們互相討論學習。

未來這個部落格將會不定期更新一些在 Google App Engine 上撰寫程式的技巧及範例,主要會以 Python 版本為主,若有筆誤或是觀念錯誤的部份,還請各位讀者不吝賜教。