PostgreSQL 在铁老大订单系统中的schemaless设计和性能压测

标签

PostgreSQL , UDF , schemaless , 自动建表 , 自动分区 , 订单查询 , 用户订单查询 , 席别订单查询


背景

数据的流动孕育数据生态,在很多大型系统中,业务系统之间的数据流动是很常见的事情。

例如铁大哥的订单数据的流动,可能带动很多围绕订单的业务需求。比如说订单查询:

1、按用户查询,返回用户30天内的所有订单。

2、按坐席和乘车日期查询,返回这个坐席的售出记录,看看有没有退票、有没有中途票可以加塞等。

以预售30天为例,

假设有20000趟车,20节车厢,200个座位,平均每个座位卖10张票(算上中途票)。 每天的订单约8000万笔。

30天约24亿订单记录数据量。

用PostgreSQL来满足这类场景,如何设计呢?性能又如何呢?我们关注三个性能指标:

1、生成订单速度

2、按用户查询,返回用户的所有订单,平均返回2.4条。(30天,24亿记录,假设有10亿用户,平均每个用户2.4笔订单)

3、按坐席和乘车日期查询,平均返回10条记录。(每个座位平均每天卖出10张票)

PS:春运数据,2016年春运20天运送旅客3.26亿人次。实际上本例的PostgreSQL设计在这个业务中已经完全满足全年的最高峰需求。

https://baike.baidu.com/item/%E6%98%A5%E8%BF%90

schemaless设计

这个场景是典型的读多写少的业务。写体现在下单、修改订单。读体现在查询。

几个需要关注的点:

1、订单数据保留30天。使用时间分区,清数据最方便。同时满足时间维度的查询性能要求。

2、高速写入,高速读取。

3、按用户查询,按坐席和乘车日期查询。按坐席和乘车日期查询,不需要特别设计,按时间分区即可。按用户查询要达到最好的性能,我们可以来一份冗余数据,按用户哈希。

采用schemaless的设计,解决自动建分区表,自动读写分区表的问题。

设计表结构

1、建表1,按get_on上车时间分区。

-- 序列,假设为订单号
create sequence seq cache 1000;  

create table train_order (
  id int8,         -- 订单号
  train_id int,    -- 车次
  box_id int2,     -- 车厢
  site_id int2,    -- 座位号
  station_1 int2,  -- 上车站
  station_2 int2,  -- 下车站
  get_on date,     -- 乘车日期
  uid int8,        -- 用户ID
  pay_stat int2 default 1,   -- 付款状态
  charge float8,             -- 金额
  weight int2 default 0,     -- 排队号
  window_id int2 default 1,  -- 购买渠道
  crt_time timestamp default now(),  -- 订单创建时间
  mod_time timestamp                 -- 订单修改时间
);  

-- 索引,提高按坐席查询的效率
create index idx_train_order_1 on train_order (train_id, box_id, site_id);

冗余一份数据,按用户哈希分区,提高按用户查询的效率。

2、建表2,按uid用户ID哈希分区。

create table train_order_u (
  id int8,         -- 订单号
  train_id int,    -- 车次
  box_id int2,     -- 车厢
  site_id int2,    -- 座位号
  station_1 int2,  -- 上车站
  station_2 int2,  -- 下车站
  get_on date,     -- 乘车日期
  uid int8,        -- 用户ID
  pay_stat int2 ,   -- 付款状态
  charge float8,    -- 金额
  weight int2 ,     -- 排队号
  window_id int2 ,  -- 购买渠道
  crt_time timestamp ,  -- 订单创建时间
  mod_time timestamp    -- 订单修改时间
) ;  

-- 索引,提高按用户查询的效率
create index idx_train_order_u on train_order_u (uid);

3、预先建分区表,按用户哈希。

do language plpgsql $$
declare
begin
  for i in 0..31
  loop
    execute 'create table train_order_u'||i||' (like train_order_u including all , check (mod(uid,32)='||i||')) inherits (train_order_u)';
  end loop;
