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

本文涉及的产品
RDS SQL Server Serverless,2-4RCU 50GB 3个月
推荐场景:
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS SQL Server,基础系列 2核4GB
简介:

标签

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


背景

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

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

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

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

全文如下:

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

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

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

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

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

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

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

优化思路:

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

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

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

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

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

pic

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

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

例如

postgres=# explain select * from bptest where ts between now()-interval '20 min' and now()-interval '10 min' order by ts;  
                                                     QUERY PLAN                                                        
---------------------------------------------------------------------------------------------------------------------  
 Merge Append  (cost=34.00..634325.20 rows=4808182 width=524)  
   Sort Key: bptest.ts  
   ->  Index Scan using idx_bptest_ts on bptest  (cost=0.14..3.15 rows=1 width=40)  
         Index Cond: ((ts >= (now() - '00:20:00'::interval)) AND (ts <= (now() - '00:10:00'::interval)))  
   ->  Index Scan using bptest_32_20170522_ts_idx on bptest_32_20170522  (cost=0.30..4802.19 rows=49918 width=524)  
         Index Cond: ((ts >= (now() - '00:20:00'::interval)) AND (ts <= (now() - '00:10:00'::interval)))  
   ->  Index Scan using bptest_64_20170522_ts_idx on bptest_64_20170522  (cost=0.30..4114.22 rows=42820 width=524)  
         Index Cond: ((ts >= (now() - '00:20:00'::interval)) AND (ts <= (now() - '00:10:00'::interval)))  
   ->  Index Scan using bptest_34_20170522_ts_idx on bptest_34_20170522  (cost=0.30..3984.00 rows=41459 width=524)  
         Index Cond: ((ts >= (now() - '00:20:00'::interval)) AND (ts <= (now() - '00:10:00'::interval)))  
   ->  Index Scan using bptest_37_20170522_ts_idx on bptest_37_20170522  (cost=0.30..4898.77 rows=50972 width=524)  
         Index Cond: ((ts >= (now() - '00:20:00'::interval)) AND (ts <= (now() - '00:10:00'::interval)))  
   ->  Index Scan using bptest_31_20170522_ts_idx on bptest_31_20170522  (cost=0.30..4682.19 rows=48668 width=524)  
   .....................  

设计

方案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是分而治之思想。

行为数据表结构

主表结构:

create table bptest  
(  
  ts timestamptz,   -- 行为发生时间  
  content text    -- 行为内容  
);        

每个用户的表名为bptest_$uid_$yyyymmdd

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

行为数据索引

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

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

create index idx_bptest_ts on bptest using brin(ts);  -- TS字段块级索引  

数据分片设计

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

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

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

数据汇聚设计

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

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

数据merge sort设计(建议业务层实现,以提升性能)

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

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

merge sort功能详见:

《PostgreSQL 10.0 preview 性能增强 - mergesort(Gather merge)》

pic

排序下推功能详见:

《PostgreSQL 10.0 preview sharding增强 - pushdown 增强》

pic

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

数据写入schema less设计(建议业务层实现,以提高性能)

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

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

实现方法详见

《PostgreSQL schemaless 的实现(类mongodb collection)》

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

create or replace function create_schemaless(target name) returns void as $$    
declare    
begin    
  execute format('create table if not exists %I (like bptest including all)', target);    
exception when others then    
  return;    
end;    
$$ language plpgsql strict;    

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

create or replace function ins_schemaless(uid int, ts timestamptz, content text) returns void as $$    
declare   
  target name := 'bptest_'||uid||'_'||to_char(ts,'yyyymmdd');  
begin    
  execute format('insert into %I values (%L, %L)', target, ts, content);    
  exception     
    WHEN SQLSTATE '42P01' THEN     
    perform create_schemaless(target);    
    execute format('insert into %I values (%L, %L)', target, ts, content);     
end;    
$$ language plpgsql strict;    

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

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

历史数据处理设计

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

如果有保留数据的需求,可以通过阿里云RDS PostgreSQL的OSS_FDW接口将数据写入OSS对象存储永久保存,要读取时,通过FDW读取。

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

《RDS PostgreSQL : 使用 oss_fdw 读写OSS对象存储》

《HybridDB PostgreSQL : 使用 oss_fdw 读写OSS对象存储》

