規模要素:數據平臺的組成和擴展

譯註:本文是作者2015年在 Progscon & JAX Finance 大會上的同名主題演講《Elements of Scale: Composing and Scaling Data Platforms》。

@何_登成 的推薦語:此文很長,但長而不臭,而且配圖非常Q。作者以簡潔易懂的文字,將數據庫設計中應該考慮的存儲、並行、架構等問題做了詳細的闡述。

本文演講視頻

作爲軟件工程師,不可避免地受到周圍計算機工具的影響,語言、框架、甚至執行過程都會影響我們構建的軟件。

數據庫亦如此,基於一種特殊的方式,不可避免地影響到我們對應用程序中易變和共享狀態的處理。

過去的十多年,我們採用不同的方式去探尋這個世界。採用不同理念的一小衆開源項目,它們不斷成長,你中有我,我中有你。平臺集成了這些工具,每個控件通常都能提高某些基礎硬件或者系統效能。結果是平臺無法通過任何單一的工具解決某些問題,不是太過笨重,就是侷限於某一特定部分。

因此當今數據平臺多種多樣,從簡單的緩存層、多語言持久化層到整個集成數據管道,針對多種特定需求的多種解決方案。在某些方面,確實有不錯的表現。

因此本對話的目的就是解釋一些流行的方式方法如何發揮作用,爲什麼會有如此表現。我們先來考慮組成它們的基本元素,這樣便於在後續的討論中對這些認識通盤地考慮。

從某種抽象的角度看,當我們處理數據時,實際上就是對其進行局部性(locality)處理,局部性到CPU、局部性到我們需要的其它數據。有序地獲取數據是其中很重要的部分,計算機很擅長序列化的操作,這些操作是可以預測的。

(譯者注:局部性是計算機中一種預測行爲,通過緩存、內存中預取指令、處理器管道分支預測等技術來提高性能;更多參見《操作系統精髓與設計原理》。)

若是有序地從硬盤中獲取數據,數據會預獲取存入硬盤緩存、頁緩存、以及不同層級的CPU緩存中,這可以極大地提升性能。但這對隨機數據尋址意義不大,這些數據存於主內存、硬盤或者網絡中。實際上,預獲取反倒會拉低隨機負載能力:不論是各種緩存或是前端總線,充滿了不太會被用到的數據。

硬盤通常被認爲性能稍低,而主內存稍快些。這種認識不見得一直是對的,隨機和有序主內存負載之間相差一兩個數量級。用某種語言管理內存,事情往往會變得更加糟糕。

從硬盤有序獲取的數據流性能確實好過隨機尋址主內存,或許硬盤並不像我們想的那樣跟烏龜似的,至少在有序獲取的情況不會很慢。固態盤(SSD),特別是採用PCIe接口,正如它們顯示不同的權衡,將事情複雜化。但採用這兩種獲取模式帶來的緩存收益是不變的。

譯者注:數據流就是大量連續到達的、潛在無限的有序數據序列,這些數據按順序存取並被讀取一次或有限次。

假設我們要創建一個簡單的數據庫,首先從基礎部分文件開始。

保持有序讀和寫,文件在硬件上會表現地很好。我們可以將寫入的數據放入文件的末尾,可以通過掃面整個文件進行數據讀取。任何我們希望的處理過程可以隨着數據流穿過CPU而成真,比如過濾,聚合、甚至做一些更復雜的操作,總之非常完美。

倘如數據發生諸如更新這樣的變化會怎樣?

我們有多個選擇,在某個位置更新這個值。我們需要利用固定長度的字段,在我們淺顯的思想實驗中這是沒有問題的。不過在某個位置更新數據意味着隨機輸入輸出流(IO),這會影響性能。

替代的辦法是將更新值放置在文件的末尾,在讀取值時對過期的數據進行處理。

我們第一次做出權衡,將“日記”或者“日誌”放在文件末尾,就能保證有序獲取進而提高性能。另外倘若某處需要更新數據,可以實現每秒300次左右的讀取,前提是更新數據刷入底層介質中。

實際上完整的讀取文件是很慢的,獲取十億字節(GB)數據,最好的硬盤也需要花費數秒,這是一個數據庫全表掃描所花費的時間。

