Apple 如何構建 iCloud 來存儲數十億個資料庫

2024-01-18     51CTO

原標題:Apple 如何構建 iCloud 來存儲數十億個資料庫

作者丨Leonardo Creed

編譯丨諾亞

在過去的幾個月里,我寫了關於大型科技公司的各種技術「幕後揭秘」的文章,例如 Meta 的內部無伺服器平台、 Google 內部喜愛的代碼審查工具等等。不過,蘋果的基礎設施並不那麼公開。我想了解 Apple 是如何構建 iCloud 的,在這篇文章中,我將介紹我所知道的一切。

Apple 將 FoundationDB 和 Cassandra 用於其雲端後端服務 iCloud 和 CloudKit。而且本文的標題並沒有弄錯:蘋果確實在其極端的多租戶架構中存儲了數十億個資料庫

閱讀指南

我發現,論文中以及蘋果的實踐經驗與Meta無伺服器平台架構的設計原則和教訓高度契合。

1、兩者都巧妙地運用了異步處理技術,以實現用戶功能的流暢性。Meta在其無伺服器架構中,將非面向用戶的函數任務利用該技術進行處理,從而避免影響用戶體驗。而蘋果則在Record Layer(在下文將詳細解釋)的幾乎全部功能上採用異步處理方式,目的是隱藏延遲,確保用戶感受到的是即時響應。

2、兩者都廣泛採用無狀態架構設計,鑒於它們都有極高的可擴展性需求。(註:無狀態架構意味著伺服器不保存任何會話或請求之間的持久化狀態信息,從而使得每個請求都能獨立處理,且可以根據需要輕鬆地增加或減少伺服器實例以應對流量變化。)

3、兩者都通過邏輯隔離資源來確保可靠性和可用性。

4、兩者都以簡化的方式處理各類需求。蘋果提到,為存儲「小數據」和「大數據」分別配置和運營獨立系統是很有誘惑力的做法,但這會增加運維的複雜性。因此,蘋果選擇用一個抽象層來處理所有類型的數據需求。同樣地,Meta在他們的無伺服器平台上也採用相同策略,提供了一個統一的抽象層,用於處理各種函數負載。

5、兩者都通過構建抽象層來優化開發者體驗,讓應用開發者無需過多關注可擴展性需求。這些需求由底層分布式系統工程師在更深層次的架構中處理。

6、深知用戶需求。無論是Meta還是Apple,它們提供的每一層架構、API設計以及每一個設計決策都是基於對特定技術使用者(無論是應用開發團隊還是可觀測性團隊)清晰理解的基礎上制定的。

Cassandra

Cassandra 是一種分布式、寬列式NoSQL資料庫管理系統,最初由Facebook開發,用於支持Facebook收件箱搜索功能的實現。有趣的是,後來的Meta自身已逐步用ZippyDB替代了大量原本使用Cassandra的地方。

根據DataStax的信息,iCloud的部分功能由Cassandra提供支持。蘋果運營著全球規模最大的Cassandra部署之一。

他們報告指出:

1. 超過30萬個實例/節點

2. 數據規模達到數百PB(甚至EB級別)

3. 每個集群處理超過2 PB的數據,且擁有數千個這樣的集群

4. 每秒處理數百萬次查詢

5. 支持數千個應用程式

Cassandra在iCloud中的應用確實彰顯了其管理海量數據的能力,達到EB級別。蘋果在其伺服器上採用多節點Cassandra部署策略,並且團隊在設計時非常注重「爆炸半徑」(blast radius)控制和數據分片(sharding),以最大程度地減少故障影響範圍並優化數據分布與訪問性能,從而確保iCloud服務的數據可用性接近100%。

與此同時,蘋果公司內部仍在積極改進Cassandra技術。來自蘋果公司的Scott Andreas最近發表了關於Cassandra未來發展的演講。同時,在蘋果的招聘頁面上,經常可以看到他們為分布式系統工程師崗位列出熟練使用Cassandra的要求。

儘管Cassandra在處理大規模分布式存儲方面表現出色,但在蘋果iCloud的特定場景下,結合使用CloudKit和Cassandra時遇到了兩個關鍵的可擴展性限制,這導致他們採用了 FoundationDB。

