使用命名空間實作多租戶架構

透過 Namespaces API,您可以在應用程式中輕鬆啟用多租戶架構,只要使用 namespace_manager 套件,在 appengine_config.py 為每個用戶群選擇一個命名空間字串即可。

設定目前的命名空間

您可以使用 namespace_manager 取得、設定並驗證命名空間。命名空間管理員可讓您針對啟用命名空間的 API 設定目前的命名空間。您可以透過 appengine_config.py 先設定目前的命名空間,然後資料儲存庫與 Memcache 就會自動使用這個命名空間。

大多數 App Engine 開發人員會使用 Google Workspace (舊稱 G Suite) 網域做為目前的命名空間。您可以使用 Google Workspace 將應用程式部署至自己擁有的任何網域,因此可以輕鬆使用這個機制為不同的網域設定不同的命名空間。接著您可以使用這些各自獨立的命名空間,區隔各網域的資料。詳情請參閱「對應自訂網域」。

以下程式碼範例顯示如何將目前的命名空間設為用於對應網址的 Google Workspace 網域。請注意,透過相同 Google Workspace 網域對應的所有網址中,這個字串都是相同的。

如要透過 Python 設定命名空間,請在應用程式的根目錄中使用 App Engine 設定系統 appengine_config.py。以下範例簡單示範如何使用 Google Workspace 網域做為目前的命名空間:

from google.appengine.api import namespace_manager

# Called only if the current namespace is not set.
def namespace_manager_default_namespace_for_request():
    # The returned string will be used as the Google Apps domain.
    return namespace_manager.google_apps_namespace()

如果沒有為 namespace 指定任何值,命名空間會設為空白字串。namespace 字串可為任意字串,不過長度上限為 100 個英數字元,且只能使用半形句號、底線和連字號。更明確地說,命名空間字串必須與規則運算式 [0-9A-Za-z._-]{0,100} 相符。

按照慣例,所有開頭為「_」(底線) 的命名空間字串都會保留供系統使用。此系統命名空間規則並未強制執行,但如果您不遵循這個規則,很可能會發生不明的負面後果。

如需更多關於設定 appengine_config.py 的一般資訊,請參閱「Python 模組設定」。

避免資料外洩

多組織用戶共享應用程式經常發生的一種風險,就是命名空間之間的資料外洩。許多原因可能意外導致資料外洩,包括:

  • 某些 App Engine API 目前還不支援命名空間,卻搭配使用命名空間。舉例來說,Blobstore 就不支援命名空間。如果您將命名空間與 Blobstore 搭配使用,就必須避免針對使用者要求而使用 Blobstore 查詢,或使用不受信任來源的 Blobstore 金鑰。
  • 在沒有為命名空間提供分隔配置的情況下,透過 URL Fetch 或其他機制使用外部儲存媒體 (而非 Memcache 與資料儲存庫)。
  • 根據使用者的電子郵件網域,設定命名空間。在大部分情況下,您不會想讓同一網域的所有電子郵件地址存取相同的命名空間。使用電子郵件網域後,使用者必須先行登入,應用程式才能使用命名空間。

部署命名空間

以下各節說明如何使用其他 App Engine 工具和 API 部署命名空間。

為個別使用者建立命名空間

有些應用程式需要為個別使用者建立命名空間。如果您想要為登入的使用者區隔使用者層級的資料,請考慮使用 User.user_id() 來傳回使用者的唯一永久 ID。以下程式碼範例示範如何使用 Users API 進行這項作業:

from google.appengine.api import users

def namespace_manager_default_namespace_for_request():
    # assumes the user is logged in.
    return users.get_current_user().user_id()

一般來說,為個別使用者建立命名空間的應用程式,也會為個別使用者提供特定的到達網頁。在這些情況下,應用程式必須提供網址通訊協定,指出要向使用者顯示哪個到達網頁。

將命名空間與 Datastore 搭配使用

根據預設,資料儲存庫將針對資料儲存庫要求,使用目前在命名空間管理員中的命名空間設定。API 會在建立 KeyQuery 物件時,為其套用目前的命名空間。因此,當應用程式以序列化形式儲存 KeyQuery 物件時,請特別小心,因為命名空間將以這些序列化格式保存。

