最佳化 Spanner 的結構定義設計

Google 儲存技術為全球許多大型應用程式提供強大的後援。然而,規模往往不是使用這些系統自動帶來的結果。設計人員必須仔細思考如何設計資料模型,確保應用程式可以隨著各方面的成長而擴充並執行。

Spanner 是「分散式資料庫」,必須使用與傳統資料庫不同的方式來思考結構定義設計和存取模式,才能有效率的使用它。分散式系統因為其本質的關係,也迫使設計人員思考資料與處理的位置。

Spanner 支援 SQL 查詢和交易,能夠水平式的擴充。縝密的設計才能讓 Spanner 的優勢全面的展現。本頁面討論一些可以協助您的重要觀念,確保應用程式可擴充至任意層級並且最大化其效能。有兩樣工具對於擴充性的影響最大:金鑰定義與交錯。

資料表配置

Spanner 資料表中的資料列是藉由 PRIMARY KEY,按字母順序組織而成。就概念上而言,金鑰是以在 PRIMARY KEY 子句中宣告的順序,依資料欄的串連排序而成。這展示出位置的標準屬性:

  • 按字母順序掃描資料表是很有效率的。
  • 夠近的資料列會儲存在相同的磁碟區塊,一起被讀取和快取。

Spanner 會跨多個區域複製資料,以便提供可用性和擴充性。每個區域都會保留完整的資料備用資源。佈建 Spanner 執行個體節點時,您必須指定其運算能力。運算能力是指在每個區域中分配給執行個體的運算資源數量。每個備用資源都是一整組資料,備用資源內的資料則依區域中的運算資源分區。

每個 Spanner 備用資源內的資料都會分成兩個實體階層層級:「資料庫分組」,然後是「區塊」。分組擁有連續的資料列範圍,也是 Spanner 將依運算資源分配資料庫的單位。隨著時間過去,分組可能會拆分為更小的部分、被合併或是移到執行個體中的其他節點,以增進平行處理,讓應用程式得以擴充。跨越分組的作業由於通訊量增加,費用比不需跨越分組的相對應作業更高。即使這些分組的服務由相同節點提供也是如此。

Spanner 中有兩種資料表:「根資料表」 (有時稱為頂層資料表) 與「交錯式資料表」。交錯式資料表是由指定其他資料表為其「父項」的方式定義,讓交錯式資料表中的資料列與父項資料列分在同一群。根資料表沒有父系,根資料表的每個資料列會定義新的頂層資料列或「根資料列」。與此根資料列交錯的資料列稱為「子資料列」,而根資料列加上其所有子資料列的集合就稱為「資料列樹狀結構」。父項資料列必須存在,您才能插入子項資料列。父項資料列可能已經存在於資料庫中,或者在相同交易中插入子項資料列之前插入。

Spanner 會視大小或負載,在必要時自動將分組分區。為保留資料本地性,Spanner 會盡量在靠近根資料表處新增分割界線,以便將任何指定的資料列樹狀結構保留在單一分割中。也就是說,資料列樹狀結構中的作業通常效率較高,因為不太需要與其他分割區進行通訊。

不過,如果子項資料列中有熱點,Spanner 會嘗試在交錯式資料表中加入分割邊界,以便隔離該熱點資料列,以及下方所有子項資料列。

在設計可擴充的應用程式時,選擇根資料表是很重要的決策。根資料表通常與使用者、帳戶和專案等相關,這些資料表的子資料表擁有相關實體的大部分其他資料。

建議做法:

  • 使用相同資料表中相關資料列的共用索引鍵前置字串來改善位置。
  • 任何適當的時候,都可將相關的資料與另一個資料表交錯。

位置的取捨

如果經常同時寫入或讀取資料,謹慎地選擇主鍵並使用交錯有助益於分群時的延遲時間和總處理量。這是由於與任何伺服器或磁碟區塊通訊都有固定成本的關係。既然有既定成本,何不想辦法讓效益極大化?此外,通訊的伺服器越多,遇到暫時性的伺服器忙碌的機會就會越高,增加尾部延遲時間。最後,跨越分割的交易在 Spanner 中雖然是自動透明化處理,但由於兩階段修訂的分散式本質,CPU 的成本稍高且延遲時間也較長。

就另一方面,如果資料相關但並未經常一起進行存取,請考慮盡力將資料分隔。如果非經常存取的資料很大量時,這個做法特別有利。例如,很多資料庫都儲存大量來自原始資料列的頻外二進位資料,只參照到大量交錯資料。

請注意,在分散式的資料庫中,有些兩階段修訂和非本機資料作業是無法避免的。請勿過度在意,期望每項作業都有完美的位置。著重於取得最重要根實體想要的位置,以及最常存取的模式,並且讓比較不常使用或不受效能影響的分散式作業在需要的時候進行。兩階段修訂和分散式讀取作業可協助簡化結構定義並減少程式設計師的工作,除了效能最重要的使用案例之外,最好如此進行。

建議做法:

  • 依階層組織資料,如此較能在附近一起讀取或寫入資料。
  • 考慮將比較少存取的大型資料欄儲存在非交錯的資料表內。

索引選項

次要索引讓您可以快速由主鍵以外的值找到資料列。Spanner 支援交錯與非交錯的索引。非交錯式的索引為預設值,也是最類似傳統 RDBMS 支援的類型。這些索引不會對編製索引的資料欄有任何限制。雖然索引功能強大,但通常不是最佳選擇。您必須定義資料欄上交錯的索引,這些資料欄與父系資料表共用前置字串,並且較能控制位置。

