使用pg_repack實現在線vacuum
- 2019 年 12 月 18 日
- 筆記
Postgresql通過數據多版本實現mvcc,刪除數據並不會真正刪除數據,而是修改標識,更新是通過刪除+插入的方式進行,所以在頻繁更新的系統,數據膨脹是個頭疼的問題,如果不進行處理,數據膨脹倍數可能達到十幾倍。
為了處理膨脹問題,pg提供了vacuum工具,vacuum分為普通vacuum和vacuum full,普通vacuum會清理死元組,但是不會進行空間重組,磁碟上的空間不會釋放,但是會釋放死元組的空間,後續插入的元組會根據空閑空間管理fsm優先插入空閑空間。Vacuum full清理會釋放磁碟空間,但是會獲取八級鎖,因為vacuum full的原理是新建一個表數據文件,然後從老表中拷貝數據到新文件中,這個過程會阻塞select。
因為影響業務,pg社區開發了pg_repack工具,老版本叫pg_reorg。Pg_repack以extension的方式存在,用戶可以自己安裝該插件。本文簡單介紹一下pg_repack的使用。
Pg_repack的源碼在github或者pgxn上都可以下載,這裡編譯安裝、創建插件的過程就不再贅述。
在安裝完後,就可以在作業系統命令行使用pg_repack命令了,下面列舉了pg_repack的一些命令用法:
pg_repack --no-order --table test_1 test pg_repack --wait-timeout 3600 --jobs 10 --no-order -d test pg_repack --wait-timeout 3600 --jobs 10 --no-order --schema=test -d test pg_repack --wait-timeout 3600 --jobs 10 --only-indexes --table test.test_1 --no-order -d test pg_repack --wait-timeout 3600 --jobs 10 --index test.idx1 --no-order -d test
經過測試,在執行pg_repack的同時對錶進行並發查詢,性能下降大概只有10%到20%,讀取操作可以正常進行。並且表的oid沒有發生變化,repack執行完成後,通過pg_relation_filepath()函數查詢發現表的數據文件發生了改變,同時會刪除原來的數據文件,其實執行vacuum full數據文件也會發生改變。
test=# select pg_relation_filepath(16475); pg_relation_filepath ---------------------- base/16387/16580 (1 row) test=# select pg_relation_filepath(16475); pg_relation_filepath ---------------------- base/16387/16601 (1 row)
下面聊聊repack的原理吧,原理其實和vacuum full是類似的,都是新建一個文件,然後將老文件數據拷貝過來,然後進行文件切換,它不阻塞讀寫的秘訣就是新建文件和拷貝的過程是在線做的,在沒有完成拷貝之前,原來的文件還是可以讀寫的,只有在切表那一瞬間可能會有影響。
那麼它是怎麼做到在線拷貝的呢?源庫的數據文件一直在變,所以表文件其實分為兩部分,一部分是基礎數據,一部分是增量數據,基礎數據的拷貝就是正常的拷貝,增量數據是通過創建觸發器來捕獲在該表上的讀寫操作來實現的,待基礎數據拷貝完後再將trigger捕獲的增量sql進行應用,達到最終結果。
我們其實可以發現,pg_repack會在庫里創建名為repack的schema,裡面有兩張表:primary_keys和tables。Primarys分為兩列,第一列indrelid代表表的oid,第二列indexrelid代表主鍵或唯一索引的oid。Tables表記錄了創建trigger以及捕獲的相關語句,語句按一條條的record進行記錄。如下所示:
test=# select * from tables; -[ RECORD 1 ]-----+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- relname | public.products relid | 16388 reltoastrelid | 16391 reltoastidxid | 16393 schemaname | public pkid | ckid | create_pktype | create_log | CREATE TABLE repack.log_16388 (id bigserial PRIMARY KEY, pk repack.pk_16388, row public.products) create_trigger | enable_trigger | ALTER TABLE public.products ENABLE ALWAYS TRIGGER repack_trigger create_table_1 | CREATE TABLE repack.table_16388 WITH (oids = false) TABLESPACE tablespace_orig | pg_default create_table_2 | AS SELECT product_no,name,price FROM ONLY public.products copy_data | INSERT INTO repack.table_16388 SELECT product_no,name,price FROM ONLY public.products alter_col_storage | drop_columns | delete_log | DELETE FROM repack.log_16388 lock_table | LOCK TABLE public.products IN ACCESS EXCLUSIVE MODE ckey | sql_peek | SELECT * FROM repack.log_16388 ORDER BY id LIMIT $1 sql_insert | INSERT INTO repack.table_16388 VALUES ($1.*) sql_delete | sql_update | sql_pop | DELETE FROM repack.log_16388 WHERE id IN ( -[ RECORD 2 ]-----+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- relname | public.test relid | 16400 reltoastrelid | 0 reltoastidxid | 0 schemaname | public pkid | ckid | create_pktype | create_log | CREATE TABLE repack.log_16400 (id bigserial PRIMARY KEY, pk repack.pk_16400, row public.test) create_trigger | enable_trigger | ALTER TABLE public.test ENABLE ALWAYS TRIGGER repack_trigger create_table_1 | CREATE TABLE repack.table_16400 WITH (oids = false) TABLESPACE tablespace_orig | pg_default create_table_2 | AS SELECT id FROM ONLY public.test copy_data | INSERT INTO repack.table_16400 SELECT id FROM ONLY public.test alter_col_storage | drop_columns | delete_log | DELETE FROM repack.log_16400 lock_table | LOCK TABLE public.test IN ACCESS EXCLUSIVE MODE ckey | sql_peek | SELECT * FROM repack.log_16400 ORDER BY id LIMIT $1 sql_insert | INSERT INTO repack.table_16400 VALUES ($1.*) sql_delete | sql_update | sql_pop | DELETE FROM repack.log_16400 WHERE id IN ( -[ RECORD 3 ]-----+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- relname | public.fruits relid | 16394 reltoastrelid | 16397 reltoastidxid | 16399 schemaname | public pkid | ckid | create_pktype | create_log | CREATE TABLE repack.log_16394 (id bigserial PRIMARY KEY, pk repack.pk_16394, row public.fruits) create_trigger | enable_trigger | ALTER TABLE public.fruits ENABLE ALWAYS TRIGGER repack_trigger create_table_1 | CREATE TABLE repack.table_16394 WITH (oids = false) TABLESPACE tablespace_orig | pg_default create_table_2 | AS SELECT number,name,price FROM ONLY public.fruits copy_data | INSERT INTO repack.table_16394 SELECT number,name,price FROM ONLY public.fruits alter_col_storage | drop_columns | delete_log | DELETE FROM repack.log_16394 lock_table | LOCK TABLE public.fruits IN ACCESS EXCLUSIVE MODE ckey | sql_peek | SELECT * FROM repack.log_16394 ORDER BY id LIMIT $1 sql_insert | INSERT INTO repack.table_16394 VALUES ($1.*) sql_delete | sql_update | sql_pop | DELETE FROM repack.log_16394 WHERE id IN ( -[ RECORD 4 ]-----+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- relname | public.test_1 relid | 16475 reltoastrelid | 0 reltoastidxid | 0 schemaname | public pkid | 16493 ckid | create_pktype | CREATE TYPE repack.pk_16475 AS (c1 integer) create_log | CREATE TABLE repack.log_16475 (id bigserial PRIMARY KEY, pk repack.pk_16475, row public.test_1) create_trigger | CREATE TRIGGER repack_trigger AFTER INSERT OR DELETE OR UPDATE ON public.test_1 FOR EACH ROW EXECUTE PROCEDURE repack.repack_trigger('INSERT INTO repack.log_16475(pk, row) VALUES( CASE WHEN $1 IS NULL THEN NULL ELSE (ROW($1.c1)::repack.pk_16475) END, $2)') enable_trigger | ALTER TABLE public.test_1 ENABLE ALWAYS TRIGGER repack_trigger create_table_1 | CREATE TABLE repack.table_16475 WITH (oids = false) TABLESPACE tablespace_orig | pg_default create_table_2 | AS SELECT c1,c2,c3,c4,c5 FROM ONLY public.test_1 copy_data | INSERT INTO repack.table_16475 SELECT c1,c2,c3,c4,c5 FROM ONLY public.test_1 alter_col_storage | drop_columns | delete_log | DELETE FROM repack.log_16475 lock_table | LOCK TABLE public.test_1 IN ACCESS EXCLUSIVE MODE ckey | sql_peek | SELECT * FROM repack.log_16475 ORDER BY id LIMIT $1 sql_insert | INSERT INTO repack.table_16475 VALUES ($1.*) sql_delete | DELETE FROM repack.table_16475 WHERE (c1) = ($1.c1) sql_update | UPDATE repack.table_16475 SET (c1, c2, c3, c4, c5) = ($2.c1, $2.c2, $2.c3, $2.c4, $2.c5) WHERE (c1) = ($1.c1) sql_pop | DELETE FROM repack.log_16475 WHERE id IN (

