2010年8月27日 星期五

App Engine 上的時區問題

在 App Engine 上如果要儲存時間,在 model 中就會用 DateTimeProperty, DateProperty 或 TimeProperty 來做為資料型態,比如說:
...
from google.appengine.ext import db

class FooModel(db.Model):
  ...
  ...
  created = db.DateTimeProperty(auto_now_add=True)
DateTimeProperty 來說,它對應到的資料結構是 python 標準函式庫中的 datetime.datetime,不過,當你加上了 auto_now_add=True 這個設定之後,每當建立一個新的 entity 時,Datastore API 就會自動加上「現在時間」,也就是 datetime.datetime.now() 的資料,但是 App Engine 所使用的預設時區是 UTC(也就是 GMT+0),簡單地說,如果你的網站使用者都是台灣的用戶,那就會感覺到這個時間晚了八個小時(因為台灣的時區是 CST,也就是 GMT+8),如果要處理這個問題,這裡提供兩個可能的解決方法:

不要依賴自動產生的時間,DIY

第一個解決方法,就是在產生/儲存 entity 時不要讓 Datastore API 幫你產生現在的時間,每次在產生或是更新 entity 資料時,自己手動產生出正確的時間。在解決問題之前,首先要製作一個台灣時區的 tzinfo,才可以去改變 datetime.datetime 所表示的時區:

...
from datetime import tzinfo, timedelta

class TaiwanTimeZone(tzinfo):
    def utcoffset(self, dt):
        return timedelta(hours=8)

    def tzname(self, dt):
        return 'CST'

    def dst(self, dt):
        return timedelta(hours=0)

接著,在產生或更新 entity 資料時:
...
foo = FooModel(......, 
               created=datetime.datetime.now(TaiwanZone()))
foo.put()
這樣就會用台灣時區(GMT+8)的時間來儲存資料了。

在輸出時用 filter 換掉

另外一個方式,就是在 template 要輸出時再換掉時區,當然還是要準備一個 TaiwanTimeZonetzinfo 類別,然而在儲存資料時,還是使用預設的 UTC 時區去處理,但是可以自訂一個 template filter (詳細的作法請見這篇文章)在 template 中顯示時動態換掉時間及時區:
# get template register
register = webapp.template.create_template_register()

@register.filter
def twtz(value):
    from datetime import datetime, timedelta
    return (value + timedelta(hours=8)).replace(tzinfo=TaiwanTimeZone())
這樣只要在 template 中加上這個 filter 來顯示就可以了。

2010年8月21日 星期六

自訂範本系統中的 filter

在 App Engine 上製作網頁時,若是沒有使用 django 或是其它的 template 函式庫,應該都會直接使用 App Engine 所提供的 django template wrapper(用的是 django 0.96 版的 template),不過這就沒辦法(很簡單地)照著 django 所提供的方式自訂標籤及 filter。不過 App Engine 還是有提供自訂 filter 的方式,只要按照下列的步驟(假設你應用程式的目錄是在 $APP):
  1. 建立一個 Python 模組來定義 filters:
    # $APP/my_filters.py
    from google.appengine.ext import webapp
    
    # 取得 template filters register
    register = webapp.template.create_template_register()
    
    @register.filter
    def tolower(string_value):
        return string_value.lower()
    
    這樣一來便建立了一個自訂的 filter: tolower,等等便可以用在 template 中。
  2. 雖然建立好了自訂的 filter(s),但是 App Engine 的 template 函式庫還不知道有這個東西的存在,所以在使用範本引擎輸出前,記得加入下列的程式碼註冊你自訂的 module(s):
    ...
    from google.appengine.ext.webapp import template
    
    # 註冊自訂 filters 的模組(載入模組的名稱)
    template.register_template_library('my_filters')
    
  3. 完成以上步驟後,就可以在範本中像這樣來使用自訂的 filters:
    ...
    Description: {{ description|tolower }}
    

如此一來,便不必在 request handler 中預先處理輸出的內容,可以把這部份的程式碼用 filter 來解決。