分析需求

如果有审计日志的分析需求,可以将RDS PostgreSQL数据写入OSS,通过HybridDB for PostgreSQL进行分析。

性能测试

环境准备

与上文 《行为、审计日志 (实时索引/实时搜索)建模 - 最佳实践》 一样,使用同样的服务器,依旧是12个实例。

1. 环境变量配置

vi env_pg10.sh   
export PS1="$USER@`/bin/hostname -s`-> "  
export PGPORT=$(($1+1920))  
export PGDATA=/disk$1/digoal/pgdata/pg_root$(($1+1920))  
export LANG=en_US.utf8  
export PGHOME=/home/digoal/pgsql10_8k  
export LD_LIBRARY_PATH=$PGHOME/lib:/lib64:/usr/lib64:/usr/local/lib64:/lib:/usr/lib:/usr/local/lib:$LD_LIBRARY_PATH  
export LD_RUN_PATH=$LD_LIBRARY_PATH  
export DATE=`date +"%Y%m%d%H%M"`  
export PATH=$PGHOME/bin:$PATH:.  
export MANPATH=$PGHOME/share/man:$MANPATH  
export PGHOST=127.0.0.1  
export PGUSER=postgres  
export PGDATABASE=postgres  
alias rm='rm -i'  
alias ll='ls -lh'  
unalias vi  

2. 初始化SQL

vi init.sql  
  
-- 清除环境  
drop table bptest cascade;  
  
-- 初始父表  
create table bptest  
(  
  ts timestamptz,   -- 行为发生时间  
  content text    -- 行为内容  
);   
  
-- 建立索引  
-- create index idx_bptest_ts on bptest using brin(ts) with(pages_per_range=1);  -- TS字段块级索引,如果不需要按ts排序输出结果,建议使用brin索引  
create index idx_bptest_ts on bptest using btree(ts) ;  -- 如果需要按ts sort输出结果,建议使用btree  
  
-- schemaless相关,自动建表函数  
create or replace function create_schemaless(target name) returns void as $$    
declare    
begin    
  execute format('create table if not exists %I (like bptest including all) inherits(bptest)', target);    
  -- 这里可以设置列的statistic  
exception when others then    
  return;    
end;    
$$ language plpgsql strict;    
  
-- 数据写入函数  
create or replace function ins_schemaless(uid int, ts timestamptz, content text) returns void as $$    
declare   
  target name := 'bptest_'||uid||'_'||to_char(ts,'yyyymmdd');  
begin    
  execute format('insert into %I values (%L, %L)', target, ts, content);    
  exception     
    WHEN SQLSTATE '42P01' THEN     
    perform create_schemaless(target);    
    execute format('insert into %I values (%L, %L)', target, ts, content);     
end;    
$$ language plpgsql strict;    
  
-- 产生随机字符串的函数, 本例的CPU消耗大量来自这个函数  
CREATE OR REPLACE FUNCTION public.gen_rand_str(integer)  
 RETURNS text  
 LANGUAGE sql  
 STRICT  
AS $function$  
  select string_agg(a[(random()*6)::int+1],'') from generate_series(1,$1), (select array['a','b','c','d','e','f',' ']) t(a);  
$function$;  
  
-- 分页评估函数, 用于评估返回多少条记录,尽量避免使用count(*)估算精确值  
CREATE FUNCTION count_estimate(query text) RETURNS INTEGER AS    
$func$    
DECLARE    
    rec   record;    
    ROWS  INTEGER;    
BEGIN    
    FOR rec IN EXECUTE 'EXPLAIN ' || query LOOP    
        ROWS := SUBSTRING(rec."QUERY PLAN" FROM ' rows=([[:digit:]]+)');    
        EXIT WHEN ROWS IS NOT NULL;    
    END LOOP;    
    RETURN ROWS;    
END    
$func$ LANGUAGE plpgsql;    

初始化每个数据库实例

psql -f ./init.sql -p 1921  
...  
psql -f ./init.sql -p 1932  

写入压测

12个库,100个UID。

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

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

vi test.sql  
  
  
\set uid random(1,100)  
select ins_schemaless(:uid, now(), gen_rand_str(512));  

测试脚本

