My favorites | 中文(繁體) | Sign in
英文版或許有比此中譯版新的內容

在「應用服務引擎」使用勾點

Jens Scheffler
2009 年 2 月 11 日

簡介

隨著程式碼越來越龐大,程式碼的維護和修改有時也變得愈加困難。因此,要在整個程式碼庫基底實作功能,例如分析資料存放區使用率或新增「稽核資料」(誰變更了這個列?),不是一件容易的事。我們要如何確認原始碼沒有任何遺漏呢?我們又要如何預防其他開發人員在日後擴展程式碼時犯錯呢?

背景

我們常常會想在應用程式的全域行為中做些變更,一些不會涉及應用程式商務邏輯的變更。因此,我們的目標是要透過最少的程式碼,做出重大的變更,並盡可能降低風險。我們想在整個程式套用一種跨領域的要素,例如記錄效能相關資料。我們想要全面達到這個目標,無論開發人員所使用的 api (模型 vs 資料存放區、Django vs webob) 多麼高階。

很自然地,我們採用 api_proxy_map 模組,這是一個 RPC 服務的低階登錄,可以將較高階的 API 呼叫委派至適當的服務實作。傳統上,修改 api_proxy_map 模組的方法是透過 monkeypatch 修補 (不需修改原始碼),再取代這個模組的主要進入點 apiproxy_stub_map.MakeSyncCall。雖然這個方法在某些情況下行得通,卻不是一點風險都沒有。為簡化單元測試,其他模組有時會選擇導入 MakeSyncCall 方法,並儲存其本機參照;如此一來,測試即可輕鬆取代模擬物件。

memcache 模組就是這種技術的最佳使用範例。不幸的是,這也表示我們在套用修補程式之前所建立的任何 memchache-Client,均不會採用我們的所有修改。舉例來說,如果套用修補程式之前,指令碼已先行匯入 memcache 模組,即可能導致這種結果。追蹤與修正這些參照是一件複雜的工作,而且新的 SDK 版本 (或是任何其他用途的外部開發工具程式庫) 日後可能會導入新的靜態相依性。

幸運的是,monkeypatching 有一個替代方法。SDK 版本 1.1.8 在 api_proxy_map 導入一種新機制 :勾點概念。任何開發人員均可定義具有 MakeSyncCall 相同特徵的方法:

def hook(service, call, request, response):
  ...

並透過下列其中一種方式,向執行階段註冊這個方法:

apiproxy_stub_map.apiproxy.GetPreCallHooks().Append('unique_name', hook, 'optional_api_identifier')
apiproxy_stub_map.apiproxy.GetPostCallHooks().Append('unique_name', hook, 'optional_api_identifier')
apiproxy_stub_map.apiproxy.GetPreCallHooks().Push('unique_name', hook, 'optional_api_identifier')
apiproxy_stub_map.apiproxy.GetPostCallHooks().Push('unique_name', hook, 'optional_api_identifier')

勾點有兩種不同的類型。PreCallHook 是在發出 RPC 呼叫之前執行,而 PostCallHook 則是在發出 RPC 呼叫之後執行。您也可以選擇指定 api 識別碼,以確保只有特定類型的 rpc (例如資料存放區或記憶體快取) 能夠叫用勾點。您也可以向 apiproxy 註冊多個勾點:Append 會在現有勾點的清單結尾處附加新的勾點,而 Push 則會在起始處附加勾點。

範例

讓我們看看一個具體範例,並實作一些資料存放區分析。這個範例的概念是要為資料庫的每個模型類型自動收集統計資料。我們希望 put/get/query/delete 擁有個別的計數器,且不用修改存取資料的模型類別和處理常式。基於效能因素,我們決定不在資料存放區中寫入任何結果。反之,我們會從記憶體快取收集一些快速且雜亂 (不永久且不精確) 的統計資料,然後將較詳細的資訊放到記錄敘述句中:每個資料存放區操作會建立一個記錄項目,可供離線擷取與分析。

