postgresql , 列存儲 , shard , 切片 , 大塊 , 小塊 , sort , 塊級索引 , bitmap scan , 索引延遲 , 歸整
資料分析系統,決策系統的資料量通常非常龐大,屬性(列)非常多,可能涉及到任意列的組合條件查詢,篩選結果、聚合結果、多元分析等。
這種場景如何優化能滿足實時的響應需求呢?
postgresql中有一些技術,可以滿足此類場景。
1. 内置bitmapand bitmapor,使用任意字段的索引搜尋時,可以快速跳過不滿足條件的塊,快速的得到組合結果。
實測10億資料,31個字段任意搜尋,約幾百毫秒的響應時間。
案例如下:
<a href="https://github.com/digoal/blog/blob/master/201706/20170607_02.md">《多字段,任意組合條件查詢(無需模組化) - 毫秒級實時圈人 最佳實踐》</a>
還有這種方法,要求每個字段都建立索引,對資料寫入會有性能影響(特别是與堆存儲線性相關性很差的字段,索引分裂會比較嚴重,導緻io巨大)。
對于資料量非常龐大的場景,建議對表進行分區,可以建立多級分區。
例如uid為一級hash分區,時間戳為二級範圍分區。這樣可以控制每個表的大小,通過時間分區我們可以将曆史資料靜态化,不影響新錄入資料的索引。進而提升整體性能。
2. postgresql支援多種索引接口,針對不同的資料類型,資料的存儲風格。在前面的案例中也有介紹。
是以并不是btree一路用到底,對于不同的類型有不同的搜尋需求,比如範圍類型,數組類型,全文檢索類型,通常會根據包含、相交來進行查詢。而對于空間資料,通常會根據相交,距離等進行查詢。對于一維類型通常會根據=,或者範圍進行查詢。
這些資料類型都能找到合适的索引來加速掃描,結合bitmapand, bitmapor,實作實時的任意字段搜尋。
3. bitmap 索引(greenplum)
對于選擇性較差的列,例如1億記錄,隻有1000個唯一值,這種情況下,使用bitmap index就可以達到非常好的效果。
案例如下
<a href="https://github.com/digoal/blog/blob/master/201705/20170512_01.md">《greenplum 最佳實踐 - 什麼時候選擇bitmap索引》</a>
<a href="https://github.com/digoal/blog/blob/master/201706/20170612_01.md">《postgresql (varbit, roaring bitmap) vs pilosa(bitmap庫)》</a>
4. 列存儲, cstore(orc)格式
<a href="https://orc.apache.org/">https://orc.apache.org/</a>
<a href="https://github.com/citusdata/cstore_fdw">https://github.com/citusdata/cstore_fdw</a>
如果單機已經做了很多優化,例如使用分區表,但是由于實時寫入的資料量依舊很龐大,資料錄入由于索引過多遇到瓶頸了,還有什麼優化手段呢?
要做到實時的海量寫入,又要實時的查詢,确實是一個比較沖突的問題。
因為要實時寫入,就要盡量少的索引,而要實時查詢,就要索引的支援。
那麼到底怎麼解決這個沖突問題呢?
1、索引延遲合并
在postgresql中有一個gin索引,這個索引是針對多值類型的索引,例如數組。
當使用者在寫入一條記錄時,(一個數組往往涉及多個元素),索引的條目可能會設定到多個,是以每寫一條記錄,io是巨大的,是以gin有一個加速資料寫入、更新、删除的特性,fastupdate。
這個特性也比較好了解,實際上就是延遲合并,比如累計1000條記錄後,合并一次到索引的條目中。進而大幅提升寫入性能。
那麼我們也可以使用類似的技術來優化以上場景,隻是需要對所有的索引接口都做類似的fastupdate功能。
2、lambda批量操作
我們可以将資料分為兩個部分,一個部分是實時寫入部分,另一部分是資料的合并部分。
資料實時的寫入部分(不需要索引),實時寫入後,再批量對其建立索引。
也就是先寫入,到達分區邊界後,寫入新的分區,而前一個分區則開始建立索引。pg支援對一張表并行的建立多個索引。
例如
另外有兩個類似的場景,有興趣的話也可以參考一下,與本例的用法不一樣。
<a href="https://github.com/digoal/blog/blob/master/201706/20170607_01.md">《塊級(ctid)掃描在iot(物聯網)極限寫和消費讀并存場景的應用》</a>
<a href="https://github.com/digoal/blog/blob/master/201705/20170509_02.md">《海量資料 "寫入、共享、存儲、計算" 最佳實踐》</a>
3、分布式
當單機的容量、寫入性能、查詢性能可能成為瓶頸時,可以建立多個執行個體。
多個執行個體有許多方法可以融合起來。
3.1 postgres_fdw + inherit
<a href="https://www.postgresql.org/docs/9.6/static/tutorial-inheritance.html">https://www.postgresql.org/docs/9.6/static/tutorial-inheritance.html</a>
<a href="https://www.postgresql.org/docs/10/static/postgres-fdw.html">https://www.postgresql.org/docs/10/static/postgres-fdw.html</a>
<a href="https://github.com/digoal/blog/blob/master/201610/20161004_01.md">《postgresql 9.6 單元化,sharding (based on postgres_fdw) - 核心層支援前傳》</a>
<a href="https://github.com/digoal/blog/blob/master/201610/20161005_01.md">《postgresql 9.6 sharding + 單元化 (based on postgres_fdw) 最佳實踐 - 通用水準分庫場景設計與實踐》</a>
3.2 postgres_fdw + pg_pathman
<a href="https://github.com/postgrespro/pg_pathman">https://github.com/postgrespro/pg_pathman</a>
<a href="https://github.com/digoal/blog/blob/master/201610/20161024_01.md">《postgresql 9.5+ 高效分區表實作 - pg_pathman》</a>
<a href="https://github.com/digoal/blog/blob/master/201610/20161027_01.md">《postgresql 9.6 sharding based on fdw & pg_pathman》</a>
3.3 plproxy
<a href="https://plproxy.github.io/">https://plproxy.github.io/</a>
<a href="https://github.com/plproxy/plproxy">https://github.com/plproxy/plproxy</a>
<a href="https://github.com/digoal/blog/blob/master/201608/20160824_02.md">《postgresql 最佳實踐 - 水準分庫(基于plproxy)》</a>
<a href="https://github.com/digoal/blog/blob/master/201512/20151220_02.md">《阿裡雲apsaradb rds for postgresql 最佳實踐 - 2 教你rds pg的水準分庫》</a>
<a href="https://github.com/digoal/blog/blob/master/201512/20151220_03.md">《阿裡雲apsaradb rds for postgresql 最佳實踐 - 3 水準分庫 vs 單機 性能》</a>
<a href="https://github.com/digoal/blog/blob/master/201512/20151220_04.md">《阿裡雲apsaradb rds for postgresql 最佳實踐 - 4 水準分庫 之 節點擴充》</a>
3.4 citusdata extension
<a href="https://github.com/citusdata/citus">https://github.com/citusdata/citus</a>
4、幹掉索引,列存儲優化
列存儲除了可以降低單列統計,少量列統計的io掃描成本,同時還可以提高壓縮比。
列存儲的方法是本文的重點,實際上可以不需要索引,使用列存,對列存的存儲進行優化和規則,使得通路資料就如有索引一樣高效。
列存的優化手段是編排,可以參考如下文章。
<a href="https://github.com/digoal/blog/blob/master/201706/20170614_01.md">《從一維編排到多元編排,從平面存儲到3d存儲 - 資料存儲優化之路》</a>
單列編排比較好實作,多列編排則需要建立列和列之間的映射關系。例如使用一個中間value(pk)進行關聯,這樣會增加一個成本。
bitmapand, bitmapor結合
bitmap掃描方法如下,隻是替換成pk。
編排的工作量和索引類似,也會涉及到塊的調整,是以也會導緻寫延時,為了降低寫延遲,同樣需要類似的優化,延遲編排、分片等。
為了達到最大的寫入吞吐,寫入時資料為無序狀态。
對寫入的資料,按每列的存儲分片進行整理,例如每個存儲分片32mb。
整理的目标是使得這32mb的分片的字段按順序存儲(對于不同的類型,整理規則可調整,例如按順序,按geohash值順序,按聚集等)。
整理好之後,生成每個分片内的元資訊,例如每8k一個機關,儲存這個機關的資料邊界(最大最小值),資料的平均值,count,方差等。
在搜尋資料時,由于每個分片存儲了每8k的邊界資訊,是以根據列條件,可以快速的過濾不滿足條件的8k機關(不需要掃描它們)。進而達到和索引一樣的效果。
直到前面的優化手段,隻是做到了分片内的資料歸整,分片和分片直接,是有可能存在資料的交集的。
例如:
第一個分片包含cola : 1-1000的資料,第二個分片可能包含cola : 2-2000的資料,這兩個分片是有交集的,如果搜尋的資料是落在交集中的 ,那麼這兩個分片都需要掃描。
分片合并歸整的目的就是要盡量的減少分片之間的資料邊界模糊問題,讓分片的邊界更加的清晰。
例如對于分區表,有32gb資料,每個分片32mb,那麼當資料寫完後,可以對整個分區做歸整,使得每個分片的邊界清晰。
這個操作類似于postgresql的cluster功能。
列存儲非常适合對列進行統計分析,傳回少量聚合結果的業務場景。
但是列存儲也可能帶來另一個負面影響,例如使用者可能要傳回多列,或者整行資料。由于列存儲每列的存儲都是獨立的,當需要傳回一整行記錄時,需要掃描更多的資料塊。當傳回的記錄數多時,可能會放大這個負面影響。
行列混合存儲就出現了,行列混合存儲的思想類似與shard的思想。(在分布式資料庫中,建表時,将表按規則切分成若幹的shard,然後再将shard存儲到不同的資料節點(shard數往往大于節點數),當擴容時,move shard即可,而不需要改變資料在shard層面的分布規則。 例如某個分布式資料庫由4個資料節點組成,建表時,可以分成4096個shard,那麼在擴容時,移動shard就可以完成擴容。而不需要改變資料的路由規則,它們隻需要按原來的方法路由到對應的shard即可。)
在行列混合存儲的應用中,也有類似shard的思想,例如将記錄數切片,每一批資料的列存儲放到一個單獨的資料檔案中。
這麼做的好處是,當使用者需要通路這批資料時,通路的是連續的資料塊,而不是離散的資料庫,進而提升傳回大量資料的性能。(當然,這裡指的是傳回相鄰的大量資料)。
大量資料實時讀寫,實時任意列搜尋的場景,優化的手段非常多。
本文詳解介紹了列存儲的優化方法。
包括分片延遲編排,分片歸整,行列混合等。
分片延遲編排,在保證資料高速寫入的前提下,可以實作資料在分片内的順序,8k機關的中繼資料。減少資料搜尋時的掃描成本。
分片歸整,指更大範圍的分片編排,進一步提升整體的邊界清晰度,進而再一次減少資料搜尋時的掃描成本。
行列混合存儲,當需要傳回大量的連續整行記錄時,可以大幅降低掃描的資料塊的離散度。