基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

Posted PostgreSQL中文社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统相关的知识,希望对你有一定的参考价值。


背景

用户画像在市场营销的应用重建中非常常见,已经不是什么新鲜的东西,比较流行的解决方案是给用户贴标签,根据标签的组合,圈出需要的用户。

通常画像系统会用到宽表,以及分布式的系统。

宽表的作用是存储标签,例如每列代表一个标签。

但实际上这种设计不一定是最优或唯一的设计,本文将以PostgreSQL数据库为基础,给大家讲解一下更加另类的设计思路,并且看看效率如何。

业务场景模拟

假设有一个2B的实时用户推荐系统,每个APPID代表一个B

业务数据包括APPIDUSERIDsTAGs。(2B的用户ID,最终用户ID,标签)

业务没有跨APPID的数据交换操作需求,也就是说仅限于APPID内的用户推荐。

查询局限在某个选定的APPID,以及TAG组合,搜索符合条件的USERID,并将USERID推送给用户。

数据总量约10亿,单个APPID的用户数最大约1亿。

TAG总数设计容量为1万个。

查询需求: 包含,不包含,或,与。

并发几百,RT 毫秒级。

接下来我会列举4个方案,并分析每种方案的优缺点。

一、表方案分析

通常表的宽度是有限制的,以PostgreSQL为例,一条记录是无法跨PAGE的(变长字段存储到TOAST存储,以存储超过1页大小的列,页内只存储指针),这就使得表的宽度受到了限制。

例如8KB的数据块,可能能存下接近2000个列。

如果要为每个TAG设计一个列,则需要1万个列的宽表。

相信其它数据库也有类似的限制,1万个列的宽表,除非改造数据库内核,否则无法满足需求。

那么可以使用APPID+USERID作为PK,存储为多个表来实现无限个TAG的需求。 以单表1000个列为例,10个表就能满足1万个TAG的需求。

create table t_tags_1(appid int, userid int8, tag1 boolean, tag2 boolean, ...... tag1000 boolean);  

.....

create table t_tags_10(appid int, userid int8, tag9001 boolean, tag9002 boolean, ...... tag10000 boolean);  

为了提升效率,要为每个tag字段创建索引,也就是说需要1万个索引。

如果TAG的组合跨表了,还有JOIN操作。

1. 优点

没有用什么特殊的优化,几乎所有的数据库都支持。

2. 缺点

性能不一定好,特别是查询组合条件多的话,性能会下降比较明显,例如(tag1 and tag2 and (tag4 or tag5) or not tag6)

二、数方案分析

使用数组代替TAG列,要求数据库有数组类型,同时有数组的高效检索能力,这一点PostgreSQL可以很好的满足需求。

1. 数据结构

APPIDUSERIDTAG[] 数组

单个数组最大长度1GB(约支持2.6亿个TAG)

2. APPID分区,随机分片

3. query语法

3.1 包含array2指定的所有TAG

数组1包含数组2的所有元素

array1 @> array2  

支持索引检索

3.2 包含array2指定的TAG之一

数组1与数组2有重叠元素

array1 &&  array2  

支持索引检索

3.3 不包含array2指定的所有tag

数组1与数组2没有重叠元素

not array1 && array2  

不支持索引检索

4. 例子

create table t_arr(appid int, userid int8, tags int2[]) with(parallel_workers=128);

create index idx_t_array_tags on t_arr using gin (tags) with (fastupdate=on, gin_pending_list_limit= 1024000000);

create index idx_t_arr_uid on t_arr(userid);

 

819200KB约缓冲1000080K的数组记录,可以自行调整.

1.每个USERID包含10000TAG(极限)

 

insert into t_arr select 1, 2000000000*random(),(select array_agg(10000*random()) from generate_series(1,10000));

 

nohup pgbench -M prepared -n -r -f ./test.sql -P 1 -c 50 -j 50 -t 2000000 > ./arr.log 2>&1 &

5. 优点

可以存储很多TAG,几亿个足够用啦(行业内有1万个TAG的已经是非常多的啦)

支持数组的索引查询,但是not不支持索引。

6. 缺点

数据量还是有点大,一条记录1万个TAG,约80KB

1亿记录约8TB,索引还需要约8TB

不是所有的数据库都支持数组类型。

三、比特位方案1分析

使用BIT存储TAG01表示有或者没有这个TAG

1. 数据结构

APPIDUSERIDTAG  比特流

