大数跨境
0
0

为PostgreSQL讨说法——浅析Uber切换MySQL

为PostgreSQL讨说法——浅析Uber切换MySQL 阿里云云栖号
2016-07-30
0
导读:最近有一篇文章在国外闹得沸沸扬扬,是关于UBER使用mysql替换Postgres……PostgreSQL真有那么“不堪”吗?
 背景 


最近有一篇文档,在国外闹得沸沸扬扬,是关于UBER使用mysql替换postgres原因的文章。


英文原文


https://eng.uber.com/mysql-migration/


来自高可用架构的 中文翻译


文章涉及到 PG数据库的部分,论点浮于表面,缺乏较为深入的讲解和分析 。容易导致用户对PostgreSQL产品的误解,这也是我决定写此文的原因之一 。


uber在文章阐述的遇到的PG问题


We encountered many Postgres limitations:


  • Inefficient architecture for writes

  • Inefficient data replication

  • Issues with table corruption

  • Poor replica MVCC support

  • Difficulty upgrading to newer releases


我接下来会依依介绍其原理,以及文章内容存在的问题。


1. Inefficient architecture for writes


uber文章的观点:


PG的MVCC机制,更新数据为新增版本,会带来两个问题


  • SSD的写放大

  • 索引的写放大


本文观点:


事实并不是MVCC的问题,所有的数据库只要支持并发读写,就需要多版本,只是版本管理的手段可能不一样。

有通过回滚段管理的,也有通过多版本(MVCC)进行管理的。


原理剖析:


基于回滚段实现MVCC的数据库

 

当更新一条记录时,有些数据库需要将整个数据块拷贝到回滚段区域(有些是基于逻辑行的拷贝,则拷贝到回滚段的是记录)。


注意写回滚段也是会产生REDO写操作的。


  • 更新可能在当前的row进行。


这种情况,只要索引字段不变化,索引就不需要变。


如果索引字段值发生变化,索引也要变化。




  • 如果更新后的记录超过原来行的长度,可能在本页找一块空闲区域(如果能装下),也可能要到其他页找一块区域进行更新,有擦除旧记录,写入新纪录的写操作。


不管怎样,索引都要变化。




基于回滚段实现MVCC的数据库,如果要回滚事务,开销会很大(特别是当事务修改的数据量很大时),因为要从回滚段将整个块拷贝到数据文件(基于逻辑行拷贝的回滚则是类似重新来一遍UNDO事务的SQL操作,同时还需要擦除之前更改的行)。


代价非常高 。


基于多版本实现MVCC的数据库


当更新一条记录时,产生一个新的版本。


  • PostgreSQL 会优先使用在当前页更新(HOT),即在当前页进行更新,不管行长度是否发生变化。这种情况,只要索引字段不变化,索引就不需要变。如果索引字段值发生变化,索引也要变化。


(hot时,索引不变,通过HEAP页内旧item指向新item来做到定位到新的记录)




  • 如果未在当前页更新,则索引才需要变化


(通过配置表的fillfactor,可以大大减少这种情况的发送,尽量走HOT)


如果读者还是担心这个问题,我们可以做一个压测试验,看看到底会不会更新索引,会不会对更新造成性能影响如何?


有几个参数需要注意,很多用户可能不关注这个,导致了膨胀:


autovacuum_work_mem = 4GB               # min 1MB, or -1 to use maintenance_work_mem
autovacuum = on                 # Enable autovacuum subprocess?  'on'autovacuum_max_workers = 8              # max number of autovacuum subprocesses
autovacuum_naptime = 1s         # time between autovacuum runs
autovacuum_vacuum_threshold = 50        # min number of row updates beforeautovacuum_analyze_threshold = 50       # min number of row updates beforeautovacuum_vacuum_scale_factor = 0.002  # fraction of table size before vacuum
autovacuum_analyze_scale_factor = 0.001 # fraction of table size before analyzeautovacuum_vacuum_cost_delay = 0        # default vacuum cost delay for


测试1000万数据,9个字段,8个索引,更新其中的mod_time字段。


