Click here to check if anything new just came in.
July 24 2010
Stashboard
Stashboard, 類似 Google 的 Apps Status Dashboard 及 Amazon 的 AWS Service Health Dashboard 的 Status page。
UI 看起來很不賴, 用 Python 在 Google App Engine 上面跑。 所以只需要下載 source code 回來改一下 app-id 就可以 deploy 上 GAE, 只需要不到一分鐘就可以架起來了。 而且有 REST API 喔!
不過我本來以為有自動監測這種功能說 XD
June 10 2010
在 app engine 使用 django
因為平常就是寫 django, 然後又對 app engine webapp 的 request handler get/post 分離有點感冒, 不過我想這是習慣問題吧, 所以後來都把 webapp 寫的東西都換成 django。
以前有看過一些文章說是把 django zip 起來, 然後用 zipimport 來跑 django, 不過其實除非你要用的是 django trunk, 否則 app engine 其實本身就內建了 0.96, 1.0 跟 1.1, 當然這邊指的是雲的那一端, 本地端還是得裝上要使用的 django 版本。
像是下面這段, 即使你使用的是 webapp, 你仍然可以透過下面這段 code, 寫在 request handler 的最前方, 來使用 django 1.1 的功能, 而非老舊的 django 0.96
from google.appengine.dist import use_library
use_library('django', '1.1')
os.environ['DJANGO_SETTINGS_MODULE'] = 'myaudiocast.settings'
至少 django 從 1.0 開始, template 的功能就大幅改進, 光 with 跟 escape_js 這兩個 template tag 就夠棒的了!
其實 google 「app engien django」就可以找到一堆教你如何在 app engine 使用 django 的文章, 不過我還是覺得 Google App Engine Helper for Django 最方便。只需要下載下來解開, 幾乎不用修改就可以開始使用。
而 Google App Engine Helper for Django 預設是使用 django 1.0, 不過只要修改 main.py 裡, 下面這一段就可以使用 django 1.1 了
InstallAppengineHelperForDjango('1.1')
當然改用 django 1.0 以上的版本, 除了 template 之外就是 form 也改進了很多, 雖然原本 app engine 就已經有提供 djangoforms 可以直接透過 datastore 的 model 來產生 form, 但是在 webapp get/post request 分開處理的狀況下, django form 在使用上顯得非常憋腳, 而且 djangoforms 用的是 django 0.96 的 form, 所以功能上的確差了許多。
以上是我對在 app engine 上使用 django 的感想。
June 09 2010
app engine datastore 效能心得
由於這一陣子用 app engine 寫了一些玩具跟 myaudiocast, 所以來記錄一下心得。雖然 app engine 不用錢, 似乎不用怕資料庫 scale 的問題, 可是比起一般使用關聯式資料庫來說, 開始 run 之前得更仔細規劃, 因為有些資料在寫入後就無法更動了。
上面這張圖是 myaudiocast 簡單的示意圖, 接下來就用這張圖來說一下關於 datastore 使用上的心得
單筆資料 ( one-to-one )
像是 User 對 Podcast 在我的規劃上是 one-to-one, 所以如果是 one-to-one 這類的資料就給定 key_name, 直接用 key_name 來抓單筆資料, 而不是先用 filter 之後在用 get 來取得單筆資料。 因為用 appstats 就發現後面這個方法常常是花最多時間的, 然而因為之前的資料都沒給 key_name 現在也來不及了 …
多筆資料 ( one-to-many, many-to-many )
像是 Episode 這類 one-to-many, 會有多筆的資料, 沒事就不用給 key_name 了, 因為通常都是透過 filter 來取出多筆資料, 所以只需要寫入時指定 parent, 讓 user 的 episodes 可以成為一個 entity group, 這樣抓取資料比較快。
盡量使用 memcache
datastore 其實並沒有很快, 甚至 model api 沒有 query cache, 而 memcache 的 quota 其實滿大的, 所以要盡量用 memcache。
避免在 template 透過關連取資料
像是 podcast 跟 episodes 都可以透過關連, 像是 user.podcast 跟 user.episodes 來取得資料, 所以因為寫 django 的經驗, 我可能就直接丟 user 變數進 template, 然後都用 user.podcast, 跟 user.episodes 來取得資料, 可是這在 app engine 則是要避免的, 因為 app engine 的 model 並沒有 query cache, 所以可能會像下面這樣的作法, 就會對 podcast 做兩次 query。
{{ user.podcast.title }}
{{ user.podcast.description }}
所以得一開始就把 user 跟 podcast 丟進 template, 直接取得資料而不是透過物件關連。
不過有時候就是一定得透過關連來做, 那麼就請使用 django 1.0 以上的 template, 透過 with 這個 template tag 來避免多次 query, 或者是在 model 新增一個 property 然後用 memcache 來解決, 像是我的作法
class User(db.Model):
...
@property
def podcast(self):
cache_key = 'user-podcast-%s' % self.key()
p = memcache.get(cache_key)
if not p:
p = self.podcasts.ancestor(self).get()
memcache.add(cache_key, p)
return p
先前說過, 以前不懂忘記給 key_name, 現在只能這樣 query 了, 頂多加上個 ancestor (默)
結論
datastore 雖然不太需要擔心 scale 的問題, 可是在 run 之前卻得更仔細規劃, 否則效能低落, 花的只是更多錢阿 XD 而 memcache 則是一定得用, 否則跑起來的確是不夠快。
大概是這樣, 希望沒有哪裡有寫錯的, 如果有的話, 也請多多留言指指教, 謝謝 :p
June 07 2010
app engine 效能檢測
先前 app engine 推出了 appstats, 一整個做的超好, 而且很簡單! 很容易就知道那邊吃掉比較多資源。
按照文件把 appstats 裝好 run 起來後, 就可以在 app engine 後台的 log 上看到 appstats 添加的 link, 如下圖反白的網址