单个BIT字段最大支持1GB长度BIT流(支持85亿个TAG

每个BIT代表一个TAG

2. APPID分区,随机分片

3. query语法

3.1 包含bit2指定的所有TAG(需要包含的TAG对应的BIT设置为1,其他为0)

bitand(bit1,bit2) = bit2   

3.2 包含bit2指定的TAG之一(需要包含的TAG对应的BIT设置为1,其他为0)

bitand(bit1,bit2) > 0  

3.3 不包含bit2指定的所有tag (需要包含的TAG对应的BIT设置为1,其他为0)

bitand(bit1,bit2) = zerobit(10000)

4. 例子

 

 

 

5. 优点

可以存储很多TAG85亿个TAG足够用啦吧(行业内有1万个TAG的已经是非常多的啦)

1万个TAG,占用1万个BIT,约1.25KB

1亿记录约120GB,无索引。

6. 缺点

没有索引方法,查询是只能通过并行计算提升性能。

PostgreSQL 9.6 支持CPU并行计算,1亿用户时,可以满足20秒内返回,但是会消耗很多的CPU资源,因此查询的并行度不能做到很高。

四、比特位方案2分析

有没有又高效,又节省资源的方法呢?

答案是有的。

因为查询通常是以TAG为组合条件,取出复合条件的USERID的查询。

所以反过来设计,查询效果就会很好,以TAG为维度,USERID为比特位的设计。

我们需要维护的是每个tag下有哪些用户,所以这块的数据更新量会很大,需要考虑增量合并与读时合并的设计。

数据流如下,数据可以快速的写入

data -> 明细表 -> 增量聚合 -> appid, tagid, userid_bits

读取时,使用两部分数据进行合并,一部分是tag的计算结果,另一部分是未合并的明细表的结果,两者MERGE

当然,如果可以做到分钟内的合并延迟,业务也能够忍受分钟的延迟的话,那么查询是就没有MERGE的必要了,直接查结果,那会非常非常快。

1. query

1.1 包含这些tags的用户

userids (bitand) userids  

结果为bit位为1的用户

1.2 不包含这些tags的用户

userids (bitor) userids  

结果为bit位为0的用户

1.3 包含这些tags之一的用户

userids (bitor) userids  

结果为bit位为1的用户

2. 优点

因为数据存储的维度发生了变化,采用以查询为目标的设计,数据的查询效率非常高。

3. 缺点

由于使用了比特位表示USERID,所以必须有位置与USERID的映射关系。

需要维护用户ID字典表,需要使用增量合并的手段减少数据的更新频率。

会有一定的延迟,通常可以控制在分钟内,如果业务允许这样的延迟,则非常棒。

通常业务的USERID会周期性的失效(例如僵尸USERID,随着时间可以逐渐失效),那么需要周期性的维护用户ID字典,同时也要更新USERID比特信息。

架构如图

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

五、比特位方案2实施细节

阿里云RDS PostgreSQL 新增的bit操作函数

本文会用到几个新增的FUNCTION,这几个function很有用,同时会加入阿里云的RDS PostgreSQL中。

get_bit (varbit, int, int) returns varbit

  从指定位置开始获取NBIT位,返回varbit

  例如 get_bit('111110000011', 3, 5)   返回11000

 

set_bit_array (varbit, int, int, vardict int[]) returns varbit  

  将指定位置的BIT设置为0|1,超出原始varbit长度的部分填充0|1 (超出部分并且在array中的位置BIT依旧按提供的BIT设置)  

  例如 set_bit_array('1111000011110000', 0, 1, array[1,15])  (1,15设置为0,超出部分设置为1)返回 10110000111100001110  

 

bit_count (varbit, int, int, int) returns int

  从第n位开始,统计NBIT位中有多少个0|1,如果N超出长度,则只计算已经存在的。  

  例如 bit_count('1111000011110000', 1, 5, 4)  返回 1  (0001)

 

bit_count (varbit, int) returns int

  统计整个bit string1|0的个数。  

  例如 bit_count('1111000011110000', 1) 返回 8

 

bit_fill (int, int) returns varbit

  填充指定长度的0 1

  例如 bit_fill(0,10) 返回 '0000000000'

 

bit_rand (int, int, float) returns varbit  

  填充指定长度的随机BIT,并指定10的随机比例  

  例如 bit_rand(10, 1, 0.3) 可能返回 '0101000001'  

 

bit_posite (varbit, int, boolean) returns int[]  

  返回 1|0 的位置信息,下标从0开始计数, true时正向返回,false时反向返回    

  例如 bit_posite ('11110010011', 1, true) 返回 [0,1,2,3,6,9,10]  

       bit_posite ('11110010011', 1, false) 返回 [10,9,6,3,2,1,0]

 

bit_posite (varbit, int, int, boolean) returns int[]  

  返回 1|0 的位置信息,下标从0开始计数,true时正向返回,false时反向返回,返回N个为止  

  例如 bit_posite ('11110010011', 1, 3, true) 返回 [0,1,2]  

       bit_posite ('11110010011', 1, 3, false) 返回 [10,9,6]  

 

get_bit_2 (varbit, int, int) returns int  

  返回指定位置的bit, 下标从0开始,如果超出BIT位置,返回指定的01.  

  例如 get_bit_2('111110000011', 100, 0)   返回 0  (100已经超出长度,返回用户指定的0)  

数据库内置的BIT操作函数请参考源码

src/backend/utils/adt/varbit.c

设计

使用bit存储用户

userid int8表示,可以超过40亿。

rowid int表示,也就是说单个APPID不能允许超过20亿的用户,从0开始自增,配合BIT下标的表示。

appid int表示,不会超过40亿个。

1. 字典表, rowid决定MAP顺序,使用窗口查询返回顺序。

drop table IF EXISTS t_userid_dic;

 

create table IF NOT EXISTS t_userid_dic(appid int not null, rowid int not null, userid int8 not null, unique (appid,userid), unique (appid,rowid));

插入用户字典表的函数,可以产生无缝的连续ROWID

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

如果以上调用返回NULL,说明插入失败,可能违反了唯一约束,应用端重试即可。

压测以上函数是否能无缝插入,压测时raise notice可以去掉。

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

验证

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

插入速度,无缝需求,完全符合要求。

生成1亿测试用户,APPID=1, 用于后面的测试

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

2. 实时变更表

为了提高写入性能,数据将实时的写入这张表,后台增量的将这个表的数据合并到TAG表。

drop table IF EXISTS t_user_tags;

 

create table IF NOT EXISTS t_user_tags(id serial8 primary key, appid int, userid int8, tag int, ins boolean, dic boolean default false);  

 

create index idx_t_user_tags_id on t_user_tags(id) where dic is false;

 

-- ins = true表示新增tag, =false 表示删除tag。  

 

-- dic = true表示该记录设计的USERID已经合并到用户字典表  

 

-- 这张表的设计有优化余地,例如最好切换使用,以清理数据,比如每天一张,保存31天。  

生成1.5千万测试数据(APPID=1 , USERID 总量20亿,随机产生, 新增tagid 范围1-10000, 删除tagid 范围1-1000)

insert into t_user_tags (appid,userid,tag,ins) select 1, 2000000000*random(),10000*random(),true from generate_series(1,10000000);

insert into t_user_tags (appid,userid,tag,ins) select 1, 2000000000*random(),5000*random(),false from generate_series(1,5000000);

3. tag + userids bitmap 表,这个是最关键的表,查询量很大,从t_user_tags增量合并进这个表。

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

生成1万个TAG测试数据,每个TAG包含1亿个用户的BIT。方便下面的测试

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

测试TAG组合查询性

这个指标显示了用户勾选一些TAG组合后,圈定并返回用户群体的性能。

测试方法很简单: 包含所有,不包含,包含任意。

1. 包含以下TAG的用户ID

userids (bitand) userids  

结果为bit位为1的用户

测试SQL如下

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

性能数据

with tmp as ( select max(bit_length(userids)) maxlen from t_tags where tag in (226833, 226830, 226836, 226834) )

select appid,userid from t_userid_dic, tmp

where appid = 1  

and rowid = any  

(

  (

    select bit_posite(res, 1, 10000, true) from

    (

      select t1.userids & t2.userids & t3.userids & t4.userids as res

       from

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226833 ) t1 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226830 ) t2 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226836 ) t3 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226834 ) t4

    ) t

  )::int[]

)