postgres=# create table tbl(id int, mod_time timestamp(0), c1 int, c2 int, c3 int, c4 int, c5 int, c6 int, c7 int) with (fillfactor=80);CREATE TABLETime: 1.906 ms
postgres=# insert into tbl select i,clock_timestamp(),i+1,i+2,i+3,i+4,i+5,i+6,i+6 from generate_series(1,10000000) t(i);INSERT 0 10000000Time: 14522.098 ms
postgres=# create index idx1 on tbl(c1) with (fillfactor=80);CREATE INDEXTime: 3005.753 ms
postgres=# create index idx2 on tbl(c2) with (fillfactor=80);CREATE INDEXTime: 2793.361 ms
postgres=# create index idx3 on tbl(c3) with (fillfactor=80);CREATE INDEXTime: 2804.031 ms
postgres=# create index idx4 on tbl(c4) with (fillfactor=80);CREATE INDEXTime: 2856.954 ms
postgres=# create index idx5 on tbl(c5) with (fillfactor=80);CREATE INDEXTime: 2895.643 ms
postgres=# create index idx6 on tbl(c6) with (fillfactor=80);CREATE INDEXTime: 2932.394 ms
postgres=# create index idx7 on tbl(c7) with (fillfactor=80);CREATE INDEXTime: 2939.927 ms
postgres=# alter table tbl add constraint pk_tbl primary key(id) with (fillfactor=80);ALTER TABLETime: 3292.544 ms


记录下当前表的大小和8个索引的大小,用于压测后对比大小变化:


postgres=# \dt+ tbl
                List of relations
Schema | Name | Type  |  Owner   |  Size  | Description 
--------+------+-------+----------+--------+-------------
public | tbl  | table | postgres | 919 MB | (1 row)
postgres=# \di+ 
                                  List of relations
Schema |         Name          | Type  |  Owner   |      Table       |  Size  | Description 
--------+-----------------------+-------+----------+------------------+--------+-------------
public | idx1                  | index | postgres | tbl              | 241 MB | public | idx2                  | index | postgres | tbl              | 241 MB | public | idx3                  | index | postgres | tbl              | 241 MB | public | idx4                  | index | postgres | tbl              | 241 MB | public | idx5                  | index | postgres | tbl              | 241 MB | public | idx6                  | index | postgres | tbl              | 241 MB | public | idx7                  | index | postgres | tbl              | 241 MB | public | pk_tbl                | index | postgres | tbl              | 241 MB | 


全力压测30分钟,更新mod_time字段


$ vi test.sql
\setrandom id 1 10000000update tbl set mod_time=now() where id=:id;
压测开始
pgbench -M prepared -n -r -P 5 -f ./test.sql -c 48 -j 48 -T 1800


压测结果,更新速度维持在 13万/s 以上。 这个压力应该可以覆盖很多的用户吧。


progress: 5.0 s, 133373.6 tps, lat 0.357 ms stddev 0.269progress: 10.0 s, 133148.2 tps, lat 0.359 ms stddev 0.310progress: 15.0 s, 134249.0 tps, lat 0.356 ms stddev 0.299progress: 20.0 s, 131037.9 tps, lat 0.364 ms stddev 0.341progress: 25.0 s, 135326.3 tps, lat 0.353 ms stddev 0.292progress: 30.0 s, 135023.9 tps, lat 0.354 ms stddev 0.289......
progress: 1385.0 s, 135997.9 tps, lat 0.351 ms stddev 0.261progress: 1390.0 s, 133152.5 tps, lat 0.359 ms stddev 0.302progress: 1395.0 s, 133540.7 tps, lat 0.357 ms stddev 0.287progress: 1400.0 s, 132034.8 tps, lat 0.362 ms stddev 0.314progress: 1405.0 s, 135366.6 tps, lat 0.353 ms stddev 0.266progress: 1410.0 s, 134606.6 tps, lat 0.355 ms stddev 0.280.....
progress: 1855.0 s, 134013.7 tps, lat 0.356 ms stddev 0.298progress: 1860.0 s, 132374.8 tps, lat 0.361 ms stddev 0.306progress: 1865.0 s, 133868.3 tps, lat 0.357 ms stddev 0.282progress: 1870.0 s, 133457.1 tps, lat 0.358 ms stddev 0.303progress: 1875.0 s, 133598.3 tps, lat 0.357 ms stddev 0.297progress: 1880.0 s, 133234.5 tps, lat 0.358 ms stddev 0.297progress: 1885.0 s, 131778.9 tps, lat 0.362 ms stddev 0.319progress: 1890.0 s, 134932.2 tps, lat 0.354 ms stddev 0.274......
progress: 2235.0 s, 135724.6 tps, lat 0.352 ms stddev 0.284progress: 2240.0 s, 136845.0 tps, lat 0.349 ms stddev 0.256progress: 2245.0 s, 136240.6 tps, lat 0.350 ms stddev 0.264progress: 2250.0 s, 136983.2 tps, lat 0.348 ms stddev 0.248progress: 2255.0 s, 137494.5 tps, lat 0.347 ms stddev 0.251......