vi test.sh  
  
  
for ((i=1;i<13;i++))  
do  
  . /home/digoal/env_pg10.sh ${i}  
  nohup pgbench -M prepared -n -r -P 3 -f ./test.sql -c 10 -j 10 -t 100000000 >/tmp/bptest_${i}.log 2>&1 &  
done  

测试结果

写入性能:

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

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

3. 服务器资源开销:

CPU:  
  
Cpu(s): 90.7%us,  7.1%sy,  0.1%ni,  0.4%id,  0.1%wa,  0.0%hi,  1.6%si,  0.0%st  
  
IOUITL:  
  
SSD: 50 %    

瓶颈分析

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

如下

perf top -ag  
  
            60953.00  6.0% ExecInterpExpr                          /home/digoal/pgsql10_8k/bin/postgres  
            33647.00  3.3% AllocSetAlloc                           /home/digoal/pgsql10_8k/bin/postgres  
            27560.00  2.7% advance_aggregates                      /home/digoal/pgsql10_8k/bin/postgres  
            22894.00  2.3% base_yyparse                            /home/digoal/pgsql10_8k/bin/postgres  
            21976.00  2.2% SearchCatCache                          /home/digoal/pgsql10_8k/bin/postgres  
            21768.00  2.2% array_seek                              /home/digoal/pgsql10_8k/bin/postgres  
            20957.00  2.1% appendBinaryStringInfo                  /home/digoal/pgsql10_8k/bin/postgres  
            19912.00  2.0% memcpy                                  /lib64/libc-2.12.so                     
            17058.00  1.7% array_get_element                       /home/digoal/pgsql10_8k/bin/postgres  
            14916.00  1.5% heap_form_minimal_tuple                 /home/digoal/pgsql10_8k/bin/postgres  
            13617.00  1.3% heap_fill_tuple                         /home/digoal/pgsql10_8k/bin/postgres  
            12201.00  1.2% __rint                                  /lib64/libm-2.12.so                     
            10938.00  1.1% palloc0                                 /home/digoal/pgsql10_8k/bin/postgres  
            10683.00  1.1% MemoryContextAllocZeroAligned           /home/digoal/pgsql10_8k/bin/postgres  
            10591.00  1.0% ExecStoreMinimalTuple                   /home/digoal/pgsql10_8k/bin/postgres  
            10292.00  1.0% string_agg_transfn                      /home/digoal/pgsql10_8k/bin/postgres  
            10202.00  1.0% advance_transition_function             /home/digoal/pgsql10_8k/bin/postgres  
             9973.00  1.0% core_yylex                              /home/digoal/pgsql10_8k/bin/postgres  

2. bcache问题

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

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND    
40207 root      20   0     0    0    0 R 100.0  0.0  34:47.86 [bch_gc-0b34a79a]  

await已经到秒级

Device:         rrqm/s   wrqm/s     r/s     w/s   rsec/s   wsec/s avgrq-sz avgqu-sz   await  svctm  %util  
bcache11          0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00   0.00   0.00  
bcache10          0.00  6054.00    1.00  105.00     8.00 48640.00   458.94    17.19  157.73   9.43 100.00  
bcache9           0.00  2431.00    0.00  100.00     0.00 29408.00   294.08     4.22  102.87   9.86  98.60  
bcache8           0.00  4013.00    0.00   85.00     0.00 37352.00   439.44    10.04  174.19  11.76 100.00  
bcache7           0.00  1661.00    0.00   58.00     0.00 12952.00   223.31     2.34   53.47  17.02  98.70  
bcache6           0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00   0.00   0.00  
bcache5           0.00    48.00    0.00   71.00     0.00 44896.00   632.34   183.24 2124.06  14.08 100.00  
bcache4           0.00  6506.00    0.00  211.00     0.00 56312.00   266.88    74.89  488.20   4.74 100.00  
bcache3           0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00   0.00   0.00  
bcache2           0.00  5502.00    0.00  103.00     0.00 44168.00   428.82   144.85  709.10   9.71 100.00  
bcache1           0.00 12302.00    0.00   34.00     0.00 13464.00   396.00    84.11  877.03  29.41 100.00  
bcache0           0.00 15148.00    0.00  260.00     0.00 42504.00   163.48    47.39  155.56   3.85 100.00  

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