我們時常只需要一些特定的數據,比如名爲“bob”的客戶,這時掃描整個文件就不妥當,我們需要一個索引。

我們可用許多不同類型的索引,最簡單的一種是固定長度的有序數組,比如本例中的客戶名,和對應的偏移量一起存放在一個堆文件中。有序數組可以進行二進制搜索查找。同樣,我們可以用樹結構、位圖索引、哈希索引、字典索引等。這裏是一個樹的結構圖。

索引就像是在數據中添加了一個總覽結構,值是有序排放的,這樣我們就能快速獲取我們想要讀取的數據。但總覽結構有個問題,數據進來時需要隨機寫。因此理想的、寫優化僅僅追加文件;考慮到寫會打散文件系統,這會使一切變慢。

如果你將許多索引放入一個數據庫表中,那你一定熟悉這個問題。假定我們使用的機械盤,用這種方式維護某個索引的硬盤完整性,速度大約慢1000倍。

幸運的是,這裏有幾種解決方案。這裏我們討論三種,它們都是一些極端地例子。在現實世界中,遠沒有這麼複雜,但在考慮海量存儲時這些概念會特別有用。

譯者加:

  • 第一種內存映射文件
  • 第二種較小的索引集合,採用元索引或者布隆過濾算法(Bloom Filter)做一些優化
  • 第三種簡單匹配算法(brute force)又叫面向列(Column Oriented)

第一種方案是將索引放入主內存,隨機寫問題分隔到隨機存儲存儲器(RAM),堆文件依舊在硬盤中。

這是一種簡單但行之有效的方案,可以解決我們隨機寫的問題。這種方式在許多數據庫中已得到應用,比如MongoDB、Cassandra、Riak、以及其他採用此優化類型的數據庫,它們常常用到內存映射文件。

譯者注:內存映射文件是虛擬內存單個分段,可以與文件或者類文件資源的某部分建立直接字節對字節的關聯,即文件中的數據存放位置在內存中有對應的地址空間,這時對文件的讀寫可以直接用指針來做,而不需要read/write函數,處理大文件時可以顯著提高輸入輸出流(IO)性能。

倘若數據量遠超主內存,這種策略就失效了。特別是存在大量小的對象時,問題特別顯眼;索引增長很大,最後存儲越過了可用主內存的容量。多數情況下,這樣做是沒有問題的,但如果存在海量數據,這樣做就會成爲一種負擔。

一種流行的方式拋開單個的“總覽”索引,轉而採用相對較小的索引集合。

這是一個簡單的理念:數據進來,我們批量地將其寫入主內存。一旦內存數據足夠多,比如達到MB,我們就對它們進行排序,而後將它們作爲單個小的索引寫入硬盤中。最後得到的是一個小的、由不變索引文件組成的年表。

那麼這樣做的好處是?這些不變的文件集合被有序地流化處理,這樣就能快速地寫,最重要的是無需將整個索引加載入內存中。真棒!

當然它也有一個缺點,當讀操作時需要詢問非常多的小索引。我們將隨機IO(RandomIO)寫問題變爲讀問題。不過這確實一個很好的權衡策略,而且隨機讀比隨機寫更容易優化。

存儲一個小的元索引(meta-index)在內存中或者採用布隆過濾算法(Bloom Filter),提供一種低內存方式,評估單個索引文件在讀操作中是否需要被詢問。即使保持快速地、有序化寫操作,這種方式的讀操作性能幾乎可以和單個總覽索引相媲美。

實際開發中,偶爾也需要清理孤子更新,但它有序讀和寫確實不錯。

我們創建的這個結構稱作日誌結構合併樹(Log Structured Merge Tree),這種存儲方式在大數據工具中應用較大,如HBase、Cassandra、谷歌的BigTable等,它能用相對較小的內存開銷平衡寫、讀性能。

將索引存儲在內存中,或者利用諸如日誌結構合併樹(Log Structured Merge Tree)這樣的寫優化索引結構,繞開“隨機寫懲罰”(random-write penalty)。這是第三種方案爲純粹的簡單匹配算法(Pure brute force)。

回到開始的文件例子,完整地讀取它。如何處理文件中的數據,可以有許多選擇。簡單匹配算法(brute force)通過列而非行來存儲數據,這種方法叫做面向列。

