雲端海量任務調度系統資料庫設計 - 阿裡雲RDS PostgreSQL案例

來源:互聯網
上載者:User

摘要: 標籤PostgreSQL , 任務調度系統, 資料庫設計, schemaless 幕後任務調度系統中的任務狀態管理,通常會用到資料庫來隱藏任務調度的程序狀態,控制任務的鎖等。《advisorylock 實現高並行非堵塞式商務鎖》如果是小量任務,是挺好實現的,但是每小時處理幾十億或者幾億的任務,如何設計這樣的任務狀態管理資料庫呢?挑戰對於一個面向多個用戶的任務調度平臺(例如雲端的任務調度平臺,將面向所有租戶使用)。

幕後

任務調度系統中的任務狀態管理,通常會用到資料庫來隱藏任務調度的程序狀態,控制任務的鎖等。

《advisorylock 實現高並行非堵塞式 商務鎖》

如果是小量任務,是挺好實現的,但是每小時處理幾十億或者幾億的任務,如何設計這樣的任務狀態管理資料庫呢?

挑戰

對於一個面向多個用戶的任務調度平臺(例如雲端的任務調度平臺,將面向所有租戶使用)。

較大的挑戰是任務資料的寫入(海量),另一個是任務狀態的更新(海量,每個任務至少被更新一次)。

雲端海量任務調度資料庫設計

雲端任務調度存在一些屬性:

1、用戶和用戶之間的任務是沒有關聯的,單個用戶的任務在調度時可能有相依性屬性。

2、資料量龐大。

3、任務通常都有最終穩定點,穩定後,對應的任務記錄就不會變化了。

針對以上幾個特點,取樣PostgreSQL設計:

1、任務資料生成後寫入任務處理表

2、任務處理表使用rotate設計(例如每小時一個rotate表),處理完的資料直接清除,不需要VACUUM。

3、分區方面,任務處理表取樣用戶級分區,在追蹤需要處理的任務時更加的精煉(減少冗餘掃描)。

4、當任務達到最終狀態時,從任務執行表移除,寫入歷史表。

5、早期的歷史表,從RDS PG中移除,寫入阿裡雲OSS,使用RDSPG OSS外部表格介面可以存取到這些歷史資料。

1、初始任務表,用於隱藏用戶生成的任務。

create table task_init (--任務初始表
uidint,--用戶id
ptid serial8,--父任務id
tid serial,--子任務ID
state int default 1,--任務狀態,1表示初始狀態,-1表示正在處理, 0表示處理結束
retry int default -1,--重試次數
info text,--其他資訊
ts timestamp--時間
);

2、任務歷史表,用於隱藏任務的最終狀態。

create table task_hist (--任務歷史表
uidint,--用戶id
ptid int8,--父任務id
tid int,--子任務ID
state int default 1,--任務狀態,1表示初始狀態,-1表示正在處理, 0表示處理結束
retry int default -1,--重試次數
info text,--其他資訊
ts timestamp--時間
);

3、為了簡化測試,按用戶ID進行分區。(前面提到的rotate設計,多級分區設計,請參考本文末尾的本文)

do language plpgsql $$
declare
begin
for i in 1..1000 loop
execute 'create table task_init_'||i||' ( like task_init including all)';
execute 'create table task_hist_'||i||' ( like task_hist including all)';
end loop;
end;
$$;

4、為了測試方便,使用schemaless的設計,將用戶任務的初始資料生成寫入放在PLPGSQL邏輯中。

create or replace function ins_task_init(
uid int,
info text,
ts timestamp
)returns void as $$
declare
target name;
begin
target := format('%I', 'task_init_'||uid);
execute format('insert into %I (uid,info,ts) values (%L,%L,%L)', target, uid,info,ts);
end;
$$ language plpgsql strict;

5、執行任務,分為幾個步驟。

5.1、從任務表讀取任務。

5.2、用戶執行任務。

5.3、回饋執行的結果,不成功的任務更新task_init表,對於執行成功(並結束)的任務,資料從task_init遷移到task_hist。

為了測試資料庫的效能,我講這三步的邏輯寫到plpgsql裡面。同時使用delete limit的屬性,一次批量取出若干條任務。

這裡使用CTID行號尋找,達到最佳的效能。不僅免去了索引的使用,而且效能更佳。

這裡使用了advisory lock,使得單個用戶不會出現並行任務。(實際商務中,可以並行。)

這裡沒有測試更新狀態,task_init還有少量更新(相比insert和delete,比例很少,可以忽略),比如任務失敗的情況。

