影響數據檢索效率的幾個因素

數據檢索有兩種主要形態。第一種是純數據庫型的。典型的結構是一個關係型數據,好比 mysql。用戶經過 SQL 表達出所須要的數據,mysql 把 SQL 翻譯成物理的數據檢索動做返回結果。第二種形態是如今愈來愈流行的大數據玩家的玩法。典型的結構是有一個分區的數據存儲,最初這種存儲就是原始的 HDFS,後來開逐步有人在 HDFS 上加上索引的支持,或者乾脆用 Elasticsearc 這樣的數據存儲。而後在存儲之上有一個分佈式的實時計算層,好比 Hive 或者 Spark SQL。用戶用 Hive SQL 提交給計算層,計算層從存儲里拉取出數據,進行計算以後返回給用戶。這種大數據的玩法起初是由於 SQL 有不少 ad-hoc 查詢是知足不了的,乾脆讓用戶本身寫 map/reduce 想怎麼算均可以了。可是後來玩大了以後,愈來愈多的人以爲這些 Hive 之類的方案查詢效率怎麼那麼低下啊。因而一個又一個項目開始去優化這些大數據計算框架的查詢性能。這些優化手段和經典的數據庫優化到今天的手段是沒有什麼兩樣的,不少公司打着搞計算引擎的旗號幹着從新發明數據庫的活。因此,迴歸本質,影響數據檢索效率的就那麼幾個因素。咱們不妨來看一看。html

數據檢索乾的是什麼事情

定位 => 加載 => 變換java

找到所須要的數據,把數據從遠程或者磁盤加載到內存中。按照規則進行變換,好比按某個字段group by,取另一個字段的sum之類的計算。node

影響效率的四個因素

  • 讀取更少的數據
  • 數據本地化,充分遵循底層硬件的限制設計架構
  • 更多的機器
  • 更高效率的計算和計算的物理實現

原則上的四點描述是很是抽象的。咱們具體來看這些點映射到實際的數據庫中都是一些什麼樣的優化措施。python

讀取更少的數據

數據越少,檢索須要的時間固然越少了。在考慮全部技術手段以前,最有效果的恐怕是從業務的角度審視一下咱們是否須要從那麼多的數據中檢索出結果來。有沒有可能用更少的數據達到一樣的效果。減小的數據量的兩個手段,聚合和抽樣。若是在入庫以前把數據就作了聚合或者抽樣,是否是能夠極大地減小查詢所須要的時間,同時效果上並沒有多少差別呢?極端狀況下,若是須要的是一天的總訪問量,好比有1個億。查詢的時候去數1億行確定快不了。可是若是統計好了一天的總訪問量,查詢的時候只須要取得一條記錄就能夠知道今天有1個億的人訪問了。mysql

索引是一種很是常見的減小數據讀取量的策略了。通常的按行存儲的關係型數據庫都會有一個主鍵。用這個主鍵能夠很是快速的查找到對應的行。KV存儲也是這樣,按照Key能夠快速地找到對應的Value。能夠理解爲一個Hashmap。可是一旦查詢的時候不是用主鍵,而是另一個字段。那麼最糟糕的狀況就是進行一次全表的掃描了,也就是把全部的數據都讀取出來,而後看要的數據到底在哪裏,這就不可能快了。減小數據讀取量的最佳方案就是,創建一個相似字典同樣的查找表,當咱們找 username=wentao 的時候,能夠列舉出全部有 wentao 做爲用戶名的行的主鍵。而後拿這些主鍵去行存儲(就是那個hashmap)裏撈數據,就一撈一個準了。算法

談到索引就不得不談一下一個查詢使用了兩個字段,如何使用兩個索引的問題。mysql的行爲能夠表明大部分主流數據庫的處理方式:
https://www.percona.com/blog/2012/12/14/the-optimization-that-often-is...
https://www.percona.com/blog/2014/01/03/multiple-column-index-vs-multi...
基本上來講,經驗代表有多個單字段的索引,最後數據庫會選一最優的來使用。其他字段的過濾仍然是經過數據讀取到內存以後,用predicate去判斷的。也就是沒法減小數據的讀取量。
在這個方面基於inverted index的數據就很是有特色。一個是Elasticsearch爲表明的lucene系的數據庫。另一個是新銳的druid數據庫。
https://www.found.no/foundation/elasticsearch-from-the-bottom-up/
http://druid.io/blog/2012/09/21/druid-bitmap-compression.html
效果就是,這些數據庫能夠把單字段的filter結果緩存起來。多個字段的查詢能夠把以前緩存的結果直接拿過來作 AND 或者 OR 操做。sql