需要注意的是真實的列存儲及其遵循的大表模式(Big Table pattern)之間存在一種不好的命名術語衝突。儘管它們有一些相似的地方,事實上它們是不同的,所以將它們視爲不同的事情是一件明智的。

面向列是一種簡單的理念,和行存儲數據不同,通過列分割每一行,將數據追加到單個文件末尾。接着在每個單獨的文件中存儲每一列,一旦需要只需讀取需要的列。

這樣可以確保文件的含有相同的序列,即每個列文件的第N行含有相同的地址或者偏移量。這個很重要,在某一時刻讀取多列,來服務一個單一的查詢。意味着“連接(joining)”列速度飛快,倘若所有的列含有相同的序列,我們就能在一個緊湊的循環中這麼做,此循環有很好緩存和CPU利用率。許多實現大量使用向量( vectorisation)進一步優化簡單連接和過濾操作吞吐量。

寫操作可以提高只在文件末尾追加( being append-only)性能。不利的地方是很多文件需要更新時,文件的每個列需要單獨寫入數據庫。最常見的解決方案是採用類似日誌結構合併(LSM)方式,進行批量化的寫操作。許多列類型的數據庫通過給表添加一個完整的序列來提升讀的性能。

通過列分割數據可以極大地減少從硬盤中讀取的數據量,只要查詢操作在所有的列的子集中。

除此之外,單獨列中的數據通常可以很好的壓縮。可以利用列數據類型優勢去壓縮,特別是在我們熟悉列的數據類型時。這意味着我們能利用有效的、低成本的編碼方式,比如行程長度編碼、delta、位組合(bit-packed)等。對一些編碼來說,謂詞可以直接用來做壓縮流。

一種簡單匹配算法(brute force)特別適合大規模掃描操作,諸如平均值、最大值、最小值、分組等聚類函數就是這方面的典型。

這和先前提到的“堆文件和索引(‘heap file & index)”方式不同,很好的理解這一點可以問自己,諸如此類的列方式和每一個字段帶有索引的“堆和索引”方式有什麼不同?

問題的關鍵是索引文件序列:多路查找樹(Btree)等會依據檢索的字段排序,兩次檢索的數據連接一端涉及流操作,另一端第二個索引位置進行檢索隨機讀取。平衡樹總體上說效率低於包含兩個相同序列索引列連接,我們再一次提高了序列化訪問。

譯者注:結論是平衡樹連接性能不如兩個相同序列索引列連接

我們都想將最好的技術作爲數據平臺控件,提升其中的某種核心功能,勝任一組特定的負載。

將索引存於內存而非堆文件爲叢多非關係型數據庫(NoSQL)所喜愛,比如Riak、Couchbase或者Mongodb,甚至一些關係型數據庫,這種簡單的模型效果不錯。

設計用來處理海量數據集的工具樂意採用LSM方式,這樣可以快速獲取數據,得到基於硬盤結構 讀一樣好的性能。HBase、 Cassandra、RocksDB、 LevelDB 甚至Mongo現在也支持這種方式。

每個文件的列(Column-per-file)引擎常用於數據庫大規模並行處理(MPP),比如Redshift或者Vertica,以及Hadoop stack中的Parquet。這些數據引擎最大的問題是需要大的遍歷,聚合是這些工具最重要的特質。

諸如卡夫卡(Kafka)採用一個簡單的、基於硬件的高效消息規範。消息可以簡單地追加到文件的末尾,或者從預定的偏移量處讀取。可以從某個偏移量讀取消息,來來回回,你可以從上次結束的偏移量處讀取。看得出是很不錯的有序輸入輸出(IO)。

這和多數面向消息的中間件不同,JMS(Java消息服務)和AMQP(高級消息隊列協議)說明文檔需要額外的索引,來管理選擇器和會話消息。這意味着它們結束某個行爲的方式更像數據庫,而非某個文件。著名的論述是1995年Jim Gray發表的隊列就是數據庫(Queue’s are Databases).

可見所有的方式都需要這樣那樣的權衡,作爲一種分佈式手段,使事情變得簡單、硬件更加用戶友好。

