閱讀280 返回首頁    go 汽車大全


PgSQL · 應用案例 · 海量用戶實時定位和圈人-團圓社會公益係統

背景

老人、兒童是最容易走丟的人群,一定要看好老人和小孩,但是萬一走丟了怎麼辦呢?

阿裏有一個公益係統,團圓,這個係統是用來幫助發布走丟人群信息的,公安通過發布的走丟人的照片,最後一次的位置信息,向社會發布。

通過公益平台的合作夥伴(例如運營商、購物軟件等)可以向最後一次走丟人士出現的位置附近的人推送尋人啟事,調動社會力量幫助尋找丟失人。

為了實現這個目的,需要收集社會人士的實時位置,現在有很多技術可以實現,例如手機基站定位、GPS定位等。

假設有10億手機用戶,用戶的位置實時變動,實時的位置信息需要更新到數據庫中。每天可能有千億次位置更新。

同時發布走失信息後,需要到數據庫中,根據走失位置圈出附近的人。

簡單粗暴設計

1、表結構設計:

create table tbl_pos(    
  id int primary key,  -- 用戶ID    
  pos point  -- 用戶實時位置    
);    

2、空間索引

create index idx_tbl_pos on tbl_pos using gist(pos);    

性能評測

實時更新10億用戶位置,使用insert on conflict語法。

vi test.sql    
    
\set id random(1,1000000000)    
insert into tbl_pos values (:id, point(random()*180,random()*90)) on conflict (id) do update set pos=excluded.pos;    

使用32個並發,實時生成用戶隨機位置.

nohup pgbench -M prepared -n -r -P 5 -f ./test.sql -c 32 -j 32 -T 120000 > ./pos.log 2>&1 &    

1、實時位置更新TPS,約18萬/s。

179799    

服務器負載,服務器還是非常空閑的,有足夠的資源提供給查詢

top - 01:52:34 up 76 days, 15:32,  2 users,  load average: 33.74, 33.56, 31.47    
Tasks: 1064 total,  34 running, 1030 sleeping,   0 stopped,   0 zombie    
%Cpu(s): 47.6 us,  5.4 sy,  0.0 ni, 46.9 id,  0.2 wa,  0.0 hi,  0.0 si,  0.0 st    
KiB Mem : 52807456+total, 32911484+free, 10949652 used, 18801006+buff/cache    
KiB Swap:        0 total,        0 free,        0 used. 42997945+avail Mem     

2、查詢性能。

在位置更新的同時,測試查詢性能。

假設走失人口最後位置出現在杭州,那麼我們需要查詢在某個平麵(例如杭州市)內的點。返回500萬個點(社會用戶),僅需28秒。

使用空間索引,返回速度杠杠的。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_pos where box(point(1,1), point(25.5,25.5)) @> pos limit 5000000;    
                                                                      QUERY PLAN                                                                           
-------------------------------------------------------------------------------------------------------------------------------------------------------    
 Limit  (cost=0.55..412954.11 rows=407872 width=20) (actual time=1.433..27536.623 rows=5000000 loops=1)    
   Output: id, pos    
   Buffers: shared hit=6183117 dirtied=31842    
   ->  Index Scan using idx_tbl_pos on public.tbl_pos  (cost=0.55..412954.11 rows=407872 width=20) (actual time=1.431..26861.352 rows=5000000 loops=1)    
         Output: id, pos    
         Index Cond: ('(25.5,25.5),(1,1)'::box @> tbl_pos.pos)    
         Buffers: shared hit=6183117 dirtied=31842    
 Planning time: 0.353 ms    
 Execution time: 27950.171 ms    
(9 rows)    

實際查詢用,可以使用遊標,流式返回。例子

postgres=# begin;    
BEGIN    
postgres=# declare cur cursor for select * from tbl_pos where box(point(1,1), point(25.5,25.5)) @> pos;    
DECLARE CURSOR    
postgres=# fetch 10 from cur;    
    id     |                 pos                     
-----------+-------------------------------------    
 680844515 | (2.08381220698357,1.25674836337566)    
 498274514 | (2.23715107887983,1.27883949782699)    
  72310072 | (2.1013452205807,1.32945269811898)    
 301147261 | (2.12246049195528,1.33455505594611)    
 186462127 | (2.13169047608972,1.24054086394608)    
 726143191 | (2.27320306934416,1.31862969137728)    
 902518425 | (2.27059512399137,1.32658164482564)    
 534516939 | (2.18118946999311,1.29441328346729)    
 329417311 | (2.27630747482181,1.2547113513574)    
 853173913 | (2.28139906190336,1.33868838194758)    
(10 rows)    
    
postgres=# \timing    
Timing is on.    
    
postgres=# fetch 10 from cur;    
    id     |                 pos                     
-----------+-------------------------------------    
 223759458 | (2.24917919375002,1.31508464924991)    
 215111891 | (2.10541740059853,1.26674327999353)    
 925178989 | (2.08201663568616,1.2974686967209)    
 954808979 | (2.10515496321023,1.32548315450549)    
 514021414 | (2.17867707833648,1.27732987515628)    
 872436892 | (2.22504794597626,1.31386948283762)    
 507169369 | (2.05484946258366,1.30171341821551)    
 317349985 | (2.25962312892079,1.30945896729827)    
 200956423 | (2.10705514065921,1.30409182514995)    
 598969258 | (1.98812280781567,1.30866004619747)    
(10 rows)    
    
Time: 0.306 ms    

通過遊標,客戶端可以邊接收,邊發短信或者向軟件推送尋人啟事。

實現流式推送,節省寶貴的尋人時間。

優化設計