關閉task_init表的autovacuum,採用rotate的形式進行處理。

create or replace function run_task(
uid int,
batch int
) returns void as $$
declare
target1 name;
target2 name;
begin
target1 := format('%I', 'task_init_'||uid);
target2 := format('%I', 'task_hist_'||uid);
execute format('with t1 as (select ctid from %I where pg_try_advisory_xact_lock(%L) limit %s) , t2 as (delete from %I where ctid = any (array(select ctid from t1)) returning *)insert into %I select * from t2;', target1, uid, batch, target1, target2);
end;
$$ language plpgsql strict;

6、測試分解動作。

寫入初始任務
postgres=# select ins_task_init(1,'test',now()::timestamp);
ins_task_init
---------------
(1 row)
postgres=# select ins_task_init(1,'test',now()::timestamp);
ins_task_init
---------------
(1 row)
執行任務
postgres=# select run_task(1,100);
run_task
----------
(1 row)
查看任務是否結束並遷移到歷史表
postgres=# select * from task_init_1;
uid | ptid | tid | state | retry | info | ts
-----+------+-----+-------+-------+------+----
(0 rows)
postgres=# select * from task_hist_1;
uid | ptid | tid | state | retry | info |ts
-----+------+-----+-------+-------+------+----------------------------
1 |1 |1 |1 |-1 | test | 2017-07-20 15:26:32.739766
1 |2 |2 |1 |-1 | test | 2017-07-20 15:26:33.233469
(2 rows)

效能壓測

1、生成任務的效能

vi ins.sql
set uid random(1,1000)
select ins_task_init(:uid,'test',now()::timestamp);
pgbench -M prepared -n -r -P 1 -f ./ins.sql -c 32 -j 32 -T 120
query mode: prepared
number of clients: 64
number of threads: 64
duration: 360 s
number of transactions actually processed: 86074880
latency average = 0.268 ms
latency stddev = 0.295 ms
tps = 239079.558174 (including connections establishing)
tps = 239088.708200 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
0.001set uid random(1,1000)
0.267select ins_task_init(:uid,'test',now()::timestamp);
postgres=# select count(*) from task_init_1;
count
-------
88861
(1 row)
postgres=# select count(*) from task_init_2;
count
-------
88196
(1 row)
....
postgres=# select count(*) from task_init_1000;
count
-------
88468
(1 row)

2、執行任務的效能(一次批量取10000條任務)

vi run.sql
set uid random(1,1000)
select run_task(:uid,10000);
pgbench -M prepared -n -r -P 1 -f ./run.sql -c 32 -j 32 -T 120
query mode: prepared
number of clients: 32
number of threads: 32
duration: 120 s
number of transactions actually processed: 3294
latency average = 1171.228 ms
latency stddev = 361.056 ms
tps = 27.245606 (including connections establishing)
tps = 27.247560 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
0.003set uid random(1,1000)
1171.225select run_task(:uid,10000);
postgres=# select count(*) from task_init_1000;
count
-------
18468
(1 row)
postgres=# select count(*) from task_hist_1000;
count
--------
224207
(1 row)

單獨的測試資料

1、生成任務,23.9萬條/s

2、消耗任務,27.2萬條/s

生成與消耗任務同時啟動並執行測試資料

1、生成任務,16.8萬條/s

2、消耗任務,大於16.8萬條/s

沒有任何任務堆積。

小結

PostgreSQL在雲端海量任務調度系統中,發揮了重要的作用。

單個PostgreSQL實例,已經可以處理每個小時 的任務生成,以及 的任務消耗。

任務調度系統比MQ更加複雜,類似MQ的超集合,所以使用者如果有MQ的需求,實際上使用RDS PostgreSQL也是可以的。效能指標比上面的測試更好。

參考

《advisorylock 實現高並行非堵塞式 商務鎖》

《PostgreSQLschemaless 的實現(類mongodbcollection)》

《行為、審計日誌 (即時索引/即時搜尋)建模 - 最佳實踐 2》

《在PostgreSQL中實現update| delete limit》

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

《PostgreSQL10.0 preview 功能美化 - 內建磁碟分割表格》

《PostgreSQL9.5+ 高效磁碟分割表格實現 - pg_pathman》

《PostgreSQL資料rotate用法介紹 - 按時間覆寫歷史資料》

相關產品:

1.雲資料庫RDS

2.巨量資料計算服務(MaxCompute)

3.資料管理

4.雲端服務器ECS

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.