2017-05-23 10:49:55.749 CST,,,25095,,59239d76.6207,9,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint starting: time",,,,,,,,"LogCheckpointStart, xlog.c:8369",""  
2017-05-23 10:50:25.434 CST,,,25095,,59239d76.6207,10,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint complete: wrote 233036 buffers (5.6%); 0 WAL file(s) added, 0 removed, 131 recycled; write=29.250 s, sync=0.018 s, total=29.685 s; sync files=300, longest=0.004 s, average=0.000 s; distance=2145650 kB, estimate=2145650 kB",,,,,,,,"LogCheckpointEnd, xlog.c:8451",""  
2017-05-23 10:54:55.529 CST,,,25095,,59239d76.6207,11,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint starting: time",,,,,,,,"LogCheckpointStart, xlog.c:8369",""  
2017-05-23 10:57:59.222 CST,,,25095,,59239d76.6207,12,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint complete: wrote 191434 buffers (4.6%); 0 WAL file(s) added, 0 removed, 131 recycled; write=118.012 s, sync=59.816 s, total=183.693 s; sync files=300, longest=16.126 s, average=0.199 s; distance=1752021 kB, estimate=2106288 kB",,,,,,,,"LogCheckpointEnd, xlog.c:8451",""  
2017-05-23 10:59:55.328 CST,,,25095,,59239d76.6207,13,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint starting: time",,,,,,,,"LogCheckpointStart, xlog.c:8369",""  
2017-05-23 11:00:25.350 CST,,,25095,,59239d76.6207,14,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint complete: wrote 176278 buffers (4.2%); 0 WAL file(s) added, 0 removed, 107 recycled; write=29.688 s, sync=0.009 s, total=30.021 s; sync files=300, longest=0.003 s, average=0.000 s; distance=1630757 kB, estimate=2058734 kB",,,,,,,,"LogCheckpointEnd, xlog.c:8451",""  
2017-05-23 11:04:55.467 CST,,,25095,,59239d76.6207,15,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint starting: time",,,,,,,,"LogCheckpointStart, xlog.c:8369",""  
2017-05-23 11:05:25.381 CST,,,25095,,59239d76.6207,16,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint complete: wrote 232981 buffers (5.6%); 0 WAL file(s) added, 0 removed, 99 recycled; write=29.555 s, sync=0.013 s, total=29.914 s; sync files=300, longest=0.006 s, average=0.000 s; distance=2142180 kB, estimate=2142180 kB",,,,,,,,"LogCheckpointEnd, xlog.c:8451",""  
2017-05-23 11:06:44.503 CST,,,36368,"127.0.0.1:44645",5923a744.8e10,1,"",2017-05-23 11:06:44 CST,,0,LOG,00000,"connection received: host=127.0.0.1 port=44645",,,,,,,,"BackendInitialize, postmaster.c:4178",""  
2017-05-23 11:06:44.504 CST,"postgres","postgres",36368,"127.0.0.1:44645",5923a744.8e10,2,"authentication",2017-05-23 11:06:44 CST,13/223,0,LOG,00000,"connection authorized: user=postgres database=postgres",,,,,,,,"PerformAuthentication, postinit.c:272",""  
2017-05-23 11:09:55.512 CST,,,25095,,59239d76.6207,17,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint starting: time",,,,,,,,"LogCheckpointStart, xlog.c:8369",""  
2017-05-23 11:09:56.221 CST,"postgres","postgres",36368,"127.0.0.1:44645",5923a744.8e10,3,"idle",2017-05-23 11:06:44 CST,,0,LOG,00000,"disconnection: session time: 0:03:11.717 user=postgres database=postgres host=127.0.0.1 port=44645",,,,,,,,"log_disconnections, postgres.c:4500","psql"  
2017-05-23 11:10:25.512 CST,,,25095,,59239d76.6207,18,,2017-05-23 10:24:54 CST,,0,LOG,00000,"checkpoint complete: wrote 232090 buffers (5.5%); 0 WAL file(s) added, 0 removed, 131 recycled; write=29.489 s, sync=0.141 s, total=29.999 s; sync files=300, longest=0.033 s, average=0.000 s; distance=2136269 kB, estimate=2141589 kB",,,,,,,,"LogCheckpointEnd, xlog.c:8451",""  