我們分析了存儲引擎的一些核心方法,其實只是做了一些簡要說明,現實世界這些是要複雜的多,不過概念確實是很有用的。分佈式數據平臺不僅僅是一個存儲引擎,還需要考慮並行。

對於橫跨多臺計算機的分佈式數據我們需要考慮兩個核心點,分區(partition)和複製(replication)。分區有時指的是分庫分表(sharding),在隨機讀取和簡單匹配工作負載(brute force workloads)表現不俗。

如果是基於哈希的分區模型,藉助哈希函數,數據就能均攤到一組機器上(譯者注:理想的結果是這樣的)。同哈希表工作方式相似,每個桶(bucket)盛放某個機器節點。

這樣通過哈希函數,直接訪問包含此數據的機器讀取來數據。這是一種很經典的分佈式模式,也是唯一一種隨着客戶端請求增加呈現線性分佈的模式(譯者注:簡單點說就是均攤)。請求隔離到單臺計算機上,由集羣中的單臺計算機爲其服務。

利用分區提供並行批量計算,比如聚合函數或者諸如聚衆或者機器學習的複雜算法。最大的不同是所有的計算機在同一時刻採用廣播的方式,在很短的時間採用分治的策略解決大規模計算問題。

批量處理系統很好地處理大規模問題,但在執行過程中少有併發,容易耗盡集羣資源。

兩個極端且特別簡單的方式:一端直接訪問,另一端分治地進行廣播。需要注意的是終端之間的中間地帶,最好的例子就是非關係型數據庫(NoSQL)中跨越多臺計算機的二級索引。

二級索引有別於主鍵索引,這就意味着數據分區不再借助索引中的值。不再使用哈希函數直接分發,而是廣播請求給所有的計算機。這會制約併發,任何一個節點與每一個請求都有關。

也是這個原因許多鍵值存儲不願採用二級索引,即使它的應用很廣泛,Hbase和Voldemort就是如此。不過諸如MongoDb、Cassandra、Riak等數據庫採用二級索引,不管咋說二級索引還是蠻有用的。但理解它們在整個系統併發的影響還是很重要的。

複製解決併發瓶頸,或許你熟悉備份,不論是異步到從服務器,還是複製到諸如Mongo或者Cassandra這樣的NoSQL存儲中。

實際上備份是不可見的(僅僅用於恢復)、只讀(增加併發量)、或者讀寫(增加網絡分區下的可用性),選擇哪種方式需要從系統的一致性出發做出權衡。這是CAP(Consistency、Availability、Partition-Tolerance)理論的簡單應用,當然CAP理論遠非我們想象中的那麼簡單。

譯者注:網絡分區( network partitions)指某個網絡設備出錯導致網絡分離,比如某個數據庫掛掉。

權衡一致性給我們帶來一個重要的問題,什麼時候需要保證數據的一致性?

一致性的代價是昂貴,在數據庫的世界裏,原子性由線性化(linearisabilty)做保障,這樣可以確保所有的操作有序排列.但代價也是昂貴的,實際上這完全是被禁止的,許多數據庫並不將此作爲一個獨立(isolation)執行單元。鑑於此,很少將此設爲默認值。

簡而言之,你想分佈式寫的系統保持強一致性,系統會變慢。

注意一致性這個術語有兩個應用場景,在原子性和CAP中,當然其意思是不同的。我通常採用CAP中的定義,對所有的節點而言數據在某一時刻是相同的。

解決一致性問題的方法其實很簡單,就是避免它。如果無法避免,隔離它爲其分配儘可能少的寫操作和計算機資源。

避免一致性問題一般不難,特別是數據爲不變的事實流時,網絡日誌集合就是一個很好的例子。無需關注一致性,因爲這些日誌作爲事實是不會改變的。

需要一致性的用例,比如轉賬、使用優惠碼這種非交換行爲。

當然從傳統的眼光看一些事情需要一致性,但實際上卻也未必。比如一個行動從一個可變狀態變成一個新的相關事實集合,就可以避免這種變化狀態。通常是直接對新字段進行更新,考慮到標記一個事務存在潛在的欺詐,我們可以簡單地利用某個事實流和原始的事務進行關聯。

譯者:好觀點