end;
$$;

4、schemaless设计来了,创建订单函数

create or replace function create_order (
  v_id int8,         -- 订单号
  v_train_id int,    -- 车次
  v_box_id int2,     -- 车厢
  v_site_id int2,    -- 座位号
  v_station_1 int2,  -- 上车站
  v_station_2 int2,  -- 下车站
  v_get_on date,     -- 乘车日期
  v_uid int8,        -- 用户ID
  v_pay_stat int2,   -- 付款状态
  v_charge float8,   -- 金额
  v_weight int2,     -- 排队号
  v_window_id int2,  -- 购买渠道
  v_crt_time timestamp
) returns void as $$
declare
  sql1 text := '';
  sql2 text := '';
  tbl1 name := 'train_order_'||to_char(v_get_on,'yyyymmdd');  -- 自动写入分区表
  tbl2 name := 'train_order_u'||mod(v_uid,32);
begin
  -- 数据冗余,一条订单数据,同时写入用户分区表,以及日期分区表。
  sql1 := format (
            'insert into %I (id, train_id, box_id, site_id, station_1, station_2, get_on, uid, pay_stat, charge, weight, window_id, crt_time) values
            (%L, %L, %L, %L, %L, %L, %L, %L, %L, %L, %L, %L, %L)' ,
            tbl1, v_id, v_train_id, v_box_id, v_site_id, v_station_1, v_station_2, v_get_on, v_uid, v_pay_stat, v_charge, v_weight, v_window_id, v_crt_time
          );
  sql2 := format (
            'insert into %I (id, train_id, box_id, site_id, station_1, station_2, get_on, uid, pay_stat, charge, weight, window_id, crt_time) values
            (%L, %L, %L, %L, %L, %L, %L, %L, %L, %L, %L, %L, %L)' ,
            tbl2, v_id, v_train_id, v_box_id, v_site_id, v_station_1, v_station_2, v_get_on, v_uid, v_pay_stat, v_charge, v_weight, v_window_id, v_crt_time
          );  

  execute sql1;
  execute sql2;  

  exception when SQLSTATE '42P01' then
    execute format('create table %I (like train_order including all) inherits(train_order)', tbl1);  -- 自动建表
    execute sql1;
    execute sql2;
end;
$$ language plpgsql strict ;

5、schemaless设计来了,修改订单函数

略,同上写法。

6、schemaless设计来了,按用户查询,返回用户的所有订单,平均返回2.4条。

create or replace function get_user_order(v_uid int8) returns setof train_order_u as $$
declare
  tbl name := 'train_order_u'||mod(v_uid,32);
begin
  return query execute format('select * from %I where uid=%L', tbl, v_uid);
end;
$$ language plpgsql strict;
postgres=# select (get_user_order(100000000)).*;
     id     | train_id | box_id | site_id | station_1 | station_2 |   get_on   |    uid    | pay_stat |      charge      | weight | window_id |          crt_time          | mod_time
------------+----------+--------+---------+-----------+-----------+------------+-----------+----------+------------------+--------+-----------+----------------------------+----------
 1557799015 |      125 |      4 |     189 |        79 |        16 | 2017-10-21 | 100000000 |        1 | 1197.33886221517 |      1 |         1 | 2017-09-27 18:14:19.076026 |
  811630684 |      997 |     18 |     179 |        41 |        12 | 2017-10-14 | 100000000 |        1 | 453.880406067241 |      1 |         1 | 2017-09-27 15:48:09.125487 |
  597179567 |      536 |      3 |     177 |         7 |         7 | 2017-10-07 | 100000000 |        1 | 7182.26095053926 |      1 |         1 | 2017-09-27 15:15:02.786637 |
(3 rows)  

Time: 0.367 ms

7、schemaless设计来了,按坐席和乘车日期查询,平均返回10条记录。