在 bar chart 上點選一個比較長的 bar 就可以看到下圖, appstats 幫你把吃掉比較多資源的 code 給標出來

超方便的! 真的很容易找出程式慢在哪。
May 24 2010
簡易的 app engine 全文檢索
之前在弄 app engine 的全文檢索, 在官方文件都沒找到, 不過小 x 大大指點小弟我說有 SearchableModel 可以用, 才去翻了一下 app engine sdk 的 svn 找到 search 的這個 module, 只是實際試用一下, 這個功能沒支援中文, 它似乎只會把空格切開, 所以對於中文來說, 基本上就是無法使用, 用下面這個訊息舉例:
前幾天在弄 app engine 的全文檢索
這句的全文檢索資料會存成下面這樣的一個 List
['前幾天在弄', 'app', 'engine', '的全文檢索']
除了 “app” 跟 “engine” 可以搜尋到之外, 就只能用 “前幾天在弄” 跟 “的全文檢索” 這幾個詞可以搜尋到這則訊息, 所以只好捨棄了 SearchableModel 自己來做, 不過因為不熟 app engine, 所以沒有跟 model 做一個很好的整合, 嗯, 是根本沒有 XD 總之就是記錄一下自己的作法, 跟碰到效能的問題。
由於 app engine 的 model 可以直接用 list 來 filter 出結果, 像是:
keywords = ['少女', '時代']
Message.all().filter('keywords IN', keywords)
所以在建立 Message model 時, 就定個叫 keywords 的 StringListProperty, 把我們切好的字都存進去, 之後便可以透過上面的方法來搜尋。
而切字的作法是基本上跟 SearchableModel 的做法一樣, 先用空白切, 這樣就可以切開有正常空格的英文字, 但是得仍常會看到英文跟中文黏在一起的狀況, 像是:
測試twitter備份功能
相上面這種狀況這也得另外處理, 然後把中文相鄰的兩個字, 一組一組切開, 像是:
['測試', 'twitter', '備份', '份功', '功能']
其實原本我是切一個字跟兩個字, 不過因為 StringListProperty 在存入大量的 List 的時候, 會吃掉許多效能, 在 app engine dashboard 會一直看到紅字, 透過 appstats 就一直看到是在寫入 keywords 得部分吃掉最多 cpu time, 所以後來就只切兩個字, 減少 List 的長度。
反正基本上不允許搜尋一個字的就是了, 不過都只切成兩個字的話, 使用者搜尋的時候就不能打三個字或四個字來進行搜尋, 這時候就得把搜尋的字串也切一下, 例如上面這一句的狀況, 使用者可能會輸入 “備份功能”, 這時候用同一個切字功能切成下面這樣就完全 match 了 XD
['備份', '份功', '功能']
總之就變成使用者一定得輸入兩個字以上才能進行搜尋。 anyway 這樣一來就可以做個簡易的全文檢索了 ~ 當然 keywords 得給個 index 囉。
May 12 2010
透過 swfupload 直接上傳到 S3
由於 app engine 無法上傳大檔案, 即使使用 BlobStore 單檔最大也只能到 50MB 而已, 對於 MyAudioCast 來說, 豬小草的 Podcast 檔案幾乎都是超過 100MB, 所以看來就只能用 S3 來放檔案, 可是 app engine 的 urlfetch 也只能傳 1MB, 所以後來就直接採用 S3 提供的 form upload 的方式, 直接用 HTML POST Form 把檔案送到 S3, 有興趣的話可以看看 Browser Uploads to S3 using HTML POST Forms 這一篇文章, 所以就可以不用透過後端程式, 直接從前端上傳檔案到 S3 然後 redirect 回來。所以在這邊記錄一下。
然而這樣出現了一個問題, 沒辦法檢查上傳的檔案類型, 甚至知道檔案大小 ( 原本的作法是上傳完之後, 再去 s3 抓檔案大小的資料 ), 所以研究了一下用 swfupload 來做檔案進度顯示以及顯示, 並限制檔案的類型、大小, 同時也可以取得檔案的大小。
原本的 POST FORM 看起來像是下面這樣
<form id="upload_form" action="http://myaudiocast.s3.amazonaws.com/"
method="post" enctype="multipart/form-data">
<input type="hidden" name="key" value="{{ key }}" />
<input type="hidden" name="AWSAccessKeyId" value="{{ AWS_ACCESS_KEY }}" />
<input type="hidden" name="acl" value="{{ acl }}" />
<input type="hidden" name="policy" value="{{ policy }}" />
<input type="hidden" name="signature" value="{{ signature }}" />
<input type="hidden" name="Content-Type" value="{{ content_type }}" />
<input type="hidden" name="Cache-Control" value="{{ cache_control }}" />
<input type="hidden" name="Expires" value="{{ expires }}" />
<input type="hidden" name="success_action_redirect" value="{{ success_action_redirect }}" />
...
</form>
基本上的作法就是把原本 policy conditions 當中的 success_action_redirect 改成 success_action_status, 也就是把轉址改成吐 201 的 status code, 然後在 policy conditions 移除 success_action_redirect, 並加上下面兩個項目即可。
["starts-with", "$Filename", ""],
["eq", '$success_action_status', "201"]
而 swfupload 的 PostParams 也是把原本要 POST 出去的欄位加上即可
swfu.setPostParams({
'key': key,
'AWSAccessKeyId': "{{ AWS_ACCESS_KEY }}",
'acl': "{{ acl }}",
'policy': "{{ policy }}",
'signature': "{{ signature }}",
'Content-Type': "{{ content_type }}",
'Cache-Control': "{{ cache_control }}",
'Expires': "{{ expires }}",
'success_action_status': {{ success_action_status }}
});
然後 s3 bucket 那邊也得加上一個 /crossdomain.xml, 否則 Flash 會有 security error, 檔案內容如下。
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM
"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*.myaudiocast.com" secure="false" />
</cross-domain-policy>
最後就是, 由於 Flash 接到 200 以外的 status code 都會視為失敗 ( 至少我查到的資訊都是這麼說 ), 所以 swfupload 接到 201 的 status code 也是會認為失敗, 所以就得再 uploadError 這個 handler 判斷 status_code 是否為 201, 如果 201 則是上傳成功, 所以這點得再注意一下, 我目前的作法是這樣。
這篇主要只是記錄一下, S3 真是好服務, 而 app engine 也很好, 不過限制太多, 得自己繞路就是了 XD
May 04 2010
在 app engine 使用 pytz
今天才意識到 timezone 的處理是有那麼一點麻煩, 存放、顯示要怎麼做之類的, 之前都沒處理過這類的問題, 不過也只能先解決問題, 之後有空在來深入研究 timezone。
這兩三天在用 appengine 寫 twitter/plurk 備份的東西, 而 twitter 跟 plurk api 吐的訊息時間分別是 UTC +0000 跟 GMT, 不過剛剛網路上查了一下, 基本上、簡單來說 UTC = GMT, hmm 細節之後再來研究。所以說 twitter 或 plurk 的推訊時間都是用 UTF +0000, 不過這兩個的處理方式比較起來, twitter 簡單多了。
twitter api 的每則推訊都會有 user 的資料, 而 user 的資料當中 utc_offset 直接帶秒數, 例如我在台灣就是 28800 秒, 也就是 8 個小時, 所以就可以用以下的程式碼來把推訊的時間修正為準確, 符合 user 當地的時間
from datetime import datetime, timedelta
fmt = '%a %b %d %H:%M:%S +0000 %Y'
created_at = datetime.strptime(message['created_at'], fmt)
utc_offset = int(message['user']['utc_offset'])
local_time = created_at + timedelta(seconds=utc_offset)
twitter 時間處理滿簡單的, 直接用回傳資料的 utc_offset 然後用 timedelta 處理即可, 即使 utc_offset 是負值也沒問題。 相對之下 Plurk 那邊可以讀到是 timezone, 如: “Asia/Taipei” 這組字串, 顯然麻煩了些。
網路上找了一下, 似乎用 pytz 就可以很方便處理。 以下為 透過 pytz 用來為 plurk 處理本地時間的方式。
from datetime import datetime
from pytz import timezone
fmt = '%a, %d %b %Y %H:%M:%S GMT'
created_at = datetime.strptime(message['posted'], fmt)
user_timezone = timezone("Asia/Taipei")
local_time = created_at + user_timezone.utcoffset(created_at)
使用 pytz 真的滿方便就可以處理掉 timezone 的 time offset 問題, 不過問題來了, pytz 的 zoneinfo 就有 600 個檔案了, 然而 app engine 好像上限是 1000 個檔案, 整個吃掉一半以上。 不過似乎有 zipimport 的方式可以把檔案先壓縮在丟上去, 所以 google 幾個方式之後, 最後找到這個方式最簡單, 不過文件沒寫太清楚, 所以在這邊記錄一下。
ok, 結果討人厭的事情又來了, pytz 現在放在 launchpad 阿 … 誰裝 bazar 阿 XD 不過幸好在 pypi 可以直接抓檔案。
首先就是抓 tz_helper 跟 pytz 回來, 然後把 pytz 解開後, 找到 zoneinfo 然後把它 zip 起來, 接著就可以把 zoneinfo 目錄刪了, 最後把這三個檔案放在 app 的根目錄就好了, 像是
/app.yaml /index.yaml /main.py ... /tz_helper.py /pytz /zoneinfo.zip
之後使用的時候直接 import tz_helper 的 timezone 來用即可。
其實現在 SNS 的時間大多都直接採用 3 hours ago 之類的方式來顯示時間, 我想最主要的原因就是, 這樣可以很容易因應世界各地使用者不同時區的問題, 而不是直接顯示這個日期。 誰知道我今天兩個方式都要有, 分別像是下面兩張圖的方式呈現, 所以就得處理 time offset 的問題。 總不能我在 5 月 5 號早上七點的推訊仍舊被記到 5 月 4 號。 而存放成台灣時間, 結果用 UTC +0000 去做 minutes ago, 顯示出來都是 0 minutes ago。
May 02 2010
寫新玩具: 推特 / 噗浪備份
這個禮拜日除了吃飯之外的時間都在弄這個, 因為想把自己的”推”都備份下來, 想說還可以弄成像日曆之類的, 不過 UI 還沒完成就是了, 目前看起來大概上面這樣。
這次在 App Engine 加上 Google App Engine Helper for Django 來寫, 找了個 app engine oauth library, 稍微改一下, twitter 的部份一下子就弄好了。 時間稍微多花了一點在 plurk 上, 因為沒找到 plurk 在 app engine 的 library 可以用, 只好自己刻一個, 不過主要是在搞清楚回傳的資料, 跟建 model 時多花了一些時間。
hmm~ 還是要有玩具, 可以用來忘記公司的一些煩人事情。
March 31 2010
Google App Engine Appstats
剛看到 Easy Performance Profiling with Appstats 就馬上開 myaudiocast 的來玩了, 有 Appstats 真的滿方便的。 這篇文章同時講到 java 跟 python 啟用 Appstats 的方式 … python 簡單多了 ( 笑 )
February 07 2010
在 app engine 上用 urlfetch 解短網址
昨天在寫一個程式, 功能是在把 twitter 上帶有 mp3 連結的訊息, 轉成 podcast 好讓我可以用 iTunes 訂閱, 自動下載 XD 推特那一段就直接用了 twitter search 產生的 json, 然後 mp3 的連結都是 tinyurl 產生的短網址, 直覺我就透過了 longurl 的 api 來回復短網址。
誰知道 longurl api 常 request 失敗, 也許可能是因為連續的 request 造成的。後來就突然想到了, 反正 header 都會帶有轉址的原網址吧, 所以就用 urlfetch 單純只抓 header, 然後 parse header 抓出來正常的網址就好啦~ 誰知道看了一下 urlfetch 的文件有居然 final_url 這個 method, 噗, 結果解短網址就變得超簡單。
result = urlfetch.fetch(tinyurl, method=urlfetch.HEAD)
if result.status_code == 200:
url = result.final_url
App Engine 405 Method Not Allowed
不太清楚為何webapp 為何沒有 implement HEAD method, 所以一開始在 MyAudioCast 的 DashBoard 就常看到一些錯誤, 多是一些 web crawler, 但卻都是 405 Method Not Allowed 的錯誤訊息, 用 curl -I 來測也是得到相同的回應, 本來不以為意, 後來為了要讓 MyAudioCast 上面的 Podcast 可以 submit 到 iTunes store, 卻也得到 405 的錯誤訊息, 無法 submit, 才覺得這事情有點大條 XD
google 之後找到一篇寫到說, 用 app engine 開發的網站無法 digg, 就是因為 405 這個問題, 因為要新增網址到 digg 的時候 digg 會用 HEAD method 來確認網頁是存在的, 所以說先前 Dashboard 上看到的 web crawler 也會先用 HEAD 來檢查網頁是否存在, 這麼一來只要是 HEAD method 都回 405 Method Not Allowed 的話, 那機器人不就都不來了嗎?
不過這問題其實很好解決, 幸好這次在開發的時候有先寫一個 RequestHandler 來繼承 webapp.RequestHandler 所以直接在 RequestHandler 增加一個 head method 就可以解決這個了。
def head(self, *args):
pass
最簡單的就是這樣 XD 不過不知道是否改為執行 get method 然後把 content 去掉只留 header 這樣會不會比較好 :p
February 06 2010
App Engine BlobStore 試玩心得
一開始構想 MyAudioCast 的時候, 原本是想說把檔案都送到 S3, 然後透過 CloudFront 還可以玩 rtmp streaming, 後來發現 urlfetch 只能傳 1MB 所以放棄, 就只好使用 BlobProperty, 沒想到 BlobProperty 也只有 1MB, 後來才知道 app engine 1.3.0 有了 BlobStore, 可以放 50MB 的檔案, 雖然還在實驗中, 不過到還是真的可以用, 所以就把東西都放在 app engine。
不過要使用 BlobStore 之前就得先開啟付費的資料, 準備開始扣款, 而 BlobStore 每天的 free quota 也只有 1GB, 原本以為有點小, 但其實只要給他 $0.10 / day 就可以有 40GB per day, 老實說其實滿划算的。 只是單檔 50MB 的限制 … 我是覺得 100MB 比較寬裕一點。
後來才發現, App Engine 真的貴的是在出去的頻寬。 outgoing bandwidth 的 free quota 也是 1GB per day, 可是當我 budget 設定 $0.66 / day, 也才提昇到 6.5GB per day。
所以最後我還是試了一下這一篇寫的方式, 來把檔案丟到 S3。 雖然剛開始用 S3 就有看過這個辦法, 可是根本不會想去試, 不過現在在 app engine 上似乎是成為唯一能傳大檔到 S3 的方式了, 只是就少了一些可以控制的東西, 也需要一些 workaround。
不過現在用 S3 + CloudFront 感覺良好 XD 另外, 我一直把 Blob 打成 Blog Orz
Maybe Soup is currently being updated? I'll try again automatically in a few seconds...