索引存在的必要是由於主存儲沒有提供直接的快速定位的能力。若是訪問的就是數據庫的主鍵,那麼須要讀取的數據也就很是少了。另一個變種就是支持遍歷的主鍵,好比hbase的rowkey。若是查詢的是一個基於rowkey的範圍,那麼像hbase這樣的數據庫就能夠支持只讀取到這個範圍內的數據,而不用讀取再也不這個範圍內的額外數據,從而提升速度。這種加速的方式就是利用了主存儲自身的物理分佈的特性。另一個更常見的場景就是 partition。好比 mysql 或者 postgresql 都支持分區表的概念。當咱們創建了分區表以後,查找的條件若是能夠過濾出分區,那麼能夠大幅減小須要讀取的數據量。比 partition 更細粒度一些的是 clustered index。它其實不是一個索引(二級索引),它是改變了數據在主存儲內的排列方式,讓相同clustered key的數據彼此緊挨着放在一塊兒,從而在查詢的時候避免掃描到無關的數據。比 partition 更粗一些的是分庫分表分文件。好比咱們能夠一天創建一張表,查詢的時候先定位到表,再執行 SQL。好比 graphite 給每一個 metric 建立一個文件存放採集來的 data point,查詢的時候給定metric 就能夠定位到一個文件,而後只讀取這個文件的數據。數據庫

另外還有一點就是按行存儲和按列存儲的區別。按列存儲的時候,每一個列是一個獨立的文件。查詢用到了哪幾個列就打開哪幾個列的文件,沒有用到的列的數據碰都不會碰到。反觀按行存儲,一張中的全部字段是彼此緊挨在磁盤上的。一個表若是有100個字段,哪怕只選取其中的一個字段,在掃描磁盤的時候其他99個字段的數據仍然會被掃描到的。緩存

考慮一個具體的案例,時間序列數據。如何使用讀取更少的數據的策略來提升檢索的效率呢?首先,咱們能夠保證入庫的時間粒度,維度粒度是正好是查詢所須要的。若是查詢須要的是5分鐘數據,可是入庫的是1分鐘的,那麼就能夠先聚合成5分鐘的再存入數據庫。對於主存儲的物理佈局選擇,若是查詢老是針對一個時間範圍的。那麼把 timestamp 作爲 hbase 的 rowkey,或者 mysql 的 clustered index 是合適。這樣咱們按時間過濾的時候,選擇到的是一堆連續的數據,不用讀取以後再過濾掉不符合條件的數據。可是若是在一個時間範圍內有不少中數據,好比1萬個IP,那麼即使是查1個IP的數據也須要把1萬個IP的數據都讀取出來。因此能夠把 IP 維度也編碼到 rowkey 或者 clustered index 中。可是假如另外還有一個維度是 OS,那麼查詢的時候 IP 維度的 rowkey 是沒有幫助的,仍然是要把全部的數據都查出來。這就是僅依靠主存儲是沒法知足各類查詢條件下都可以讀取更少的數據的緣由。因此,二級索引是必要的。咱們能夠把時間序列中的全部維度都拿出來創建索引,而後查詢的時候若是指定了維度,就能夠用二級索引把真正須要讀取的數據過濾出來。可是實踐中,不少數據庫並不由於使用了索引使得查詢變快了,有的時候反而變得更慢了。對於 mysql 來講,存儲時間序列的最佳方式是按時間作 partition,不對維度創建任何索引。查詢的時候只過濾出對應的 partition,而後進行全 partition 掃描,這樣會快過於使用二級索引定位到行以後再去讀取主存儲的查詢方式。究其緣由,就是數據本地化的問題了。ruby

數據本地化

數據本地化的實質是軟件工程師們要充分尊重和理解底層硬件的限制,而且用各類手段規避問題最大化利用手裏的硬件資源。本地化有不少種形態

  • 最多見的最好理解的本地化問題是網絡問題。咱們都知道網絡帶寬不是無限的,比本地磁盤慢多了。若是可能儘可能不要經過網絡去訪問數據。即使要訪問,也應該一次抓取多一些數據,而不是一次搞一點,而後搞不少次。由於網絡鏈接和來回的開銷是很是高的。這就是 data locality 的問題。咱們要把計算儘量的靠近數據,減小網絡上傳輸的數據量。
  • 這種帶寬引發的本地化問題,還有不少。網絡比硬盤慢,硬盤比內存慢,內存比L2緩存慢。作到極致的數據庫可讓計算徹底發生在 L2 緩存內,儘量地避免頻繁地在內存和L2之間倒騰數據。
  • 另一種形態的問題化問題是磁盤的順序讀和隨機讀的問題。當數據彼此靠近地物理存放在磁盤上的時候,順序讀取一批是很是快的。若是須要隨機讀取多個不連續的硬盤位置,磁頭就要來回移動從而使得讀取速度快速降低。即使是 SSD 硬盤,順序讀也是要比隨機讀快的。

基於儘量讓數據讀取本地化的原則,檢索應該儘量地使用順序讀而不是隨機讀。若是能夠的話,把主存儲的row key或者clustered index設計爲和查詢提交同樣的。時間序列若是都是按時間查,那麼按時間作的row key能夠很是高效地以順序讀的方式把數據拉取出來。相似地,按列存儲的數據若是要把一個列的數據都取出來加和的話,能夠很是快地用順序讀的方式加載出來。