;

2. 不包含以下TAG的用户

userids (bitor) userids  

结果为bit位为0的用户

测试SQL如下

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

性能数据

with tmp as ( select max(bit_length(userids)) maxlen from t_tags where tag in (226833, 226830, 226836, 226834) )

select appid,userid from t_userid_dic, tmp

where appid = 1  

and rowid = any  

(

  (

    select bit_posite(res, 0, 10000, true) from

    (

      select t1.userids | t2.userids | t3.userids | t4.userids as res

       from

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226833 ) t1 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226830 ) t2 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226836 ) t3 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226834 ) t4

    ) t

  )::int[]

)

;

3. 包含以下任意TAG

userids (bitor) userids  

结果为bit位为1的用户

测试SQL如下

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

性能数据

with tmp as ( select max(bit_length(userids)) maxlen from t_tags where tag in (226833, 226830, 226836, 226834) )

select appid,userid from t_userid_dic, tmp

where appid = 1  

and rowid = any  

(

  (

    select bit_posite(res, 1, 10000, true) from

    (

      select t1.userids | t2.userids | t3.userids | t4.userids as res

       from

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226833 ) t1 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226830 ) t2 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226836 ) t3 ,

          ( select set_bit_array(userids, 0, 0, array[tmp.maxlen-1]) userids from t_tags,tmp where tag = 226834 ) t4

    ) t

  )::int[]

)