create or replace function get_site_order(v_get_on date, v_train_id int, v_box_id int2, v_site_id int2) returns setof train_order as $$
declare
  tbl name := 'train_order_'||to_char(v_get_on,'yyyymmdd');
begin
  return query execute format('select * from %I where train_id=%L and box_id=%L and site_id=%L', tbl, v_train_id, v_box_id, v_site_id);
end;
$$ language plpgsql strict;
postgres=# select (get_site_order('2017-10-09', 1999, 3::int2, 1::int2)).* ;
     id     | train_id | box_id | site_id | station_1 | station_2 |   get_on   |    uid    | pay_stat |      charge      | weight | window_id |          crt_time          | mod_time
------------+----------+--------+---------+-----------+-----------+------------+-----------+----------+------------------+--------+-----------+----------------------------+----------
 2300300781 |     1999 |      3 |       1 |        68 |        14 | 2017-10-09 | 419271707 |        1 |  7061.5369010889 |      1 |         1 | 2017-09-27 20:54:56.710393 |
 2234878940 |     1999 |      3 |       1 |        85 |        45 | 2017-10-09 | 885259649 |        1 |  5214.0475622518 |      1 |         1 | 2017-09-27 20:41:22.906565 |
 2107427991 |     1999 |      3 |       1 |        35 |        98 | 2017-10-09 | 985856266 |        1 | 8642.02400384098 |      1 |         1 | 2017-09-27 20:12:33.85781  |
 1794488978 |     1999 |      3 |       1 |        94 |        20 | 2017-10-09 | 351055777 |        1 | 1718.21027540183 |      1 |         1 | 2017-09-27 19:06:43.643281 |
 1374974043 |     1999 |      3 |       1 |        16 |        26 | 2017-10-09 | 923638283 |        1 | 4711.63916029176 |      1 |         1 | 2017-09-27 17:33:47.147368 |
 1351805341 |     1999 |      3 |       1 |        85 |        74 | 2017-10-09 | 212355965 |        1 | 4541.93220470985 |      1 |         1 | 2017-09-27 17:28:20.974681 |
 1182159676 |     1999 |      3 |       1 |        15 |        31 | 2017-10-09 | 567906740 |        1 | 8566.17715372145 |      1 |         1 | 2017-09-27 16:54:26.478221 |
   35506838 |     1999 |      3 |       1 |         1 |        47 | 2017-10-09 | 884099213 |        1 | 983.503066563047 |      1 |         1 | 2017-09-27 13:55:21.280078 |
(8 rows)

Time: 0.391 ms

8、调度清理数据,由于数据只需要保留30天,所以自动清理。

8.1 用户哈希表数据清理(清理7天以前的数据)

delete from train_order_u where get_on < current_date - 7;
vacuum train_order_u;

8.2 时间分区订单表,清理7天前的分区。

drop table train_order_20170920;
.....

9、生成订单数据压测。

测试最极端的情况,未来30天的票全部售完。

20000趟车,20节车厢,200个座位,平均每个座位卖10张票。 8000万订单/天。

24亿数据量。

vi test.sql  

select create_order(nextval('seq'),(random()*19999)::int,(random()*19)::int2,(random()*199)::int2,(random()*99)::int2,(random()*99)::int2,current_date+(random()*30)::int,(random()*1000000000)::int8,1::int2,(random()*9999)::float8,1::int2,1::int2,now()::timestamp);  

nohup pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -t 37500000 >./log.csv 2>&1 &

性能非常平稳。