單表十億空間數據,對於查詢來說,前麵已經看到了,毫無壓力。但是隨著頻繁的更新,可能到GiST索引的膨脹,膨脹後,PostgreSQL提供了並行創建索引的方法(不影響堵塞,可以在一個列創建同樣的索引),來維護索引。但是10億數據創建索引會變得很久。

為了解決這個問題,建議使用分區表。例如將ID哈希,分成64個分區,每個分區1500萬左右數據。

在PostgreSQL中,目前性能最好的分區是pg_pathman插件。或者使用schemaless的方式。下麵以schemaless為例子。其實在我曾經寫過的另外的案例中也非常常見

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

《PostgreSQL 時序最佳實踐 - 證券交易係統數據庫設計 - 阿裏雲RDS PostgreSQL最佳實踐》

定義基表

postgres=# create table tbl_pos(id int primary key, pos point);  
CREATE TABLE  
postgres=# create index idx_tbl_pos_1 on tbl_pos using gist(pos);  
CREATE INDEX  

定義自動建表函數

create or replace function create_schemaless(  
  target name,   -- 目標表名  
  src name       -- 源表名  
) returns void as $$      
declare      
begin      
  execute format('create table if not exists %I (like %I including all)', target, src);      
  execute format('alter table %I inherit %I', target, src);      
exception when others then      
  return;      
end;      
$$ language plpgsql strict;      

定義以schemaless的方式寫數據的函數

創建一個插入數據的函數,使用動態SQL,如果遇到表不存在的錯誤,則調用建表函數進行建表。

create or replace function ins_schemaless(  
  id int,   -- id  
  md int,   -- 取模數  
  pos point -- 位置  
) returns void as $$      
declare     
  target name := 'tbl_pos_'||mod(id,md) ;    
begin      
  execute format('insert into %I values (%L, %L) on conflict (id) do update set pos=point_add(%I.pos, point(random()*10-5, random()*10-5))', target, id, pos, target);     
  -- 為了模擬真實情況,因為人的移動速度有限,即使駕車,飛機(少數情況),所以用了pos=point_add(%I.pos, point(random()*10-5, random()*10-5))這種方法模擬更真實的情況  
  -- 實際場景,請改成pos=excluded.pos  
  exception       
    WHEN SQLSTATE '42P01' THEN       
    perform create_schemaless(target, 'tbl_pos');      
    execute format('insert into %I values (%L, %L) on conflict (id) do update set pos=point_add(%I.pos, point(random()*10-5, random()*10-5))', target, id, pos, target);       
    -- 為了模擬真實情況,因為人的移動速度有限,即使駕車,飛機(少數情況),所以用了pos=point_add(%I.pos, point(random()*10-5, random()*10-5))這種方法模擬更真實的情況  
    -- 實際場景,請改成pos=excluded.pos  
end;      
$$ language plpgsql strict;      

數據庫端的schemaless會犧牲一部分性能,因為無法使用綁定變量。

如果可能的話,建議業務層實現schemaless(自動拚接表名,自動建表,自動寫入),以提高性能。

測試功能

postgres=# select ins_schemaless(2,32,point(1,2));  
 ins_schemaless   
----------------  
   
(1 row)  
  
postgres=# select ins_schemaless(1,32,point(1,2));  
 ins_schemaless   
----------------  
   
(1 row)  
  
postgres=# select tableoid::regclass,* from tbl_pos;  
 tableoid  | id |  pos    
-----------+----+-------  
 tbl_pos_2 |  2 | (1,2)  
 tbl_pos_1 |  1 | (1,2)  
(2 rows)  

schemaless設計壓測

vi ~/test.sql  
\set id random(1,1000000000)  
select ins_schemaless(:id, 32, point(random()*360-180, random()*180-90));  
  
  
nohup pgbench -M prepared -n -r -P 5 -f ./test.sql -c 32 -j 32 -T 120000 > ./pos.log 2>&1 &  

性能依舊杠杠的。

125977 tps  

小結

1、通過PostgreSQL的空間數據類型、空間索引。加上insert on conflict的特性。實現了單機約18萬行/s的10億用戶的實時位置更新,同時輸出500萬個點的量級,僅需20幾秒。真正實現了團圓公益係統的時效性。

2、采用遊標,流式返回,實現了邊獲取數據,邊向社會各界發送尋人啟事的目的。

3、另一方麵,用戶位置的變更,實際上是有一定過濾性的,比如用戶從辦公室去上個洗手間,雖然位置可能發生了變化,但是非常細微,這種變化在這套係統中可以過濾(不更新),從而減少數據的更新量。

按照現有的測試數據,可以做到每天155億次的更新。假設每10條更新僅有1條是有效更新,那麼實際上可以支持1550億次的MOVE采集。

4、PostgreSQL是一個很有愛心的數據庫係統哦。

5、將來流計算引擎pipelinedb插件化後,PostgreSQL內部將整合這個流計算引擎,通過流計算引擎,理論上可以輕鬆實現40萬行/s級別的更新速度,每天支撐300多億次的實時位置更新。

6、采用流計算的方法除了提高性能,同時也降低了XID的消耗,在目前32BIT XID的情形下,可以有效的環節FREEZE帶來的負擔。如果不使用流計算,也建議合並更新,例如一個事務中更新若幹條,比如100條,那麼一天的事務數就將到了1.5億。

7、參考

https://www.postgresql.org/docs/9.6/static/gist-implementation.html

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

《PostgreSQL 時序最佳實踐 - 證券交易係統數據庫設計 - 阿裏雲RDS PostgreSQL最佳實踐》

最後更新:2017-09-21 09:03:53

  上一篇:go  MySQL · 源碼分析 · 一條insert語句的執行過程
  下一篇:go  MSSQL · 應用案例 · 日誌表設計優化與實現