2010年7月14日 星期三

使用 OpenID 作為帳號驗證

AppEngine 在 1.3.4 版本之後,開始實驗支援 OpenID 的身份驗證,除了可以在建立 app 時選擇使用 OpenID 作帳號驗證之外,也可以在後台設定。


建立 app 時可以選擇使用 Open ID


在管理後台的 Administration > Application Settings 中設定

設定好了之後,其實程式也不用修改太多,還是可以直接使用 google.appengine.api.users 模組中的函式來做身份認證,App Engine 已經實作了 Open ID 的規格,所以可以根據 Open ID 找出認證網址,但首先要在 app.yaml 檔案中加入 /_ah/login_required 的 URL 像是這樣:
(app.yaml)
...
- url: /_ah/login_required
  script: openid_login.py
...

然後在你的登入頁面中,將登入的動作導向 /_ah/login_required 這個 URL,而處理的程式就像這樣:

(openid_login.py)...
from google.appengine.ext import webapp
from google.appengine.api import users

class OpenIdHandler(webapp.RequestHandler):
    def get(self):
        ....
        # 使用者輸入的 Open ID URL
        openid_url = self.request.get('openid')
        # Open ID 認證結束後導向的 URL
        continue_url = '....'
        # 將使用者導向 Open ID provider 的認證網址:
        self.redirect(users.create_login_url(continue_url, None, openid_url))
...
如果 openid_url 是空值,則 App Engine 會利用 Google Account API 來完成認證。

當使用者用 OpenID 認證成功之後,就可以使用利用下面的方式來取得使用者的 OpenID 資訊:

...
from google.appengine.api import users
...
user = users.get_current_user()
if user:
    # 取得 openid identity
    id = user.federated_identity()
    # 取得 openid provider URL
    provider = user.federated_provider()
...

2010年1月7日 星期四

[Mac] 在 Snow Leopard 上開發 Google App Engine

Mac OSX 在 Snow Leopard (10.6) 之後,已經將預設的 python 設定為 2.6,不過系統還是有安裝 2.5 版本,所以開發基本上沒有什麼問題,只是要稍微作一些調整:

  1. 如果你的電腦還沒有安裝過 XCode(Mac 系統安裝光碟內及iPhone SDK 都有),必須要先安裝,讓系統有安裝編譯的工具

  2. 雖然系統內建了 Python 2.5,不過並沒有安裝 PIL 這個 Python 處理影像的函式庫,因為 App Engine 中的 image API 會用到 PIL,所以也要安裝這個函式庫。為了讓函式庫能支援 JPEG 檔案的處理,所以就要先來安裝 libjpeg。

  3. 首先到這裡下載 jpegsrc.v7.tar.gz 檔案,然後在文字模式下依照下列步驟編譯及安裝:

    $ tar zxvf jpegsrc.v7.tar.gz
    ....
    $ cd jpeg-7
    $ export CC=/usr/bin/gcc-4.0
    $ ./configure --enable-shared --enable-static
    $ make
    $ sudo make install

  4. 如果一切都很順利的話,那就可以到 PIL 網站下載 Python Imaging Library 1.1.6 Source Kit 原始檔案回來編譯:

    $ tar zxvf Imaging-1.1.6.tar.gz
    ...
    $ cd Imaging-1.1.6
    # 將 setup.py 檔案中找到 JPEG_ROOT 然後改成 JPEG_ROOT = "/usr/local/lib"
    $ /usr/bin/python2.5 setup.py build
    $ sudo /usr/bin/python2.5 setup.py install

如果一切都沒有問題的話,那應該就沒什麼問題了。只是記住當你在啟動 dev_appserver.py 時,要使用 /usr/bin/python2.5 來啟動,而不要使用 /usr/bin/python 以免用到 Python 2.6 版。

若是使用了 GoogleAppEngineLauncher.app 這個應用程式的話,可以在 Preferences... 中設定 Python 的路徑為 /usr/bin/python2.5

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

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