1、在Cassandra單一分區內,即使編輯的是不同的記錄,同一時間也只能進行一個操作。這意味著對於那些需要多個用戶或設備同時處理共享數據的應用程式來說,可能會出現性能瓶頸和並發控制問題。

2、在Cassandra中,如果需要在一個原子操作內同時更新多個記錄,這些更新操作會受限於單個Cassandra分區。每個分區都有其能夠處理的最大數據量限制,隨著分區內數據的不斷增長,Cassandra的性能往往會隨之下降。

FoundationDB 和 Record Layer 解決了這兩個問題。

FoundationDB

蘋果對FoundationDB的公開程度要高得多。他們於 2015 年收購了 FoundationDB,此後發表了多篇論文,詳細介紹了他們對 FoundationDB 的使用。

FoundationDB 是一個開源的分布式事務型鍵值存儲系統,旨在處理大規模的數據量,並且在讀寫混合負載以及寫入密集型工作負載方面表現出色。此外,FoundationDB 也符合 ACID(原子性、一致性、隔離性和持久性)原則。

蘋果在CloudKit(其雲端後端服務)中廣泛使用了FoundationDB Record Layer。

來源:《FoundationDB Record Layer:開源結構化存儲》

從GitHub上的描述來看,Record Layer是一個基於FoundationDB的Java API,它提供了一種面向記錄的存儲方式,可以大致類比為一個簡單的關係型資料庫。具體特性包括:

1. 結構化類型:記錄以Protocol Buffer(protobuf)消息的形式進行定義和存儲,Protocol Buffer是一種最初由Google設計的數據序列化協議。

2. 索引:Record Layer支持多種索引類型,如值索引(大多數資料庫都提供的那種)、排名索引和聚合索引。索引和主鍵可以通過protobuf選項或程序化方式來定義。

3. 複雜類型:支持複雜數據類型,例如列表和嵌套記錄,並且能夠針對這些嵌套結構定義索引。

4. 查詢功能:雖然Record Layer並未提供查詢語言,但它提供了API接口,支持對一個或多個記錄類型進行掃描、過濾和排序操作,同時還包含一個能夠自動選擇合適索引的查詢規劃器。

5. 多個記錄存儲與共享模式:Record Layer允許創建並管理多個獨立的記錄存儲實例,所有實例都採用共享(且可動態演變)的模式。舉例來說,不同於在一個單一資料庫中存儲所有用戶數據的方式,每個用戶可以擁有自己的記錄存儲,甚至可以根據需要跨不同的FDB集群實例進行分片處理。

6. 極輕量級:Record Layer被設計用於大型、分布式、無狀態環境,旨在實現從打開存儲到執行首次查詢之間的時間間隔達到毫秒級別。

7.擴展性強:新的索引類型以及自定義索引鍵表達式可以動態地融入到記錄存儲中。

根據FoundationDB Record Layer論文所述,蘋果使用FoundationDB Record Layer為服務數億用戶的大型應用提供強大的抽象層支持。CloudKit 使用Record Layer來託管數十億個獨立的資料庫,其中許多資料庫共享相同的模式(schema)。

為什麼要使用 FoundationDB Record Layer

FoundationDB、Record Layer 和 CloudKit 的結構如下所示:

來源:《FoundationDB Record Layer: 開源結構化存儲》

  • FoundationDB 負責所有的分布式系統和並發控制工作。
  • Record Layer 作為中間層,充當了關係數據庫,以便開發者能夠更輕鬆地與 FoundationDB 進行交互。
  • CloudKit 是最頂層的服務,為應用開發者提供了豐富的功能和API。雖然CloudKit是構建在Record Layer之上的一個典型應用案例,但內部還有其他服務和組件也基於Record Layer構建,比如用於處理JSON文檔存儲等需要結構化存儲的場景。

Record Layer 使蘋果能夠在大規模上實現多租戶支持。

實際上,這樣的描述可能還顯得保守了。