......
progress: 5778.0 s, 127956.8 tps, lat 0.500 ms stddev 0.198
progress: 5779.0 s, 128701.9 tps, lat 0.497 ms stddev 0.208
progress: 5780.0 s, 127889.8 tps, lat 0.500 ms stddev 0.195
progress: 5781.0 s, 128157.5 tps, lat 0.499 ms stddev 0.204
progress: 5782.0 s, 128529.6 tps, lat 0.498 ms stddev 0.207
progress: 5783.0 s, 128740.1 tps, lat 0.497 ms stddev 0.190
progress: 5784.0 s, 128211.0 tps, lat 0.499 ms stddev 0.191
progress: 5785.0 s, 128414.9 tps, lat 0.498 ms stddev 0.197
progress: 5786.0 s, 127032.1 tps, lat 0.504 ms stddev 0.201
progress: 5787.0 s, 128371.0 tps, lat 0.499 ms stddev 0.186
progress: 5788.0 s, 128116.8 tps, lat 0.500 ms stddev 0.202
progress: 5789.0 s, 127408.5 tps, lat 0.502 ms stddev 0.207
progress: 5790.0 s, 127691.7 tps, lat 0.501 ms stddev 0.204
progress: 5791.0 s, 128833.5 tps, lat 0.497 ms stddev 0.195
progress: 5792.0 s, 128363.8 tps, lat 0.499 ms stddev 0.204
progress: 5793.0 s, 128307.7 tps, lat 0.499 ms stddev 0.203
progress: 5794.0 s, 128599.4 tps, lat 0.498 ms stddev 0.186
......

写入总数据量48亿,占用空间 549 GB。

postgres=# \l+
                                                               List of databases
   Name    |  Owner   | Encoding  | Collate | Ctype |   Access privileges   |  Size  | Tablespace |                Description
-----------+----------+-----------+---------+-------+-----------------------+--------+------------+--------------------------------------------
 postgres  | postgres | SQL_ASCII | C       | C     |                       | 549 GB | pg_default | default administrative connection database

10、查询压测

10.1、按用户查询,返回用户的所有订单,平均返回2.4条。

vi test1.sql  

\set uid random(1,1000000000)
select * from get_user_order((:uid)::int8);  

pgbench -M prepared -n -r -P 1 -f ./test1.sql -c 64 -j 64 -T 120
transaction type: ./test1.sql
scaling factor: 1
query mode: prepared
number of clients: 64
number of threads: 64
duration: 120 s
number of transactions actually processed: 25440118
latency average = 0.302 ms
latency stddev = 4.141 ms
tps = 211975.134822 (including connections establishing)
tps = 212024.097764 (excluding connections establishing)
script statistics:
 - statement latencies in milliseconds:
         0.002  \set uid random(1,1000000000)
         0.301  select * from get_user_order((:uid)::int8);

10.2、按坐席和乘车日期查询,平均返回10条记录。

vi test2.sql  

\set train_id random(0,19999)
\set box_id random(0,19)
\set site_id random(0,199)
select * from get_site_order(current_date+(random()*30)::int, (:train_id)::int, (:box_id)::int2, (:site_id)::int2);  

pgbench -M prepared -n -r -P 1 -f ./test2.sql -c 64 -j 64 -T 120
transaction type: ./test2.sql
scaling factor: 1
query mode: prepared
number of clients: 64
number of threads: 64
duration: 120 s
number of transactions actually processed: 25650025
latency average = 0.299 ms
latency stddev = 0.047 ms
tps = 213746.298310 (including connections establishing)
tps = 213763.615290 (excluding connections establishing)
script statistics:
 - statement latencies in milliseconds:
         0.002  \set train_id random(0,19999)
         0.000  \set box_id random(0,19)
         0.000  \set site_id random(0,199)
         0.302  select * from get_site_order(current_date+(random()*30)::int, (:train_id)::int, (:box_id)::int2, (:site_id)::int2);

小结

1、生成订单速度,约 12.8万行/s。

2、按用户查询,返回用户的所有订单,平均返回2.4条。约 21.2万 tps。

3、按坐席和乘车日期查询,平均返回10条记录。约 tps。

4、表格:

订单记录数 空间占用 表占用 索引占用
48 亿 549 GB 449 GB 100 GB
CASE 返回记录数 TPS 响应速度
生成订单 - 12.8 万 0.5 毫秒
按用户查询订单 3 条 21.2 万 0.3 毫秒
按坐席和日期查询订单 10 条 21.3 万 0.3 毫秒

5、目前分区性能最好的还是pg_pathman, 10内置的分区表写入、查询方面的性能不佳。如果要快速写入24亿,建议直接写分区子表,单步写入可以达到 35万条/s 左右。

为了达到均衡的目的,本方案使用了schemaless方案,无需建表,无需维护分区。牺牲一些些性能。

6、其他schemaless设计的案例:

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

《PostgreSQL 时序最佳实践 - 证券交易系统数据库设计 - 阿里云RDS PostgreSQL最佳实践》

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

7、采用schemaless的方案,表分区可以更加自由,可以更细,因为直击目标表。按时间分区的订单表,可以再按train_id, site_id进行二级分区(例如32*8个分区,当未来30天的票全部售完的情况下,每个分区31万数据,cluster很快很快。)。将单表记录数降到更低,采用train_id+site_id索引进行cluster,使得数据密集存储,查询时可以扫描更少的数据块。

8、采用schemaless的方案,表分区可以更加自由,可以更细,因为直击目标表。按用户ID哈希分区的订单表,模数可以设更大,例如设置为2048,当未来30天的票全部售完的情况下,单表则降到120万记录。cluster很快很快。将单表记录数降到更低,采用uid索引进行cluster,使得数据密集存储,查询时可以扫描更少的数据块。

9、监测pg_stats.correlation,当相关性绝对值低于0.8时,触发cluster。可以使得数据自动维持在一个较高密集度。

时间: 2024-09-20 00:07:33

PostgreSQL 在铁老大订单系统中的schemaless设计和性能压测的相关文章

在Linux系统中使用tar命令压缩和解压文件的教程

  linux中的tar命令 tar(磁带归档)命令是linux系统中被经常用来将文件存入到一个归档文件中的命令. 其常见的文件扩展包括:.tar.gz 和 .tar.bz2, 分别表示通过了gzip或bzip算法进一步进行了压缩. 在本教程中我们会管中窥豹一下在linux桌面或服务器版本中使用tar命令来处理一些创建和解压归档文件的日常工作的例子. 使用tar命令 tar命令在大部分linux系统默认情况下都是可用的,所以你不用单独安装该软件. tar命令具有两个压缩格式,gzip和bzip,

Linux系统中使用iostat分析IO性能

对于I/O-bond类型的进程,我们经常用iostat工具查看进程IO请求下发的数量.系统处理IO请求的耗时,进而分析进程与操作系统的交互过程中IO方面是否存在瓶颈. 下面通过iostat命令使用实例,说明使用iostat查看IO请求下发情况.系统IO处理能力的方法,以及命令执行结果中各字段的含义. 1.不加选项执行iostat 我们先来看直接执行iostat的输出结果: linux # iostat Linux 2.6.16.60-0.21-smp (linux) 06/12/12 avg-c

Win8/win8.1系统中怎样快速破解rar解压密码

  方法/步骤1.首先,我们选择rar压缩文件点属性! 2.点属性-选择注释-看密码就在这,一般网站下载的软件解压密码都在这! 3.把这密码复制到解压密码框里,点确认即可! 4.还有,有时候,解压密码都在下载的网站软件下面! 二.另类方法 AdvancedRARRepair 这个软件只能修复rar文档但没有清除rar密码的功能.现在没有软件能直接清除rar的密码,有暴力破解的软件,就是你输入可能的密码组合然后软件自动对每个密码进行尝试破解,这种的只能对密码比较简单的情形进行破解.

PgSQL · 应用案例 · 流式计算与异步消息在阿里实时订单监测中的应用

