|
| 1 | +## PostgreSQL 备库apply延迟原理分析与诊断 |
| 2 | + |
| 3 | +### 作者 |
| 4 | +digoal |
| 5 | + |
| 6 | +### 日期 |
| 7 | +2016-03-01 |
| 8 | + |
| 9 | +### 标签 |
| 10 | +PostgreSQL , 物理流复制 , IO不对称 |
| 11 | + |
| 12 | +---- |
| 13 | + |
| 14 | +## 背景 |
| 15 | +开车的同学都喜欢一马平川,最好是车道很多,车很少,开起来爽。 |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +大家想象一下,同样的车速,6车道每秒可以通过6辆车,而1车道每秒就只能通过1辆车。 |
| 20 | + |
| 21 | +好了,我们回到IO层面,我们在使用fio测试块设备的IO能力时,可以选择多少个线程进行压测,实际可以理解为开多少车道的意思。 |
| 22 | + |
| 23 | +只要没到通道或者设备本身的极限,当然开的车道(并发)越多,测出来的IO数据越好看。比如单线程可以做到每秒处理1万次请求,而开8个并发,可能处理能达到8万次请求。 |
| 24 | + |
| 25 | +这个可以理解之后,我们来看看PostgreSQL的物理复制,为了保证数据一致性,备库在APPLY时,目前只有一个startup进程,对于partial block从REDO中读取出块的变化,并从数据文件读出对应的完整块,在shared buffer中完成合并,最后bg writer会将shared buffer的dirty page write(异步写)到数据文件,对于FPW,则直接写入SHARED BUFFER,后期bg write会负责处理dirty page。 |
| 26 | + |
| 27 | +虽然PostgreSQL备库已经使用shared buffer减少了写操作(比如单个数据块的多次变更,只要对应的dirty page没有从shared buffer evict出去,就不需要多次读IO;写IO也可以降低(比如OS层IO合并,或者bgwrite调度机制也可以降低写IO)),但是这些技术在主库也存在,除非备库设置的shared buffer更大,那么备库的写IO也许能降低。 |
| 28 | + |
| 29 | +另一方面,备库在恢复非FPW块时,需要从数据文件读取数据块,进行合并,这个动作实际上会产生离散读,在bgwrite将数据块写出shared buffer时产生离散写。 |
| 30 | + |
| 31 | +小结一下,主库是多进程离散读写转换为单进程顺序写,而备库单进程顺序读转换为单进程离散读写。 |
| 32 | + |
| 33 | +主节点 |
| 34 | + |
| 35 | +下面指非分组提交、采用同步提交、开启FSYNC时的流程。 |
| 36 | + |
| 37 | +1\. 数据库后台进程wal writer,负责将wal buffer的REDO数据,批量写入(fsync) wal文件。 |
| 38 | + |
| 39 | +2\. 数据库后台进程bg writer,负责将shared buffer的dirty page数据(根据page lsn判断,该页WAL已fsync),写入(write) datafile。OS调度,将PAGE CACHE持久化写入数据文件(datafile)。 |
| 40 | + |
| 41 | +3\. 用户进程(S),将需要用到的数据页,读入shared buffer,当shared buffer不够用时,会evict一些page,和bg writer操作类似。 |
| 42 | + |
| 43 | +4\. 用户进程(S),非提交事务时,将产生的变更写入wal buffer,提交事务时,会触发XLogFlush(src/backend/access/transam/xact.c),将wal buffer写入(fsync)wal文件。 |
| 44 | + |
| 45 | + |
| 46 | + |
| 47 | +备节点 |
| 48 | + |
| 49 | +1\. wal receiver进程,负责将收到的wal写入wal buffer。 |
| 50 | + |
| 51 | +2\. wal writer进程,负责将wal buffer写入(fsync)wal文件。 |
| 52 | + |
| 53 | +3\. startup进程,从WAL文件读取日志,同时从数据文件读取对应数据块,合并(apply redo)后,写入shared buffer。 |
| 54 | + |
| 55 | +4\. bgwriter进程,将shared buffer的dirty page数据(根据page lsn判断,该页WAL已fsync),写入(write) datafile。OS调度,将PAGE CACHE持久化写入数据文件(datafile)。 |
| 56 | + |
| 57 | + |
| 58 | + |
| 59 | +对比主节点和备节点的操作,可以观察到一些不对等的地方。 |
| 60 | + |
| 61 | +1\. 写(fsync)WAL文件时,主节点有用户进程、wal writer并发的情况出现。而备节点只有wal writer单一进程。 |
| 62 | + |
| 63 | +2\. 写(write)数据文件时,主节点有用户进程、bg writer并发的情况出现。而备节点只有bg writer单一进程。 |
| 64 | + |
| 65 | +3\. 写shared buffer时,主节点有用户进程并发读写。而备节点只有startup单一进程。 |
| 66 | + |
| 67 | +由于以上不对称的情况(主库多数操作是多车道,备库多数操作是单车道),当主库产生的XLOG量非常庞大,或者包含一些非常耗时的操作(例如(大量离散IO,大量系统调用()))时,备库可能会出现延时。 |
| 68 | + |
| 69 | +## 哪些情况可能导致备库apply延迟 |
| 70 | +通常来说备库接收日志不会有延迟,只要网络带宽比主库产生REDO的速度快。 |
| 71 | + |
| 72 | +延迟通常发生在apply阶段。前面分析了主库多数操作是多车道,备库多数操作是单车道,成为备库apply延迟的主要原因。 |
| 73 | + |
| 74 | +1\. 恢复时,需要消耗大量CPU时,例如开启了数据文件checksum时,会额外消耗startup进程的cpu。 |
| 75 | + |
| 76 | +2\. 主库频繁的离散IO操作,SEEK等。例如大量的索引变更,例如大量的索引VACUUM,例如大量的VACUUM操作。 |
| 77 | + |
| 78 | +3\. 频繁或者大量的系统调用,例如大批量删除对象,如drop schema。 |
| 79 | + |
| 80 | +[《PostgreSQL DaaS设计注意 - schema与database的抉择》](../201610/20161012_01.md) |
| 81 | + |
| 82 | +4\. 冲突,例如备库开放用户查询,某些查询操作和replay操作冲突时,可能短暂的影响恢复。 |
| 83 | + |
| 84 | +## 如何避免apply延迟 |
| 85 | +1\. checksum,除非你要防物理篡改,否则通常不需要开启checksum。checksum只是帮助你了解块是否损坏,并不能起到修复作用。(redo的checksum是默认强制打开的,但是数据文件的checksum可选) |
| 86 | + |
| 87 | +2\. 删除没有必要使用的索引。 |
| 88 | + |
| 89 | +3\. 垃圾回收的调度,根据业务进行调整,默认是20%,越低越频繁,越频繁,垃圾越少。但是越频繁可能导致产生的VACUUM DIRTY PAGE会增加。可以选择一个较为折中的值,例如5%。 |
| 90 | + |
| 91 | +4\. 检查点拉长,可以减少FULL PAGE的量。FULL PAGE是指每次检查点后,第一次被更改的页,需要将这个页写入WAL日志,当数据库CRASH后,可以保证数据的完整性。但是由于FULL PAGE的引入,日志量会增加。 |
| 92 | + |
| 93 | +拉长检查点的间距,可以减少FULL PAGE。对于COW文件系统例如(zfs, btrfs),不需要开启FULL PAGE WRITE。 |
| 94 | + |
| 95 | +5\. 加大备库shared buffer,可以减少write datafile。 |
| 96 | + |
| 97 | +6\. 关闭IO时间的跟踪,可以提高IO操作效率。 |
| 98 | + |
| 99 | +7\. 备库使用IOPS能力更强、IO延迟更低的机器(例如NVME的SSD),从而抹平不对称的情况,注意,不建议使用RAID 5机器。 |
| 100 | + |
| 101 | +8\. 增加单个进程可打开的文件数,可以减少文件开启和关闭,特别是数据库的文件数很多时,可以有效的减少系统调用的时间耗费。 |
| 102 | + |
| 103 | +配置例子 |
| 104 | + |
| 105 | +``` |
| 106 | +checkpoint_segments=1024 |
| 107 | +track_io_timing=off |
| 108 | +wal_buffers=512MB |
| 109 | +synchronous_commit=off |
| 110 | +wal_writer_delay = 10ms |
| 111 | +max_files_per_process = 65536 |
| 112 | +autovacuum_vacuum_scale_factor = 0.05 |
| 113 | +``` |
| 114 | + |
| 115 | +## case |
| 116 | +我们可以根据以上分析,模拟一个场景,让备库处于apply延迟的状态,你可以使用perf , pstack , strace等工具分析是否符合我前面从原理或代码层面的分析。 |
| 117 | + |
| 118 | +假设主备已经搭建好了。 |
| 119 | + |
| 120 | +在主库创建几张表,这几张表涉及大量的索引。 |
| 121 | + |
| 122 | +``` |
| 123 | +create table test( |
| 124 | +id int8 primary key, |
| 125 | +info text, |
| 126 | +crt_time timestamp, |
| 127 | +c0 serial8 unique check(c0>0) , |
| 128 | +c1 serial8 unique check(c1>0) , |
| 129 | +c2 serial8 unique check(c2>0) , |
| 130 | +c3 serial8 unique check(c3>0) , |
| 131 | +c4 serial8 unique check(c4>0) , |
| 132 | +c5 serial8 unique check(c5>0) , |
| 133 | +c6 serial8 unique check(c6>0) , |
| 134 | +c7 serial8 unique check(c7>0) , |
| 135 | +c8 serial8 unique check(c8>0) , |
| 136 | +c9 serial8 unique check(c9>0) , |
| 137 | +c10 serial8 unique check(c10>0) , |
| 138 | +c11 serial8 unique check(c11>0) , |
| 139 | +c12 serial8 unique check(c12>0) , |
| 140 | +c13 serial8 unique check(c13>0) , |
| 141 | +c14 serial8 unique check(c14>0) , |
| 142 | +c15 serial8 unique check(c15>0) , |
| 143 | +c16 serial8 unique check(c16>0) , |
| 144 | +c17 serial8 unique check(c17>0) , |
| 145 | +c18 serial8 unique check(c18>0) , |
| 146 | +c19 serial8 unique check(c19>0) , |
| 147 | +c20 serial8 unique check(c20>0) , |
| 148 | +c21 serial8 unique check(c21>0) , |
| 149 | +c22 serial8 unique check(c22>0) , |
| 150 | +c23 serial8 unique check(c23>0) , |
| 151 | +c24 serial8 unique check(c24>0) , |
| 152 | +c25 serial8 unique check(c25>0) , |
| 153 | +c26 serial8 unique check(c26>0) , |
| 154 | +c27 serial8 unique check(c27>0) , |
| 155 | +c28 serial8 unique check(c28>0) , |
| 156 | +c29 serial8 unique check(c29>0) , |
| 157 | +c30 serial8 unique check(c30>0) , |
| 158 | +c31 serial8 unique check(c31>0) , |
| 159 | +c32 serial8 unique check(c32>0) , |
| 160 | +c33 serial8 unique check(c33>0) , |
| 161 | +c34 serial8 unique check(c34>0) , |
| 162 | +c35 serial8 unique check(c35>0) , |
| 163 | +c36 serial8 unique check(c36>0) , |
| 164 | +c37 serial8 unique check(c37>0) , |
| 165 | +c38 serial8 unique check(c38>0) , |
| 166 | +c39 serial8 unique check(c39>0) , |
| 167 | +c40 serial8 unique check(c40>0) , |
| 168 | +c41 serial8 unique check(c41>0) , |
| 169 | +c42 serial8 unique check(c42>0) , |
| 170 | +c43 serial8 unique check(c43>0) , |
| 171 | +c44 serial8 unique check(c44>0) , |
| 172 | +c45 serial8 unique check(c45>0) , |
| 173 | +c46 serial8 unique check(c46>0) , |
| 174 | +c47 serial8 unique check(c47>0) , |
| 175 | +c48 serial8 unique check(c48>0) , |
| 176 | +c49 serial8 unique check(c49>0) , |
| 177 | +c50 serial8 unique check(c50>0) , |
| 178 | +c51 serial8 unique check(c51>0) , |
| 179 | +c52 serial8 unique check(c52>0) , |
| 180 | +c53 serial8 unique check(c53>0) , |
| 181 | +c54 serial8 unique check(c54>0) , |
| 182 | +c55 serial8 unique check(c55>0) , |
| 183 | +c56 serial8 unique check(c56>0) , |
| 184 | +c57 serial8 unique check(c57>0) , |
| 185 | +c58 serial8 unique check(c58>0) , |
| 186 | +c59 serial8 unique check(c59>0) , |
| 187 | +c60 serial8 unique check(c60>0) , |
| 188 | +c61 serial8 unique check(c61>0) , |
| 189 | +c62 serial8 unique check(c62>0) , |
| 190 | +c63 serial8 unique check(c63>0) , |
| 191 | +c64 serial8 unique check(c64>0) , |
| 192 | +c65 serial8 unique check(c65>0) , |
| 193 | +c66 serial8 unique check(c66>0) , |
| 194 | +c67 serial8 unique check(c67>0) , |
| 195 | +c68 serial8 unique check(c68>0) , |
| 196 | +c69 serial8 unique check(c69>0) , |
| 197 | +c70 serial8 unique check(c70>0) , |
| 198 | +c71 serial8 unique check(c71>0) , |
| 199 | +c72 serial8 unique check(c72>0) , |
| 200 | +c73 serial8 unique check(c73>0) , |
| 201 | +c74 serial8 unique check(c74>0) , |
| 202 | +c75 serial8 unique check(c75>0) , |
| 203 | +c76 serial8 unique check(c76>0) , |
| 204 | +c77 serial8 unique check(c77>0) , |
| 205 | +c78 serial8 unique check(c78>0) , |
| 206 | +c79 serial8 unique check(c79>0) , |
| 207 | +c80 serial8 unique check(c80>0) , |
| 208 | +c81 serial8 unique check(c81>0) , |
| 209 | +c82 serial8 unique check(c82>0) , |
| 210 | +c83 serial8 unique check(c83>0) , |
| 211 | +c84 serial8 unique check(c84>0) , |
| 212 | +c85 serial8 unique check(c85>0) , |
| 213 | +c86 serial8 unique check(c86>0) , |
| 214 | +c87 serial8 unique check(c87>0) , |
| 215 | +c88 serial8 unique check(c88>0) , |
| 216 | +c89 serial8 unique check(c89>0) , |
| 217 | +c90 serial8 unique check(c90>0) , |
| 218 | +c91 serial8 unique check(c91>0) , |
| 219 | +c92 serial8 unique check(c92>0) , |
| 220 | +c93 serial8 unique check(c93>0) , |
| 221 | +c94 serial8 unique check(c94>0) , |
| 222 | +c95 serial8 unique check(c95>0) , |
| 223 | +c96 serial8 unique check(c96>0) , |
| 224 | +c97 serial8 unique check(c97>0) , |
| 225 | +c98 serial8 unique check(c98>0) , |
| 226 | +c99 serial8 unique check(c99>0) |
| 227 | +); |
| 228 | + |
| 229 | + |
| 230 | +create or replace function create_test(int,int) returns void as $$ |
| 231 | +declare |
| 232 | +begin |
| 233 | +for i in $1..$2 loop |
| 234 | +execute 'create table test'||i||' (like test including all)'; |
| 235 | +end loop; |
| 236 | +end; |
| 237 | +$$ language plpgsql strict; |
| 238 | + |
| 239 | + |
| 240 | +select create_test(1,16); |
| 241 | +``` |
| 242 | + |
| 243 | +创建了17张表,涉及1818个索引。 |
| 244 | + |
| 245 | +创建测试脚本 |
| 246 | + |
| 247 | +``` |
| 248 | +vi test.sql |
| 249 | + |
| 250 | +\set id random(1,100000000) |
| 251 | +insert into test values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 252 | +insert into test1 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 253 | +insert into test2 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 254 | +insert into test3 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 255 | +insert into test4 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 256 | +insert into test5 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 257 | +insert into test6 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 258 | +insert into test7 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 259 | +insert into test8 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 260 | +insert into test9 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 261 | +insert into test10 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 262 | +insert into test11 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 263 | +insert into test12 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 264 | +insert into test13 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 265 | +insert into test14 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 266 | +insert into test15 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 267 | +insert into test16 values (:id,'test',now()) on conflict(id) do update set info=excluded.info, crt_time=excluded.crt_time; |
| 268 | +``` |
| 269 | + |
| 270 | +压测 |
| 271 | + |
| 272 | +``` |
| 273 | +pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 1000 |
| 274 | +``` |
| 275 | + |
| 276 | +观察延迟 |
| 277 | + |
| 278 | +``` |
| 279 | +select pg_size_pretty(pg_xlog_location_diff(pg_current_xlog_insert_location(),sent_location)) sent_delay, |
| 280 | + pg_size_pretty(pg_xlog_location_diff(pg_current_xlog_insert_location(),replay_location)) replay_delay, |
| 281 | + * from pg_stat_replication ; |
| 282 | +``` |
| 283 | + |
| 284 | +分析 |
| 285 | + |
| 286 | +``` |
| 287 | +pstack startup进程 |
| 288 | + |
| 289 | +perf record -avg |
| 290 | + |
| 291 | +perf report --stdio |
| 292 | +``` |
| 293 | + |
| 294 | +## 参考 |
| 295 | +[《PostgreSQL DaaS设计注意 - schema与database的抉择》](../201610/20161012_01.md) |
| 296 | + |
0 commit comments