压测结束后,查看表和索引的大小,如果按UBER文中指出的,会更新索引,但实际上,结果说话,表和索引根本没有膨胀。


UBER 文章对用户的误导不攻自破。


表的大小未变化
postgres=# \dt+
                      List of relations
Schema |       Name       | Type  |  Owner   |  Size   | Description 
--------+------------------+-------+----------+---------+-------------
public | tbl              | table | postgres | 919 MB  | 索引的大小也未变化
postgres=# \di+
                                  List of relations
Schema |         Name          | Type  |  Owner   |      Table       |  Size  | Description 
--------+-----------------------+-------+----------+------------------+--------+-------------
public | idx1                  | index | postgres | tbl              | 241 MB | public | idx2                  | index | postgres | tbl              | 241 MB | public | idx3                  | index | postgres | tbl              | 241 MB | public | idx4                  | index | postgres | tbl              | 241 MB | public | idx5                  | index | postgres | tbl              | 241 MB | public | idx6                  | index | postgres | tbl              | 241 MB | public | idx7                  | index | postgres | tbl              | 241 MB | public | pk_tbl                | index | postgres | tbl              | 241 MB | 


另外再给大家分析一个信息,PostgreSQL nbtree 索引方法针对更新做了优化,可以大幅降低锁的产生,所以并发的更新性能是非常棒的。


我们来看一个测试,更新c1与mod_time两个字段,其中c1是索引字段。


压测脚本变更如下:


vi test.sql
\setrandom id 1 10000000update tbl set mod_time=now(),c1=c1+1 where id=:id; 


压测数据截取,可以看出性能是很好的,和单纯更新非索引列差不多


...
progress: 1025.0 s, 138077.5 tps, lat 0.346 ms stddev 0.264progress: 1030.0 s, 138746.9 tps, lat 0.344 ms stddev 0.270progress: 1035.0 s, 137590.2 tps, lat 0.347 ms stddev 0.273progress: 1040.0 s, 139072.3 tps, lat 0.343 ms stddev 0.258progress: 1045.0 s, 140480.3 tps, lat 0.340 ms stddev 0.255...


欲了解PostgreSQL nbtree的原理,可以参考Lehman & Yao Algorithm
src/backend/access/nbtree/README


小结


  • 基于回滚段实现MVCC的数据库,在更新数据时SSD写放大 > 100%(因为回滚段是一定要写的,并行写回滚段的操作也需要写REDO);而基于多版本实现MVCC的数据库,SSD写放大的概率低于100%(因为可能发生HOT,发生在当前页),而且旧记录只改行的xmax标记,产生的REDO极少。

  • 基于回滚段实现MVCC的数据库,在删除数据时SSD写放大是100%(因为回滚段是一定要写的,并行写回滚段的操作也需要写REDO);而基于多版本实现MVCC的数据库,SSD写放大的概率为0 (因为只需要改一下行头部的xmax的标记)。

  • 基于回滚段或MVCC的数据库,索引的写放大,都与是否发生行迁移有关,概率差不多。

  • 基于回滚段实现MVCC的数据库,如果要回滚事务,开销会很大(特别是当事务修改的数据量很大时),因为要从回滚段将整个块拷贝到数据文件(基于逻辑行拷贝的回滚则是类似重新来一遍UNDO事务的SQL操作,同时还需要擦除之前更改的行)。

  • 基于多版本实现MVCC的数据库,事务回滚非常快,因为不需要拷贝行或者数据块,也不需要修改已更新的记录,只是记录clog时将当前事务标记为ABORT即可,也就是说只需要改2个比特位。

  • 早在2007年,PostgreSQL就已经使用HOT技术完美的解决了索引更新的问题,根本不存在UPDATE数据时一定需要更新索引的问题。
    我在很多场合分享过HOT的原理,也有相应的文章介绍。
    要了解细节的话,可以看一下PostgreSQL源码中的 src/backend/access/heap/README.HOT
    另外PostgreSQL还支持hash, gin, gist, sp-gist, brin索引,用户如果想了解这些索引,可以参考
    https://www.pgcon.org/2016/schedule/attachments/434_Index-internals-PGCon2016.pdf

  • UBER文章指出的基于B+tree即secondary index指向PK,仅仅是一种避免UPDATE数据需要索引的方法。


