|
| 1 | +## 高并发队列处理业务的数据库性能优化 - IO扫描|CPU计算浪费 , 锁冲突 , 垃圾索引扫描浪费 |
| 2 | + |
| 3 | +### 作者 |
| 4 | +digoal |
| 5 | + |
| 6 | +### 日期 |
| 7 | +2023-08-05 |
| 8 | + |
| 9 | +### 标签 |
| 10 | +PostgreSQL , PolarDB , 队列 , 锁 , hash mod , advisory lock , cte , update limit , delete limit , vacuum , index , IO浪费 , CPU浪费 |
| 11 | + |
| 12 | +---- |
| 13 | + |
| 14 | +## 背景 |
| 15 | +在电商业务中可能涉及这样的场景, 由于有上下游关系的存在, 1、用户下单后, 上下游厂商会在自己系统中生成一笔订单记录并反馈给对方, 2、在收到反馈订单后, 本地会先缓存反馈的订单记录队列, 3、然后后台再从缓存取出订单并进行处理. |
| 16 | + |
| 17 | +这个过程的核心流程: 高速写入队列、从队列按先后顺序提取并高速处理、从队列清除已处理订单记录. |
| 18 | + |
| 19 | +如果是高并发的处理, 因为大家都按一个顺序获取, 容易产生热点, 可能遇到取出队列遇到锁冲突瓶颈、IO扫描浪费、CPU计算浪费的瓶颈. 以及在清除已处理订单后, 索引版本未及时清理导致的回表版本判断带来的IO浪费和CPU运算浪费瓶颈等. |
| 20 | +- 文末的《打车与宇宙大爆炸的关系》一文有相似问题和优化方法, 思路类似. |
| 21 | + |
| 22 | +本文将给出“队列处理业务的数据库性能优化”优化方法和demo演示. 性能提升10到20倍. |
| 23 | + |
| 24 | +想体验一下的同学, 也可以通过云起实验启动环境来进行体验, 这个实验室是永久免费的. |
| 25 | +- https://developer.aliyun.com/adc/scenario/exp/f55dbfac77c0467a9d3cd95ff6697a31 |
| 26 | +- 参考: |
| 27 | + - https://github.com/digoal/blog/blob/master/202307/20230710_03.md |
| 28 | + |
| 29 | +## DEMO |
| 30 | +1、测试环境 |
| 31 | +``` |
| 32 | +MacBook Pro (15-inch, 2018) |
| 33 | +2.2 GHz 六核Intel Core i7 |
| 34 | +32 GB 2400 MHz DDR4 |
| 35 | + |
| 36 | +PostgreSQL 15.1 |
| 37 | +``` |
| 38 | + |
| 39 | +因为是macos, 可能需要设置一下ulimit. |
| 40 | +``` |
| 41 | +ulimit -n 1000000 |
| 42 | +``` |
| 43 | + |
| 44 | +2、上游写入订单处理队列表 |
| 45 | + |
| 46 | +``` |
| 47 | +create table t_order_q ( |
| 48 | + id serial8 primary key, -- 自增主键 |
| 49 | + order_id uuid unique, -- 上游传递过来的订单号 |
| 50 | + cts timestamp not null -- 上游传递过来的订单创建时间 |
| 51 | +); |
| 52 | + |
| 53 | +-- create index on t_order_q (cts); -- 如果按订单时间先后取出处理, 则需要创建时间字段索引. 也可以按自增主键顺序处理, 则不需要时间索引. |
| 54 | +``` |
| 55 | + |
| 56 | +3、取出并处理后的订单状态表 |
| 57 | + |
| 58 | +``` |
| 59 | +create table t_order_u ( |
| 60 | + id serial8 primary key, -- 自增主键 |
| 61 | + order_id uuid unique, -- 上游传递过来的订单号 |
| 62 | + cts timestamp not null, -- 上游传递过来的订单创建时间 |
| 63 | + uts timestamp not null, -- 订单处理时间 |
| 64 | + status int not null -- 订单处理状态标记 |
| 65 | +); |
| 66 | +``` |
| 67 | + |
| 68 | +4、写入1000万条订单队列 |
| 69 | + |
| 70 | +``` |
| 71 | +insert into t_order_q (order_id, cts) select gen_random_uuid(), clock_timestamp() from generate_series(1,1000000); |
| 72 | +``` |
| 73 | + |
| 74 | +5、写pgbench压测脚本, 从队列取出, 并且使用ad lock对队列ID加事务锁, 判断是否正在处理, 事务结束自动释放ad lock. ad lock也经常被用于秒杀场景泄压. |
| 75 | + |
| 76 | +``` |
| 77 | +vi t.sql |
| 78 | + |
| 79 | +with tmp as |
| 80 | + (delete from t_order_q where ctid = (select ctid from t_order_q where pg_try_advisory_xact_lock(id) order by id limit 1) returning order_id, cts) |
| 81 | +insert into t_order_u (order_id,cts,uts,status) select tmp.order_id, tmp.cts, now(), 1 from tmp; |
| 82 | +``` |
| 83 | + |
| 84 | +6、压测256个并发消耗队列, 平均每个连接处理39062个事务. |
| 85 | + |
| 86 | +``` |
| 87 | +select 1000000/256.0; |
| 88 | +3906.2500000000000 |
| 89 | +``` |
| 90 | + |
| 91 | +7、压测结果 |
| 92 | + |
| 93 | +``` |
| 94 | +pgbench -M prepared -f ./t.sql -n -r -P 1 -c 256 -j 2 -t 3906 |
| 95 | +``` |
| 96 | + |
| 97 | +``` |
| 98 | +transaction type: ./t.sql |
| 99 | +scaling factor: 1 |
| 100 | +query mode: prepared |
| 101 | +number of clients: 256 |
| 102 | +number of threads: 2 |
| 103 | +maximum number of tries: 1 |
| 104 | +number of transactions per client: 3906 |
| 105 | +number of transactions actually processed: 999936/999936 |
| 106 | +number of failed transactions: 0 (0.000%) |
| 107 | +latency average = 8.111 ms |
| 108 | +latency stddev = 5.376 ms |
| 109 | +initial connection time = 429.698 ms |
| 110 | +tps = 25379.081141 (without initial connection time) |
| 111 | +statement latencies in milliseconds and failures: |
| 112 | + 8.114 0 with tmp as |
| 113 | +``` |
| 114 | + |
| 115 | +### 未优化前的性能如何? |
| 116 | + |
| 117 | +1、写pgbench压测脚本, 从队列取出, 并且使用ad lock对队列ID加事务锁, 判断是否正在处理, 事务结束自动释放ad lock. ad lock也经常被用于秒杀场景泄压. |
| 118 | + |
| 119 | +``` |
| 120 | +vi t1.sql |
| 121 | + |
| 122 | +begin; |
| 123 | +select id as vid from t_order_q order by id for update limit 1 \gset |
| 124 | +with tmp as |
| 125 | + (delete from t_order_q where id = :vid returning order_id, cts) |
| 126 | +insert into t_order_u (order_id,cts,uts,status) select tmp.order_id, tmp.cts, now(), 1 from tmp; |
| 127 | +end; |
| 128 | +``` |
| 129 | + |
| 130 | +2、压测结果 |
| 131 | + |
| 132 | +``` |
| 133 | +pgbench -M prepared -f ./t1.sql -n -r -P 1 -c 256 -j 2 -t 3906 |
| 134 | +``` |
| 135 | + |
| 136 | +``` |
| 137 | +TPS 约 1200. |
| 138 | +``` |
| 139 | + |
| 140 | +增加了skip locked后, TPS也只能到2500左右. |
| 141 | + |
| 142 | +### 还有什么可以提升性能的点? |
| 143 | +1、减少浪费的IO和cpu计算: |
| 144 | +- 在并发的情况下, order by id limit 1需要扫描若干行, 而不是1行, 因为可能有些ID已经被ad lock touch了, 浪费的pg_try_advisory_xact_lock() cpu ops计算次数约等于 n + n-1 + n-2 + ... + n-n, 浪费的IO约等于N. |
| 145 | + |
| 146 | +优化方法: |
| 147 | +- 固定N个链接, 按ID hash mod 取不同的数据分片, 从而减少浪费的IO和cpu计算. |
| 148 | +- 或者将队列表拆分成几个分区表, 入库的时候 按id hash mode, 每个分区分配给不同的进程取数, 从而减少冲突和浪费的扫描提高并发. |
| 149 | + |
| 150 | +2、提高index vacuum的频率, 减少因没有index version导致的垃圾数据判断带来的cpu和回表的IO浪费. 提升autovacuum_work_mem, 容纳下所有dead tuple ctid避免多次扫描index. |
| 151 | + |
| 152 | +优化方法: |
| 153 | +- 配置参数即可. |
| 154 | + |
| 155 | +3、一次取出多条, 批量处理. |
| 156 | + |
| 157 | +更多请参考末尾文章. |
| 158 | + |
| 159 | + |
| 160 | +## 参考 |
| 161 | + |
| 162 | +[《DB吐槽大会,第69期 - PG 不支持update | delete limit语法》](../202110/20211002_03.md) |
| 163 | + |
| 164 | +[《在PostgreSQL中实现update | delete limit - CTID扫描实践 (高效阅后即焚)》](../201608/20160827_01.md) |
| 165 | + |
| 166 | +[《PostgreSQL skip locked与CTE妙用 - 解决并发批量更新锁冲突带来的锁等待,提高处理吞吐》](../201803/20180314_03.md) |
| 167 | + |
| 168 | +[《PostgreSQL SELECT 的高级用法(CTE, LATERAL, ORDINALITY, WINDOW, SKIP LOCKED, DISTINCT, GROUPING SETS, ...) - 珍藏级》](../201802/20180226_05.md) |
| 169 | + |
| 170 | +[《PostgreSQL 秒杀4种方法 - 增加 批量流式加减库存 方法》](../201801/20180105_03.md) |
| 171 | + |
| 172 | +[《HTAP数据库 PostgreSQL 场景与性能测试之 30 - (OLTP) 秒杀 - 高并发单点更新》](../201711/20171107_31.md) |
| 173 | + |
| 174 | +[《聊一聊双十一背后的技术 - 不一样的秒杀技术, 裸秒》](../201611/20161117_01.md) |
| 175 | + |
| 176 | +[《PostgreSQL 垃圾回收参数优化之 - maintenance_work_mem , autovacuum_work_mem》](../201902/20190226_01.md) |
| 177 | + |
| 178 | +[《[直播]为什么打车和宇宙大爆炸有关?》](../202009/20200926_02.md) |
| 179 | + |
| 180 | + |
| 181 | +#### [期望 PostgreSQL|开源PolarDB 增加什么功能?](https://github.com/digoal/blog/issues/76 "269ac3d1c492e938c0191101c7238216") |
| 182 | + |
| 183 | + |
| 184 | +#### [PolarDB 云原生分布式开源数据库](https://github.com/ApsaraDB "57258f76c37864c6e6d23383d05714ea") |
| 185 | + |
| 186 | + |
| 187 | +#### [PolarDB 学习图谱: 训练营、培训认证、在线互动实验、解决方案、内核开发公开课、生态合作、写心得拿奖品](https://www.aliyun.com/database/openpolardb/activity "8642f60e04ed0c814bf9cb9677976bd4") |
| 188 | + |
| 189 | + |
| 190 | +#### [PostgreSQL 解决方案集合](../201706/20170601_02.md "40cff096e9ed7122c512b35d8561d9c8") |
| 191 | + |
| 192 | + |
| 193 | +#### [德哥 / digoal's github - 公益是一辈子的事.](https://github.com/digoal/blog/blob/master/README.md "22709685feb7cab07d30f30387f0a9ae") |
| 194 | + |
| 195 | + |
| 196 | + |
| 197 | + |
0 commit comments