二級索引的訪問方式典型的隨機讀。當查詢條件通過了二級索引查找以後獲得一堆的主存儲的 key,那麼就須要對每一個 key 進行一次隨機讀。即使彼此僅靠的key能夠用順序讀作一些優化,整體上來講仍然是隨機讀的模式。這也就是爲何時間序列數據在 mysql 裏創建了索引反而比沒有建索引還要慢的緣由。

爲了儘量的利用順序讀,人們就開始想各類辦法了。前面提到了 mysql 裏的一行數據的多個列是彼此緊靠地物理存放的。那麼若是咱們把所須要的數據建成多個列,那麼一次查詢就能夠批量得到更多的數據,減小隨機讀取的次數。也就是把以前的一些行變爲列的方式來存放,減小行的數量。這種作法的經典案例就是時間序列數據,好比能夠一分鐘存一行數據,每一秒的值變成一個列。那麼行的數量能夠變成以前的1/60。

可是這種行變列的作法在按列存儲的數據庫裏就不能直接照搬了,有些列式數據庫有column family的概念,不一樣的設置在物理上存放多是在一塊兒的也多是分開的。對於 Elasticsearch 來講,要想減小行的數量,讓一行多pack一些數據進去,一種作法就是利用 nested document。內部 Elasticsearch 能夠保證一個 document 下的全部的 nested document是物理上靠在一塊兒放在同一個 lucene 的 segment 內。

網絡的data locality就比較爲人熟知了。map reduce的大數據計算模式就是利用map在數據節點的本地把數據先作一次計算,每每計算的結果能夠比原數據小不少。而後再經過網絡傳輸彙總後作 reduce 計算。這樣就節省了大量網絡傳輸數據的時間浪費和資源消耗。如今 Elasticsearch 就支持在每一個 data node 上部署 spark。由 spark 在每一個 data node 上作計算。而不用把數據都查詢出來,用網絡傳輸到 spark 集羣裏再去計算。這種數據庫和計算集羣的混合部署是高性能的關鍵。相似的還有 storm 和 kafka 之間的關係。

網絡的data locality還有一個老大難問題就是分佈式大數據下的多表join問題。若是隻是查詢一個分佈式表,那麼把計算用 map reduce 表達就沒有多大問題了。可是若是須要同時查詢兩個表,就意味着兩個表可能不是在物理上一樣均勻分佈的。一種最簡單的策略就是找出兩張表中最小的那張,而後把表的內容廣播到每一個節點上,再作join。複雜一些的是對兩個單表作 map reduce,而後按照相同的 key 把部分計算的結果聚集在一塊兒。第三種策略是保證數據分佈的方式,讓兩張表查詢的時候須要用到的數據總在一塊兒。沒有完美的方案,也不大可能有完美的方案。除非有一天網絡帶寬能夠大到忽略不計的地步。

更多的機器

這個就沒有什麼好說的了。多一倍的機器就多一倍的 CPU,能夠同時計算更多的數據。多一倍的機器就多一倍的磁頭,能夠同時掃描更多的字節數。不少大數據框架的故事就是講如何如何經過 scale out 解決無限大的問題。可是值得注意的是,集羣能夠無限大,數據能夠無限多,可是口袋裏的銀子不會無限多的。堆機器解決問題比升級大型機是要便宜,可是機器堆多了也是很是昂貴的。特別是 Hive 這些從一開始就是分佈式多機的檢索方案,剛開始的時候效率並不高。堆機器是一個乘數,當數據庫原本單機性能不高的時候,乘數大並不能起到決定性的做用。

更高效的計算和計算實現

檢索的過程不只僅是磁盤掃描,它還包括一個可簡單可複雜的變換過程。使用 hyperloglog,count min-sketch等有損算法能夠極大地提升統計計算的性能。數據庫的join也是一個常常有算法創新的地方。
計算實現就是算法是用C++實現的仍是用java,仍是python實現的。用java是用大Integer實現的,仍是小int實現的。不一樣的語言的實現方式會有一些固定的開銷。不是說快就必定要C++,可是 python 寫 for 循環是顯然沒有期望的。任何數據檢索的環節只要包含 python/ruby 這些語言的逐條 for 循環就必定快不起來了。

結論

但願這四點能夠被記住,成爲一種指導性的優化數據檢索效率的思惟框架。不管你是設計一個mysql表結構,仍是優化一個spark sql的應用。從這四個角度想一想,都有哪些環節是在拖後腿的,手上的工具備什麼樣的參數能夠調整,讓隨機讀變成順序讀,表結構怎麼樣設計能夠最小化數據讀取的量。要作到這一點,你必須很是很是瞭解工具的底層實現。而不是盲目的相信,xx數據庫是最好的數據庫,因此它必定很快之類的。若是你不瞭解你手上的數據庫或者計算引擎,當它快的時候你不知道爲什麼快,當它慢的時候你就更加無從優化了。

相關文章
相關標籤/搜索