但是这种方法引入了一些问题如下 :


  1. 插入数据会变慢,因为数据存储是B+Tree结构的。

  2. 如果插入的是随机的PK值,则会频繁的带来页分裂,会造成IO写放大。

  3. 为了解决索引分裂的问题,导致了写堵塞读。 原因是引入了ChangeBuffer,当读的数据还在ChangeBuffer中时,需要先将其merge到b+tree,merge过程是堵塞读的。

  4. 查询secondary时,要再走一遍primary index,带来了额外的离散扫描开销,如果secondary是范围扫描或者多点扫描,这个放大倍数是很可观的。 例如用户要从secondary index扫描10条记录,primary index的深度是4,那么除了secondary index的数据块扫描,还有额外多扫描40个primary的块。



PostgreSQL是记录的(block_number, offset),所以1条记录只需要扫描1个数据块。



如果没有深刻的理解以上原理,恐怕又要踩坑,到时候会不会又要从MySQL迁移到PG呢?其实UBER已经在2013年的时候从MySQL迁移过一次到PG,这来来回回要闹哪一出啊。


彩蛋


  • PostgreSQL TOAST机制


PostgreSQL的TOAST机制,可以将变长类型的值,自动压缩存储到另一片区域,通过内部的POINT指向,而不影响行的其他值。 例如存储文档,或者图片的表,如果这个表上有一些字段要更新,有一些字段不要更新,那么在更新时,PostgreSQL数据库会有非常大的优势,因为行很小。



基于回滚段实现MVCC的数据库,需要拷贝旧的记录或数据块到回滚段,记录或块越大,这个开销越大。



存储文档、图像、非结构化数据,使用PostgreSQL很有优势。


  • MySQL innodb是基于B+树的存储,当PK数据随机数据写入时存在巨大写放大,因为经常要分裂,不仅影响插入速度和查询速度,同时数据存放也会变得非常无序。


即使按PK顺序扫描时,也可能出现大量的离散IO。



基于B+树结构的存储,为了提高插入速度,如果使用index cache的话,则影响并发的查询,因为查询时要先合并索引。



另一方面,B+树的存储,必须要求表需要一个PK(即使表没有PK的需求,也要硬塞一个PK列进来),secondary index则指向这个PK。


如果PK发生更新,则所有的secondary index都要更新,也就是说,为了保证secondary不更新,务必确保PK不更新。


如果要对secondary index进行范围扫描,物理的扫描上是离散的。




所以uber本文提出的,secondary index 不需要变更的好处,其实背后是有以上代价存在的(例如一定要加PK,插入速度更慢,插入时PK不能随机否则分裂带来的IO巨大,使用secondary index范围扫描时会造成离散的IO等弊端),把原理,代价都交代清楚,才能看得更明白。


PostgreSQL 有几种方法来消除这种离散IO。


1. bitmap scan,获取heap tuple前,先根据ctid的blockid排序然后再从heap获取记录,以获得物理上顺序的扫描。




2. cluster by index,将表的物理存储顺序按照索引的顺序来存放,从而使用该索引进行范围扫描时,则是顺序的扫描。


但是请注意cluster的行为是一次性的,表依旧是堆表,只是物理存储的顺序与索引的顺序相关性一致,从而达到了查询时消除离散扫描的功效,它更适合静态的历史数据。


