當要最佳化應用程式的效能時,請考慮使用 NDB。舉例來說,如果應用程式讀取的值不在快取中,則讀取作業會花費一段時間。您可以同時執行 Datastore 動作和其他動作,或是同時執行多個 Datastore 動作,藉此加快應用程式的執行速度。
NDB 用戶端程式庫提供許多非同步 (「async」) 函式。每個函式都會讓應用程式向 Datastore 傳送要求。函式會立即傳回內容,並傳回一個 Future
物件。應用程式可以在 Datastore 處理要求的同時,執行其他工作。資料儲存庫處理要求後,應用程式就能從 Future
物件取得結果。
簡介
假設應用程式中的某個要求處理程序需要使用 NDB 寫入內容,例如記錄要求。也需要執行其他 NDB 作業,例如擷取資料。
將對 put()
的呼叫替換為對其非同步等價 put_async()
的呼叫,應用程式就能立即執行其他作業,而不會在 put()
上阻斷。
這能讓 Datasore 在寫入資料時,顯示其他 NBD 函式以及範本。應用程式必須先從 Datastore 取得資料,才能在 Datastore 上進行封鎖。
在這個範例中,呼叫 future.get_result
有點不切實際:應用程式從未「使用」NDB 的結果。這段程式碼只是為了確保要求處理常式不會在 NDB put
完成前結束;如果要求處理常式過早結束,put 可能永遠不會發生。為了方便起見,您可以使用 @ndb.toplevel
修飾要求處理常式。這告訴處理常式在完成非同步要求之前不要退出。如此一來,您就能傳送要求,而不必擔心結果。
您可以將整個 WSGIApplication
指定為 ndb.toplevel
。這可確保每個 WSGIApplication
的處理常式在傳回前等待所有非同步要求。(不會將所有 WSGIApplication
的處理程序設為「頂層」)。
使用 toplevel
應用程式比使用所有處理常式更方便。不過,如果處理程序方法使用 yield
,則該方法仍需在另一個修飾符 @ndb.synctasklet
中包裝;否則,它會在 yield
處停止執行,而不會完成。
使用非同步 API 及 Future
幾乎所有同步 NDB 函式都有 _async
對應項目。例如:put()
有 put_async()
。非同步函式的引數一律與同步版本的引數相同。非同步方法的傳回值一律為 Future
,或 (針對「多重」函式) Future
清單。
「Future」是一個物件,用來維持已啟動但尚未完成的作業狀態。所有非同步 API 都會傳回一或多個 Futures
。您可以呼叫 Future
的 get_result()
函式,要求其提供作業結果。如果需要,Future 會封鎖,直到結果可用為止,然後再將結果提供給您。get_result()
會傳回 API 同步版本所傳回的值。
附註:
若您在其他程式語言使用過 Future,您可能會認為您可以直接使用 Future 做為結果,這裡不適用。這些語言使用
隱含的未來值,而 NDB 則使用明確的未來值。呼叫 get_result()
即可取得 NDB Future
的結果。
如果作業引發例外狀況怎麼辦?這取決於例外狀況發生的時間。如果 NDB 在「發出」要求時發現問題 (可能是引數類型錯誤),則 _async()
方法會引發例外狀況。不過,如果例外狀況是由 Datastore 伺服器偵測到,_async()
方法會傳回 Future
,並在應用程式呼叫其 get_result()
時觸發例外狀況。請不要過度擔心,因為最終結果都會是自然的行為;或許最大的差異是,如果列印追蹤記錄,您會看到一些低階非同步機制的部分。
舉例來說,假設您正在編寫留言板應用程式。如果使用者已登入,您可以提供一個網頁,顯示最近的留言板貼文。此頁面應該亦要向使用者顯示他們的暱稱。應用程式需要兩種資訊:已登入使用者的帳戶資訊,以及留言板所發布的內容。此應用程式的「同步」版本可能如下所示:
這裡有兩個獨立的 I/O 動作:取得 Account
實體,以及擷取近期的 Guestbook
實體。使用同步 API 的話,這些將接連發生,我們在擷取留言板實體之前,等待收到帳戶資訊。但是應用程式不需要立即取得帳戶資訊。我們可以利用這一點並使用非同步 API:
這個版本的程式碼會先建立兩個 Futures
(acct_future
和 recent_entries_future
),然後等待這些項目。伺服器會並行處理這兩項要求。每次 _async()
函式呼叫都會建立 Future 物件,並將要求傳送至 Datastore 伺服器。伺服器可以立即開始處理要求。伺服器回應可能以任意順序傳回;Future 物件會將回應連結至相應要求。

