閱讀330 返回首頁    go 阿裏雲 go 技術社區[雲棲]


facebook\微博 like場景 數據庫設計與性能壓測

標簽

PostgreSQL , pipelinedb , facebook , 微博 , 流式統計


背景

高可用架構的一個PCC大賽,看了一下比賽規則,發現PostgreSQL很適合做這個場景,原樣複刻,使用PG實現以及性能表現到底如何?

比賽內容介紹如下

https://github.com/archnotes/PCC

實現類似 facebook 中的 like 功能,需要:

  • 可以對一個對象(一條feed、文章、或者url)進行 like 操作,禁止 like 兩次,第二次 like 返回錯誤碼

  • 有 isLike 接口,返回參數指定的對象有沒有被當前用戶 like 過

  • 需要看到一個對象的 like 計數

  • 可以看到一個對象的 like 用戶列表(類似 QQ 空間);

  • 上述列表加分項:Like優先顯示我的好友列表(social list)。

  • 數據量:每天新增的 like 對象數為 1 千萬,每秒 like 計數器查詢量為 30 萬次 / 秒。

數據量

  • 用戶數量級1億,好友數量級1~1萬,單個對象的Like數量1-100萬

  • 提供比賽數據集(純文本格式),需要參賽人員自行導入到自己數據庫

測試數據集格式定義

測試數據下載:https://github.com/archnotes/PCC/tree/master/data (非壓測數據)

用戶數據格式

uid為uint64,1億條

1,Tom    
2,Jerry    

用戶好友數據格式

uid, friend_id為uint64,隻存在雙向好友關係,1億個用戶*1000,好友關係通常是一個長尾分布,90% 100個以下,8%長尾分布300-1000,2% 1000-10000

1,2    

對象Like列表數據格式

oid,uid為uint64,2億個objects, 每個1-100w

101:[1,2,3,4,5]    

數據庫設計

人與人的關係為關注,或者相互關注的關係。

人與對象為喜歡或者不喜歡的關係。

pic

在設計時,分明細數據和統計數據,統計數據為了更快速的查詢關係,被關注數。

明細可以記錄在日誌,也可以記錄在數據庫中。統計數據(關係,計數,被LIKE等),以流處理的方式寫入數據庫。

pic

關係設計

結構設計

create table user_like(uid int8 primary key, like_who int8[]);   -- 關注誰?    
    
create table user_liked(uid int8 primary key, who_like int8[]);  -- 被誰關注了?本場景未涉及(如果需要的話,建立反向關係表)。    
    
create table obj(id int8 primary key, who_like int8[], like_cnt int8);   -- 對象被誰關注,總共被多少人關注,like_cnt 峰值每秒被查詢30萬次    
    
create table user_like_obj(uid int8 primary key, ids int8[]);   -- 用戶like了哪些對象,本場景未涉及(如果需要的話,建立反向關係表)。    

查詢實現

1. 人關注了哪些人,

select like_who from user_like where uid=?;    

2. 人被哪些人關注,本場景未涉及(如果需要的話,建立反向關係表)。

select who_like from user_liked where uid=?;    

3. 人LIKE了哪些對象,本場景未涉及(如果需要的話,建立反向關係表)。

select ids from user_like_obj where uid=?;    

4. 對象被哪些人LIKE,

select who_like from obj where id=?;    

5. 對象被LIKE了多少次

select like_cnt from obj where id=?;    

6. LIKE某對象的用戶中,哪些是我的好友?

CREATE FUNCTION array_intersect(anyarray, anyarray)    
  RETURNS anyarray    
  language sql    
as $FUNCTION$    
    SELECT ARRAY(    
        SELECT UNNEST($1)    
        INTERSECT    
        SELECT UNNEST($2)    
    );    
$FUNCTION$;    
    
select array_intersect(t1.who_like, t2.like_who) from     
(    
select who_like from obj where id=?    
) t1    
,    
(    
select array[like_who] as like_who from user_like where uid=?    
) t2;    

DEMO

建立流,關注的行為將寫入流,同時寫入明細(可選)。

create STREAM user_like(uid int8, like_who int8);   -- 關注誰?    
    
create STREAM obj(id int8, who_like int8);   -- 對象被誰關注?    

建立持續視圖,根據關注行為實時統計。