在數據平臺中移除所有一致性需求、或者隔離它都是很有用的。一種隔離方式是利用單個寫原則,涉及幾個方面,比如Datomic;另一種方式是拆分可變的和非可變的來隔離一致性需求。

諸如Bloom/CALM擴展了這些理念,支持默認狀態下的無序概念,除非需要才做排序。因此我們有必要做一些基本的權衡,那我們如何利用這些特性去建立一個數據平臺?

一個典型的應用架構或許應該是這樣的:有一組處理將數據寫入某個數據庫,然後將其讀出,對於許多簡單的工作負載這是沒有問題的,許多成功的應用都是基於此模式。但隨着吞吐量的增加,此模式越來越難以適用;在應用領域這個問題或許可以通過消息傳遞、演員(actors)、負載均衡加以解決。

另外一個問題是這種方式將數據庫作爲一個黑盒,數據庫是一個透明的軟件。它們提供了海量的特徵,但也提供了極少的原子拆分的機制。這樣做有很多好處,默認狀態下是安全的;但保護過度地扼殺我們的需求進而限制系統的分佈式,這就很煩人。

命令查詢職責分離(CQRS  Command Query Responsibility Segregation)可以簡單地解決此問題。

譯註:

  1. 實現一Druid
  2. 實現二操作分析橋(Operational/Analytic Bridge)
  3. 實現三批量管道
  4. 實現四拉姆達框架(Lambda Architecture)
  5. 實現五卡帕(Kappa)框架又叫流數據平臺

想法其實很簡單,分離讀寫工作負載:最佳寫入狀態時寫入,最切貼的例子比如某個簡單日誌文件;最佳讀取狀態時讀取。有多種實現方式,比如用於關係型數據庫的Goldengate工具、內部複製集成的諸如MongoDB的Replica Sets這樣的產品。

許多數據庫底層的行爲就是這樣,Druid是一個不錯的例子,它是一個開源的、分佈式、時序化、列式分析引擎。列式存儲表現不俗,特別是大規模數據錄入,數據必須分散到許多文件中。爲了得到更好的寫性能,Druid存儲近期的新數據到某個最佳寫入狀態中,然後逐漸轉移到最佳讀取存儲狀態。

一旦查詢Druid,請求就會同時派發到最佳寫和最佳讀控件中,對結果進行組合(移除冗餘),返回給用戶。Druid藉助時間標記每條記錄來進行排序。

諸如此類的組合方式提供了單個抽象下的CQRS好處。

另一種相似的方式是操作分析橋(Operational/Analytic Bridge),利用單個事件流拆分最佳讀以及最佳寫視圖。流處在一種不斷變化的狀態,因此異步視圖可以在隨後的日子裏被重寫和增強。

前端提供了同步讀和寫,這麼做即可以簡單快速地讀取已寫入的數據,又可以支持複雜的原子事務。

後端採用異步、不變狀態的優勢來提高性能,比如藉助複製、反範式化、甚至完全不同的存儲引擎擴展線下處理。前後端之間的消息橋連方便應用通過平臺去監聽數據流。這種模型很適合中等規模的部署,可變視圖至少存在一部分、不可避免的需求。

設計不變的狀態,以便容易地去支持大規模數據集和更加複雜的分析。Hadoop棧中獨一無二的實現——批量管道,就是一個典型的例子。

Hadoop棧最精彩的地方就是其叢多的工具,不管是快速讀寫訪問、還是廉價地存儲、抑或批量處理、高吞吐消息、或者提取、處理、分析數據,hadoop生態體系應有盡有。

批量管道從多種資源中獲取數據,將其放入HDFS,接着對其進行處理,進而提供一個原始數據持續優化的版本。

數據可能得到富集、清理、反範式化、聚集、移到一個諸如Parquet的最佳讀模式,或者加載進服務器層或者數據集市,處理之後的數據可以被檢索和處理。

此框架適用於不變數據、以及對數據進行大規模獲取和處理,比如100太字節(TBs)。此框架處理過程很緩慢,以小時爲單位。

批量管道的問題是通常我們不想等幾個小時去獲取一個結果。常見的做法是添加一個流層,有時又叫拉姆達框架(Lambda Architecture)。