例如微博类的应用,可以将历史数据按用户ID和时间索引进行cluster化,那么在根据时间或用户ID查询这个用户的历史记录时,就不会产生离散的IO。




3. BRIN索引,这个是针对流式记录的一种索引,只记录块或者相邻块的元数据,如取值范围。 从而实现快速检索的目的。 详见
https://yq.aliyun.com/articles/27860


  • PostgreSQL的表是基于HEAP存储的,不存在以上B+树存储的问题,随便怎么插入,速度都很快。

  • SSD的原子写,通常SSD写入时是以最小单位为4K的写入,即使修改很小的数据。

  • 那么以directio或buffer io为主的数据库,哪个对SSD的伤害更大呢?

  • 对于directio的数据库,因为每次都是真实的伤害,而buffer io的数据库,OS层还会合并IO,可以大幅降低SSD的真实写(os 层调整vm.dirty_background_ratio可以调整写频率,从而影响合并粒度)。

  • PostgreSQL的shared buffer管理是基于buffer io的管理,对SSD来说是一种很好的保护,有兴趣的童鞋可以测试验证一下。




2. Inefficient data replication


uber文章的观点


PG的复制低效,有写放大。


本文观点


PostgreSQL的流复制非常高效,延迟几乎为0,同时还支持流的压缩和加密传输,很多企业用流复制来实现异地容灾,HA,读写分离的应用场景。


同时PostgreSQL也支持逻辑复制(>=9.4支持流式逻辑复制, <9.4的版本则支持基于触发器或者基于异步消息的逻辑复制)。


原理剖析


  • 问题反驳 1 (复制低效) 


我第一次听说PG的复制低效的,要知道PG的复制是业界有名的高效,延迟极低(关键是复制延迟与事务大小无关),网络好的话,几乎是接近0的延迟。


  • PostgreSQL流复制原理


即时唤醒,流式复制,所以延迟极低。




  • 问题反驳 2 (REDO写放大)


基于回滚段实现MVCC的数据库,在更新时,拷贝到回滚段的旧版本,是要写REDO的。


  • 而基于多版本实现MVCC的数据库,旧版本仅仅需要写修改行头bit位的REDO,所以基于多版本实现MVCC的数据库,更新时写入的REDO应该是基于回滚段实现MVCC的数据库的一半甚至更少(比如基于物理的回滚段要拷贝整个块,产生的REDO也很大)。



  • 同时,由于基于回滚段实现MVCC的数据库回滚时,要将回滚段的数据拷贝回数据文件,是会产生REDO的,这一点,基于多版本实现MVCC的数据库不存在这种写放大的问题。



  • 问题反驳 3(复制流量放大)


基于REDO的物理复制,意思就是要把REDO复制一份到备库。


所以REDO写了多少,就要复制多少到备库,网络的流量也是这样的。


  • 另一种是基于REDO的逻辑复制,需要复制的数据不仅仅包括新的数据,还要包括旧的版本数据(PK或者full row)。


可能一条记录更新前和更新后的数据都要复制。


  • 对更新操作来说,物理复制,不需要复制旧的记录(因为产生REDO的仅仅是XMAX的变化)过去,而逻辑复制则需要复制旧的记录过去。


另外需要注意的是,目前PG的垃圾回收也是以物理恢复的形式复制的,在实现上还有改进空间,比如通过逻辑的方式复制垃圾回收(只复制block id),可以大大减少网络传输的流量。




  • 而 uber 文章并没有指出,事实上 MySQL 目前只支持逻辑复制,并且如果要开启逻辑复制,不仅仅要写redo,同时还要写 binlog,等于写了双份日志,这个写放大也是很大的。


MySQL redo 用于恢复数据库,binlog用于复制。




自PostgreSQL 9.4开始,PG内核层就同时支持物理复制和逻辑复制,而且仅仅写一份日志就能同时支持物理以及逻辑复制。


在9.4版本之前,则可以通过其他软件进行逻辑复制(例如Londiste3, slone-I)




逻辑复制有一个弊端,被复制的表一定要有PK。 物理复制不存在这个问题 。