在非同步版本中所花費的總時間大約等於作業的最大時間。同步版本的總耗用時間超過運算時間總和。如果您可以並行執行更多作業,非同步作業的效益就會更大。
如要查看應用程式查詢所需的時間長度,或每項要求執行的 I/O 作業數量,建議您使用 Appstats。這項工具可根據實際應用程式的檢測資料,顯示類似上方圖表的圖表。
使用 Tasklet
NDB 的 tasklet 是一小段程式碼,可能與其他程式碼一同執行。如果您撰寫了工作片段,應用程式可以使用該工作片段,就像使用非同步 NDB 函式一樣:呼叫工作片段會傳回 Future
;之後,呼叫 Future
的 get_result()
方法即可取得結果。
tasklet 是一種可在沒有執行緒的情況下編寫並行函式的方法,由事件迴圈執行,且可以使用 yield 陳述式暫停自身,以封鎖 I/O 或其他作業。封鎖作業的概念會抽象化為 Future
類別,但工作項也可能 yield
RPC,以便等待 RPC 完成。當 tasklet 產生結果時,會 raise
一個 ndb.Return
例外狀況;接著 NDB 會將結果與先前 yield
的 Future
建立關聯。
編寫 NDB 子工作時,您會以不尋常的方式使用 yield
和 raise
。因此,如果您想查看如何使用這些功能的範例,可能不會找到像 NDB 子工作這類的程式碼。
若要將函式轉換成 NBD tasklet:
- 使用
@ndb.tasklet
修飾函式, - 將所有同步資料儲存庫呼叫替換為非同步資料儲存庫呼叫的
yield
, - 使函式「傳回」其傳回值
raise ndb.Return(retval)
(函式未傳回任何內容時則不需要)。
應用程式可以使用 tasklet 以更精細地控制非同步 API。例如,不妨考慮以下結構定義:
...
顯示訊息時,建議顯示作者的暱稱。擷取資料以顯示訊息清單的「同步」方式可能如下所示:
不過這種方法的效率很低。若您在 Appstats 中查看,您會看到一連串的「Get」要求。您可能會看到下列「樓梯」模式。

如果這些「Get」可以重疊,程式的這部分執行速度就會更快。您可以重寫程式碼來使用 get_async
,但要追蹤哪些非同步要求和訊息屬於同一組,其實相當棘手。
應用程式能夠將其做為 tasklet 來定義自己的「非同步」函式,可讓您以較不易混淆的方式整理這些程式碼。
此外,函式應使用 acct = yield key.get_async()
,而非 acct = key.get()
或 acct = key.get_async().get_result()
。這個 yield
會告訴 NDB,這是停用 tasklet 且讓其他 tasklet 執行的好時機。
使用 @ndb.tasklet
修飾產生器函式,可讓函式傳回 Future
,而非產生器物件。在 tasklet 中,任何 Future
的 yield
都會等待並傳回 Future
的結果。
例如:
請注意,雖然 get_async()
會傳回 Future
,但工作小工具架構會導致 yield
運算式將 Future
的結果傳回至變數 acct
。
map()
會呼叫 callback()
數次。不過,callback()
中的 yield ..._async()
會讓 NDB 的排程器在等待任何非同步要求完成之前,先傳送多個非同步要求。