Spanner 儲存索引資料的方式與資料表相同,採用每個索引項目一個資料列的方式。許多資料表設計的考量也適用於索引。非交錯式的索引會將資料儲存在根資料表中。由於根資料表可以在任何根資料列之間分組,如此可確保非交錯式的索引能夠忽略熱點,擴充至任意大小,幾乎適用於任何工作負載。遺憾的是,這也表示索引項目通常不存在於主要資料的相同分組中。這會引起更多的工作,並造成任何寫入程序的延遲,並且增加讀取時間需要查閱的額外分組。

相反地,交錯式的索引會將資料儲存在交錯式資料表。當您在單一實體的網域內搜尋時,就很適合使用這些索引。交錯式索引強制資料和索引項目在相同的資料列樹狀結構中,讓兩者的聯結更具效率。交錯式索引的使用範例:

  • 藉由各種排序順序來存取照片,例如拍照日期、上次修改日期、標題或相簿等。
  • 找出具備特定一組標記的所有貼文。
  • 尋找之前包含特定項目的購物訂單。

建議做法:

  • 如要找出資料庫任何位置的資料列,請使用非交錯式索引。
  • 當您的搜尋範圍是單一實體時,則建議使用交錯式索引。

STORING 索引子句

次要索引可讓您藉由非主鍵的屬性來找出資料列。如果所有要求的資料都位於索引本身當中,即可自己查詢而無需讀取主要記錄。由於不需要彙整,如此可節省相當多的資源。

遺憾的是,索引鍵數字限制為 16,而匯總的大小限制為 8 KiB,因此限制了其中可以放置的內容。為了彌補這些限制,Spanner 可利用 STORING 子句,在任何索引中儲存額外資料。在索引中 STORING 資料欄會導致其值遭複製,副本會儲存於索引中。您可以把具備 STORING 的索引想成簡單的單一資料表具體化資料檢視 (Spanner 目前尚未原生支援資料檢視)。

其他對於 STORING 的有效應用則是當做 NULL_FILTERED 索引的一部分。這讓您可以針對資料表中稀疏子集,定義有效的具體化資料檢視,以更有效率地掃描資料表。例如,您可以在信箱的 is_unread 資料欄上建立這樣的索引,處理單一資料表掃描中未讀取的郵件檢視畫面,而無需因為完整複製每個信箱而付費。

建議:

  • 審慎使用 STORING,權衡讀取時間效能與儲存空間大小和寫入時間效能。
  • 使用 NULL_FILTERED 控制稀疏索引的儲存空間成本。

反模式

反模式:時間戳記排序

許多架構設計師都傾向於依時間戳記排序定義根資料表,然後在每一次寫入時更新。可惜的是,這是最不具擴充性的做法之一。這樣的設計會導致資料表尾端產生大量熱點,不易處理。隨著寫入速率增加,如同在單一分組進行遠端程序呼叫 (RPC),會鎖定爭用事件和其他問題。通常這類問題都不會出現在小量負載的測試,而是於應用程式在實際工作環境中的某個的時刻才發生。而那時候發現,通常為時已晚。

如果應用程式絕對需要包含以時間戳記排序的記錄,請考量是否能夠將記錄與一個其他根資料表交錯,以讓記錄留在本機。這樣的優點就是可以將熱點分散在許多根實體。但您仍須注意每個根實體是否都有足夠的低寫入率。

如果需要全域 (跨根實體) 依時間戳記排序的資料表,您必須使用應用程式層級的「資料分割」,提供該資料表高於單一節點所能提供的寫入速率。分割資料表代表將資料表大約平均分區成 N 個稱為資料分割的部分。一般而言,這項作業只需在原始主鍵之前中加上額外的 ShardId 資料欄 (值介於整數值 [0, N) 之間)。指定寫入作業的 ShardId 通常經由隨機選取或由雜湊部分基礎鍵而來。比較建議的方法是雜湊,因為它可用來確認指定類型的所有記錄都前往相同資料分割,改善擷取的效能。不論是哪一種方式,目標都是確保寫入作業可以隨著時間平均地分散在所有資料分割。這個方法有時也表示讀取作業必須掃描所有資料分割,以重新建構寫入項目的原始整體排序。

圖示平行處理的資料分割及每個資料分割依時序的資料列

建議:

  • 竭盡全力避免使用依高寫入速度時間戳記排序的資料表和索引。
  • 使用一些技巧來分散熱點,可與另一個資料表或資料分割交錯。

反模式:序列

應用程式開發人員喜歡使用資料庫序列 (或自動遞增) 來產生主鍵。可惜的是,這個來自於 RDBMS 時期 (稱為代理鍵) 的舊習慣,幾乎與上述的時間戳記排序的反模式一樣有害。原因就是,資料庫序列傾向以擬單調的方式,隨著時間產生在彼此附近的值。如果當做主鍵使用,尤其是用於根資料列時,通常會產生熱點。

我們與 RDBMS 的傳統看法相反,建議您在合理的情況下使用實際的屬性做為主鍵,特別是在屬性永久不變的情況下更應如此。

如果您想要產生不重複的數字主鍵,請將目標放在取得後續號碼的高階位元上,讓號碼可以大致平均地散佈在整個數字空間。有個技巧是藉由傳統的方式產生序號,然後反轉位元以取得最終值。或者,您也可以尋求 UUID 產生器,只是要注意的是,不是所有 UUID 函式都會平均地建立,有些會在高階位元儲存時間戳記,大幅抵消它所帶來的優勢。請確認您的 UUID 產生器會偽隨機地選擇高階位元。

建議做法:

  • 避免使用遞增的序列值當做主鍵。改用位元反轉的序列值,或小心選用 UUID。
  • 使用實際值當做主鍵,而不使用代理鍵。