如果您使用的是去序列化的 KeyQuery 物件,請確定這些物件能如預期執行。針對使用資料儲存庫 (put/query/get) 而沒有使用其他儲存機制的大多數簡易應用程式,如能在呼叫任何資料儲存庫 API 之前先設定目前的命名空間,則這些應用程式大多都能夠如預期運作。

QueryKey 物件在命名空間方面展示的獨特行為如下:

  • 除非您已設定明確的命名空間,否則 QueryKey 物件會在建構時繼承目前的命名空間。
  • 當應用程式從祖系建立新的 Key 時,這個新的 Key 將繼承祖系的命名空間。

以下程式碼範例是要求處理常式實例,這個處理常式可針對全域命名空間與任意指定的命名空間,遞增資料儲存庫中的計數器。

from google.appengine.api import namespace_manager
from google.appengine.ext import ndb
import webapp2


class Counter(ndb.Model):
    count = ndb.IntegerProperty()


@ndb.transactional
def update_counter(name):
    """Increment the named counter by 1."""
    counter = Counter.get_by_id(name)
    if counter is None:
        counter = Counter(id=name, count=0)

    counter.count += 1
    counter.put()

    return counter.count


class DatastoreCounterHandler(webapp2.RequestHandler):
    """Increments counters in the global namespace as well as in whichever
    namespace is specified by the request, which is arbitrarily named 'default'
    if not specified."""

    def get(self, namespace='default'):
        global_count = update_counter('counter')

        # Save the current namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            namespace_count = update_counter('counter')
        finally:
            # Restore the saved namespace.
            namespace_manager.set_namespace(previous_namespace)

        self.response.write('Global: {}, Namespace {}: {}'.format(
            global_count, namespace, namespace_count))


app = webapp2.WSGIApplication([
    (r'/datastore', DatastoreCounterHandler),
    (r'/datastore/(.*)', DatastoreCounterHandler)
], debug=True)

將命名空間與 Memcache 搭配使用

根據預設,Memcache 會針對 Memcache 要求,使用命名空間管理員目前的命名空間。在大部分情況下,您不需要在 Memcache 中明確設定命名空間,否則可能造成未預期的錯誤。

不過於某些特殊案例中,在 Memcache 明確設定命名空間會是適當的做法。舉例來說,應用程式可能有在所有命名空間之間共用的常用資料 (例如包含國碼的表格)。

以下程式碼片段示範如何在 Memcache 中明確設定命名空間:

使用適用於 Memcache 的 Python API,即可向命名空間管理員取得目前的命名空間,您也可以在建立 Memcache 服務時明確地設定命名空間。

以下程式碼範例顯示的是簡易的要求處理常式,這個處理常式可針對全域命名空間與任意指定的命名空間,遞增 Memcache 中的計數器。

from google.appengine.api import memcache
from google.appengine.api import namespace_manager
import webapp2


class MemcacheCounterHandler(webapp2.RequestHandler):
    """Increments counters in the global namespace as well as in whichever
    namespace is specified by the request, which is arbitrarily named 'default'
    if not specified."""

    def get(self, namespace='default'):
        global_count = memcache.incr('counter', initial_value=0)

        # Save the current namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            namespace_count = memcache.incr('counter', initial_value=0)
        finally:
            # Restore the saved namespace.
            namespace_manager.set_namespace(previous_namespace)

        self.response.write('Global: {}, Namespace {}: {}'.format(
            global_count, namespace, namespace_count))


app = webapp2.WSGIApplication([
    (r'/memcache', MemcacheCounterHandler),
    (r'/memcache/(.*)', MemcacheCounterHandler)
], debug=True)

下列範例會在您將值儲存於 Memcache 時明確設定命名空間:

  // Store an entry to the memcache explicitly
memcache.add("key", data, namespace='abc')

將命名空間與工作佇列搭配使用

根據預設,發送佇列會使用工作建立時在命名空間管理員中設定的目前命名空間。在大部分情況下,您不需要在工作佇列明確設定命名空間,否則可能造成意料之外的錯誤。

所有命名空間都將共用工作名稱。您不能建立兩個具有相同名稱的工作,即使這些工作使用不同的命名空間亦同。如果您要讓多個命名空間使用相同的工作名稱,可以將各個命名空間附加到工作名稱。

當新的工作呼叫工作佇列 add() 方法時,工作佇列會從命名空間管理員複製目前的命名空間及 (如果適用) Google Workspace 網域。執行工作時,則會復原目前的命名空間與 Google Workspace 命名空間。