查询压测

单节点2100万记录。

postgres=# select count(*) from bptest_1_20170522;  
  count     
----------  
 21106948  
(1 row)  
Time: 1776.889 ms (00:01.777)  

查询需求:

1. 范围查询,排序输出

返回462万记录,2.5秒。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from bptest_1_20170522 where ts between '2017-05-22 19:04:19.05347+08' and '2017-05-22 20:04:19.05347+08' order by ts;  
                                                                                        QUERY PLAN                                                                                           
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Index Scan using bptest_1_20170522_ts_idx on public.bptest_1_20170522  (cost=0.44..2373942.95 rows=4631011 width=524) (actual time=0.015..2326.653 rows=4622534 loops=1)  
   Output: ts, content  
   Index Cond: ((bptest_1_20170522.ts >= '2017-05-22 19:04:19.05347+08'::timestamp with time zone) AND (bptest_1_20170522.ts <= '2017-05-22 20:04:19.05347+08'::timestamp with time zone))  
   Buffers: shared hit=4639344  
 Planning time: 0.207 ms  
 Execution time: 2578.147 ms  
(6 rows)  
Time: 2578.789 ms (00:02.579)  

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

返回2941196万记录,8.5秒。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from bptest_1_20170522    
where ts between '2017-05-22 19:04:19.05347+08' and '2017-05-22 20:04:19.05347+08'   
and content ~ 'abc' and content ~ 'bcd'   
order by ts;  
                                                     QUERY PLAN                                                                                                    
--------------------------------------------------------------------------------------------------------------------------  
 Gather Merge  (cost=1890834.11..1933136.32 rows=2870936 width=524) (actual time=6601.842..8136.187 rows=2941196 loops=1)  
   Output: ts, content  
   Workers Planned: 4  
   Workers Launched: 4  
   Buffers: shared hit=84046, temp read=102440 written=102588  
   ->  Sort  (cost=1890834.06..1892628.39 rows=717734 width=524) (actual time=6584.684..6804.063 rows=588239 loops=5)  
         Output: ts, content  
         Sort Key: bptest_1_20170522.ts  
         Sort Method: external merge  Disk: 313080kB  
         Buffers: shared hit=347169, temp read=501609 written=502338  
         Worker 0: actual time=6582.649..6803.139 rows=588224 loops=1  
           Buffers: shared hit=66037, temp read=100242 written=100388  
         Worker 1: actual time=6590.768..6813.019 rows=587934 loops=1  
           Buffers: shared hit=66168, temp read=100191 written=100337  
         Worker 2: actual time=6579.297..6799.509 rows=587915 loops=1  
           Buffers: shared hit=66014, temp read=100172 written=100318  
         Worker 3: actual time=6569.191..6785.155 rows=578030 loops=1  
           Buffers: shared hit=64904, temp read=98564 written=98707  
         ->  Parallel Bitmap Heap Scan on public.bptest_1_20170522  (cost=72481.78..1603389.84 rows=717734 width=524) (actual time=338.604..5182.340 rows=588239 loops=5)  
               Output: ts, content  
               Recheck Cond: ((bptest_1_20170522.ts >= '2017-05-22 19:04:19.05347+08'::timestamp with time zone) AND (bptest_1_20170522.ts <= '2017-05-22 20:04:19.05347+08'::timestamp with time zone))  
               Rows Removed by Index Recheck: 19  
               Filter: ((bptest_1_20170522.content ~ 'abc'::text) AND (bptest_1_20170522.content ~ 'bcd'::text))  
               Rows Removed by Filter: 336268  
               Heap Blocks: exact=7063 lossy=60173  
               Buffers: shared hit=347141  
               Worker 0: actual time=336.885..5215.415 rows=588224 loops=1  
                 Buffers: shared hit=66030  
               Worker 1: actual time=337.105..5239.414 rows=587934 loops=1  
                 Buffers: shared hit=66161  
               Worker 2: actual time=337.128..5213.433 rows=587915 loops=1  
                 Buffers: shared hit=66007  
               Worker 3: actual time=337.078..5129.862 rows=578030 loops=1  
                 Buffers: shared hit=64897  
               ->  Bitmap Index Scan on bptest_1_20170522_ts_idx  (cost=0.00..71764.05 rows=4631011 width=0) (actual time=336.215..336.215 rows=4622534 loops=1)  
                     Index Cond: ((bptest_1_20170522.ts >= '2017-05-22 19:04:19.05347+08'::timestamp with time zone) AND (bptest_1_20170522.ts <= '2017-05-22 20:04:19.05347+08'::timestamp with time zone))  
                     Buffers: shared hit=16810  
 Planning time: 1.996 ms  
 Execution time: 8560.577 ms  