Record Layer 被用於極端的多租戶環境,其中每個應用程式的每個用戶都能獲得獨立的記錄存儲空間。這意味著Record Layer 託管著數十億個共享數千種模式的獨立資料庫。

來源:《FoundationDB Record Layer: 開源結構化存儲》

Record Layer 能夠在如此大規模上成功處理多租戶問題,主要歸功於其兩個核心架構決策

1.無狀態操作:Record Layer 設計為無狀態模式,這意味著通過簡單地增加更多無狀態實例,就可以輕鬆擴展計算資源。

這種設計使得負載均衡器和路由器的工作變得更為簡化,它們只需關注數據的位置而非計算伺服器的具體能力。同時,由於無狀態伺服器無需維護會話狀態等信息,因此分配給客戶端的資源集合得以減少。

2.記錄存儲抽象化管理:Record Layer 使用記錄存儲抽象層來高效管理資源分配和可擴展性。這個抽象層代表了整個邏輯資料庫,包含了序列化數據、索引以及運行時狀態。

每個記錄存儲都有特定的鍵範圍分配,確保不同租戶的數據在邏輯上保持分離。如有必要遷移某個租戶的數據,過程十分直接,只需將分配給該租戶的鍵範圍遷移到新的集群中即可,因為管理與使用該記錄存儲所需的所有信息都包含在這個鍵範圍內。

CloudKit 如何使用 FoundationDB 和Record Layer

來源:《FoundationDB Record Layer: 多租戶結構化數據存儲系統》

在CloudKit中,每個應用程式由一個遵循特定模式的「邏輯容器」來表示。這個模式詳細定義了必要的記錄類型、欄位和索引,以實現高效的數據檢索和查詢功能。應用程式在CloudKit內部將其數據組織到不同的「區域」(zones)中,這樣可以按邏輯分組記錄,便於與客戶端設備進行選擇性同步。

對於每一位用戶,CloudKit在FoundationDB中分配一個唯一的子空間。在這個子空間內,針對用戶使用的所有應用程式,CloudKit都會為每個應用創建一個記錄存儲。換言之,CloudKit實際上管理著大量邏輯資料庫——即用戶數量乘以應用程式數量所得到的數量級,每一個都包含其自身的記錄集、索引和元數據,總量高達數十億個獨立資料庫。

當CloudKit接收到來自客戶端設備的請求時,它會通過負載均衡機制將請求導向可用的CloudKit服務進程。該服務進程隨後與Record Layer中的相應記錄存儲進行交互,以完成請求操作。

CloudKit將定義好的應用程式模式轉換為Record Layer中的元數據定義,並將其存儲在獨立的元數據存儲中。此外,CloudKit還會添加特定的系統欄位來豐富這些元數據,如記錄的創建時間、修改時間以及記錄所在的區域信息。為了實現對每個區域內記錄的有效訪問,區域名稱會被作為前綴附加到主鍵上。除了用戶自定義的索引外,CloudKit還管理「系統索引」,例如為了管理存儲配額而維護的一種根據記錄類型追蹤其大小的索引。

FoundationDB和Record Layer結合使用,共同解決了蘋果面臨的一些關鍵問題,這些問題單靠Cassandra或FoundationDB都無法完美解決

已解決的問題

個性化全文搜索

FoundationDB在解決用戶個性化全文搜索,以快速訪問其數據方面發揮了重要作用。蘋果的系統利用了FoundationDB的鍵順序特性,能夠實現對文本開頭(前綴匹配)進行快速搜索,並且無需額外開銷即可處理更複雜的搜索需求,如查找相近詞或特定順序排列的詞語(鄰近搜索和短語搜索)。

在傳統的搜索系統中,通常需要後台運行額外的進程來保持搜索索引的實時更新。而蘋果的系統則實現了所有操作的實時性,這意味著一旦數據發生變化,搜索索引會立即得到更新,無需任何額外步驟。這種設計不僅提高了搜索效率,還確保了數據的一致性和時效性,為用戶提供更為流暢、準確的搜索體驗。

高並發區域

FoundationDB為CloudKit處理同時發生的大量更新提供了更為平滑和一致的方式。