create CONTINUOUS VIEW cv_user_like as select uid, array_agg(like_who) as like_who from user_like group by uid;   -- 關注誰?    
create unique index idx1_cv_user_like on cv_user_like(uid);    
    
create CONTINUOUS VIEW cv_obj as select id, array_agg(who_like) as who_like, count(*) as like_cnt from obj group by id;  -- 對象被誰關注,總共被多少人關注,like_cnt 峰值每秒被查詢30萬次    
create unique index idx1_cv_obj on cv_obj(id);    

激活流計算

pipeline=# activate ;    
ACTIVATE    

關注(LIKE)操作函數,判斷是否已關注,如果已關注,返回異常,否則關注。(這個也可以寫在程序中,但是需要與數據庫交互多次,差評)

函數可以根據實際需求進行調整,比如需要返回被LIKE後的數組,查詢一下continue view即可。

create or replace function f_user_like(i_uid int8, i_like_who int8) returns void as     
$$    
declare    
begin    
  perform 1 from cv_user_like where uid=i_uid and like_who @> array[i_like_who];  -- 未關注則關注   
  if not found then    
    insert into user_like values (i_uid, i_like_who);    
  end if;  
end;    
$$ language plpgsql strict;    
    
create or replace function f_obj(i_id int8, i_who_like int8) returns void as     
$$    
declare    
begin    
  perform 1 from cv_obj where id=i_id and who_like @> array[i_who_like];    
  if not found then    
    insert into obj values (i_id, i_who_like);    
  end if;    
end;    
$$ language plpgsql strict;    

測試

1喜歡1號文章    
pipeline=# select f_obj(1,1);    
 f_obj     
-------    
     
(1 row)    
    
1喜歡1號文章    
pipeline=# select f_obj(1,1);    
 f_obj     
-------    
     
(1 row)    
    
1號文章被誰喜歡    
pipeline=# select * from cv_obj;    
 id | who_like | like_cnt     
----+----------+----------    
  1 | {1}      |        1    
(1 row)    
    
2喜歡1號文章    
pipeline=# select f_obj(1,2);    
 f_obj     
-------    
     
(1 row)    
    
1號文章被誰喜歡    
pipeline=# select * from cv_obj;    
 id | who_like | like_cnt     
----+----------+----------    
  1 | {1,2}    |        2    
(1 row)    
    
100喜歡1號文章    
pipeline=# select f_obj(1,100);    
 f_obj     
-------    
     
(1 row)    
    
1號文章被誰喜歡    
pipeline=# select * from cv_obj;    
 id | who_like  | like_cnt     
----+-----------+----------    
  1 | {1,2,100} |        3    
(1 row)    

壓測1

1. 用戶ID範圍

1-1億

2. 文章ID範圍

1-2億

3. 熱點文章ID範圍

總共2億文章,使用高斯分布進行LIKE,95%的文章ID分布在鍾鼎為中心的2.0/xx這個區間內,67%分布在1.0/xx這個區間。 橫坐標越靠近鈡的頂端的值,產生的概率越高。xx越小,鈡越尖,也就是說高頻值越少。

假設有2.7萬高頻文章,分布在95%的區間,那麼XX=14900。

原理參考

《生成泊鬆、高斯、指數、隨機分布數據 - PostgreSQL pg_bench 》

pic

4. 隨機用戶喜歡隨機文章

5. 隨機用戶喜歡熱點文章

首先根據以上要求生成基礎數據

壓測腳本,LIKE文章,100萬熱點文章,使用高斯分布產生

vi test.sql    
\setrandom uid 1 100000000    
\setrandom id 1 200000000 gaussian 14900.0  
select f_obj(:id,:uid);    

256個連接進行壓測,測試結果,每秒產生17.9萬次LIKE請求。

pgbench -M prepared -n -r -P 1 -f ./test.sql -c 256 -j 256 -T 120    
    
transaction type: Custom query  
scaling factor: 1  
query mode: prepared  
number of clients: 256  
number of threads: 256  
duration: 120 s  
number of transactions actually processed: 21500685  
latency average: 1.427 ms  
latency stddev: 1.204 ms  
tps = 179035.949606 (including connections establishing)  
tps = 179047.297058 (excluding connections establishing)  
statement latencies in milliseconds:  
        0.002314        \setrandom uid 1 100000000    
        0.002261        \setrandom id 1 200000000 gaussian 14900.0  
        1.422216        select f_obj(:id,:uid);    

