使用社會網路可以更輕鬆地獲取並聚合數據,從而創建富有革新精神的新 Web 應用程序。但是,仍然必須處理創建可伸縮 Web 應用程序的所有常見問題。現在,使用 Google App Engine (GAE) 也可以簡化工作。使用 GAE,可以不必考慮管理應用伺服器池的所有事務,而是集中精力創建優秀的 mashup。本文是共分三部分的系列文章 “使用 Eclipse 在 Google App Engine 上創建 mashup” 的第二部分,在本文中,將利用並增強在第 1 部分中構建的應用程序。我們將通過 GAE 的更多數據建模功能來提高性能。然後使用 GAE 的 Memcache 服務進一步提高性能。
關於本系列
在本系列中,將了解如何開始使用 Google App Engine (GAE)。在 第 1 部分 中,了解了如何設置開發環境,以便可以開始創建運行在 GAE 上的應用程序。了解如何使用 Eclipse 簡化應用程序的開發和調試。本文是第 2 部分,將使用 Eclipse 構建 Ajax mashup 並將其部署到 GAE 中。最後,在第 3 部分中,將通過為應用程序創建 RESTful Web 服務返回到生態系統,這樣其他人就可以使用它創建自己的 mashup。
GAE 是創建 Web 應用程序的平台。使用它的最重要的先決條件是具備 Python 知識,因為要在 GAE 中使用 Python 作為編程語言(目前為 Python V2.5.2)。對於本系列,具備一些典型的 Web 開發技能將會有幫助(例如,HTML、JavaScript 和 CSS 知識)。要針對 GAE 進行開發,需要下載三個軟體包。
如何安裝后兩個軟體包已經在 第 1 部分 中討論過。如果您剛開始使用 Eclipse,請參閱 參考資料 了解入門知識。
增強功能
在 第 1 部分 中,我們構建了一個小型應用程序,用來聚集內容提要並通過 GAE 處理它們。我們可以在此基礎上繼續開發並將該應用程序部署到 GAE 中,但是在這之前,讓我們對它實現一些增強功能。第一組增強與性能有關。第 1 部分的版本將在每次請求頁面時從訂閱的服務中提取數據。這可能需要花費很長時間,尤其是如果任何一項服務響應較慢或一個用戶訂閱了多項服務。這是常見問題,但是對於運行在 GAE 上的程序來說,這個問題尤為嚴重。要讓 GAE 具有可伸縮性,就需要減少長時間運行的請求。如果處理時間過長,則會終止該請求並向用戶發送一條錯誤消息。這並不是我們想要的結果,因此將更多地使用 GAE 的數據建模和 Bigtable 特性來提高性能。Bigtable 是用於管理結構化數據的分散式存儲系統(有關更多信息,請參閱 參考資料)。還將使用它的 Memcache API 來做出更多改進。
我們將在本文中實現的另一組增強將處理用戶體驗。通過嚮應用程序添加 Ajax 元素改進用戶界面。不僅將使用 Ajax,還將綁定一些數據建模及緩存增強以進一步提高應用程序的性能。實現了這些增強后,我們就可以將應用程序部署到 GAE 中。首先來看一看數據建模增強。
使用關係
在 第 1 部分 中,我們使用了一個數據模型:Account。它使用了 GAE 的 Expando 屬性特性來存儲服務的 URL。為了提高性能,需要存儲提要中的實際數據。訪問 Bigtable 絕不會像訪問傳統的關係資料庫(或者至少是負載較輕的關係資料庫)一樣快,但是應當比從數據源中提取提要快得多。不過,如果只依賴 Bigtable,則永遠得不到新功能。因此,需要跟蹤何時提取實時數據並將其插入 Bigtable 中,因此如果數據過時,那麼我們可以返回到數據源。
在創建新數據模型之前,還有一件事需要考慮。不同的用戶可以擁有相同的提要。提要與帳戶之間存在多對多關係。了解這些之後,讓我們看一看新模型。清單 1 中顯示了修改後的 Account 模型。
class Account(db.Model): user = db.UserProperty(required=True) |
這裡的主要更改是從模型中移除了服務信息。如何確定服務的 URL?該信息已被移到獨立的模型級數據結構(目錄)中,如下所示:
service_templates = { 'twitter': "http://twitter.com/statuses/user_timeline/%s.rss", 'del.icio.us': "http://del.icio.us/rss/%s", 'last.fm': "http://ws.audioscrobbler.com/1.0/user/%s/recenttracks.rss", 'YouTube': "http://www.youtube.com/rss/user/%s/videos.rss", } |
這將允許使用簡單的字元串替換來創建基於用戶名的服務 URL。換言之,使用服務名稱(用作 service_templates 字典中的關鍵字)與用戶名(用於對從字典中檢索到的值進行字元串替換)組合可以計算 URL。這將把我們引向 Feed 數據模型。
class Feed(db.Model): service = db.StringProperty(required=True) username = db.StringProperty(required=True) content = db.TextProperty() timestamp = db.DateTimeProperty(auto_now=True) |
服務和用戶名如上所示。服務屬性將用作 service_templates 字典中的關鍵字,而用戶名將與該值結合使用以計算 URL。內容屬性是從 Web 服務中提取的實際內容。時間戳是提取內容的日期和時間。auto_now=True 將告訴 Bigtable 在每次更新記錄時更新屬性。需要使用連接表定義 Account 與 Feed 之間的多對多關係,如下所示:
class AccountFeed(db.Model): account = db.ReferenceProperty(Account, required=True, collection_name='feeds') feed = db.ReferenceProperty(Feed, required=True, collection_name='accounts') |
ReferenceProperty 是 Bigtable 用來將一個模型與另一個模型關聯起來的工具。它類似於關係資料庫中的外鍵。您可能要注意使用的 collection_name 屬性。如果需要在查詢中使用引用,則使用它作為指代引用的名稱。如果不設置此屬性,則它將被設為模型名加上 _set(類似 account_set)。
數據建模已完成。我們已經為提要建立了模型並用多對多關係將這些模型與帳戶關聯了起來。使用 Bigtable 和 GAE 的 API 可以輕鬆地為實體建模,但是怎樣版本化?我們剛剛從一個版本的數據模型轉到另一個版本的數據模型。讓我們看看如何在 GAE 中處理此問題。
在開發期間更改模式
修改模式通常十分棘手。幸運的是,在這裡仍處於開發模式,在此時更改模式要比在生產應用程序中進行更容易。在開發期間更改模式十分常見,使用 GAE 可以輕鬆完成。只需要為 GAE 的本地 Web 伺服器提供一個附加參數,如圖 1 所示:
我們只是添加了 --clear-datastore 參數作為傳遞到啟動腳本中的命令行實參。使用 Eclipse 和 PyDev 可以輕鬆地根據需要添加這些參數。有一點需要注意,Eclipse 將記住這些實參。如果留下實參,它將在每次啟動開發伺服器時刪除本地資料庫。這可能不會造成問題,但是應當引起注意。
現在具備了允許使用 Bigtable 存儲提要的新模式。從 Bigtable 中查找數據的代價十分高昂。不會像許多開發人員掌握如何使用關係資料庫那樣快。幸運的是,GAE 提供了額外的 API,可以更快地訪問數據:Memcache。
Memcache
GAE 包括內存緩存:Memcache。此內存緩存的靈感來源於流行的開源分散式緩存 memcached,但這是 GAE 的專用實現。它擁有類似的語義:只需在 Memcache 中放置或獲得名稱-值對。使用 Memcache 可以極大地提高應用程序的性能。
對於 aggroGator 應用程序,將緩存兩類內容。首先也是最明顯的內容是用戶的服務。這隻能在 AddService 操作中更改,因此可以很容易地確保緩存的精確性。下面的 Cache 類顯示了相關代碼。
class Cache: @staticmethod def setUserServices(account): userServices = [{'service': accountFeed.feed.service, 'username': accountFeed.feed.username} for accountFeed in account.feeds] if not memcache.set(account.user.email(), pickle.dumps(userServices)): logging.error('Cache set failed: userServices') return userServices @classmethod def getUserServices(cls, user): userServices_pickled = memcache.get(user.email()) if userServices_pickled: userServices = pickle.loads(userServices_pickled) else: account = DB.getAccount(user) userServices = cls.setUserServices(account) return userServices |
下面簡單解釋了這些代碼。首先解釋用來在緩存中設置用戶服務的靜態方法(不依賴於類)。這將使用列表理解(list comprehension)創建對象數組,其中每個對象都是一個服務以及該服務的用戶的用戶名。用戶的電子郵件隨後用作 Memcache 的關鍵字。使用 pickle 模塊序列化數據並將其放入 Memcache 中。
getUserServices 方法與之相似。它是一個類方法,因為它是靜態的,但是需要能夠在丟失緩存時調用 setUserServices 方法。它將嘗試檢索上面所述的序列化對象。如果在緩存中什麼都沒找到,它將從 Bigtable 中查找數據並把找到的數據放到緩存中。
提要中的緩存條目使用了類似的策略。這裡有一個主要差別:必須注意時效性。畢竟,用戶隨時都可以創建新條目,並且我們必須返回到數據源。需要使用一個到期策略,如下所示:
class Cache: #user service methods omitted @staticmethod def setEntries(feed): entries = GenericFeed.entries(feed) if not memcache.set("%s_%s" % (feed.service, feed.username), pickle.dumps(entries), CACHE_FEED_TIME): logging.error('Cache set failed: entries') return entries @classmethod def getEntries(cls, service, username): entries_pickled = memcache.get("%s_%s" % (service, username)) if entries_pickled: entries = pickle.loads(entries_pickled) else: feed = DB.getFeed(service, username) entries = cls.setEntries(feed) return entries |
這裡再次使用了類似的模式。序列化數據並存儲在 Memcache 中,這一次結合使用了服務與用戶名。這將允許進行跨用戶緩存,從而獲得更高的效率。當嘗試從緩存中載入時,如果丟失了緩存,則將轉到 Bigtable。另請注意,使用了 CACHE_FEED_TIME 到期值使緩存中的數據過期。如果不進行設置,Memcache 將把所有內容都保存在緩存中,直至用光內存。對於用戶服務和條目,我們將使用 DB 類查詢 Bigtable。此類如下所示:
class DB: @staticmethod def getAccount(user): return Account.gql("WHERE user = :1", user).get() @staticmethod def getFeed(service, username): return Feed.gql("WHERE service = :1 AND username = :2", service, username).get() |
此類將使用非常簡單的查詢,這些查詢都使用 GAE 的 GQL 語法。此語法非常簡單,但是是功能強大的 SQL 語法的子集。在上例中,使用了編號的參數,但是也可以使用指定參數以進行更複雜的查詢。通過查詢 Bigtable 中的提要,實際上使用了 Bigtable 作為另一個緩存層。讓我們看看如何通過 Ajax 從客戶機協調所有這些高性能緩存。
Ajax
大多數人想到 Ajax 時,都會想到它能夠增強用戶體驗。當然,這是對的,但是 Ajax 有許多其他優點。特別是,有許多架構優點。它允許將大量應用程序邏輯移到客戶機中,並且從伺服器中檢索較少量的數據。在 GAE 上運行並沒有改變這一點,但是它可以利用應用程序的可伸縮特性。讓我們看一看如何把 Ajax 組織到 aggroGator 應用程序中。
初始頁面視圖
在上一版的 aggroGator 中,當頁面裝入后將顯示各項服務條目的列表。使用了 Django 樣式模板來完成這項工作。為了使頁面更具有動態性,需要分離數據與表示邏輯。要查看工作原理,讓我們看看如何更改模板。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <title>Aggrogator</title> <link rel="stylesheet" href="/css/aggrogator.css" type="text/css" /> <script type="text/javascript" src="/js/prototype.js"></script> <script type="text/javascript" src="/js/effects.js"></script> <script type="text/javascript" src="/js/builder.js"></script> <script type="text/javascript" src="/js/aggrogator.js"></script> </head> <body onload="initialize();"> <ul id="cache"></ul> <img id="spinner" alt="spinner" src="/gfx/spinner.gif" style="display: none; float: left;" /> <p id="logout"> {{ user.nickname }} <a href="{{ logout_url }}">Logout</a> </p> <div class="clearboth"></div> <form id="form_addService" onsubmit="addService(); return false;"> <fieldset> <legend>Add New Service</legend> <label for="service">Service: </label> <select name="service" id="service"> <option>twitter</option> <option>del.icio.us</option> <option>last.fm</option> <option>YouTube</option> </select><br/> <label for="username">Username: </label> <input type="text" name="username" id="username" /> <input type="submit" value="Add" /> </fieldset> </form> <table> <tbody style="vertical-align: top;"> <tr> <td> <div id="userServices"><span /></div> <div id="entries"><span /></div> <td> <table><tbody id="allEntries"></tbody></table> </td> </tr> </tbody> </table> </body> </html> |
對於模板,有兩個重要的注意事項。首先,它幾乎沒有任何動態內容 — 只有用戶名和登錄/退出鏈接。其次,包括大量 JavaScript。使用 Prototype 和 script.aculo.us JavaScript 庫(有關更多信息,請參閱 參考資料)。還包括自定義 JavaScript 庫:aggrogator.js。當頁面載入時它調用了 initialize() 方法,如下所示:
function initialize() { getUserServices(); new PeriodicalExecuter(getUserServices, 300); } function getUserServices() { var handler = function(xhr) { var json = xhr.responseJSON; if (json.error) { // display the error } else { cacheStats(json.stats); userServicesTable(json.userServices); updateEntries(json.userServices); } }; // create options for request var options = { method: "get", onSuccess: handler }; // send the request new Ajax.Request("/getUserServices", options); } |
正如您所見,初始化代碼僅僅調用了另一個函數:getUserServices。它還將啟動一個輪詢進程以使用 Prototype 的 PeriodicalExecutor 類定期調用 getUserServices。在本例中,它將每 300 秒或者每 5 分鐘調用一次 getUserServices。此輪詢將提供數據正在從伺服器推出(也稱為 Comet 或反向 Ajax)的錯覺。因此,舉例來說,當在 Twitter 中發布新內容后,將把它立即推給 aggroGator 中的用戶。
getUserServices 類執行了更多有趣的工作。它發出了一個 Ajax 請求,該請假載入當前用戶訂閱的服務。然後構建服務表,如下所示:
function userServicesTable(json) { var table = Builder.node('table', Builder.node('tbody', function() { var l = []; json.each(function(s) { l.push(Builder.node('tr', [ Builder.node('td', Builder.node('a', {href: "", onclick: "getEntries('" + s.service + "', '" + s.username + "'); return false;"}, s.service + ':' + s.username) ) ])); }); return l; }() ) ); $('userServices').replaceChild(table, $('userServices').firstChild); } |
此函數主要使用 script.aculo.us 的 Builder 庫創建一個 HTML 表,在其中顯示所有用戶的服務。在繼續之前,讓我們看看此服務中使用的數據。正如我們在清單 9 中所見,它要向 GetUserServices 發出請求。這隻能在應用程序的 main 方法中配置。
def main(): app = webapp.WSGIApplication([ ('/', MainPage), ('/addService', AddService), ('/getEntries', GetEntries), ('/getUserServices', GetUserServices), ], debug=True) util.run_wsgi_app(app) |
正如您所見,/getUserServices URL 被映射到名為 GetUserServices 的新類上。該類如下所示:
class GetUserServices(webapp.RequestHandler): def get(self): user = users.get_current_user() # get the user's services from the cache userServices = Cache.getUserServices(user) stats = memcache.get_stats() self.response.headers['content-type'] = 'application/json' self.response.out.write(simplejson.dumps({'stats': stats, 'userServices': userServices})) |
該類十分簡單,但是非常強大。它將從 Cache 類中檢索數據,該類實際上是位於 Bigtable 和 Memcache 頂部的抽象。然後將數據作為 JSON 傳回。有許多第三方庫可用於相互轉換 Python 對象與 JSON,但是我們不需要這些庫。GAE SDK 包括 Django,因此我們將使用 Django 的 django.utils.simplejson 函數把 Python 對象序列化為 JSON。您可能還注意到我們將傳回一些緩存統計數據。這些是一些簡單的統計數據,比較了在 Memcache 中找到數據的頻率與找不到數據的次數。當然,這些數據不是必需的,但是十分有趣,至少對於開發人員是這樣。您可以在 Web 頁面中製作一段視圖源代碼以查看這些統計數據。最後,注意將 content-type 頭部設為 application/json,這可以向 Prototype 表示有效負荷是 JSON,因此它將為我們處理 JSON 的安全反序列化。
現在我們已經了解了如何通過在 GAE 上運行應用程序處理數據。如果返回到清單 9,則不只是構建服務表。還將通過調用 updateEntries 函數檢索每項服務的所有條目。您可以在本文附帶的完整代碼中找到該函數及處理該函數的 Python 類。它將遵循類似的模式:
可以為應用程序構建更多優秀功能,但是從某種程度上說,需要把它部署到 GAE 中。讓我們接下來看看如何部署以及如何監視和調試生產應用程序。
部署
部署到生產環境經常是個痛苦的過程。它可能包括用 FTP 傳輸代碼、運行構建等等。但是,使用 GAE 可以輕鬆完成部署,這是 GAE 的一個特性。有一個名為 appcfg.py 的簡單部署腳本,GAE 安裝程序應當已經把它放到路徑中(如果未使用安裝程序,而只是解壓縮了包,則它位於 GAE 主目錄中)。只需用它的 update 命令及應用程序目錄(含有 app.yaml 文件的目錄,因為它需要讀取此文件)調用此腳本,並且您應當會看到類似清單 13 的內容。
$ appcfg.py update aggrogator/src/ Loaded authentication cookies from /Users/michael/.appcfg_cookies Scanning files on local disk. Initiating update. Email: your_email@here Password for your_email@here: Saving authentication cookies to /Users/michael/.appcfg_cookies Cloning 7 static files. Cloning 3 application files. Uploading 1 files. Closing update. Uploading index definitions. |
就是這樣。現在已經把應用程序部署到 GAE 中。您可以在瀏覽器中轉到該應用程序並嘗試使用。您需要在部署前在 GAE 中註冊應用程序,因為在 app.yaml 中需要它的名稱(如 第 1 部分 中所示)。請不要選用 aggroGator 作為該名稱,因為它被用於此應用程序。您可以在此處查看運行中的應用程序:http://aggrogator.appspot.com。
監視應用程序
對於任意一個生產 Web 站點,都需要能夠監視並確保它能夠正常運行。使用 GAE 可以輕鬆完成此操作。如果登錄到 Google App Engine 中,則將看到您自己的應用程序列表,如下所示:
單擊鏈接,然後將打開應用程序指示板。
這裡有很多有用信息可以使用。最有用的信息之一是日誌。當初次部署 aggroGator 應用程序時,它的 del.icio.us 服務沒運行。在開發階段,該服務運行正常,但在生產時卻不能正常運行。幸運的是,GAE SDK 將提供日誌記錄。問題出在從 del.icio.us 中提取 RSS 提要的代碼,因此在那裡添加了日誌記錄,如下所示。
class GenericFeed: @staticmethod def fetch(service, username): content = None # construct service url service_url = SERVICE_TEMPLATES[service] % username # fetch feed from service result = urlfetch.fetch(service_url) if result.status_code == 200: content = unicode(result.content, 'utf-8') else: logging.error("Error fetching content, HTTP status code = " + str(result.status_code)) return content |
現在,可以使用 GAE 中的日誌控制台查看日誌記錄,如下所示。
正如您所見,del.icio.us 返回了 HTTP 503(服務不可用狀態代碼)。代碼沒問題,只是 GAE 與 del.icio.us Web 站點之間的通信發生錯誤。
結束語
我們已經了解了如何利用 Google App Engine 的特性為應用程序提供更優秀的可伸縮性和性能。這包括同時使用 Bigtable 和 Memcache 來提供 “昂貴” 數據的緩存 — 需要花費很長時間才能從遠程資源中檢索到的數據。將此特性與 Ajax 結合使用,您將能夠高效地使用 GAE 並且為最終用戶提供吸引人的特性,例如從伺服器推出數據。在第 3 部分中,將繼續增強特性集,進一步探索 GAE 的數據建模功能,並且查看如何將 aggroGator 轉換為其他 mashup 的數據提供者。(責任編輯:A6)
[火星人 ] 使用 Eclipse 在 Google App Engine 上創建 mashup,第 2 部分: 構建 Ajax已經有749次圍觀