塊級(ctid)掃描在IoT(物聯網)極限寫和消費讀並存場景的應用

德哥發表於2017-06-08

標籤

PostgreSQL , 塊掃描 , 行號掃描 , ctid , tid scan , IoT , 物聯網 , 極限寫入 , 實時消費 , 實時讀 , 堆表 , heap , 時序


背景

在物聯網有一個非常普遍的資料需求,就是資料的寫入,另一個普遍的需求則是資料的消費(按時序讀取),以及流式計算。

關於流式計算,請參考

《(流式、lambda、觸發器)實時處理大比拼 – 物聯網(IoT)金融,時序處理最佳實踐》

《流計算風雲再起 – PostgreSQL攜PipelineDB力挺IoT》

《”物聯網”流式處理應用 – 用PostgreSQL實時處理(萬億每天)》

接下來我們談一談極限寫入和消費。

寫入

從資料儲存結構來看,PostgreSQL的HEAP儲存是非常適合高速寫入的,追加式寫入。以下文章中已得到高速寫入的驗證。

《PostgreSQL 如何瀟灑的處理每天上百TB的資料增量》

塊(時序列)索引

BRIN索引,也被稱為塊索引,是針對資料塊後設資料建立的索引(例如某個自增長欄位,物理儲存和欄位的值存在很好的線性相關性,那麼每個塊的資料區間就具有非常強的獨立性),BRIN索引非常小,對寫入效能的影響可以忽略。

BRIN適合物理儲存和欄位的值存在很好的線性相關性的欄位,例如時序欄位。

或者使用cluster或order 重排後,適合對應欄位。

消費

消費是指非同步的讀取資料,處理資料的過程,例如IoT場景,資料的寫入延遲要求非常低,所以要求寫入吞吐特別大。

而處理方面,則通過消費機制,進行處理。

那麼如何消費呢?

通常可以根據索引進行消費,比如前面提到的BRIN索引,對寫入吞吐的影響小,同時支援=,以及範圍的檢索。如果有時序欄位的話,BRIN是非常好的選擇。

然而並非所有的資料寫入場景都有時序欄位(當然使用者可以新增一個時間欄位來解決這個問題)。當沒有時序欄位時,如何消費效率最高呢?

塊掃描

塊掃描是很好的選擇,前面提到了資料儲存是HEAP,追加形式。

PostgreSQL提供了一種tid scan的掃描方法,告訴資料庫你要搜尋哪個資料塊的哪條記錄。

select * from tbl where ctid=`(100,99)`;  

這條SQL指查詢100號資料塊的第100條記錄。

這種掃描效率非常之高,可以配合HEAP儲存,在消費(讀取記錄)時使用。

評估塊記錄數

PostgreSQL暫時沒有提供返回整個資料塊的所有記錄的介面,只能返回某個資料塊的某一條記錄,所以如果我們需要讀取某個資料塊的記錄,需要列舉該資料塊的所有行。

如何評估一個資料塊有多少條記錄,或者最多有多少條記錄?

PAGE layout

https://www.postgresql.org/docs/10/static/storage-page-layout.html

HeapTupleHeaderData Layout

Field Type Length Description
t_xmin TransactionId 4 bytes
t_xmax TransactionId 4 bytes delete XID stamp
t_cid CommandId 4 bytes insert and/or delete CID stamp (overlays with t_xvac)
t_xvac TransactionId 4 bytes XID for VACUUM operation moving a row version
t_ctid ItemPointerData 6 bytes current TID of this or newer row version
t_infomask2 uint16 2 bytes number of attributes, plus various flag bits
t_infomask uint16 2 bytes various flag bits
t_hoff uint8 1 byte offset to user data

Overall Page Layout

Item Description
PageHeaderData 24 bytes long. Contains general information about the page, including free space pointers.
ItemIdData Array of (offset,length) pairs pointing to the actual items. 4 bytes per item.
Free space The unallocated space. New item pointers are allocated from the start of this area, new items from the end.
Items The actual items themselves.
Special space Index access method specific data. Different methods store different data. Empty in ordinary tables.

單頁最大記錄數估算