逻辑复制另一个弊端,大事务导致主备的延迟非常大,因为备库一定要等主库事务结束,备库才能开始回放该事务。物理复制不存在这个问题 。


小结


  • PG的复制是业界有名的高效,延迟极低(关键是复制延迟与事务大小无关),网络好的话,几乎是接近0的延迟。

  • 基于多版本实现MVCC的数据库,就版本仅仅需要写修改行头bit位的REDO,所以基于多版本实现MVCC的数据库,更新时写入的REDO应该是基于回滚段实现MVCC的数据库的一半甚至更少(比如物理回滚段要拷贝整个块,产生的REDO也很大)。

  • 对更新操作来说,基于REDO的物理复制,不需要复制旧的记录过去,而逻辑复制则需要复制旧的记录过去,物理复制产生的网络流量更小。

  • 逻辑复制有一个弊端,一定要PK。 物理复制不存在这个问题 。

  • 逻辑复制另一个弊端,大事务导致主备的延迟非常大,因为备库一定要等主库事务结束,备库才能开始回放该事务。 物理复制不存在这个问题,不需要等待主库事务结束后再回放redo 。


彩蛋


  • PostgreSQL可以开启协议层压缩,同时可以选择是否加密传输,压缩传输REDO。更高效,更安全。

  • PG的用户如果有主备环境,可以关闭FULL_PAGE_WRITE,产生的REDO更少(第一次更新的PAGE不需要写FULL PAGE)。
    但是需要注意,如果关闭了FPW并且主库因主机问题或在OS问题挂了,需要从备份环境恢复。

  • PG用户,可以将checkpoint拉长,减少FULL PAGE的产生,从而减少REDO的产生。

  • PG的用户,如果需要从PG或者MYSQL复制到阿里云的rds PG,可以使用阿里dbsync插件,目前支持全量复制,增量的逻辑复制正在开发中。
    https://help.aliyun.com/document_detail/35458.html
    https://help.aliyun.com/document_detail/35459.html
    http://yunpan.taobao.com/s/11YcWhIiBxV (提取码:Ed23Sh)


3. Issues with table corruption


uber文章的观点


用户在使用PG 9.2 时,因为主备切换,导致了一些数据问题。


本文观点


UBER在文中并没有描述清楚这个问题的始末,如何复现,同时也没有听说过PG的其他用户遇到这样的问题。


而且我在测试环境模拟TPC-B,同时不停的进行主备切换,也没有遇到类似问题。


PG的物理复制是可以保证主备完全一致的。


uber给出的观点心理暗示比较可怕。


PG一直以来就是一个以稳定性和功能强大著称的数据库,在企业市场有非常好的口碑。


国内的银行,运营商,保险,互联网公司都有在核心环境使用


  • 平安科技、阿里巴巴、高德、去哪儿、腾讯、用友、阳光、中移动、探探、智联、典典、华为、斯凯、通策医疗、同花顺、核电、国家电网、邮储银行、友盟、莲子。。。。。。


海外的汽车生产巨头,政府部门,医疗,物流等各个行业也都有非常多的用户 


  • 生物制药 {Affymetrix(基因芯片), 美国化学协会, gene(结构生物学应用案例), …}

  • 电子商务 { CD BABY, etsy(与淘宝类似), whitepages, flightstats, Endpoint Corporation …}

  • 学校 {加州大学伯克利分校, 哈佛大学互联网与社会中心, .LRN, 莫斯科国立大学, 悉尼大学, …}

  • 金融 {Journyx, LLC, trusecommerce(类似支付宝), 日本证券交易交所, 邮储银行, 同花顺…}

  • 游戏 {MobyGames, …}

  • 政府 {美国国家气象局, 印度国家物理实验室, 联合国儿童基金, 美国疾病控制和预防中心, 美国国务院, 俄罗斯杜马…}

  • 医疗 {calorieking, 开源电子病历项目, shannon医学中心, …}

  • 制造业 {Exoteric Networks, 丰田, 捷豹路虎}

  • 媒体 {IMDB.com, 美国华盛顿邮报国会投票数据库, MacWorld, 绿色和平组织, …}

  • 零售 {ADP, CTC, Safeway, Tsutaya, Rockport, …}

  • 科技 {Sony, MySpace, Yahoo, Afilias, APPLE, 富士通, Omniti, Red Hat, Sirius IT, SUN, 国际空间站, Instagram, Disqus, …}

  • 通信 {Cisco, Juniper, NTT(日本电信), 德国电信, Optus, Skype, Tlestra(澳洲电讯), 中国移动…}

  • 物流 {SF}