(39 rows)  
Time: 8563.154 ms (00:08.563)  

3. 分页数评估

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

《论count与offset使用不当的罪名 和 分页的优化》

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

postgres=# \timing  
Timing is on.  
  
-- 评估记录数  
postgres=# select count_estimate($$select * from bptest_1_20170522 where ts between '2017-05-22 19:04:19.05347+08' and '2017-05-22 20:04:19.05347+08'$$);  
 count_estimate   
----------------  
        4631011  
(1 row)  
Time: 0.733 ms  
  
-- 实际记录数  
postgres=# select count(*) from bptest_1_20170522 where ts between '2017-05-22 19:04:19.05347+08' and '2017-05-22 20:04:19.05347+08';  
  count    
---------  
 4622534  
(1 row)  
Time: 1389.424 ms (00:01.389)  
  
-- 评估记录数  
postgres=# select count_estimate($$select * from bptest_1_20170522    
where ts between '2017-05-22 19:04:19.05347+08' and '2017-05-22 20:04:19.05347+08'   
and content ~ 'abc' and content ~ 'bcd' and content ~ 'cdef'$$);  
 count_estimate   
----------------  
         914755  
(1 row)  
Time: 3.713 ms  
  
-- 实际记录数  
postgres=# select count(*) from bptest_1_20170522    
where ts between '2017-05-22 19:04:19.05347+08' and '2017-05-22 20:04:19.05347+08'   
and content ~ 'abc' and content ~ 'bcd' and content ~ 'cdef';  
 count    
--------  
 962780  
(1 row)  
Time: 7756.863 ms (00:07.757)  

4. 分页查询返回

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

postgres=# begin;  
BEGIN  
Time: 0.170 ms  
postgres=# declare cur cursor for select * from bptest_1_20170522    
postgres-# where ts between '2017-05-22 19:04:19.05347+08' and '2017-05-22 20:04:19.05347+08'   
postgres-# and content ~ 'abc' and content ~ 'bcd'   
postgres-# order by ts;  
DECLARE CURSOR  
  
postgres=# fetch 10 from cur;  
 .........................  
(10 rows)  
Time: 0.562 ms  
  
下一页,fetch继续获取  

如果要回翻,使用SCROLL游标

postgres=# begin;  
BEGIN  
Time: 0.114 ms  
postgres=# declare cur SCROLL cursor for select * from bptest_1_20170522    
where ts between '2017-05-22 19:04:19.05347+08' and '2017-05-22 20:04:19.05347+08'   
and content ~ 'abc' and content ~ 'bcd'   
order by ts;  
DECLARE CURSOR  
Time: 2.362 ms  
  
回翻  
postgres=# fetch BACKWARD 10 from cur;  
前翻  
postgres=# fetch FORWARD 10 from cur;  

小结

性能指标

1. 数据量:

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

2. 写入性能

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

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

3. 范围查询,排序输出

返回462万记录,2.5秒。

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

返回294万记录,8.5秒。

5. 分页数评估

精确度:+- 5% 左右

响应速度:1毫秒左右。

6. 精确分页数

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

7. 分页查询

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

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

随机分布与可用性、数据倾斜问题

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

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

db1: host1:port1,host2:port2,host3:port3,host4:port4  
  
db2: host2,port2,host3:port3,host4:port4,host1:port1  
  
db3: host3:port3,host4:port4,host1:port1,host2,port2  
  
db4: host4:port4,host1:port1,host2:port2,host3:port3  

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

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

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

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

参考

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

《PostgreSQL schemaless 的实现(类mongodb collection)》

《PostgreSQL 最佳实践 - 水平分库(基于plproxy)》