拉姆達框架保留了批量管線,不過增加了快速流層實現迂迴,就像在忙亂的小鎮架了一個支路,流層採用諸如Storm、Samza流處理工具。

拉姆達框架核心是我們最樂意快速粗略作答的,但我想在最後做一個精確的回答。

流層繞過了批量層,提供了最佳回答,它的核心就在流視圖中。這些會寫入一個服務器層。稍好批量管道計算出精確的數據並覆蓋之前的值。

用響應來平衡精度是個不錯的做法,兩個分支在流和批量處理層都有編碼,這種模式的一些實現是有問題的。解決辦法,一是將此邏輯簡單抽象到一個可複用的通用庫中,比如處理都寫入了諸如Python、R語言這樣的外源庫中。二是諸如Spark這樣的系統同時提供了流和批量處理功能,當然spark中的流只是少量的批處理。

因此這種模式適合比如100TB的海量數據平臺,將流和已存、富集的、批量分析函數結合起來。

另外一種解決慢數據管道的方式,稱之爲卡帕(Kappa)框架。起初我以爲這個架構名稱不對,現在我不太確定。不管它是什麼,我叫它流數據平臺,其實這個已經有人這麼叫了。

流數據平臺相對批量模式更有優勢:與將數據存儲在HDFS中劃分給新的批量任務不同,數據分散存儲在消息系統或者諸如kafka日誌中。批處理就變成了記錄系統,數據流經過實時處理生成三層結構:視圖、索引、服務或者數據集市。

與拉姆達(lambda)框架的流層相似,不一樣的是沒有批處理層。顯然這就要求消息層能夠存儲、供應海量數據,並且具有強大有效的流處理器來處理此過程。

天下沒有免費的午餐,問題很棘手,流數據平臺運行速度並沒有同等批量處理系統快多少。但將默認的方法“存儲和處理”切換爲“流和處理”,可以極大地提高快速獲取結果的可能性。

流數據平臺方式還可以用來解決“應用集成”問題,應用集成這個棘手的問題困惑Informatica、Tibco和Oracle等大的供應商好多年了。對許多數據庫而言是有益的,但不是一種變革性方案。應用集成至今停留在找尋切實可行方案的話題上。

流數據平臺提供了一個潛在的解決方案:利用操作分析橋的叢多優勢—多種異步存儲格式以及重新創建視圖的能力—但這會增加已有資源中一致性需求:

系統記錄變爲日誌,易於增強數據的不變性。諸如Kafka等產品內部保留了足夠的數據量和吞吐量,將其作爲歷史記錄來用。這就意味着回覆是一個重演、重新生成狀態的處理過程,而非常態化地檢驗。

相似的方式很在就有應用,早於最新出現的數據湖或者Goldengate等工具,後者將數據放入企業級數據倉庫。複製層缺乏吞吐量和管理複雜的schema變化使此方法大打折扣。看似最後第一個問題已經解決,但作爲最後一個問題,還沒有定論。

~

回到局部性,讀和寫按序尋址,是控件內部最需要權衡的部分。我們觀看了如何拓展這些控件,提高了分庫分表和複製最基本的性能。重新審視一致性將其作爲一個問題,在構建平臺時隔離它。

不過數據平臺本身需要用單一、全局的方式來平衡這些控件達到最佳狀態。不斷重建,從最佳寫狀態遷移到最佳讀狀態,從一致性約束轉移到流、異步、不變狀態的開放地帶。

需要記住幾件事,一是schema,二是時間、分佈式、異步系統風險。但這些問題都是可控的,前提是你認真對待。未來大數據領域可能會出現這樣一些新的工具、革新,逐漸摻入到平臺中,解決過去和現在更多的問題。
譯者注:schema 指數據庫完整性約束。

題外話:

ben大叔熱情地答覆了大家的關切,現在他那裏也沒有ppt,不過他說漂亮的slide是藉助觸控筆用Paper by 53繪製的。plus,祝他新婚快樂。

下面是英文原文:I don’t have a copy of the PPT easily available but the slides are made with an app called Paper by 53 on the IPad. It works best with a stylus too.


本文轉自:http://blog.jobbole.com/88453/

發佈了1 篇原創文章 · 獲贊 29 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章