小结


仅凭一个没有始末的结论,似乎很难说明什么。


基于逻辑复制的数据库,主库压力大时通常会遇到备库追不上。


又或者因为某些原因导致主备不一致,即使发现了,可能并没有很好的修复手段,因为你不知道该以哪个数据为准。


逻辑复制导致主备不一致的原因较多,例如 主库执行失败,备库执行成功,或者备库执行成功,主库执行失败。


又或者 主库和备库的环境不一致,例如字符集,或者其他的,都非常容易导致主和备的不一致。


对于要求主备严格一致的场景,强烈建议使用物理复制。


4. Poor replica MVCC support


uber文章的观点


PG备库的MVCC支持较差,查询会与恢复堵塞


本文观点


首先,PG的备库分两种,一种是物理备库,一种是逻辑备库。


对于逻辑备库来说,与MYSQL的恢复机制是一样的,既然是一样的就不需要讨论了。


UBER文章说的 查询会与恢复堵塞,说的是物理备库,但必须纠正一个观点,查询是否堵塞恢复,是要论看场景的,况且堵塞的情况极为少见,还有一点要注意,逻辑复制也会有堵塞。


原理剖析


物理复制,什么情况下查询会堵塞、或与恢复冲突?


当以下操作产生的REDO被复制到备库,并且备库准备拿这些REDO来恢复时。


  • Access Exclusive locks taken on the primary server, including both explicit LOCK commands and various DDL actions, conflict with table accesses in standby queries.


    主库的访问排它锁,与备库对应的锁产生冲突。


    例如主库truncate a表, 备库查询a表。


    这种情况的冲突面很窄。


  • Dropping a tablespace on the primary conflicts with standby queries using that tablespace for temporary work files.


    主库删除表空间,备库使用这个表空间产生临时文件。 例如主库删除TBS,备库的一个大的查询需要写临时文件,并且这个临时文件是写到这个表空间的。


    这种情况非常少见,也很容易规避,新建一个临时表空间不要删除即可。


  • Dropping a database on the primary conflicts with sessions connected to that database on the standby.


    主库删除数据库,备库刚好连在这个数据库上。
    这种情况也非常的少见。


  • Application of a vacuum cleanup record from WAL conflicts with standby transactions whose snapshots can still "see" any of the rows to be removed.


    主库回收dead tuple的REDO,同事备库当前的query snapshot需要看到这些记录。


    这种情况可以通过参数控制,恢复优先,或查询优先。 可以配置时间窗口。


    而且这种冲突出现的概率也非常的小,除非用户在备库使用repeatable read,同时是非常大的事务。


    而通常用户用的都是read committed.


  • Application of a vacuum cleanup record from WAL conflicts with queries accessing the target page on the standby, whether or not the data to be removed is visible.


    同上,但是当query访问的页就是要清理垃圾的页时,也是有冲突的。
    这是物理复制与逻辑复制唯一有差别的地方,但是对现实场景来说,这种情况出现的概率也不大。


PostgreSQL提供了3种解决备库上查询与恢复冲突的办法:


  • 在主库配置vacuum_defer_cleanup_age来解决以上最后两种冲突。


  • vacuum_defer_cleanup_age = 0   # number of xacts by which cleanup is delayed


  • 在备库配置recovery延迟来解决以上所有冲突,给备库的QUERY设置一个执行窗口


  • max_standby_archive_delay = 30s        # max delay before canceling queries# when reading WAL from archive;# -1 allows indefinite delay# max_standby_streaming_delay = 30s      # max delay before canceling queries# when reading streaming WAL;# -1 allows indefinite delay


  • 在备库配置hot_standby_feedback,备库会反馈给主库QUERY情况(可能是快照,而不是QUERY本身)(如果是级联环境,则会反馈给最上层的主库)从而主库知道备库在干什么,在cleanup dead tuple时,会考虑备库的情况,防止冲突。


  • hot_standby_feedback = off             # send info from standby to prevent query conflicts# wal_retrieve_retry_interval = 1s 