階段性壓測後文章數

pipeline=# select count(*) from cv_obj;    
  count       
----------    
 27612942    
(1 row)    
    
-- 查詢鍾鼎附近的詞被LIKE的次數  
  
pipeline=# select like_cnt from cv_obj where id=100000000;
 like_cnt 
----------
    15060
(1 row)

pipeline=# select like_cnt from cv_obj where id=100000001;
 like_cnt 
----------
    14927
(1 row)

pipeline=# select like_cnt from cv_obj where id=100000002;
 like_cnt 
----------
    15156
(1 row)
  
鈡的底部邊緣被LIKE就很少  
pipeline=# select * from cv_obj where id>199999990;  
    id     |  who_like  | like_cnt   
-----------+------------+----------  
 199999991 | {89433428} |        1  
(1 row)  

符合預期,繼續壓測。(或者我們也可以選擇指數分布進行測試)

暫時沒有進行優化,CPU使用情況如下

Cpu(s): 35.2%us, 17.4%sy, 13.8%ni, 33.2%id,  0.3%wa,  0.0%hi,  0.1%si,  0.0%st    
    
主機上其他不相幹進程的開銷    
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                                                                             
16725 digoal    20   0 18.4g  11m  948 S 320.6  0.0   1:15.63 pgbench -M prepared -n -r -P 1 -f ./test.sql -c 256 -j 256 -T 120                                                                                                                  
18411 root      20   0  445m  58m 7048 R 98.8  0.0   0:03.25                                                            
18434 root      20   0  445m  58m 7040 R 87.5  0.0   0:02.71     

持續壓測like,產生2億文章的LIKE數據,然後進入測試2。

或者隨機生成2億LIKE數據,根據場景提到的LIKE次數分布。另外還需要隨機生成關係數據,根據場景提到的關注分布。

生成用戶關係數據

1. 用戶ID範圍

1-1億

2. 用戶好友分布

90% 100個以下,8%長尾分布300-1000, 2% 1000-10000

關係表    
create table user_like_agg(uid int8 primary key, like_who int8[]);    
    
產生指定元素個數範圍的數組    
create or replace function gen_uids(c1 int, c2 int) returns int8[] as    
$$     
select array(select (random()*100000000)::int8 from generate_series(1, c1+(random()*(c2-c1))::int));    
$$ language sql strict;    
    
序列    
create sequence seq cache 100;    

產生90%的用戶關係

vi gen90.sql    
insert into user_like_agg select nextval('seq'), gen_uids(1,100);    
    
pgbench -M prepared -n -r -P 1 -f ./gen90.sql -c 100 -j 100 -t 900000    

產生8%的用戶關係

vi gen8.sql    
insert into user_like_agg select nextval('seq'), gen_uids(300,1000);    
    
pgbench -M prepared -n -r -P 1 -f ./gen8.sql -c 100 -j 100 -t 80000    

產生2%的用戶關係

vi gen2.sql    
insert into user_like_agg select nextval('seq'), gen_uids(1000,10000);    
    
pgbench -M prepared -n -r -P 1 -f ./gen2.sql -c 100 -j 100 -t 20000    

最終生成1億用戶,占用123GB空間,2.7GB索引。

pipeline=#     
pipeline=# \dt+ user_like_agg     
                        List of relations    
 Schema |     Name      | Type  |  Owner   |  Size  | Description     
--------+---------------+-------+----------+--------+-------------    
 public | user_like_agg | table | postgres | 123 GB |     
(1 row)    
    
pipeline=# \di+ user_like_agg_pkey     
                                   List of relations    
 Schema |        Name        | Type  |  Owner   |     Table     |  Size   | Description     
--------+--------------------+-------+----------+---------------+---------+-------------    
 public | user_like_agg_pkey | index | postgres | user_like_agg | 2706 MB |     
(1 row)    
    
pipeline=# select count(*) from user_like_agg ;    
   count       
-----------    
 100000000    
(1 row)    

壓測2

1. 查詢文章被誰like?

2. 查詢文章被like了多少次?

3. 查詢LIKE某文章的用戶中,哪些是我的好友?

壓測腳本1, 查詢文章被誰like?查詢文章被like了多少次?

vi test1.sql    
\setrandom id 1 200000000    
select who_like,like_cnt from cv_obj where id=:id;    
    
