- 浏览: 144532 次
- 性别:
- 来自: 北京
文章分类
最新评论
行为、审计日志_(实时索引/实时搜索)建模_-_最佳实践_2
http://click.aliyun.com/m/21904/
摘要: 标签 PostgreSQL , ES , 搜索引擎 , 全文检索 , 日志分析 , 倒排索引 , 优化 , 分区 , 分片 , 审计日志 , 行为日志 , schemaless 背景 在很多系统中会记录用户的行为日志,行为日志包括浏览行为、社交行为、操作行为等。
标签
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和应用配合更紧密》
本文为云栖社区原创内容,未经允许不得转载,如需转载请发送邮件至yqeditor@list.alibaba-inc.com
http://click.aliyun.com/m/21904/
摘要: 标签 PostgreSQL , ES , 搜索引擎 , 全文检索 , 日志分析 , 倒排索引 , 优化 , 分区 , 分片 , 审计日志 , 行为日志 , schemaless 背景 在很多系统中会记录用户的行为日志,行为日志包括浏览行为、社交行为、操作行为等。
标签
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和应用配合更紧密》
本文为云栖社区原创内容,未经允许不得转载,如需转载请发送邮件至yqeditor@list.alibaba-inc.com
http://click.aliyun.com/m/21904/
相关推荐
- **审计跟踪**:记录用户的访问行为,以便于安全审计。 - **数据加密**:对敏感数据进行加密处理。 #### 1.10 OLAP(在线分析处理) - **多维数据分析**:通过立方体模型来进行多维分析。 - **预聚合技术**:预先...
6. **最佳实践** - 使用Compass进行数据建模,以确保良好的数据库设计。 - 利用查询分析功能优化查询性能,合理设置索引。 - 定期检查和调整内存分配和磁盘使用,以保持数据库健康运行。 总之,MongoDB Compass...
- **审计日志与监控**:学习如何启用审计日志记录功能,监控系统访问情况,以便及时发现潜在的安全威胁。 #### 5. 实战案例分析 - **日志数据分析**:介绍如何利用Elasticsearch进行日志数据的收集、存储、分析及...
《Wrox-Oracle专家高级编程-Expert_one-on-one_Oracle》这本书是针对Oracle数据库系统的一本专业指南,旨在帮助IT专业人士深入理解Oracle的高级特性和最佳实践。书中的内容覆盖了Oracle系统的多个重要方面,包括...
2. **数据模型**:数据模型是数据库设计的基础,包括层次模型、网状模型、关系模型和对象模型等。其中,关系模型是最常用的一种,基于二维表格结构,易于理解和操作。 3. **关系数据库**:描述了由多个表组成的...
- 索引原理和最佳实践,包括单列、多列和覆盖索引。 - 性能监视工具,如mysqldumpslow和SHOW STATUS命令。 6. **备份与恢复** - 使用mysqldump进行全量和增量备份。 - 数据恢复策略,包括如何从二进制日志恢复...
- **审计日志**:记录重要的操作事件用于审查。 - **安全检查清单**: - **网络隔离**:确保只有授权的 IP 地址可以访问数据库。 - **数据加密**:保护静态数据的安全性。 #### 七、聚合框架 - **概念**: - *...
MongoDB 提供审计日志功能,可以记录数据库的关键操作,便于后续的审查和分析。 **6.6 安全加固** 通过实施一系列最佳实践和配置更改,可以进一步加强数据库的安全性。 **6.7 实现字段级隐藏** 允许根据用户的...
本资料涵盖了Oracle数据库的基础知识、高级特性以及最佳实践,旨在帮助学习者成为Oracle数据库的熟练管理员或开发者。 1. **Oracle数据库基础** - 数据库概念:了解数据库的基本定义,关系型数据库模型,以及...
《SQLServer数据库教程(第4版)PPT》是一份全面深入讲解SQL Server数据库管理系统的教学资料,旨在帮助学习者掌握SQL Server的核心概念、功能和最佳实践。此压缩包包含的PPT文件提供了丰富的视觉展示和详细讲解,是...
- 提供安全策略,如VPC隔离、访问控制和审计日志。 9. **MySQL的挑战与解决方案** - 大表处理:分区、分片、归档策略。 - 性能监控:使用Percona Toolkit、pt-query-digest等工具。 - 数据库设计:遵循范式理论...
- 优化JOIN操作:掌握内连接、外连接和自连接的最佳实践,减少全表扫描。 2. **存储与分区策略**: - 表分区:了解范围、列表、哈希和复合分区,以及如何根据业务需求选择合适的分区方式。 - 表空间管理:学习...
- 审计与日志:了解MySQL的日志系统,包括错误日志、慢查询日志和审计日志。 - 性能监控:使用工具(如MySQL Enterprise Monitor或Percona Toolkit)进行性能监控和调优。 - 扩展性:探讨水平和垂直扩展策略,如...
- 审计与报告:跟踪数据库活动,生成审计报告。 6. **自动化任务**: - 脚本管理:创建、调度和执行数据库维护脚本。 - 任务自动化:设置定时任务,如备份、数据清理等。 7. **跨平台兼容性**: - Toad for ...
"第9章 索引.ppt"深入讨论了索引的概念,包括其作用、类型(如B树索引、全文索引和空间索引)以及如何创建、优化和维护索引以提高查询性能。 "第8章 视图.ppt"讲解了视图的概念,它是数据库中的虚拟表,可以用来...
数据安全则涵盖访问控制、加密、审计日志等方面,保护数据不被未经授权的访问和滥用。 7. BI工具与前端展现:现代BI工具如Tableau、Power BI和Looker提供了丰富的可视化功能,使非技术人员也能轻松分析数据。这些...
- 讲解了安全概念,例如认证、授权、加密和审计日志。 - 提供了安全教程和参考,帮助管理员和开发者实现安全机制。 6. 聚合(Aggregation) - 聚合框架是MongoDB中一个强大的数据处理工具,用于对数据进行复杂的...
这款防火墙系统具备多种关键功能,包括屏蔽直接访问数据库的通道、多因子认证、自动建模、攻击检测、访问控制以及审计,为企业提供全方位的数据安全保障。 首先,VS-FW能够有效地屏蔽直接访问数据库的通道,部署在...
5. **报告与审计**:内置的报告功能可生成详细的数据库健康检查报告和审计日志,帮助DBA了解数据库状态并满足合规要求。 6. **自动化运维**:通过脚本化和自动化功能,Quest Central可以自动化重复性的管理任务,...
- 日志管理:理解事务日志,以及如何利用日志进行故障恢复。 7. **性能优化** - 查询优化:识别慢查询,使用索引、查询改写和统计信息优化性能。 - 系统调优:资源监控,调整内存分配、锁机制和并发控制。 - 表...