如果來源要求中並未設定目前的命名空間 (即如果 get_namespace() 傳回 ''),您可以使用 set_namespace() 為工作設定目前的命名空間。

在某些特殊情況下,建議針對在所有命名空間中運作的工作明確設定命名空間。舉例來說,您可能建立一個工作,匯總所有命名空間的使用統計資料。接著,您可以明確設定這個工作的命名空間。下列的程式碼範例說明如何為工作佇列明確設定命名空間。

from google.appengine.api import namespace_manager
from google.appengine.api import taskqueue
from google.appengine.ext import ndb
import webapp2


class Counter(ndb.Model):
    count = ndb.IntegerProperty()


@ndb.transactional
def update_counter(name):
    """Increment the named counter by 1."""
    counter = Counter.get_by_id(name)
    if counter is None:
        counter = Counter(id=name, count=0)

    counter.count += 1
    counter.put()

    return counter.count


def get_count(name):
    counter = Counter.get_by_id(name)
    if not counter:
        return 0
    return counter.count


class DeferredCounterHandler(webapp2.RequestHandler):
    def post(self):
        name = self.request.get('counter_name')
        update_counter(name)


class TaskQueueCounterHandler(webapp2.RequestHandler):
    """Queues two tasks to increment a counter in global namespace as well as
    the namespace is specified by the request, which is arbitrarily named
    'default' if not specified."""
    def get(self, namespace='default'):
        # Queue task to update global counter.
        current_global_count = get_count('counter')
        taskqueue.add(
            url='/tasks/counter',
            params={'counter_name': 'counter'})

        # Queue task to update counter in specified namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            current_namespace_count = get_count('counter')
            taskqueue.add(
                url='/tasks/counter',
                params={'counter_name': 'counter'})
        finally:
            namespace_manager.set_namespace(previous_namespace)

        self.response.write(
            'Counters will be updated asyncronously.'
            'Current values: Global: {}, Namespace {}: {}'.format(
                current_global_count, namespace, current_namespace_count))


app = webapp2.WSGIApplication([
    (r'/tasks/counter', DeferredCounterHandler),
    (r'/taskqueue', TaskQueueCounterHandler),
    (r'/taskqueue/(.*)', TaskQueueCounterHandler)
], debug=True)

將命名空間與 Blobstore 搭配使用

命名空間不會區隔 Blobstore。如要在 Blobstore 保留命名空間,您需要透過能感知命名空間的儲存媒體 (目前只有 Memcache、資料儲存庫和工作佇列) 來存取 Blobstore。舉例來說,如果 Blob 的 Key 儲存於資料儲存庫實體中,您就可使用能感知命名空間的資料儲存庫 KeyQuery 來加以存取。

如果應用程式存取 Blobstore 時使用的金鑰儲存在可感知命名空間的儲存空間中,就無需透過命名空間來區隔 Blobstore。應用程式必須藉由以下方式,避免命名空間之間的 Blob 外洩:

  • 使用者要求不使用 BlobInfo.gql()。您可以針對管理要求 (例如產生所有應用程式 Blob 的相關報表) 使用 BlobInfo 查詢,但對使用者要求使用 BlobInfo 查詢可能造成資料外洩,因為所有 BlobInfo 記錄尚未透過命名空間加以區隔。
  • 不使用來源不受信任的 Blobstore 金鑰。

為 Datastore 查詢設定命名空間

在 Google Cloud 控制台中,您可以為 Datastore 查詢設定命名空間

如果您不想要使用預設值,請從下拉式選單中選取您要使用的命名空間。

將命名空間與 Bulk Loader 搭配使用

Bulk Loader 支援 --namespace=NAMESPACE 標記,可讓您指定要使用的命名空間。系統會分別處理每個命名空間,如果您要存取所有命名空間,則須逐一查看。

建立 Index 的新例項時,系統預設會將其指派給目前的命名空間:

# set the current namespace
namespace_manager.set_namespace("aSpace")
index = search.Index(name="myIndex")
# index namespace is now fixed to "aSpace"

您也可以透過建構函式明確指派命名空間:

index = search.Index(name="myIndex", namespace="aSpace")

建立索引規格後,其命名空間就無法變更:

# change the current namespace
namespace_manager.set_namespace("anotherSpace")
# the namespaceof 'index' is still "aSpace" because it was bound at create time
index.search('hello')