背景
業務背景
某系統數據量:
20億行左右,64個字段,原始數據多爲字符串類型。(大多數字段的唯一值有限)
需求:
1. 查詢,任意字段組合查詢,求聚合值。
2. 查詢併發,1000左右查詢併發,每次查詢響應時間要求100ms以內。
3. 寫入、更新,要求延遲1秒內。
高峯時寫入、更新可達20萬行/s。
業務上允許批量寫入。
4. 要求加字段方便。
5. 要求實時計算(無需建模),或者說要求加統計維度方便,不需要等建模結束。
PostgreSQL 該場景特性
使用PostgreSQL可以很好的滿足這樣的需求,PostgreSQL具備以下特性,適合ADHoc的非建模查詢:
1、索引接口:
bloom接口,支持多字段組合索引,任意字段組合的查詢,實現lossy的過濾,收斂目標數據到一定的BLOCKs。
gin接口,倒排索引,廣泛應用於多值類型(如全文檢索類型、數組、JSON、K-V等),多字段組合索引等。支持多值類型或任意字段組合搜索,bitmap index scan將目標數據收斂到一定的BLOCKs,加速查詢。
rum接口,新版本的rum不僅支持tsvector類型,同時還支持了array類型。rum的優勢是不需要bitmap scan,因此沒有recheck的過程,查詢時的CPU消耗比GIN索引接口更低。
《PostgreSQL bitmap scan的IO放大的原理解釋和優化》
2、索引掃描方法
index scan,索引掃描,直接命中數據。
bitmap index scan,返回包含目標數據的BLOCK,數據庫進行CPU RECHECK。這種方法支持多個字段合併掃描。
《PostgreSQL bitmapAnd, bitmapOr, bitmap index scan, bitmap heap scan》
3、其他特性,輔助這個業務場景:
並行計算(支持並行掃描、過濾、排序、JOIN、聚合、創建索引等),(例如 100億數據,並行排序求top-k只要40秒),更多指標參考:
《阿里雲 PostgreSQL 產品生態;案例、開發實踐、管理實踐、學習資料、學習視頻》
異步調用與聚合,也支持支持DBLINK異步調用,實現並行計算。
分區表。
水平拆庫。
序列,可用於字典化。例子:
《PostgreSQL 全局ID分配(數據字典化)服務 設計實踐》
UDF。可以支持非常複雜的數據庫函數編程,實現複雜邏輯。
RULE。實現數據寫入、更新時自動對數據進行字典化。
PostgreSQL 場景優化手段
1. 字典化(大多數字段的唯一值有限,唯一值個數100-5000萬左右),30個左右字段需要字典化(可做成ETL,實時字典化)。字典化的目的是壓縮空間,提高處理效率。如果性能OK可以不做字典化。
2. 寫入自動字典化(可以使用RULE來實現)
3. 查詢時自動翻譯
4. bloom, rum, gin, 數組, tsvector, 多字段BITMAP SCAN
5. 分庫,分表。dblink異步並行調用。
dblink異步調用加速介紹
《PostgreSQL 全局ID分配(數據字典化)服務 設計實踐》
《PostgreSQL VOPS 向量計算 + DBLINK異步並行 - 單實例 10億 聚合計算跑進2秒》
《PostgreSQL 相似搜索分佈式架構設計與實踐 - dblink異步調用與多機並行(遠程 遊標+記錄 UDF實例)》
《PostgreSQL 相似搜索設計與性能 - 地址、QA、POI等文本 毫秒級相似搜索實踐》
《驚天性能!單RDS PostgreSQL實例 支撐 2000億 - 實時標籤透視案例 (含dblink異步並行調用)》
《阿里雲RDS PostgreSQL OSS 外部表 - (dblink異步調用封裝)並行寫提速案例》
水平分庫方法介紹
1、使用plproxy水平分庫
《PostgreSQL 最佳實踐 - 水平分庫(基於plproxy)》
《阿里雲ApsaraDB RDS for PostgreSQL 最佳實踐 - 4 水平分庫 之 節點擴展》
《阿里雲ApsaraDB RDS for PostgreSQL 最佳實踐 - 3 水平分庫 vs 單機 性能》
《阿里雲ApsaraDB RDS for PostgreSQL 最佳實踐 - 2 教你RDS PG的水平分庫》
2、使用postgres_fdw + pg_pathman水平分庫
《PostgreSQL 9.6 sharding based on FDW & pg_pathman》
3、其他基於PostgreSQL的NewSQL或MPP開源產品
pg-xl
citusdb
greenplum
pg_shardman
https://github.com/postgrespro/pg_shardman
方案1 - 全局字典化 + 數組類型 + rum索引
全局字典化
全局字典化的意思是,所有字段的取值空間構成一個大的取值空間,“字段名+字段值”在取值空間內唯一。
字典化後,可以選擇INT4或INT8作爲字典化後的元素類型。
數組
由於使用了全局字典,所以可以使用一個數組字段,代替所有字段。
create table tbl(
id int8 primary key,
c1 int,
c2 int,
...
c50 int
);
代替爲
create table tbl(
id int8 primary key,
dict int[]
);
使用數組的好處多多,例如加字段易如反掌,因爲你不需要改結果,只需要把新加的字段的內容填充到數組中。
原來的AND查詢使用數組包含操作代替,原來的OR查詢,使用數組相交操作代替。
RUM索引
RUM索引,已經支持數組類型。支持包含、相交查詢。
DEMO
DEMO將拋開如何將文本轉換爲字典的部分,你可以參考如下:
《PostgreSQL 全局ID分配(數據字典化)服務 設計實踐》
1、創建插件
create extension rum;
2、創建生成隨機值的函數(即字典值),輸入一個範圍,返回這個範圍內的隨機值
create or replace function gen_rand(
int, -- 最小值(包含)
int -- 最大值(包含)
) returns int as $$
select $1+(random()*($2-$1))::int;
$$ language sql strict;
3、創建一個函數,用於生成長度爲50的隨機數組,規則是這樣的,字典取值空間100萬個元素的16個字段,字典取值空間1000萬個元素的16個字段,字典取值空間5000萬個元素的18個字段。
總共50個字段,消耗10.76億個字典取值空間。因此可以使用INT4作爲字典元素類型。
create or replace function gen_ran_array() returns int[] as $$
declare
res int[] := '{}'; -- 結果
x int; -- 組範圍
offset1 int; -- 偏移量
begin
-- 第1段消耗1600萬值
offset1 := (-2147483648); -- 第1批段偏移量爲int4最小值
x := 1000000; -- 每段取值範圍爲100萬
for i in 1..16
loop
res := res||gen_rand(offset1+(i-1)*x, offset1+i*x-1);
end loop;
-- 第2段消耗1.6億值
offset1 := (-2147483648)+16*1000000; -- 第2批段偏移量
x := 10000000; -- 每段取值範圍爲1000萬
for i in 1..16
loop
res := res||gen_rand(offset1+(i-1)*x, offset1+i*x-1);
end loop;
-- 第3段消耗9億值
offset1 := (-2147483648)+16*1000000+16*10000000; -- 第3批段偏移量爲
x := 50000000; -- 每段取值範圍爲5000萬
for i in 1..18
loop
res := res||gen_rand(offset1+(i-1)*x, offset1+i*x-1);
end loop;
-- 總共消耗10.76億值,在INT4的取值空間內
return res;
end;
$$ language plpgsql strict;
4、數據示例
postgres=# select gen_ran_array();
gen_ran_array
--------------------------------------------------------------------------------------------------------
{-2146646308,-2145683415,-2145349222,-2143926381,-2143348415,-2141933614,-2141364249,-2140223009,-2138645116,-2138311094,-2137328519,-2136424380,-2134763612,-2134461767,-2132675440,-2131727900,-2125512613,-2117580976,-2108206637,-2093806503,-2084537076,-2072042857,-2071092129,-2060488058,-2043914532,-2039914771,-2025797284,-2021177739,-2004046058,-1997857659,-1988910392,-1975672648,-1963342019,-1901896072,-1864565293,-1806580356,-1724394364,-1708595351,-1643548404,-1582467707,-1549967665,-1485791936,-1429504322,-1413965811,-1334697903,-1289093865,-1226178368,-1204842726,-1169580505,-1109793310}
(1 row)
5、建表
create table tbl_test(
id serial primary key,
dict int[] -- 使用數組代替了50個字段
);
6、建數組rum索引
create index idx_tbl_test on tbl_test using rum (dict rum_anyarray_ops);
7、單實例,單表寫入2億條測試數據
vi test2.sql
insert into tbl_test (dict) select gen_ran_array() from generate_series(1,10);
pgbench -M prepared -n -r -P 1 -f ./test2.sql -c 56 -j 56 -t 357143
8、單實例寫入速度,約3.3萬行/s。
寫入約3.3萬行/s,10個節點約33萬行/s。
CPU 約 20% 空閒。
progress: 2.0 s, 3363.5 tps, lat 16.716 ms stddev 4.362
progress: 3.0 s, 3568.0 tps, lat 15.707 ms stddev 3.707
progress: 4.0 s, 3243.0 tps, lat 17.239 ms stddev 4.529
9、2億數據空間佔比
表:49 GB
索引:184 GB
10、創建返回N個有效空間內隨機值的函數,用於查詢測試
create or replace function gen_test_arr(int) returns int[] as $$
select array(select * from unnest(gen_ran_array()) order by random() limit $1);
$$ language sql strict immutable;
結果舉例
postgres=# select gen_test_arr(4);
gen_test_arr
---------------------------------------------------
{-2012641247,-2133910693,-1626085823,-2136987009}
(1 row)
postgres=# select gen_test_arr(4);
gen_test_arr
---------------------------------------------------
{-1664820600,-1321104348,-1410506219,-2116164275}
(1 row)
11、ADHoc查詢壓測
關閉bitmap scan
set enable_bitmapscan=off;
1、1個字段查詢
select * from tbl_test where dict @> gen_test_arr(1);
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict @> gen_test_arr(1);
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=14.40..852142.09 rows=753011 width=228) (actual time=0.410..4.444 rows=132 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict @> '{-2139078302}'::integer[])
Buffers: shared hit=28 read=126 dirtied=10
Planning time: 0.616 ms
Execution time: 4.492 ms
(6 rows)
2、2個字段and查詢
select * from tbl_test where dict @> gen_test_arr(2);
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict @> gen_test_arr(2);
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=28.80..4627.28 rows=3776 width=228) (actual time=0.084..0.084 rows=0 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict @> '{-1229103789,-2117549196}'::integer[])
Buffers: shared hit=27
Planning time: 0.428 ms
Execution time: 0.098 ms
(6 rows)
3、3個字段and查詢
select * from tbl_test where dict @> gen_test_arr(3);
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict @> gen_test_arr(3);
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=43.20..67.53 rows=19 width=228) (actual time=0.145..0.145 rows=0 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict @> '{-1297850230,-1598505025,-1409870549}'::integer[])
Buffers: shared hit=32
Planning time: 0.621 ms
Execution time: 0.165 ms
(6 rows)
4、4個字段and查詢
select * from tbl_test where dict @> gen_test_arr(4);
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict @> gen_test_arr(4);
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=57.60..60.01 rows=1 width=228) (actual time=0.301..0.301 rows=0 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict @> '{-2143045247,-1543382864,-2132603589,-2146917034}'::integer[])
Buffers: shared hit=37
Planning time: 0.651 ms
Execution time: 0.321 ms
(6 rows)
5、2個字段or查詢
select * from tbl_test where dict && gen_test_arr(2);
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict && gen_test_arr(2);
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=28.80..1626373.60 rows=1538286 width=228) (actual time=0.222..12.367 rows=308 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict && '{-2141077184,-2146768682}'::integer[])
Buffers: shared hit=40 read=295 dirtied=44
Planning time: 0.590 ms
Execution time: 12.439 ms
(6 rows)
6、3個字段or查詢
select * from tbl_test where dict && gen_test_arr(3);
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict && gen_test_arr(3);
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=43.20..2265424.89 rows=2282542 width=228) (actual time=0.254..19.038 rows=174 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict && '{-1620795514,-1639870542,-2139239663}'::integer[])
Buffers: shared hit=40 read=166 dirtied=31
Planning time: 0.612 ms
Execution time: 19.093 ms
(6 rows)
7、4個字段or查詢
select * from tbl_test where dict && gen_test_arr(4);
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict && gen_test_arr(4);
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=57.60..2847470.08 rows=3043456 width=228) (actual time=0.598..17.606 rows=328 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict && '{-1705307460,-2136144007,-2132774019,-1953195893}'::integer[])
Buffers: shared hit=46 read=319 dirtied=54
Planning time: 0.652 ms
Execution time: 17.690 ms
(6 rows)
8、更多字段AND查詢
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict @> gen_test_arr(50);
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=600.00..602.41 rows=1 width=228) (actual time=2.203..2.203 rows=0 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict @> '{-2132669865,-2137249848,-2042878341,-2088316247,-2143000973,-2143620433,-2133871891,-1209554329,-1528596632,-2134772182,-1897199994,-1104232704,-1704082437,-2141239524,-1968035285,-2131776457,-139302331
4,-1622173835,-2021025608,-1143009897,-1793901515,-1510483843,-2142162388,-2000639730,-2139063117,-2079775594,-1329895944,-1447777707,-2145106996,-2059425427,-1307088506,-2136236994,-1731136990,-1257663719,-2110797445,-2094280348,-212741
5326,-1990393443,-2040274978,-2022798000,-2118667926,-2070083767,-2145499074,-1979076804,-2137973932,-2004407692,-2146950560,-2140049095,-1610110401,-1866288627}'::integer[])
Buffers: shared hit=217
Planning time: 1.124 ms
Execution time: 2.230 ms
(6 rows)
9、更多字段OR查詢
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_test where dict && gen_test_arr(50);
QUERY PLAN
------------------------------------------------------------------------
Index Scan using idx_tbl_test on public.tbl_test (cost=600.00..1271996.70 rows=6602760 width=228) (actual time=2.338..6.521 rows=547 loops=1)
Output: id, dict
Index Cond: (tbl_test.dict && '{-1610700436,-1085141127,-2014816431,-1549709010,-2137440391,-1263750440,-1973015812,-1129115246,-2007733110,-2081342072,-1654458135,-2062905475,-1702363876,-2141009261,-1948730625,-2035766373,-214289408
0,-1502295300,-1732512476,-2131960156,-2053099607,-2140187767,-2117547749,-2133816635,-1875496311,-2139047408,-2145616325,-1177249426,-2135287970,-2123144611,-1298794740,-1389925076,-2138430551,-2144850436,-2084170210,-2132759222,-214442
2424,-1819252191,-1995606281,-1988618306,-2135969961,-2105761786,-1435016071,-2141623972,-2147011919,-2049887148,-2100968914,-2030470574,-1368944612,-1826083272}'::integer[])
Buffers: shared hit=764 dirtied=1
Planning time: 0.627 ms
Execution time: 6.619 ms
(6 rows)
壓測結果
4個維度AND查詢,輸入隨機條件,壓測結果:平均RT 1.3毫秒,TPS 4.3萬+
vi test.sql
select count(*) from tbl_test where dict @> gen_test_arr(4);
由於使用了IMMUTABLE函數來實現走索引,所以不能用prepare statement來測,否則變量就固定了.因此這裏用了extended協議
pgbench -M extended -n -r -P 1 -f ./test.sql -c 56 -j 56 -T 120
主要瓶頸在IO上面,如果內存更大一些,或者IO能力再好一些,性能會更好。
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
34 5 15 45 0 0| 937M 0 |5540B 5804B| 0 0 | 116k 132k
33 5 15 46 0 0| 937M 0 |4616B 4976B| 0 0 | 115k 129k
transaction type: ./test.sql
scaling factor: 1
query mode: extended
number of clients: 56
number of threads: 56
duration: 120 s
number of transactions actually processed: 5190552
latency average = 1.295 ms
latency stddev = 0.791 ms
tps = 43242.325550 (including connections establishing)
tps = 43247.431982 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
1.296 select count(*) from tbl_test where dict @> gen_test_arr(4);
4個維度OR查詢,輸入隨機條件,壓測結果:平均RT 2.9毫秒,TPS 1.8萬+
vi test.sql
select count(*) from tbl_test where dict && gen_test_arr(4);
由於使用了IMMUTABLE函數來實現走索引,所以不能用prepare statement來測,否則變量就固定了.因此這裏用了extended協議
pgbench -M extended -n -r -P 1 -f ./test.sql -c 56 -j 56 -T 120
主要瓶頸在IO上面,如果內存更大一些,或者IO能力再好一些,性能會更好。
transaction type: ./test.sql
scaling factor: 1
query mode: extended
number of clients: 56
number of threads: 56
duration: 120 s
number of transactions actually processed: 2260125
latency average = 2.973 ms
latency stddev = 2.724 ms
tps = 18828.318071 (including connections establishing)
tps = 18830.742359 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
2.974 select count(*) from tbl_test where dict && gen_test_arr(4);
機器,阿里雲ECS ,56核,224G內存,本地SSD雲盤。(這樣規格的RDS PostgreSQL,只要幾千/month)
小結
《ADHoc(任意字段組合)查詢 與 字典化 (rum索引加速) - 實踐與方案1》,使用 “全局字典化+數組+RUM索引”,實現了高效的寫入和查詢性能。
單實例單表寫入:約3.3萬行/s
單實例寫入同時伴隨查詢:任意維度查詢,20毫秒以內響應。
4個維度AND查詢,平均RT 1.3毫秒,TPS 4.3萬+,遠超業務1000的併發需求。
4個維度OR查詢,平均RT 2.9毫秒,TPS 1.8萬+,遠超業務1000的併發需求。
結合 “全局字典化服務+分庫” 可以實現更大體量的adhoc實時查詢需求。
得空再介紹PostgreSQL ADHoc實時查詢的其他方法。
參考
1、RUM索引接口
https://github.com/postgrespro/rum
《PostgreSQL結合餘弦、線性相關算法 在文本、圖片、數組相似 等領域的應用 - 3 rum, smlar應用場景分析》
《從難纏的模糊查詢聊開 - PostgreSQL獨門絕招之一 GIN , GiST , SP-GiST , RUM 索引原理與技術背景》
《PostgreSQL 全文檢索加速 快到沒有朋友 - RUM索引接口(潘多拉魔盒)》
《PostgreSQL bitmapAnd, bitmapOr, bitmap index scan, bitmap heap scan》
《PostgreSQL bitmap scan的IO放大的原理解釋和優化》
2、函數穩定性介紹
《函數穩定性講解 - retalk PostgreSQL function's [ volatile|stable|immutable ]》
《函數穩定性講解 - 函數索引思考, pay attention to function index used in PostgreSQL》
《函數穩定性講解 - Thinking PostgreSQL Function's Volatility Categories》