最大記錄數=block_size/(ctid+tuple head)=block_size/(4+27);

postgres=# select current_setting(`block_size`);  
 current_setting   
-----------------  
 32768  
(1 row)  
  
postgres=# select current_setting(`block_size`)::int/31;  
 ?column?   
----------  
     1057  
(1 row)  

如果需要評估更精確的行數,可以加上欄位的固定長度,變長欄位的頭(4BYTE)。

例子

生成指定block TID的函式

create or replace function gen_tids(blkid int) returns tid[] as $$  
select array(  
  SELECT (`(`||blkid||`,` || s.i || `)`)::tid  
    FROM generate_series(0,current_setting(`block_size`)::int/31) AS s(i)  
)  ;  
$$ language sql strict immutable;  

讀取某個資料塊的記錄

postgres=# create table test(id int);  
CREATE TABLE  
postgres=# insert into test select generate_series(1,10000);  
INSERT 0 10000  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test where ctid = any  
(  
  array  
  (  
    SELECT (`(0,` || s.i || `)`)::tid  
      FROM generate_series(0, current_setting(`block_size`)::int/31) AS s(i)  
  )  
);  
                                                                QUERY PLAN                                                                  
------------------------------------------------------------------------------------------------------------------------------------------  
 Tid Scan on postgres.test  (cost=25.03..40.12 rows=10 width=4) (actual time=0.592..0.795 rows=909 loops=1)  
   Output: test.id  
   TID Cond: (test.ctid = ANY ($0))  
   Buffers: shared hit=1057  
   InitPlan 1 (returns $0)  
     ->  Function Scan on pg_catalog.generate_series s  (cost=0.01..25.01 rows=1000 width=6) (actual time=0.087..0.429 rows=1058 loops=1)  
           Output: (((`(0,`::text || (s.i)::text) || `)`::text))::tid  
           Function Call: generate_series(0, ((current_setting(`block_size`::text))::integer / 31))  
 Planning time: 0.106 ms  
 Execution time: 0.881 ms  
(10 rows)  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test where ctid = any(gen_tids(1));  
  
 Tid Scan on postgres.test  (cost=1.32..1598.90 rows=1058 width=4) (actual time=0.026..0.235 rows=909 loops=1)  
   Output: id  
   TID Cond: (test.ctid = ANY (`{"(1,0)","(1,1)","(1,2)","(1,3)","(1,4)","(1,5)","(1,6)","(1,7)","(1,8)","(1,9)","(1,10)","(1,11)","(1,12)","(1,13)","(1,14)","(1,15)","(1,16)","(1,17)","(1,18)","(1,19)","(1,20)","(1,21)","(1,22)","(1,23)  
","(1,24)","(1,25)"  
....  
   Buffers: shared hit=1057  
 Planning time: 1.084 ms  
 Execution time: 0.294 ms  
(6 rows)  
postgres=# select ctid,* from test where ctid = any(gen_tids(11));
  ctid  |  id   
--------+-------
 (11,1) | 10000
(1 row)

postgres=# select ctid,* from test where ctid = any(gen_tids(9));
  ctid   |  id  
---------+------
 (9,1)   | 8182
 (9,2)   | 8183
 (9,3)   | 8184
 (9,4)   | 8185
 (9,5)   | 8186
 (9,6)   | 8187
 ...
 (9,904) | 9085
 (9,905) | 9086
 (9,906) | 9087
 (9,907) | 9088
 (9,908) | 9089
 (9,909) | 9090
(909 rows)

擴充套件場景

如果資料沒有更新,刪除;那麼CTID還可以作為索引來使用,例如全文檢索(ES),可以在建立索引時使用ctid來指向資料庫中的記錄,而不需要另外再建一個PK,也能大幅度提升寫入效能。

參考

https://www.citusdata.com/blog/2016/03/30/five-ways-to-paginate/

https://www.postgresql.org/message-id/flat/be64327d326568a3be7fde1891ed34ff.squirrel%40sq.gransy.com#be64327d326568a3be7fde1891ed34ff.squirrel@sq.gransy.com


相關文章