注1:参考 http://www.sql-workbench.net/dbms_comparison.html
以上对比基于MySQL 5.7和PostgreSQL 10。MySQL 8.0在SQL特性上有非常明显的提升,比如DDL事务一致性保证,支持窗口函数和CTE。
通过实际的测试对比,我们发现PostgreSQL在某些场景下比MySQL有明显的优势:
sysbench高并发主键查询测试,PostgreSQL和MySQL性能相当;
sysbench高并发纯写入测试,PostgreSQL的性能是MySQL的4倍。测试MySQL时发现系统过早触及IO瓶颈,可能和MySQL的SQL加存储的两层架构导致IO放大有关;
TPCH测试,PostgreSQL性能比MySQL高一个数量级。这个测试做的比较早,使用的MySQL和PostgreSQL数据库的版本都比较低,MySQL使用的是5.6,PostgreSQL使用的是9.5。当时MySQL 5.6中有一个SQL跑了2天都没执行完,还有一个SQL执行的结果是错误的。相信新版的MySQL和PostgreSQL上的性能都会有所提升,而且新版的MySQL上不应该还存这个Bug。
OLTP测试,如下图:
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gre7WlS8C17YVoOuay98IQn8OzicBe5hW9wIHKXUicHXRNkVIevibUctTA/640?wx_fmt=png)
OLAP测试,如下图:
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438g954icsR79BeibrxKG0o9iaZlbVeLY50PQhJTePqS63wl2WCma7iapX3ZCw/640?wx_fmt=png)
在可靠性方面,根据我们的测试和实际的生产运行,发现PostgreSQL也优于MySQL。
1)复制延迟
MySQL在高并发写入场景很容易产生复制延迟,相同测试条件下PostgreSQL不仅写入的TPS更高而且没有观察到复制延迟。根据我们的测试,MySQL从库应用事务的速度在1w/s左右,PostgreSQL备库应用事务的速度则在10w/s以上。
产生这么大差异的原因在于MySQL是逻辑复制,主库的每条SQL在从库上都要完整的执行一遍,MySQL的从库即使启用并行复制,也难以达到主库的并行度。PostgreSQL的物理复制,备库直接修改被变更的数据块即可,所以应用日志的速度很快。为确保MySQL的稳定运行,我们要求业务在使用MySQL时,单库的写入TPS不超过5000。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gn7LJ5oyzxd6wndHVbn6OkkTNhRuEdIK0ptzhXzLTa62iaYYCn11OzHA/640?wx_fmt=png)
2)故障恢复
在故障模拟测试和实际的生产运行环境中,我们发现MySQL在遇到宕机,RAID卡重置等硬件故障后容易出现主备数据不一致或者数据损坏无法启动的问题;同等条件下PostgreSQL出问题的概率小得多,通常重启就可以自动恢复。
对于普通的宕机我们可以通过切备机快速恢复生产,但是如果是断电导致的大面积宕机,就需要数据库在供电恢复后能快速启动快速恢复。宕机恢复对数据库来说是一项基本能力,但是在一些极端条件下,数据库也可能无法自动恢复。
下面这个表是在数据库的存储设备缺少掉电保护情况下发生断电故障后的测试数据。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gna7PE7Ofj3aveOib2u9UCjfSs2GWUcFCfvf8bFWibZWyRwvcxZfrfibmA/640?wx_fmt=png)
从上面可以看出PostgreSQL表现出了很强的健壮性,我们分析其主要原因有下面几点:
PostgreSQL的checkpoint周期一般比较长(我们配置的是半小时),可以修复更长时间内的数据错误;
PostgreSQL的WAL中保留有被更新页面的完整数据,可以整体替换数据文件中错误的的页面;
PostgreSQL的数据文件是heap结构,页面之间各自独立,容易恢复。
作为对比,MySQL使用了fuzzy checkpoint,每隔7秒甚至更短的时间就要进行一次checkpoint。在flush不能确保持久化的情况下,很近时间内产生的数据不一致就会导致数据库无法恢复,即使用了强制恢复模式(innodb_force_recovery=6)。
另外,极端情况下数据文件出现坏页又不能通过备份恢复时,PostgreSQL支持设置一个参数再重启就可将坏掉的页面清零快速恢复生产;MySQL没有类似的功能,而且MySQL的索引组织表的页面之间有逻辑关系,技术上要做到这一点也比较困难。
通过技术选型,我们决定引入PostgreSQL,通过MySQL和PostgreSQL的组合来替代线上的商业数据库。在具体系统的数据库选型上,还需要考虑业务的使用习惯,周边工具配套等实际情况。
总体上遵守以下的规范进行数据库选型:
MySQL + MyCAT
PostgreSQL + Citus
PostgreSQL + Citus
GreenPlum
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gRXoYHThX0YeAaDibt0EDFmJSVRg11CXGAFaPMFPJOrOz8BWgzvNN0IQ/640?wx_fmt=png)
我们上线的第一个PostgreSQL业务系统是一个实时大数据处理系统。这个系统的主要业务流程是从各个其它业务系统里面抽取相关数据放到它的数据库明细表里,然后再定时通过存储过程汇总明细表生成报表提供给分析平台进行展示。
这个系统一个很大的特点就是对数据库的性能要求特别苛刻,当时使用的是单库的商业数据库,平时数据库的CPU利用率都在45%以上,大促期间更是超过80%,可以说不堪重负。而且为了支撑今后的业务发展,这个系统必须在2017年双11前扩容十倍的容量,很显然单机的数据库已经没有任何性能扩容的空间,无法满足这一需求。
这个系统对数据库的使用主要包含下面3个不同的场景,其中每一个场景都对数据库有很高的性能要求。
1)明细表更新
实时更新包含400多个字段的宽表。数据加载速度要求达到5w/s以上,其中90%是UPDATE。
2)报表计算
支持200+/min的实时报表计算
3)报表和明细查询
支持高并发的报表和明细查询
考虑到这个系统当前对数据库的业务需求和未来的发展规划,我们希望扩容方案是基于SQL的开源分布式数据库。我们比较了几个候选方案,考虑到和业务场景的匹配度和今后的运维的便利性,最终选择了Citus,一个能把多个单机的PostgreSQL变成分布式数据库集群的插件。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gMzDndu1JaFRF7gSyvgK9FiahoJiazNrtnIGITaoJEialzKz5omxOzMDQQ/640?wx_fmt=png)
下面是这个系统使用Citus的部署架构,为了优化性能,我们做了一些架构上的优化。明细表更新不经过Coordinator节点,而是先到Coordinator节点批量查询待更新记录的位置信息,再直接到对应位置的Worker上以批量INSERT ON CONFLICT的方式更新明细数据。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gueoQsj9g372icwn2jMGHVr32iaxZXR3vYMasibbnpHjn1n9kKianB7UeOg/640?wx_fmt=png)
根据POC压测的结果,把原单库的商业数据库替换成1CN + 8个Worker的Citus集群后,性能提升了10多倍,圆满达成扩容目标。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gDN5p46kubSQ6JqA4PP9niaySv7qO3oYwicfsAdtXibxhRF0jEhIicgzdmw/640?wx_fmt=png)
新的系统从2017年上线后至今已平稳运行多年,生产集群规模也从最初上线的4个Worker逐步扩容到16个Worker,而且CN和Worker平时的CPU利用率都保持在10%左右,很好的支撑了业务的发展。
从2017上线第一套PostgreSQL以来,截止2019双11,我们已经部署了3000+PostgreSQL实例,其中80%以Citus集群的形式部署,其余是普通的PostgreSQL。
使用PostgreSQL的应用,既有OLTP类的业务也有OLAP类的业务。下面介绍其中的3个业务案例。
我们在很多计费结算类业务中使用了PostgreSQL + Citus的数据库架构,其中规模最大的系统是物流的计费平台。它有几百个数据库节点,多个大表的数据量超过百亿。这个系统原来使用的数据库是商业数据库,应用在业务层做分库分表。在业务层维护大量数据库节点的方式,大大增加了日常开发和管理上的成本。
举一个简单的例子,如果要在某个表上添加一个字段,需要先在几十个库的几百张分表里依次把字段加上,然后再修改上百个应用的配置,中间不能有任何错误和遗漏。而一次发布不会只改一个表,所有实际上每次发布光编辑发布脚本就是一项很大的工作。
后来我们把这个系统的数据库从一堆单机商业数据库迁移到了分布式的Citus。迁移后不仅省掉了商业数据库的许可和维保成本,而且应用层去掉复杂的分库分表逻辑,使用体验得到了极大的提升。
迁移到Citus后,以前一些不容易做甚至没法做的事情变得可以做而且简单了,比如执行一些跨库的查询,包括跨库事务。这个系统中大量的业务请求是涉及跨库事务的。Citus实现了基于2PC的分布式事务,支持分布式死锁检测和故障时的自动事务恢复,透明地支撑了业务的跨库访问。Citus上的日常DDL发布也很简单,只要在Citus的Coordinator节点对逻辑表执行DDL就可以了,使用体验上和在普通单库上的DDL发布没什么区别。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gibzG6jMOWVlHEiafD3xITyOwu36ibGK030gUzUpjVc3icfgIChn2OQCJicw/640?wx_fmt=png)
对电商来说,实时把握客户留存实施精准营销是一项非常重要的功课。但是对于有数亿会员和大量商品的大型电商平台,常规的处理方式是很低效的。通过探索和演进,现在我们使用citus + pg_roaringbitmap插件的技术方案。
roaringbitmap是一种高效的bitmap压缩存储格式,在标签类的应用中,已被业界广泛使用。pg_roaringbitmap插件则把roaringbitmap作为一种新的数据类型引入到了PostgreSQL里。这体现了PostgreSQL非常容易扩展的特点,不仅数据类型,索引类型,FDW甚至存储过程语言等等都可以扩展。再结合Citus的水平扩展能力,我们实现了百亿级标签的实时存储和查询。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gdZPPBVt2jj1icb3xiaN0X8EVmWmgJwUQXOictu1CTzLrerc9ALPgHJHrA/640?wx_fmt=png)
苏宁有大量线下门店,为支撑线下业务的运营,需要基于地理位置为用户提供个性化的搜索和推荐服务。并且在大促期间,需要支撑非常高的访问量。我们使用PostgreSQL + PostGiS插件支持位置数据的存储和高效查询,通过一主多从和基于JDBC多主机URL的读写分离水平扩展数据库处理能力。大促期间会临时增加从节点提升数据库的吞吐能力,最多时扩容到11个节点。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438g3fvxr5T2sHw0Ip2ibW1jibtDyRpRzVLRWF7sBVuzDDMtiaHiaxy6lrAia6Q/640?wx_fmt=png)
在使用PostgreSQL的过程中,我们踩过一些坑也积累了一些经验。下面我介绍三个PostgreSQL使用过程中需要注意的地方。
Gin索引是PostgreSQL的一个特色功能,很多研发小伙伴反映,数据库从商业数据库迁到PG后,他们最惊喜的PG特性就是GIN索引。很多系统的用户都有灵活的数据查询需求,查询条件涉及的字段不固定,之前应用开发者不得不为每种查询组合都创建一个Btree索引。这样做导致需要创建的索引非常多,不仅占用空间,影响更新性能,而且仍然难以覆盖所有查询场景。
对于这种搜索类的场景,有些业务会将数据导入到Elasticsearch进行查询。但是这种方案增加了系统的复杂度,需要保证ES中的数据和原始数据库的数据的保持一致,而且ES的索引更新相对与原始数据还存在一定延迟。而使用PostgreSQL的Gin索引,不存在这些问题。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gDBn1hM3dnsM3icdUWzbdXyXDV4JFAtm8KsurNdh4AiavqyUics4dWkUPg/640?wx_fmt=png)
Gin的基本原理和ES的倒排索引一样。它把每个(列号,Key值)作为倒排索引的键,将匹配这些键的元组的物理位置,即TID,作为值存在Entry Tree中。其中TID值存储在Entry Tree的叶子节点。匹配同一个Key的元组比较少时,TID集合以列表的形式存储;匹配同一个Key的元组比较多时,TID集合以Btree的形式存储,即Posting Tree。新插入的元组先写入Fast update list,达到一定数量时在批量合并到Entry Tree,这一优化可以大大提升插入数据到Gin索引的性能。另外Gin索引存储TID集合时做了压缩处理,因此对于低基数的索引字段,Gin索引能显著地节省存储空间。
和常规的Btree索引相比,Gin索引主要有如下优势:
下面的测试数据可以更直观的反映Gin索引的优点。
对于低基数的索引字段,Gin占用空间甚至不到Btree的十分之一。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438g1QCo2OaUx59bbIGa3unItr6NPVic1JVPhKXicaNeFNvhunicNibf2pG23A/640?wx_fmt=png)
在性能上,多字段and组合查询时Gin的性能可达到Btree的3倍。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gwDEfbnaWYIdd71lLHeC8vIicTlI9coVZSMibagkiccXDvr2ZKvhPdb6tg/640?wx_fmt=png)
但是Gin也不是万能的,它也有一些短板,有些场景下使用Gin索引会适得其反。概况起来主要有以下几点,需要注意:
对固定的多字段AND组合查询(比如 a=1 and b=2),效率不如btree;
不支持唯一索引;
不支持使用索引优化like前缀匹配,比如like ‘abc%‘(pg_trgm支持);
不支持使用索引优化组合字段的oredr by,group by(单字段支持);
对更新的性能影响比btree更大;
比btree更容易膨胀。
PostgreSQL是多进程架构,每一个连接对于一个进程。每个进程的私有内存空间中会缓存一些元数据,比如系统表数据,表定义,执行计划等等。如果使用不当,可能会由于后端进程私有内存占用过大导致系统内存不足,导致内存SWAP等问题。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gTkTxE7vOlfA6w2RXSmFicibUGjlJf9TpKuPjFjkiblHQeU58LLoibQYQjQ/640?wx_fmt=png)
我们可以用下面的命令查看某个后端进程的私有内存分配情况:
gdb --batch-silent -ex ‘call MemoryContextStatsDetail(TopMemoryContext,100)’ -p ${后端进程号}
执行上面的命令后,相关输出反应在PostgreSQL的日志文件中。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438g2ibTeoXeAYKqmyKHoicOzdSmwCaUXPKibicMBwrT1fJ7vOd44hiaicOFaABg/640?wx_fmt=png)
上面这个图里,私有内存的大部分被元数据缓存占用了,即CacheMemoryContext。
通常,元数据缓存中占用内存最多的是下面两个系统表
当数据库里表,分区以及表字段很多时,它们的元数据会占用的内存也会比较多。下表的例子可以反映这一点。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gb1LOeYyhY9ibPEiahic25FFM7WYsOiagupRIl6v49N7D6U6ibD6oB0OCgvw/640?wx_fmt=png)
从上面的数据我们可以知道,表相关的元数据缓存占用内存的大小主要和下面几个因素有关:
查询实际访问的表或分区的数量
查询实际访问的字段的数量
如果在使用PostgreSQL的过程中出现内存不足的问题,我们可以采取以下回避措施:
调小应用端连接池大小(max pool size和min pool size);
部署pgbouncer连接池;
减少分区数;
访问不同表或分区的连接尽量隔离,比如需要访问全部分区的连接使用单独的连接池;
通过ALTER TABLE SET STATISTICS对不需要通过柱状图评估选择性的字段,减少收集的统计信息;
拆分负载。
PostgreSQL的MVCC实现机制和其它传统的关系数据库不太一样。更新记录时不是在原地更新并且把修正的前镜像记录到UNDO日志,而是在数据文件中把原来的记录标记为“被删除”再插入一条新的记录,以后再通过VACUUM把这些”被删除“记录占用的空间回收掉。也就是说PostgreSQL中只有REDO日志,没有UNDO日志。在这种MVCC设计下,PostgreSQL回滚事务可以立即完成,和事务大小无关,回滚事务时只需要在CLOG事务状态文件中标记这个事务的状态为ABORTED即可。
PostgreSQL的MVCC实现机制带来的问题是如果垃圾回收不及时容易导致数据文件膨胀,这也是很多人吐槽PostgreSQL的地方。不过,不能单纯地把PostgreSQL的MVCC机制视作一个槽点,只是MVCC的不同实现方式而已。不知道大家注意到没有,很多新兴的分布式数据库库,也都采用了类似PostgreSQL标记删除的MVCC机制。
作为PostgreSQL的使用者,我们需要做的事是要确保PostgreSQL的垃圾回收工作能够正常运作。具体有下面几件事情要做。
1)autovacuum参数调优
PostgreSQL中有一个后台的autovacuum进程专门负责回收垃圾,我们可以根据数据库的配置和业务特点对autovacuum进行合理的参数设置,确保autovacuum回收垃圾的速度足够快,对数据库负载冲击又比较平滑。
比如下面几个参数:
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438gSJkRIjIKET2qnoP4eDjUVgSc8icYm1GsHVibcgo4Hibb6kwm2Tzzw4E4g/640?wx_fmt=png)
其中,最需要修改的是autovacuum_vacuum_cost_limit,其含义是一次回收最多消耗多少cost则暂停一会(autovacuum_vacuum_cost_delay的设定值)。默认值为200,这个值对于现代的设备显得太小,容易导致垃圾回收速度跟不上垃圾产生的速度,使用SSD时可以考虑把它设置为10000。
另外,autovacuum默认在表中被更新的元组比率达到20%的时候启动垃圾回收,对于亿级别的大表,会导致一个问题。就是一次垃圾回收的任务太大,垃圾回收时间过长。
所以建议采用以下优化措施:
关于autovacuum,详细可以参考一下这篇文章:
http://www.postgres.cn/v2/news/viewone/1/387
2)自动释放过长事务
PostgreSQL在垃圾回收时会保留对当前的存活事务可见的死元组。如果数据库中有一个执行时间特别长的事务,那么在这个事务存活期间数据库中产生的所有垃圾元组都无法被回收。这种长事务有时侯不是来自我们正常的业务访问,而是来自一些异常场景。我们可以在数据库中设置一些超时参数,使得在异常情况下,PostgreSQL可以及时把事务终止。
可以参考下面几类超时参数设置:
idle_in_transaction_session_timeout = 3600s
tcp_keepalives_idle = 60
tcp_keepalives_count = 10
tcp_keepalives_interval = 5
3)监控垃圾回收相关的数据库状态
除了前面介绍的措施,我们还需要对垃圾回收相关的一些数据库指标进行监控,一旦发生异常可以及时发现和处理。以下是我们在生成环境部署的一些和垃圾回收相关的监控项,大家可以参考一下。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvQlk6AaGCPqxBQaMNl438ghu9gooKF6FxQgtrSysKa5WCVTtqibg1ic49sIcK3h5pOFR53hwK4icD6Q/640?wx_fmt=png)
http://www.sql-workbench.net/dbms_comparison.html
《PostgreSQL+CITUS在苏宁物流经营结算中的实践》
《苏宁大规模标签场景应用实践》
https://bigdata.51cto.com/art/202006/617771.htm
https://yq.aliyun.com/articles/68244
https://postgrespro.com/blog/pgsql/4261647
http://www.postgres.cn/v2/news/viewone/1/387
Q1:目前PosgreSQL的recovery功能与Oracle相比怎么样?
A:PostgreSQL本身的备份恢复功能很完备,支持恢复到指定时间点,使用也很方便。另外还有很多第三方的开源备份管理工具,比如pg_rman,barman等等,可以更加方便的管理备份。所以,我觉得PG的备份恢复不弱于Oracle这样的商业数据库。
Q2:老师你们的高可用方案是怎样的?