天天看点

行为、审计日志 (实时索引/实时搜索)建模 - 最佳实践 2

postgresql , es , 搜索引擎 , 全文检索 , 日志分析 , 倒排索引 , 优化 , 分区 , 分片 , 审计日志 , 行为日志 , schemaless

在很多系统中会记录用户的行为日志,行为日志包括浏览行为、社交行为、操作行为等。

典型的应用例如:数据库的sql审计、企业内部的堡垒机(行为审计)等。

前面写了一篇最佳实践,通过postgresql来存储审计日志,同时对审计日志需要检索的字段建立全文索引。

ssd机器可以达到7万/s的写入(换算成全文索引条目,约280万/s的条目建立速度)。达到这个性能指标时,cpu,磁盘io全部吃满。

全文如下:

<a href="https://github.com/digoal/blog/blob/master/201705/20170516_01.md">《行为、审计日志 (实时索引/实时搜索)建模 - 最佳实践》</a>

除了使用全文索引,还有其他方法呢?

本文将采用另一个角度来解决审计日志的检索和高速写入问题。

审计日志带三个维度的查询条件,一个是uid,一个是时间范围,最后是词条匹配。

1. uid表示客户id,用来区分不同用户产生的行为数据。

2. ts字段,表示日志是什么时间点产生的。

3. 行为数据字段,表示用户的行为。

优化思路:

1. 将uid作为表名的一部分,每个uid一张表。

(好处:省一个字段,节约空间。同时在数据组织时不会混淆不同用户的数据,查询时消除了io放大的问题,提升了查询效率。)

(缺点:每个uid一张表,表可能很多,元数据会变大。变更结构时,可能需要涉及较多表。)

2. ts字段,采用brin块级索引,因为每个用户产生的行为数据,都是时间顺序的,所以堆存储与值顺序有非常强的线性相关性。

3. 将数据打散存放,使用元数据记录uid对应的db list,随机写入对应的dbs,查询时按ts范围查询,查询所有的dbs汇聚(应用层负责merge sort)后返回(行为字段不使用索引)。

行为、审计日志 (实时索引/实时搜索)建模 - 最佳实践 2

postgresql 10内置了merge sort的功能,所以你如果需要一个中间层来实现merge sort的话,pg也是个不错的选择。

只需要将所有的数据源配置为fdw子表即可。

例如

方案1:

gin索引 build全文索引的方式,6万tps时,基本榨干了cpu和io资源。bcache gc或轻微的io抖动,会导致比较严重的性能变化。

方案2:

通过uid+ts_prefix分区,确保一个用户的数据在一份堆存储中,减少检索时的io开销。

ts字段具备时序属性,通过brin块级索引降低索引大小。

当数据量达到一定程度时,自动触发pg10并行查询特性,提升查询性能。

由于uid数据已经分片,查询时会输入ts和文本匹配两个变量,数据分配到每个节点已经不多,使用模糊查询代替全文检索,加上pg10的多核并行,完全可以满足查询响应时延需求。

create table db_meta

(

dbid int primary key, -- 每个数据库节点一条记录,表示一个数据库分片

groupid int, -- 每个分片属于一个分组

conn_info text -- 连接信息(url)

);

create table uid_mapping

uid int primary key, -- 客户唯一标示

dbgroupid int -- 数据库分组,表示这个用户的数据随机写入这个分组的所有分片中。

行为数据保留一段时间后清除。

如果用户觉得这样设计比较麻烦,可以将所有的数据库作为一个大池,所有用户都随机写入这个大池。

这种设计就好像greenplum和hawq的设计理念。greenplum是大池思想,hawq是分而治之思想。

主表结构:

每个用户的表名为<code>bptest_$uid_$yyyymmdd</code>。

结构和索引与主表保持一致。

ts字段的存储顺序与值的顺序有非常强的线性相关性,采用块级索引。

brin索引相比btree索引节省几百倍空间,同时提升写入性能。

每个分片属于一个组,每个uid的数据随机的写入一个指定组的所有分片。

就好像greenplum和hawq的设计理念。greenplum是大池思想,hawq是分而治之思想。

当需要查询某个uid的行为数据时,并行查询所有分片的数据,按ts字段merge sort并返回。

merge sort可以放在数据库中实现,也可以在应用层实现。

如果merge sort放在数据库层实现,可以使用postgresql 10的postgres_fdw,每个uid的每个分片对应一张fdw table,挂在uid对应的父表中。

当查询父表时,按ts排序,会使用merge sort。

merge sort功能详见:

<a href="https://github.com/digoal/blog/blob/master/201703/20170313_09.md">《postgresql 10.0 preview 性能增强 - mergesort(gather merge)》</a>

行为、审计日志 (实时索引/实时搜索)建模 - 最佳实践 2

排序下推功能详见:

<a href="https://github.com/digoal/blog/blob/master/201703/20170312_20.md">《postgresql 10.0 preview sharding增强 - pushdown 增强》</a>

行为、审计日志 (实时索引/实时搜索)建模 - 最佳实践 2

如果在应用层实现,方法与之类似,并行的查询uid对应的所有分片,每个分片都是有order by返回,在应用层使用merge sort的方法返回给客户端。

由于每个uid对应若干张表<code>bptest_$uid_$yyyymmdd</code>,我们可以在数据库端设计类似mongo的schemaless写入风格:

有表时则插入,没有表时则创建后再插入。

实现方法详见

<a href="https://github.com/digoal/blog/blob/master/201705/20170511_01.md">《postgresql schemaless 的实现(类mongodb collection)》</a>