第一步是建立一個能夠收集記憶體快取資料並記錄較詳細資訊的輔助程式。值得一提的是,記憶體快取中的計數只是大約值,因為我們無法鎖定資料,也無法透過交易方式增加計數。但是這不會造成問題,因為比起記錄中的詳細資料,計數僅負責提供一個粗略的概況。下方的 db_log 會完成這個目標。

def db_log(model, call, details=''):
  """Call this method whenever the database is invoked.
  
  Args:
    model: the model name (aka kind) that the operation is on
    call: the kind of operation (Get/Put/...)
    details: any text that should be added to the detailed log entry.
  """
  
  # First, let's update memcache
  if model:
    stats = memcache.get('DB_TMP_STATS')
    if stats is None: stats = {}
    key = '%s_%s' % (call, model)
    stats[key] = stats.get(key, 0) + 1
    memcache.set('DB_TMP_STATS', stats)
  
  # Next, let's log for some more detailed analysis
  logging.debug('DB_LOG: %s @ %s (%s)', call, model, details)

注意: 記憶體快取擁有增加計數的交易功能,例如 incr() 和 decr(),但是這些功能只能處理單一值,不能處理計數器的字典。

現在,讓我們善用這個輔助程式,並實際編寫我們的勾點。在發出 rpc 呼叫之前,我們使用 PreCallHook 來評估資料 (在這個特定案例中,也可以使用 PostCallHook):

def patch_appengine():
  """Apply a hook to app engine that logs db statistics."""
  def model_name_from_key(key):
    return key.path().element_list()[0].type()
    
  def hook(service, call, request, response):
    assert service == 'datastore_v3'
    if call == 'Put':
      for entity in request.entity_list():
        db_log(model_name_from_key(entity.key()), call)
    elif call in ('Get', 'Delete'):
      for key in request.key_list():
        db_log(model_name_from_key(key), call)
    elif call == 'RunQuery':
      kind = datastore_index.CompositeIndexForQuery(request)[1]
      db_log(kind, call)
    else:
      db_log(None, call)
      
  apiproxy_stub_map.apiproxy.GetPreCallHooks().Append(
      'db_log', hook, 'datastore_v3')

我們也可以建立一個十分簡單的指令碼,在瀏覽器中顯示我們的統計資料:

def main():
  """A very simple handler that will print the temporary statistics."""
  print 'Content-Type: text/plain'
  print ''
  print 'Mini stats'
  print '----------'
  stats = memcache.get('DB_TMP_STATS')
  if stats is None: stats = {}
  for name, count in sorted(stats.items(), key=operator.itemgetter(0)):
    print '%s : %s' % (name, count)
  print '----------'

  
if __name__ == "__main__":
  main()

這段程式碼的主要工作是要判斷我們使用的模型名稱。如果您想要知道如何取得這項資料,請參閱 SDK 的 entity_pb.py 模組和 datastore_pb.py 模組。這兩個模組包含用於資料存放區 RPC 的 通訊協定緩衝區

目前,我們已經建立了

  • 一個輔助程式,可以編譯記憶體快取 (以及記錄) 中的資料存放區資訊;
  • 一個勾點,可以執行輔助程式;以及
  • 一個簡易處理常式,可以顯示結果。

這三個元件可以儲存在單一模組中 (讓我們將模組命名為 db_log.py)。我們可以將下列兩行程式碼加到應用程式,以啟動應用程式中的漏洞:

import db_log
db_log.patch_appengine()

這個程式碼片段必須放在一個絕對會執行的位置,無論叫用的處理常式為何。舉例來說,假設我們使用的是 django 應用程式,而當中有一個通用的主要方法,則我們建議您在主要方法的定義上方,加上這段程式碼。此外,請不要忘記將 db_log.py 加到您的 app.yaml,以取得快速且雜亂的記憶體快取統計資料。您可以在「Google 應用服務引擎編程食譜」中,找到本篇文章的完整原始碼。