背景 在很多业务系统中,为了定位问题.运营需要.分析需要或者其他需求,会在业务中设置埋点,记录用户的行为在业务系统中产生的日志,也叫FEED日志. 比如订单系统.在业务系统中环环相扣,从购物车.下单.付款.发货,收货(还有纠纷.退款等等),一笔订单通常会产生若干相关联的记录. 每个环节产生的属性可能是不一样的,有可能有新的属性产生,也有可能变更已有的属性值. 为了便于分析,通常有必要将订单在整个过程中产生的若干记录(若干属性),合并成一条记录(订单大宽表). 通常业务系统会将实时产生的订单FEE

云计算系统中对开发者的API设计问题

本文讲的是云计算系统中对开发者的API设计问题,[IT168 资讯]近年来,随着互联网应用的普及与深化,网络信息与服务趋于海量,用户体验需求不断增长,数据海量.分布异构.处理复杂.使用繁琐等问题逐渐突显,旨在解决这些问题的云计算(Cloud Computing)相关技术得到了迅猛发展.云计算概念的提出在成为新的发展机遇的同时也在云计算技术方面受到挑战.特别是云计算系统中的API设计问题受到极大挑战. 云计算是分布式处理(Distributed Computing).并行处理(Parallel C

系统稳定性保障核武器——全链路压测

为什么要做全链路压测? 对阿里巴巴而言,每年最重要的一天莫过于双11.这是因为在双11的零点,系统会遭遇史无前例的巨大洪峰流量冲击,保证双11当天系统的稳定性对高可用团队来说是巨大的挑战.在这个挑战中会有很多不确定因素,大致分为两方面: 技术架构带来的不确定性,阿里在08年开始对系统进行拆分,由原有的单一系统拆分成了分布式架构,包括CDN.网关.负载均衡.分布式页面系统等,整体的技术生态十分丰富.分布式环境任意环节出了问题都可能会对系统造成影响: 业务发展带来的不确定性,系统的可用性随着业务增长

网上商城系统中商品数量不为1,是怎么合并到一个订单中才能完成的?老师让我们做的大作业

问题描述 网上商城系统中商品数量不为1,是怎么合并到一个订单中才能完成的?老师让我们做的大作业 网上商城系统中商品数量不为1,是怎么合并到一个订单中才能完成的?老师让我们做的大作业 解决方案 做成以商品id为key的hashmap,如果添加商品,查找这个hashmap,如果有,数量+1,否则,新建一个条目,数量为1.反之类似. 解决方案二: 表的关联关系,一张订单可以有多个商品.我上学的时候也是做这个,昨天找了一下发现经验删了... 解决方案三: 根据注册 ID分组合并到一个订单

在Ubuntu系统中安装和配置PostgreSQL服务器

PostgreSQL是一个强大的关系型数据库管理系统,它按照BSD许可[1]发行.PostgreSQL包含很多高级的特性,拥有良好的性能和很好的适用性.PostgreSQL绑定了很多种编程语言,例如 C++, C++, Python, Java, PHP, http://www.aliyun.com/zixun/aggregation/13430.html">Ruby等 ,它可以操作很多东西,从简单的Web应用程序到拥有数百万条记录的庞大数据库. 安装 你可以使用命令行安装PostgreS

关于支付系统中的同步通知和异步通知电商的区别,为什么需要通知

问题描述 关于支付系统中的同步通知和异步通知电商的区别,为什么需要通知 在电商对应的支付系统中当支付完成之后需要做相关的同步通知和异步通知操作,请大神解释一下什么是同步通知和异步通知,为什么需要同步通知和异步通知. 解决方案 同步用于即时通知支付完成 异步用于防止信息漏发漏收 解决方案二: 可以这样理解, 1.用户(买家)支付完成后,电商平台需要实时的给用户一个通知,如支付已经处理等待订单确认. 2.电商平台,这块就需要考虑系统技术方面的各个环节,考虑应对复杂多变的并发用户量.业务.流量.网络环