在以前使用Cassandra時,CloudKit依賴於一個特殊的索引來追蹤各個區域內的數據變化以實現跨設備同步。當設備需要更新數據時,會通過檢查這個索引來獲取最新信息。但這種方法存在一個問題:當多台設備幾乎同時進行更新操作時,可能會引發衝突。

而採用FoundationDB後,CloudKit利用了一種特殊類型的索引,它可以精確地跟蹤每一次更改的順序,而且不會導致衝突。這種機制是通過為每次變更分配一個唯一的「版本號」來實現的,當CloudKit需要進行同步時,它會根據這些版本號來確定設備錯過了哪些更新內容。

然而,在將數據從一個存儲集群轉移到另一個存儲集群(可能是為了更均勻地分布負載)時,情況變得複雜起來,因為每個集群都有自己獨立的、不匹配的版本號。為解決這一問題,CloudKit為每位用戶的每份數據賦予了一個稱為「化身」的「遷移計數」,每當用戶的數據被遷移到新集群時,「化身」值就會遞增。每個記錄更新都會包含用戶當前的「化身」號碼,從而確保即使在遷移之後,CloudKit仍可以根據化身號和版本號來正確判斷出更新的順序。

當CloudKit切換到這個新系統時,面臨的挑戰之一是如何處理那些尚未帶有版本號的老數據。他們巧妙地解決了這個問題,通過運用一種特殊函數,該函數可以先按照舊系統的方式對老的更新進行排序,然後再加入新系統的更新。這意味著無需對應用程式進行複雜的改動或遺留過時代碼。此函數綜合考慮了化身、版本以及舊的更新計數器值,確保了記錄順序的準確性。

高延遲查詢

FoundationDB 是為高並發設計的,而非針對低延遲。這意味著它能夠同時處理大量任務,而不是專注於單個任務的速度。

來源:《FoundationDB Record Layer: 開源結構化存儲》

為了充分利用這種設計,Record Layer在處理任務時採用了大量的異步操作方式——它會將任務排隊等待未來完成,期間可以繼續進行其他工作。這種方法有助於掩蓋這些任務執行過程中可能出現的延遲。

然而,FoundationDB用於與資料庫通信的工具最初是採用單線程模式設計,一次只做一件事並使用一個網絡線程。在早期版本中,這種設置導致了系統內部的擁堵,因為所有任務都在等待輪到自己在這條網絡線程上執行。Record Layer也沿用了這種單線程處理方法,這導致了性能瓶頸。

為了解決這一問題,蘋果公司著手減輕這條網絡線程的工作負載。現在,通過讓系統同時從多個角度與資料庫協同工作,而非形成單一的任務隊列,使得複雜的任務看上去執行速度更快。這樣一來,由於系統無需等待一個任務完成後再開始另一個任務,所以延遲或所謂的「緩慢感」被有效地隱藏起來。

衝突事務

在FoundationDB中,如果一個事務正在讀取某些鍵值,而另一個事務在同一時刻修改了這些相同的鍵值,則會導致「事務衝突」。FoundationDB通過提供對可能導致衝突的鍵集合進行精確控制的能力,從而允許精細管理這些衝突。

避免不必要的衝突的一個常見方法是對一組鍵執行一種特殊的、不會引發衝突的讀取操作,即所謂的「快照」讀取。如果這種讀取發現重要鍵值,那麼事務只會針對那些特定鍵標記潛在衝突,而不是整個鍵範圍。這樣可以確保事務只受到與其結果真正相關的更改影響。

Record Layer採用了這一策略來高效地管理其排名索引系統中的一部分結構——跳表(skip list)。然而,手動設置這些衝突範圍可能較為複雜,並且可能導致難以識別的錯誤,特別是當它們與應用程式的主要邏輯混合在一起時。因此,建議構建於FoundationDB之上的系統創建更高級別的工具,如自定義索引,以處理這些模式。這種方法有助於避免將放寬衝突規則的責任留給每個客戶端應用,否則可能會導致錯誤和一致性問題的發生。

文章來源: https://twgreatdaily.com/zh-tw/81e56dfe9e58e7276e0bcabba7a6157c.html