《阿里云ApsaraDB RDS for PostgreSQL 最佳实践 - 2 教你RDS PG的水平分库》

《PostgreSQL 10.0 preview 性能增强 - mergesort(Gather merge)》

《PostgreSQL 10.0 preview sharding增强 - 支持Append节点并行》

《PostgreSQL 10.0 preview sharding增强 - pushdown 增强》

《PostgreSQL 10.0 preview sharding增强 - postgres_fdw 多节点异步并行执行》

《PostgreSQL 9.6 sharding based on FDW & pg_pathman》

《PostgreSQL 9.6 单元化,sharding (based on postgres_fdw) - 内核层支持前传》

《PostgreSQL 9.6 sharding + 单元化 (based on postgres_fdw) 最佳实践 - 通用水平分库场景设计与实践》

《PostgreSQL 物联网黑科技 - 瘦身几百倍的索引(BRIN index)》

《论count与offset使用不当的罪名 和 分页的优化》

《RDS PostgreSQL : 使用 oss_fdw 读写OSS对象存储》

《HybridDB PostgreSQL : 使用 oss_fdw 读写OSS对象存储》

《PostgreSQL 10.0 preview 功能增强 - libpq支持多主机连接(failover,LB)让数据库HA和应用配合更紧密》

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
3月前
|
存储 搜索推荐 大数据
阿里泛日志设计与实践问题之schema-on-read技术的发展对日志搜索的影响是啥,如何解决
阿里泛日志设计与实践问题之schema-on-read技术的发展对日志搜索的影响是啥,如何解决
|
12天前
|
XML JSON 监控
告别简陋:Java日志系统的最佳实践
【10月更文挑战第19天】 在Java开发中,`System.out.println()` 是最基本的输出方法,但它在实际项目中往往被认为是不专业和不足够的。本文将探讨为什么在现代Java应用中应该避免使用 `System.out.println()`,并介绍几种更先进的日志解决方案。
35 1
|
2月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
43 2
|
2月前
|
开发者 Python
基于Python的日志管理与最佳实践
日志是开发和调试过程中的重要工具,然而,如何高效地管理和利用日志常常被忽略。本文通过Python中的logging模块,探讨如何使用日志来进行调试、分析与问题排查,并提出了一些实际应用中的优化建议和最佳实践。
|
3月前
|
JSON Java fastjson
Java日志通关(五) - 最佳实践
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第五篇。
|
3月前
|
SQL 数据库 Java
Hibernate 日志记录竟藏着这些秘密?快来一探究竟,解锁调试与监控最佳实践
【8月更文挑战第31天】在软件开发中,日志记录对调试和监控至关重要。使用持久化框架 Hibernate 时,合理配置日志可帮助理解其内部机制并优化性能。首先,需选择合适的日志框架,如 Log4j 或 Logback,并配置日志级别;理解 Hibernate 的多级日志,如 DEBUG 和 ERROR,以适应不同开发阶段需求;利用 Hibernate 统计功能监测数据库交互情况;记录自定义日志以跟踪业务逻辑;定期审查和清理日志避免占用过多磁盘空间。综上,有效日志记录能显著提升 Hibernate 应用的性能和稳定性。
48 0
|
3月前
|
消息中间件 Prometheus 监控
Producer的监控与日志记录最佳实践
【8月更文第29天】在分布式系统中,消息队列作为关键组件之一,其稳定性和性能至关重要。生产者(Producer)负责生成并发送消息到消息队列中,因此确保生产者的健康运行是非常重要的。本文将探讨如何为生产者设置监控和日志记录,以跟踪其健康状况和性能指标。
58 0
|
4月前
|
存储 分布式计算 监控
日志数据投递到MaxCompute最佳实践
日志服务采集到日志后,有时需要将日志投递至MaxCompute的表中进行存储与分析。本文主要向用户介绍将数据投递到MaxCompute完整流程,方便用户快速实现数据投递至MaxCompute。
187 2
|
3月前
|
监控 安全 Linux
在Linux中,如何查看和审计系统日志文件以检测异常活动?
在Linux中,如何查看和审计系统日志文件以检测异常活动?
|
3月前
|
存储 运维 Kubernetes
在k8S中,日志索引的作用是什么?
在k8S中,日志索引的作用是什么?