很显然UBER没有意识到PostgreSQL防止冲突的手段。


在备库查询与恢复冲突只会导致apply延迟,并不会堵塞备库接收主库产生的REDO。


对于PG来说,主备冲突导致的备库apply延迟,理论上不会有逻辑复制在碰到大事务时那么可怕,逻辑复制遇到大事务,导致的延迟是很严重。


在现实应用场景中,很少有用户担心PG的备库延迟,即使有短暂的冲突,因为是基于块的恢复,恢复速度是很快的,马上就能追平(只要备库的IO能力够好,通常追平是瞬间完成的)。


接下来聊一下逻辑复制,难道逻辑复制就不会出现查询与恢复的堵塞、冲突吗? 当然也会有冲突。


逻辑复制,什么情况下查询会堵塞、与恢复冲突?


  • 备库发起一个repeatable read的事务,由于备库不断的恢复,备库的该查询事务有可能因为snapshot too old失败。

  • 主库发起的DDL语句,回放时会与备库的查询冲突,DDL的回放会被完全堵塞。

  • 主库删除一个数据库,回放时如果备库正好连在这个数据库上,产生冲突。


我并不清楚UBER在使用MYSQL时是如何解决这些冲突的,或者有没有解?


小结


基于物理复制或逻辑复制,只要备库拿来使用,都有可能出现查询与恢复冲突的情况。


PG对于冲突的处理非常的人性化,你可以选择恢复优先 or 查询优先,设置时间窗口即可。


同时PG还支持备库的QUERY反馈机制,主库可以根据备库的QUERY,控制垃圾回收的延迟窗口,避免QUERY和垃圾回收的冲突。


5. Difficulty upgrading to newer releases


uber文章的观点


PG的跨版本升级较难,跨版本不支持复制。


本文观点


看起来文章的作者或者UBER里可能没有熟悉PG的人,PG的大版本升级的途径很多,也很方便。


我这里给出两个方法:


方法1 :通过迁移元数据的方式升级,这种升级方式,取决于元数据的大小(即数据结构,函数,视图等元信息)所以不管数据库多大,都能很快的完成升级。


例如以10万张表,1万个函数,1000个视图为例,这样的元数据大小可能在几十MB的水平。 自动化程度高的话,导出再导入应该可以控制在分钟级别完成。
关键是它能支持原地升级,也就是说,你不需要再准备一套环境,特别是数据库非常庞大的情况下,再准备一套环境是很恐怖的开销。


当然,如果企业有环境的话,为了保险,通常的做法是,复制一个备库出来,在备库实现原地升级,然后激活备库转换为主库的角色。


备库升级结束后,再升级老的主库,由于只动到元数据,所以主备的差异很小,rsync一小部分数据给老的主库,就能让老的主库实现升级,同时将老的主库切换成备库即可。


简单的几步就完成了主备的大版本升级。



基于pg_upgrade的大版本升级可以参考我以前写的文章……(由于文章字数限制,请点击最下方“阅读原文”浏览完整内容。)PS:原文下面的讨论很深度,也很精彩。


本文为云栖社区文章,如需转载,请注明出处,并附上云栖社区微信公众号:yunqiinsight。


点击“阅读原文”可查看原文。




想和这群聪明人共事吗?加入阿里云云栖社区翻译志愿者:yqeditor@list.alibaba-inc.com


投稿或入驻云栖社区,请联系:yqeditor@list.alibaba-inc.com


2016,为了实现更多技术梦想,云栖社区与你携手并行。

yunqiinsight

长按二维码,一网打尽所有深度技术文章



戳原文,更有料!

【声明】内容源于网络
0
0
阿里云云栖号
云栖官方内容平台,汇聚云栖365优质内容。
内容 3553
粉丝 0
阿里云云栖号 云栖官方内容平台,汇聚云栖365优质内容。
总阅读366
粉丝0
内容3.6k