创建一个自动建表的函数,用于自动创建目标表。

创建一个插入数据的函数,使用动态sql,如果遇到表不存在的错误,则调用建表函数进行建表。

数据库端的schemaless会牺牲一部分性能,因为无法使用绑定变量。

建议业务层实现schemaless(自动拼接表名,自动建表),以提高性能。

历史数据,可以清除,直接drop分表即可(bptest_$uid_$yyyymmdd)。

如果有保留数据的需求,可以通过阿里云rds postgresql的oss_fdw接口将数据写入oss对象存储永久保存,要读取时,通过fdw读取。

云端存储与计算分离用法:

<a href="https://help.aliyun.com/document_detail/44461.html">《rds postgresql : 使用 oss_fdw 读写oss对象存储》</a>

<a href="https://help.aliyun.com/document_detail/35457.html">《hybriddb postgresql : 使用 oss_fdw 读写oss对象存储》</a>

如果有审计日志的分析需求,可以将rds postgresql数据写入oss,通过hybriddb for postgresql进行分析。

1. 环境变量配置

2. 初始化sql

初始化每个数据库实例

12个库,100个uid。

每个uid每个库写入1000万记录,每个uid总共写入1.2亿,所有uid总共写入120亿记录。

使用gen_rand_str生成指定长度的随机字符串。

测试脚本

写入性能:

1. 使用brin索引时 9.47万/s

2. 使用btree索引时 7.9万/s

3. 服务器资源开销:

1. 大部分cpu开销在产生随机串的函数中,所以实际场景,cpu的消耗会小很多。

如下

2. bcache问题

bcache垃圾回收时,对io的影响非常严重。

await已经到秒级

3. 配置了smooth checkpoint后,checkpoint已经没有问题, sync时间非常短暂。

单节点2100万记录。

查询需求:

1. 范围查询,排序输出

返回462万记录,2.5秒。

2. 范围+全文检索查询,排序输出

返回2941196万记录,8.5秒。

3. 分页数评估

如果业务允许,建议使用评估值,评估值的准确性取决于统计信息的准确性,使用<code>alter table 表名 alter column 列名 set statistics 1000</code>可以调整列的统计精准度,默认为100。

<a href="https://github.com/digoal/blog/blob/master/201605/20160506_01.md">《论count与offset使用不当的罪名 和 分页的优化》</a>

评估记录数与实际记录数对比如下,足够精确:

4. 分页查询返回

流式返回,返回10行仅需0.562毫秒。

如果要回翻,使用scroll游标

1. 数据量:

单个uid,单节点,一天2100万记录(12gb, 索引600mb)。(100个节点/分片,单个用户一天约21亿数据量)

2. 写入性能

2.1. 使用brin索引时 9.47万/s

2.2. 使用btree索引时 7.9万/s

3. 范围查询,排序输出

4. 范围+全文检索查询,排序输出

返回294万记录,8.5秒。

5. 分页数评估

精确度:+- 5% 左右

响应速度:1毫秒左右。

6. 精确分页数

与实际数据量、条件有关。1秒以上

7. 分页查询

范围+全文检索查询,排序输出: 每获取1000条记录约11毫秒。

(与命中率有关),极端情况为处理所有记录,只有最后一条记录满足条件。

使用jdbc或libpq时,一个连接可以设置多个实例,将从先到后,自动选择一个可读写的实例。(相当于客户端自动failover)。

配置示例,假设有4个数据库实例,可以配置4个数据源如下:

当任意一个实例出现问题时,每个数据源还是能获取到下一个可用的连接,不会堵塞写入。

当实例修复后,依旧使用首选实例。

使用这种方法,可以最大化的提高可用性,无需备库。

另外异常的实例活了之后,就会继续被首选,无需担心倾斜问题,因为不保留历史。时间会抹平倾斜问题。

<a href="https://github.com/digoal/blog/blob/master/201608/20160824_02.md">《postgresql 最佳实践 - 水平分库(基于plproxy)》</a>

<a href="https://github.com/digoal/blog/blob/master/201512/20151220_02.md">《阿里云apsaradb rds for postgresql 最佳实践 - 2 教你rds pg的水平分库》</a>

<a href="https://github.com/digoal/blog/blob/master/201703/20170312_11.md">《postgresql 10.0 preview sharding增强 - 支持append节点并行》</a>

<a href="https://github.com/digoal/blog/blob/master/201703/20170312_07.md">《postgresql 10.0 preview sharding增强 - postgres_fdw 多节点异步并行执行》</a>

<a href="https://github.com/digoal/blog/blob/master/201610/20161027_01.md">《postgresql 9.6 sharding based on fdw &amp; pg_pathman》</a>

<a href="https://github.com/digoal/blog/blob/master/201610/20161004_01.md">《postgresql 9.6 单元化,sharding (based on postgres_fdw) - 内核层支持前传》</a>

<a href="https://github.com/digoal/blog/blob/master/201610/20161005_01.md">《postgresql 9.6 sharding + 单元化 (based on postgres_fdw) 最佳实践 - 通用水平分库场景设计与实践》</a>

<a href="https://github.com/digoal/blog/blob/master/201604/20160414_01.md">《postgresql 物联网黑科技 - 瘦身几百倍的索引(brin index)》</a>

<a href="https://github.com/digoal/blog/blob/master/201704/20170420_01.md">《postgresql 10.0 preview 功能增强 - libpq支持多主机连接(failover,lb)让数据库ha和应用配合更紧密》</a>