直到最近,Odnoklassniki 在 SQL Server 中儲存了約 50 TB 即時處理的資料。 對於這樣的捲,使用 SQL DBMS 幾乎不可能提供快速可靠、甚至是資料中心容錯的存取。 通常,在這種情況下,會使用 NoSQL 儲存之一,但並非所有內容都可以轉移到 NoSQL:某些實體需要 ACID 事務保證。
這導致我們使用 NewSQL 存儲,即提供 NoSQL 系統的容錯性、可擴展性和效能的 DBMS,但同時保持經典系統所熟悉的 ACID 保證。 這種新類型的工業系統很少,所以我們自己實現了這樣的系統並投入商業運作。
它是如何運作的以及發生了什麼 - 請閱讀下面的內容。
如今,Odnoklassniki 每月的獨立訪客數量超過 70 萬。 我們
我們從 2010 年開始使用 Cassandra,從 0.6 年版本開始。 如今,有數十個叢集正在運作。 最快的叢集每秒處理超過 4 萬次操作,最大的儲存容量為 260 TB。
然而,這些都是用於儲存的普通NoSQL集群
為了跨 SQL Server 節點分佈數據,我們使用了垂直和水平
感謝分片並加速 SQL:
- 我們不使用外鍵約束,因為分片時實體 ID 可能位於另一台伺服器上。
- 由於 DBMS CPU 上的額外負載,我們不使用預存程序和觸發器。
- 由於上述所有原因以及從磁碟進行的大量隨機讀取,我們不使用 JOIN。
- 在事務之外,我們使用「讀取未提交」隔離等級來減少死鎖。
- 我們僅執行短事務(平均短於 100 毫秒)。
- 由於有大量死鎖,我們不使用多行 UPDATE 和 DELETE - 我們一次只更新一筆記錄。
- 我們總是只對索引執行查詢 - 對我們來說具有全表掃描計劃的查詢意味著資料庫超載並導致其失敗。
這些步驟使我們能夠從 SQL 伺服器中獲得幾乎最大的效能。 然而,問題卻越來越多。 讓我們看看它們。
SQL 問題
- 由於我們使用了自己編寫的分片,因此新增分片是由管理員手動完成的。 一直以來,可擴展資料副本都沒有為請求提供服務。
- 隨著表中記錄數量的增加,插入和修改的速度會降低;向現有表添加索引時,速度會下降一倍;建立和重新建立索引會伴隨停機。
- 在生產環境中使用少量 Windows for SQL Server 會使基礎架構管理變得困難
但主要問題是
容錯
經典的SQL Server容錯能力較差。 假設您只有一台資料庫伺服器,並且每三年發生一次故障。 在此期間,網站當機 20 分鐘,這是可以接受的。 如果您有 64 台伺服器,則該網站每三週就會關閉一次。 如果您有 200 台伺服器,那麼該網站每週都無法運作。 這是問題。
如何提升 SQL Server 的容錯能力? 維基百科邀請我們共同構建
這需要一組昂貴的設備:大量的重複、光纖、共享存儲,並且包含的儲備不能可靠地工作:大約 10% 的交換以備份節點的故障結束,就像主節點後面的火車一樣。
但這種高可用叢集的主要缺點是,如果其所在的資料中心發生故障,則可用性為零。 Odnoklassniki 有四個資料中心,我們需要確保其中一個資料中心完全故障時仍能正常運作。
為此我們可以使用
所有這些問題都需要根本性的解決,我們開始詳細分析。 這裡我們需要熟悉一下SQL Server主要做的事情-事務。
交易簡單
讓我們從應用 SQL 程式設計師的角度考慮最簡單的事務:將照片新增至相簿。 相簿和照片儲存在不同的盤中。 該相簿有一個公共拍照櫃檯。 那麼這樣一個交易分為以下幾個步驟:
- 我們用鑰匙鎖定相簿。
- 在照片表中建立一個條目。
- 如果照片具有公開狀態,則將公開照片計數器新增至相簿中,更新記錄並提交交易。
或用偽代碼:
TX.start("Albums", id);
Album album = albums.lock(id);
Photo photo = photos.create(…);
if (photo.status == PUBLIC ) {
album.incPublicPhotosCount();
}
album.update();
TX.commit();
我們看到,業務事務最常見的場景是將資料從資料庫讀取到應用程式伺服器的記憶體中,更改某些內容並將新值保存回資料庫。 通常在這樣的事務中,我們會更新多個實體、多個表。
當執行事務時,可能會發生來自另一個系統的相同資料的並發修改。 例如,反垃圾郵件可能認為使用者有些可疑,因此使用者的所有照片不應再公開,需要將其發送以進行審核,這意味著將 photo.status 更改為其他值並關閉相應的計數器。 顯然,如果在不保證應用程式原子性和競爭修改隔離的情況下發生此操作,如
在 Odnoklassniki 的整個存在過程中,已經編寫了許多類似的程式碼,在一個事務中操作各種業務實體。 基於從 遷移到 NoSQL 的經驗
其他同樣重要的要求是:
- 如果資料中心發生故障,新儲存的讀取和寫入都必須可用。
- 保持目前的發展速度。 也就是說,當使用新的儲存庫時,程式碼量應該大致相同;不需要向儲存庫添加任何內容、開發解決衝突的演算法、維護二級索引等。
- 新儲存的速度必須相當高,無論是在讀取資料還是處理事務時,這實際上意味著學術上嚴格的、通用的但緩慢的解決方案,例如,不適用
兩階段提交 . - 自動動態縮放。
- 使用常規的廉價伺服器,無需購買奇特的硬體。
- 公司開發人員可以進行儲存開發。 換句話說,優先考慮專有或開源解決方案,最好是 Java。
決定,決定
透過分析可能的解決方案,我們得出了兩種可能的架構選擇:
第一個是採用任何 SQL 伺服器並實現所需的容錯、擴展機制、故障轉移叢集、衝突解決以及分散式、可靠且快速的 ACID 事務。 我們認為這個選項非常重要且是勞力密集的。
第二種選擇是採用現成的 NoSQL 存儲,並實現擴展、故障轉移叢集、衝突解決,並自行實現事務和 SQL。 乍一看,即使是實作 SQL 的任務,更不用說 ACID 事務,看起來也是一項需要數年時間的任務。 但後來我們意識到我們在實務上使用的 SQL 功能集與 ANSI SQL 相差甚遠
卡桑德拉和 CQL
那麼,Cassandra 有什麼有趣的地方,它有哪些功能呢?
首先,在這裡您可以建立支援各種資料類型的表;您可以對主鍵執行 SELECT 或 UPDATE。
CREATE TABLE photos (id bigint KEY, owner bigint,…);
SELECT * FROM photos WHERE id=?;
UPDATE photos SET … WHERE id=?;
為了確保副本資料的一致性,Cassandra 使用
當我們聯繫三個節點並收到兩個節點的回應時的方法稱為
Cassandra 的另一個好處是批次日誌,這是一種確保您所做的一批更改要么完全應用要么根本不應用的機制。 這使我們能夠解決 ACID 中的 A - 開箱即用的原子性。
Cassandra 中最接近交易的是所謂的“
我們在 Cassandra 中缺少什麼
因此,我們必須在 Cassandra 中實現真正的 ACID 事務。 使用它,我們可以輕鬆實現經典 DBMS 的另外兩個方便的功能:一致的快速索引,這將使我們不僅可以透過主鍵執行資料選擇,還可以使用單調自動遞增 ID 的常規產生器。
錐體
於是一個新的DBMS誕生了 錐體,由三種類型的伺服器節點組成:
- 儲存 –(幾乎)標準 Cassandra 伺服器負責在本機磁碟上儲存資料。 隨著數據負載和數據量的增長,它們的數量可以輕鬆擴展到數十甚至數百。
- 交易協調員 - 確保交易的執行。
- 客戶端是實現業務操作和發起事務的應用伺服器。 這樣的客戶可能有數千個。
所有類型的伺服器都是公共叢集的一部分,使用內部 Cassandra 訊息協定相互通信,
客戶
使用胖客戶端模式而不是標準驅動程式。 這樣的節點不會儲存數據,但可以充當請求執行的協調器,即客戶端本身充當其請求的協調器:它查詢儲存副本並解決衝突。 這不僅比需要與遠端協調器通訊的標準驅動程式更可靠、更快速,而且還允許您控制請求的傳輸。 在客戶端上開啟的事務之外,請求將傳送到儲存庫。 如果客戶端開啟了一個事務,那麼該事務內的所有請求都會傳送到事務協調器。
C*One 事務協調器
協調器是我們從頭開始為 C*One 實現的東西。 它負責管理事務、鎖以及事務應用的順序。
對於每個服務事務,協調器都會產生一個時間戳:每個後續事務都大於前一個事務。 由於 Cassandra 的衝突解決系統是基於時間戳(對於兩個衝突記錄,具有最新時間戳的記錄被視為當前記錄),因此衝突將始終以有利於後續事務的方式解決。 因此我們實現了
鎖具
為了確保隔離性,我們決定使用最簡單的方法—基於記錄主鍵的悲觀鎖。 換句話說,在事務中,必須先鎖定記錄,然後才能讀取、修改和保存。 只有成功提交後才能解鎖記錄,以便競爭事務可以使用它。
在非分散式環境中實現這種鎖定很簡單。 在分散式系統中,有兩個主要選擇:要麼在叢集上實現分散式鎖定,要麼分散式事務,以便涉及相同記錄的事務始終由相同協調器提供服務。
由於在我們的例子中,資料已經分佈在SQL 中的本地事務組中,因此決定將本地事務組分配給協調器:一個協調器使用0 到9 的令牌執行所有事務,第二個協調器使用10 到19 的令牌執行所有事務,等等。 結果,每個協調器實例都成為事務組的主實例。
然後鎖可以在協調器的記憶體中以平庸的 HashMap 的形式實現。
協調器故障
由於一個協調器專門服務於一組事務,因此快速確定其失敗的事實非常重要,以便在第二次嘗試執行事務時會逾時。 為了使其快速可靠,我們使用了完全連接的仲裁心跳協議:
每個資料中心至少託管兩個協調器節點。 每個協調器定期向其他協調器發送心跳訊息,並通知它們其功能以及上次從群組中的哪些協調器接收到的心跳訊息。
從其他節點接收到類似的資訊作為其心跳訊息的一部分,每個協調器根據仲裁原則自行決定哪些叢集節點正在運行,哪些不運行:如果節點X 已從叢集中的大多數節點接收到有關正常運行的訊息,收到來自節點 Y 的訊息,則 , Y 工作。 反之亦然,一旦大多數人報告節點 Y 遺失訊息,那麼 Y 就會拒絕。 奇怪的是,如果仲裁通知節點 X 它不再接收來自它的訊息,那麼節點 X 本身就會認為自己發生了故障。
心跳訊息發送頻率較高,每秒約20次,週期為50ms。 在 Java 中,由於垃圾收集器造成的暫停時間相當長,很難保證應用程式在 50 毫秒內回應。 我們能夠使用 G1 垃圾收集器來實現此回應時間,這使我們能夠指定 GC 暫停持續時間的目標。 然而,有時收集器暫停超過 50 毫秒(這種情況很少見),這可能會導致錯誤的故障偵測。 為了防止這種情況發生,當遠端節點的第一個心跳訊息消失時,只有當多個心跳訊息連續消失時,協調器才會報告遠端節點的故障。這就是我們在200 中偵測到協調器節點故障的方法多發性硬化症。
但僅僅快速了解哪個節點已停止運作還不夠。 我們需要對此做點什麼。
預訂
經典方案涉及,如果主節點發生故障,則使用以下之一開始新的選舉
假設我們要執行第 50 組中的交易。我們事先確定替換方案,即當主協調器發生故障時,哪些節點將執行第 50 組中的交易。 我們的目標是在資料中心發生故障時維持系統功能。 讓我們確定第一個保留將是來自另一個資料中心的節點,第二個保留將是來自第三個資料中心的節點。 該方案被選擇一次,並且不會改變,直到叢集的拓撲發生變化,即直到新節點進入其中(這種情況很少發生)。 如果舊主站發生故障,選擇新活動主站的過程始終如下:第一個備用主站將成為活動主站,如果它已停止運行,則第二個備用站將成為活動主站。
該方案比通用演算法更可靠,因為要啟動新的主設備,確定舊主設備的故障就足夠了。
但客戶如何了解現在哪個師傅在工作呢? 在 50 毫秒內向數千個客戶端發送訊息是不可能的。 當客戶端發送開啟交易的請求時,可能會發生這種情況,但還不知道該主伺服器不再運行,並且該請求將逾時。 為了防止這種情況發生,客戶會推測性地立即向群組主及其兩個儲備發送開啟交易的請求,但只有當前活躍的主才會回應此請求。 客戶端將僅與活動主機進行事務內的所有後續通訊。
備份主機將收到的不屬於自己的交易請求放入未出生交易佇列中,並在其中儲存一段時間。 如果活動主控器死亡,新主控器會處理從其佇列中開啟交易的請求並回應用戶端。 如果客戶端已經與舊主伺服器開啟了交易,則第二個回應將被忽略(並且顯然,這樣的交易將不會完成並且將被客戶端重複)。
交易如何進行
假設客戶端向協調器發送了一個請求,要求為具有這樣那樣主鍵的這樣那樣的實體打開事務。 協調器鎖定該實體並將其放入記憶體中的鎖定表中。 如有必要,協調器會從儲存中讀取該實體,並將結果資料以事務狀態儲存在協調器的記憶體中。
當客戶端想要更改事務中的資料時,它會向協調器發送修改實體的請求,協調器將新資料放入記憶體中的事務狀態表中。 這樣就完成了錄製 - 不會對儲存進行任何錄製。
當客戶端請求自己變更的資料作為活動事務的一部分時,協調器將執行以下操作:
- 如果 ID 已存在於交易中,則從記憶體中取得資料;
- 如果記憶體中沒有ID,則從儲存節點讀取缺失的數據,與記憶體中已有的資料結合,並將結果給予給客戶端。
因此,客戶端可以讀取自己的更改,但其他客戶端看不到這些更改,因為它們僅儲存在協調器的記憶體中;它們尚未儲存在 Cassandra 節點中。
當用戶端傳送提交時,協調器會將服務記憶體中的狀態保存在記錄批次中,並作為記錄批次傳送至 Cassandra 儲存。 商店會做一切必要的事情來確保該包被原子地(完全)應用,並向協調器返回響應,協調器釋放鎖並向客戶端確認事務成功。
而要回滾,協調器只需要釋放事務狀態佔用的記憶體即可。
由於上述改進,我們實施了 ACID 原則:
- 原子性。 這是保證系統中不會部分記錄任何事務;要么其所有子操作都將完成,要么一個都不會完成。 我們透過在 Cassandra 中記錄批次來遵守這項原則。
- 一致性。 根據定義,每筆成功的交易僅記錄有效結果。 如果開啟交易並執行部分操作後發現結果無效,則進行回溯。
- 隔離。 執行事務時,並發事務不應影響其結果。 使用協調器上的悲觀鎖來隔離競爭事務。 對於事務外的讀取,在讀已提交層級遵守隔離原則。
- 可持續發展。 無論較低等級出現什麼問題(系統中斷、硬體故障),成功完成的事務所所做的變更都應在操作復原時保留。
按索引讀取
讓我們來看一個簡單的表格:
CREATE TABLE photos (
id bigint primary key,
owner bigint,
modified timestamp,
…)
它有一個 ID(主鍵)、擁有者和修改日期。 您需要提出一個非常簡單的請求 - 選擇所有者的數據,更改日期為「最後一天」。
SELECT *
WHERE owner=?
AND modified>?
為了快速處理這樣的查詢,在經典的 SQL DBMS 中,您需要按列(所有者、修改)建立索引。 我們可以輕鬆做到這一點,因為我們現在有 ACID 保證!
C*One 中的索引
有一個包含照片的來源表,其中記錄 ID 是主鍵。
對於索引,C*One 建立一個新表,該表是原始表的副本。 鍵與索引表達式相同,也包括來源表中記錄的主鍵:
現在,「最後一天的所有者」的查詢可以重寫為從另一個表中進行選擇:
SELECT * FROM i1_test
WHERE owner=?
AND modified>?
來源表photos和索引表i1中資料的一致性由協調器自動維護。 僅基於資料模式,當收到更改時,協調器不僅會在主表中產生並儲存更改,還會在副本中產生更改。 不對索引表執行任何其他操作,不讀取日誌,也不使用鎖定。 也就是說,添加索引幾乎不消耗任何資源,並且對應用程式修改的速度幾乎沒有影響。
使用 ACID,我們能夠實作類似 SQL 的索引。 它們是一致的、可擴展的、快速的、可組合的,並且內建於 CQL 查詢語言中。 無需更改應用程式程式碼即可支援索引。 一切都像 SQL 一樣簡單。 而且最重要的是,索引不會影響原始事務表的修改的執行速度。
發生了什麼事
我們三年前開發了C*One並投入商業營運。
我們最終得到了什麼? 讓我們使用照片處理和儲存子系統的範例來評估這一點,照片處理和儲存子系統是社交網路中最重要的資料類型之一。 我們談論的不是照片本身,而是各種元資訊。 現在 Odnoklassniki 擁有約 20 億筆此類記錄,系統每秒處理 80 萬個讀取請求,每秒最多處理 8 個與資料修改相關的 ACID 事務。
當我們使用複製因子 = 1 的 SQL(但在 RAID 10 中)時,相片元資訊儲存在由 32 台執行 Microsoft SQL Server 的電腦組成的高可用叢集上(加上 11 個備份)。 還分配了 10 台伺服器用於儲存備份。 總共50輛昂貴的汽車。 同時,系統在額定負載下運行,無備用。
遷移到新系統後,我們收到複製因子 = 3 - 每個資料中心都有副本。 該系統由63個Cassandra儲存節點和6個協調器機器組成,總共69台伺服器。 但這些機器便宜得多,它們的總成本約為 SQL 系統成本的 30%。 同時,負載保持在30%。
隨著 C*One 的引入,延遲也減少了:在 SQL 中,寫入操作大約需要 4,5 毫秒。 在 C*One 中 - 大約 1,6 毫秒。 事務時長平均小於40ms,提交2ms完成,讀寫時間平均2ms。 第 99 個百分位——僅 3-3,1 毫秒,超時次數減少了 100 倍——這一切都歸功於投機的廣泛使用。
目前,大部分SQL Server節點已經退役;新產品僅使用C*One進行開發。 我們調整了 C*One 以在我們的雲端工作
現在我們正在努力將其他儲存設施轉移到雲端——但這是一個完全不同的故事。
來源: www.habr.com