如果您在 Appstats 中查看這項資訊,可能會發現這些 Get 不僅重疊,而且都會在同一項要求中執行。NDB 會實作「autobatcher」。AutoBatcher 會將多個要求合併為單一批次 RPC 傳送至伺服器;其運作方式是,只要還有工作要執行 (可能會執行其他回呼),就會收集索引鍵。只要需要其中一個結果,自動批次處理器就會傳送批次 RPC。與大多數要求不同,查詢不會「分批處理」。
當一個 tasklet 執行時,會從 tasklet 生成時的任何預設內容,或在執行過程 tasklet 更改的任何內容中,取得其預設的命名空間。換句話說,預設的命名空間不會與 context 關聯,亦不會儲存在 Context 中,且更改一個 tasklet 中的預設命名空間不會影響其他 tasklet 中的預設命名空間,除非是由 context 所生成。
Tasklet、並行查詢、並行 yield
您可以使用子工作,讓多個查詢同時擷取記錄。舉例來說,假設您的應用程式有一個頁面,用來顯示購物車內容和特價商品清單。結構定義可能會像這樣:
擷取購物車項目以及特別優惠的「同步」函式可能如下所示:
這個範例會使用查詢擷取購物車商品和優惠清單,然後使用 get_multi()
擷取商品目錄項目的詳細資料。(此函式不會直接使用 get_multi()
的傳回值。它會呼叫 get_multi()
來擷取所有廣告空間詳細資料至快取,以便日後快速讀取。get_multi
將多個 Get 合併為單一要求。但查詢擷取作業會依序發生。為了要讓這些擷取同時發生,請重疊兩個查詢:
get_multi()
呼叫仍是獨立的:它取決於查詢結果,因此無法與查詢結合。
假設此應用程式有時需要購物車,有時需要優惠,有時兩者皆需。您需要要整理您的程式碼,以做成能取得購物車的函式以及取得優惠的函式。如果應用程式同時呼叫這些函式,理想情況下,這些函式的查詢可以「重疊」。如要這麼做,請將這些函式設為 Tasklet:
yield x, y
很重要,但卻容易受到忽視。如果是兩個獨立的 yield
陳述式,則這兩個陳述式會一連串地發生。不過,對 tasklet 組合進行 yield
屬於「並行 yield」:tasklet 可並行執行,且 yield
會等待所有內容完成並傳回結果。(在一些程式語言中,會稱為「障礙 (barrier)」)。
若您將一小段程式碼轉換為一個 tasklet,您可能希望盡快繼續轉換。如果您發現「同步」程式碼可與子工作單元並行執行,建議您也將該程式碼設為子工作單元。接著,您可以使用平行 yield
並行執行。
若您編寫一個要求函式 (webapp2要求函式、Django 查看函式等) 做為 tasklet,其作業並不會符合您的預期:tasklet 會在 yield 後即停止執行。在這種情況下,您需要使用 @ndb.synctasklet
修飾函式。@ndb.synctasklet
與 @ndb.tasklet
類似,但會在子工作單元上呼叫 get_result()
。這會將 Tasklet 轉換為以一般方式傳回結果的函式。
在 tasklet 中查詢迭代
若要在 tasklet 中迭代查詢結果,請使用以下模式:
此與以下內容相等且適用 tasklet:
第一個版本中的三個粗體行,是第二個版本中單一粗體行的友善工作項等效值。您只能在 yield
關鍵字處暫停子工作。沒有 yield
的 for 迴圈不會讓其他子工作執行。
您可能會想知道為何這個程式碼會使用查詢疊代器,而不是使用 qry.fetch_async()
擷取所有實體。應用程式可能含有太多實體,無法放入 RAM。也許您正在尋找實體,並且在找到實體後可以停止重複運算,但您無法只使用查詢語言來表達搜尋條件。您可以使用疊代器載入要檢查的實體,然後在找到所需項目時退出迴圈。
使用 NDB 進行非同步的 Urlfetch
NDB Context
具有非同步的 urlfetch()
函式,能與 NDB 的 tasklet 順暢地平行處理,例如:
網址擷取服務有其專屬的 非同步要求 API。這沒問題,但不一定能輕鬆搭配 NDB 工作單元使用。
使用非同步交易
交易亦可以同步執行。您可以將現有函式傳遞至 ndb.transaction_async()
,或使用 @ndb.transactional_async
裝飾器。與其他非同步函式一樣,這個函式會傳回 NDB Future
:
交易也可與 tasklet 一起執行。舉例來說,我們可以在等待阻斷式 RPC 時,將 update_counter
程式碼變更為 yield
:
使用 Future.wait_any()
有時您可能會想要提出多個非同步要求,並在第一個要求完成時傳回。您可以使用 ndb.Future.wait_any()
類別方法執行此操作:
不過,您無法輕鬆地將上述程式碼轉換為 tasklet;平行 yield
會等待所有 Future
完成,包含那些您不想等待的。