;

功能

1. 结合bit_posite,可以实现正向取若干用户,反向取若干用户(例如有100万个结果,本次推广只要1万个用户,而且要最近新增的1万个用户,则反向取1万个用户即可)

2. 结合get_bit则可以实现截取某一段BIT,再取得结果,很好用哦。

测试新增数据的性

新增数据即往t_user_tags表插入数据的性能。

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

测试如

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

性能数据(单步操作的QPS12.2万,包括新增,删除TAG)

更新的动作需要拆成两个部分,新增和删除,不要合并到一条记录中。

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

测试数据的合并性

数据的合并包括3个部分,

1. 更新用户字典表t_userid_dic

2. 批量获取并删除t_user_tags的记录,

3. 合并标签数据到t_tags

以上三个动作应该在一个事务中完成。

考虑到t_tagsuserids字段1亿BIT12.5MB,更新单条记录耗时会比较长,所以建议采用并行的模式,每个TAG都可以并行。

更新字典封装函数如下

t_user_tags表取出数据并更新数据字典,被取出的数据标记为允许合并。

此操作没有必要并行,串行即可,搞个后台进程无限循环。

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

-- 插入本次需要处理的临时数据到数组

  -- f_uniq失败也不会报错,这里需要修改一下f_uniq仅对UK冲突不处理,其他错误还是需要处理的,否则t_user_tags改了,但是USER可能没有进入字典。

with tmp as (update t_user_tags t set dic=true where id>=min_id and id<=min_id+v_rows returning *)

  select count(*) into f_uniq_res from (select f_uniq(appid,userid) from (select appid,userid from tmp group by 1,2) t) t;

end;

$$ language plpgsql;

由于批量操作,可能申请大量的ad lock, 所以需要增加max_locks_per_transaction, 数据库参数调整

max_locks_per_transaction=40960

验证

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统


虽然没有必要并行,但是个函数需要保其并行的安全性,所以接下来验证并行安全性

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

验证并行结果的安全性,结果可

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

数据合并封装函数如下

前面已经处理了字典的更新,接下来就可以将t_user_tags.dic=true的数据,合并到t_tags中。

考虑更新T_TAGS可能较慢,尽量提高并行度,不同TAG并行。

-- 不要对同一个APPID并行使用APPIDAPPID+tag的模式.

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

速度测试

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

验证方法,merge的结果数据与被merge的数据一致即可。

符合要求

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

查询单个用户有哪些TAG

找到userid对应的rowid, 根据userids rowid位置的bit,判断是否有该tag

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

查询单个用户有哪些TAG是一个比重的操作,如果碰到有很多TAG并且用数非常,建使用并行。

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统

基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统


分片

APPID,按USER号段切分

APPID+号段分片

如果一个APPID 1万个TAG1亿用户,只占用120GB

通常是出现倾斜时才需要重分布。

PostgreSQL使用postgres_fdw可以原生支持数据分片。

参考之前写的文档



同一个APPID的一批TAG必须在一个节点

扩展阅读

机器学习,生成TAG,本文不涉及。

参考 MADlibR ,聚类分析 PostgreSQL都可以非常好的支持。

 

 

Postgres大象会2016官方名通道:

http://www.huodongxing.com/event/8352217821400

扫码报名



以上是关于基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统的主要内容,如果未能解决你的问题,请参考以下文章

阿里云RDS PostgreSQL时序数据的优化

小白用户如何远程连接阿里云服务器RDS?

阿里云上的RDS数据的本地同步

云数据库RDS MySQL 版

阿里云云中沙箱自助实验-从Gitlab数据库被删看数据备份的重要性!

基于阿里云RDS创建ECS自建从库