pgbench -M prepared -n -r -P 1 -f ./test1.sql -c 128 -j 128 -T 120    

壓測腳本2, 查詢LIKE某文章的用戶中,哪些是我的好友?

vi test2.sql    
\setrandom id 1 200000000    
\setrandom uid 1 100000000    
select array_intersect(t1.who_like, t2.like_who) from (select who_like from cv_obj where id=:id) t1,(select array[like_who] as like_who from user_like_agg where uid=:uid) t2;    
    
pgbench -M prepared -n -r -P 1 -f ./test2.sql -c 128 -j 128 -T 120    

壓測結果1,基於對象ID的PK查詢,達到 104萬/s 並不意外。

transaction type: Custom query    
scaling factor: 1    
query mode: prepared    
number of clients: 128    
number of threads: 128    
duration: 120 s    
number of transactions actually processed: 125251141    
latency average: 0.122 ms    
latency stddev: 0.210 ms    
tps = 1043643.576926 (including connections establishing)    
tps = 1043716.991815 (excluding connections establishing)    
statement latencies in milliseconds:    
        0.001711        \setrandom id 1 1000000000    
        0.119755        select who_like,like_cnt from cv_obj where id=:id;    

壓測結果2,查詢LIKE某文的用戶中,哪些是我的好友?82.2萬/s。

transaction type: Custom query    
scaling factor: 1    
query mode: prepared    
number of clients: 128    
number of threads: 128    
duration: 120 s    
number of transactions actually processed: 98735109    
latency average: 0.155 ms    
latency stddev: 2.237 ms    
tps = 822678.853360 (including connections establishing)    
tps = 822803.996869 (excluding connections establishing)    
statement latencies in milliseconds:    
        0.001786        \setrandom id 1 1000000000    
        0.000748        \setrandom uid 1 100000000    
        0.151807        select array_intersect(t1.who_like, t2.like_who) from (select who_like from cv_obj where id=:id) t1,(select array[like_who] as like_who from user_like_agg where uid=:uid) t2;    

小結

微博、facebook最常用的操作:

1. 關注人或者喜歡某條消息、微博等。

這個屬於寫操作,要求寫入快,並且要求寫入(LIKE或關注)後立即反映出來。

2. 查詢好友列表

為了查詢快速,最快的方法是PK查詢,但是一個人可能關注很多人,如果是查詢多條記錄,很顯然會比較慢。

因此考慮使用數組存儲好友列表。

但是,使用數組存儲列表,又需要考慮寫入速度的問題。

所以使用流計算聚合是最好的,因為PG有流計算插件,可以在數據庫中完成流計算。

3. 查詢被關注的好友列表

反向好友關係,同樣要求查詢快速,使用正向關係一樣的方法。

4. 查詢文章(微博)被關注的次數,被關注人,被關注的人裏有哪些是我的好友。

首先被關注的次數,實際上就是個計數器。為了提高查詢速度,它必須是一個VALUE而不是查詢時使用COUNT(*)進行聚合。

查詢文章(微博)被關注的人,為了提高查詢速度,同樣考慮使用數組存儲。使用PG內置的流計算進行聚合。

被關注的人裏麵有哪些是我的好友,這個問題就很簡單了,好友關係與文章(微博)被關注人的兩個數組求交集即可。

使用PG的流計算解決了實時寫入,實時聚合的問題。

同時由於數據實時被聚合,所以幾個查詢需求就顯得非常輕鬆。

測試得到的性能指標(未優化):

1. 關注微博(文章)

17.9萬/s,預計可以優化到30萬以上。

2. 查詢文章被誰like?查詢文章被like了多少次?

104.3萬/s

3. 查詢LIKE某文章的用戶中,哪些是我的好友?

82.2萬/s

pic

機器:

(10W左右價位的X86,12*8TB SATA盤,1塊SSD作為BCACHE)

數據庫內置流計算功能,是一件不錯的事情。

參考

《facebook linkbench 測試PostgreSQL社交關係圖譜場景性能》

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

《PostgreSQL on Linux 最佳部署手冊》

《生成泊鬆、高斯、指數、隨機分布數據 - PostgreSQL pg_bench 》

最後更新:2017-05-13 08:43:41

  上一篇:go  MySQL InnoDB事務結構體代碼變量全攻略(附源碼)
  下一篇:go  如何基於Raft繞過​分布式算法一致性的那些痛?