我们用PG存储着大量关键数据(当然不是单实例),同时也维护着一个MongoDB集群,存储着大量的日志、报表、,另外在一些设备上运行着CouchDB。三种类型的数据库各司其职,协作良好。但我们更偏爱PG,一来是因为我们更熟悉,二来它确实低成本地解决了我们很多问题。
号称最强大的开源关系数据库的PostgreSQL,大家简称它Postgres或PG,已经发展二十多年了。其发起者是图灵将获得者Michael Stonebraker,所以PG从诞生那一刻起就带着浓浓的学术味道。至今,他也是开源关系数据库里对SQL标准、数据库范式支持最好的,一些特性甚至超过商业数据库。
至于它为什么没有像MySQL那样在互联网应用里如此流行(这只是在中国,PG在日本非常流行),我认为原因归结于以下三点:
1. 在互联网发展最快的年头里,它显得过于重量,比如触发器、存储过程这些东西,简单的互联网应用用不上。直到今天,互联网圈仍然对存储过程比较抵触,而在传统行业,开发者却很喜欢用。我想这是不同领域的业务需求不同所导致的。
2. 早期PG功能强大,性能却不如同时期的MySQL。早期的MySQL没有InnoDB引擎,对事务、触发器、存储过程的支持都不好,但性能却甩开PG很大一截。在唯快不破的互联网界,它被选择是再自然不过的事。至到今天,大家仍然对这两种数据库有着类似的刻版印象。
3. 商业支持。MySQL很早就有商业支持,而PG却要晚一些。大家印象中,都觉得有商业支持的东西才靠谱,即使多数人都未购买商业支持。CentOS与Debian也有类似的现象——即使多数人都未购买RedHat的商业支持,但仍然觉得CentOS就是比Debian要靠谱。某种程度上来,这种担心是正常的。小规模的开源软件被随时放弃的例子并不少见,但规模大到PG、Debian这种程度,它已经与很多公司业务息息相关,不可能说倒就倒。比如巴斯夫、索尼就在大量使用PG。
今天,数据库产品之间不断地互相学习、借鉴,特性差异在缩小。即使如此,他们由于基因(更确切地说是初始架构和发展重点)的不同,会导致在许多细节方面的不同。比如,MySQL是线程模型,而PG是进程模型,这就导致在进程创建性能更弱的Windows上,PG无法与MySQL相比,这里值得说的是,Oracle在Windows上是使用线程模型的。另外,虽然PG支持原生的JSON格式,导致它能像MongoDB一样作为文档数据库使用,但MongoDB在创建的那一刻起,就为了其横向扩展性牺牲了ACID(目前最新版已经支持跨文档事务),这导致PG无法像MongoDB一样方便地创建分片集群,这也是所有关系数据库与NoSQL数据库在特性上很大的区别之一。
丢掉过去的刻版印象,PG在互联网、物联网一些特定场景中,也是非常适合的。
对于我们来说,在它可以处理的数据量内,PG就是个多面手,能处理你的绝大多数数据问题,甚至是当作一个搜索引擎(使用它的全文搜索功能,代替Elasticsearch)+消息队列+ 缓存 + 关系数据库 +时序数据库的综合体。
1. JOSN字段原生支持:不仅可以将json作为原生字段进行存储,还可以对其进行索引,甚至对json文档中某个很深的字段进行索引(当然更适合的是Gin索引,而非B树索引)。一些数据,比如用户的一些扩展数据,其模型可能经常变化,同时并不要求严格约束,我们会将其存于用户表中的一个json字段中。这样,既能利用json文档的灵活性,又能利用关系数据库对关键字段的约束。当然,也可以使用映射表形式构建非常灵活的数据模型,但维护性、可编程性不如json,只有在要求极其灵活的字段配置和极其严格的字段约束的情况下我们才会构建映射表形成多对多关系。
2. 外部表的支持(FDW):这是一个非常惊艳的功能。你能将外部的redis、mysql中的表、csv文件当作一个PG中的内建的表来使用!其它任何你能想到的数据源都可以这样用——只要你能找到它的fdw驱动,或者自己动手写一个。我们会在PG用触发器将一些数据写入到一个伪装成本地表的redis中,这样,不仅外部维护成本降低了,你还能得到一个延迟很低、数据一致性得到更多保障的缓存,外部用户根本没意识到这个redis的存在。
3. 流复制:这个功能PG早就提供了。我们以这种方式建了一主多备的架构,以便于数据的备份和读写分离。从前,PG不支持级联流复制,现在支持了,就是说,你可以在备机下面再挂备机,这样就降低了主库的负担,能承受更大的写入量(不使用同步复制的情况下),更多的备机。
4. 无锁定并发:PG的无锁定特性非常突出。因为它使用了MVCC机制,即,它在内部,对数据的修改不是原地修改,而是版本化写入。如果发生冲突,只需要将某个版本弃用即可。这可以说是一种以空间换时间的方法。所以,在使用事务时,你的库不会锁住——当然这并不影响你使用传统的行锁、表锁。在一些操作中,我们需要较严格的事务,比如一些账户余额类的数据,我们需要保证至少Repeatable Read这一事务隔离级别。即使有冲突要回滚,对PG性能影响也非常有限。这正得益于PG的MVCC无锁定机制。但这种机制会带来一定负面影响,比如vacuum目的之一就是回收这些脏版本。早期的PG也正是由于定时vacuum对于性能影响过大而被人嫌弃,MySQL并没有此问题。今天,这对于PG来说也不再是问题,vacuum非常高效。
5. 表分区:其本质就是将一个大表拆成多个小表,这样单个子表的索引就能全部放入内存,大表操作得到性能优化。早期,表分区可通过表继承手动实现,现在PG对于表分区是自动的,还支持索引、外键、跨分区移动行等功能,使用体验非常好。
6. 表空间:我们会将不同的表放到不同的磁盘上,这样能提高不少性能。各操作可以利用多个磁盘的缓存和I/O,而不争用同一个磁盘资源。结合表分区、表空间,我们PG的单机性能得到很大提高。
基于以上原因,我们在互联网应用中使用了PG,而并未发现有什么特别难受的地方。虽说这些功能不是非PG不可,用MySQL也同样可以实现,但有些特性,比如FDW,就要多花很多精力自己实现。
我们还用PG(TimescaleDB)存储物联网时序数据,目前为止运行稳定高效。当然,这部分数据是使用独立的PG存储,没有与其它数据处于同一个实例上。
TimescaleDB本质上是PG的一个插件,它对时序数据进行了优化,如,将一个大表分成多个chunk(可以理解为片)存储,这能保证数据量上升而性能不下降。同时这也导致丢弃旧数据迅速但不精确的原因,因为其实只是删除chunk而非行,用不精确换性能是应用很广的策略,比如redis删除stream中过期数据时,也可以采用这种策略——谁在乎丢弃一年前的数据时少删了一天呢?
在时序表上,我们建立了自动聚合视图,比如,其中一个视图是把数据聚合成小时及分钟进行统计,得到每小时的均值、方差、最大最小值、5分位数。这些操只需要一个create view就能自动实现,其结果表现为一个定时自动更新的物化视图,你无需在原始数据上进行聚合,只需要在视图上进行查询即可。类似的特性,极大支持了我们数据统计业务。过去,TimescaleDB只支持在一个hypertable中创建一个自动聚合视图,现在最新版没这限制了,大福利。
目前,TimescaleDB处理我们的数据量完全没问题。但如果写入速度翻上两番,同时需要保存、查询所有历史数据……我们曾讨论那时是否需要切换到MongoDB或者HBase上去,因为它们可以方便地横向扩展,这是TimescaleDB所不具备的(据官方所说,这个功能正forthcoming,就是不知道开源版能否享受到)。
虽然我们有现成的MongoDB集群可用,但问题在于它对于时序数据的处理不如TimescaleDB方便,这是我们尚未迁移的原因。估计在达到单机性能瓶颈的时候,我们会首先考虑分库,而不是迁移。毕竟在我们的系统架构下,根据设备哈希分库比开发那一堆的时序处理功能要容易。
此处不写具体的调优参数,因为不同的应用场景、软硬件条件,调优参数是不一样的。如果有一样的参数,那么DBA这个职业就不会存在了。这里的思路也适用于其它数据库。
更快的磁盘,更快的CPU、更大的内存、更好的网卡。这没什么说的,数据库是资源大户,在生产系统上它不应该与其它应用处于同一台机器,也不要用Docker,在Docker环境下,即使你用-v参数外挂磁盘,仍然有不少性能损失。何况,数据库这种稳定的应用从容器化中得不到什么好处。
硬盘永远是瓶颈,尤其在数据较大无法缓存所有数据,或者写入极其频繁的情况下。更快、更稳定的存储永远是第一考虑因素。毕竟,硬盘与内存速度有上万倍的差距。
同时可靠性也非常重要,丢数据比机器报废更让人崩溃。但硬盘是不可能不坏的。这就需要通过联机热备进行备份(这会消耗你很多磁盘空间),联机热备是连续地备份WAL日志,它相对于用备机进行流复制备份的好处在于:联机热备能恢复到任何时间点,即使你误删数据仍然能够恢复,而流复制不能,因为它会将你的操作基本实时传播到所有备机上。
我在一些机器中使用了FreeBSD + ZFS文件系统来运行PG,我很喜欢FreeBSD。ZFS是个很好的东西,它本质上是个软RAID,但却拥有比普通硬件RAID卡更好的性能和可靠性,使用机械硬盘创建ZFS Pool的同时使用SSD进行缓存,你就得到一个兼具性能、可靠性、低成本的存储,太漂亮了。它最先出现于Sun时期的Solaris上,后被FreeBSD采用。不得不佩服Sun的工程师。至今,Linux由于内核许可证原因都没法用上这一文件系统。
PG有max_connections设置,这需要根据你的系统来确定。允许过多的连接数,也许在达到连接上限之前,你的数据库就已经没有响应了,过小的连接数,在达到连接上限的时候,也许你的机器利用率还不到20%。有人会认为连接数越大越好,并不是。因为PG是进程模型,过多的连接数导致进程数的上升会吃掉比MySQL更大的资源。即使是线程模型,更大的连接数也会不必要地吃掉你的资源。一个普通系统中连接数超过500,你就要想想了。应该考虑使用ORM模型中的持久连接参数,或者使用一个连接池(如pgbouncer),连接的开销是比较大的,应该尽量降低,复用连接能极大提升整体性能。另外,你还得考虑操作系统打开文件数是否足够,这在大系统上尤其重要。
这涉及写性能。默认情况下PG是同步写入的,好处是最大可能保证了数据的安全性,无论怎么断电你的数据都不会崩(除非磁盘或FS本身崩了),由于WAL日志的存在,你总是可以恢复,虽然可能未来得及写入WAL的数据会丢失,但至少你的数据库是完整的。我们一般配置synchronous_commit=off,但fsync=on,这样能达到性能与安全的平衡,当然,我们有联机热备和流复制备份,并不太担心数据丢失。
另外,在流复制时,你也得决定到底是同步还是异步复制。同步复制会等待WAL在主、备机均落盘后让操作成功,PG默认是异步的。同步还是异步,影响到你备机数据落后的时间、数据丢失的程度、整体性能,等等。需要综合考虑。
PG现在支持并行查询,一些较大的查询可能会进行并行查询,这能提高性能。如果工作进程数太少,这会导致服务器性能无法发挥,而进程数太多,则会产生无谓的资源消耗。
索引是好东西,但并不是所有查询都会走索引,PG可能会根据以前运行的统计结果,认为某个操作走全表扫描更划算,所以忽略索引。所以,你的查询结果要尽可能小而精确。
联合索引创建和使用顺序也很重要,它会遵从“前序规则”。这会导致虽然你建立了联合索引,但却没使用到的情况。当然,如果建多个单列索引,就不会有这问题,但多个单列索引性能并不如单个联合索引。
同时,过多的索引会降低你的写入速度、占用过多的内存,甚至导致单表索引无法全部放入内存,此时表现就是性能直线下降。应对的方法大致有:砍掉不必要的索引,删除不必要的数据(或区分历史数据、最新数据),将大表拆成多个小表(分表),将大表分布到多个实例上(分库),使用表分区,等等。Greenplum是PG的集群版,由商业公司维护,如果希望使用关系数据库的功能,又想像MongoDB一样能分片,那么使用Greenplum是个很好的选择。
另外,索引的类型也对性能有极大影响。一般来说默认是B树索引,但PG上还有Hash、GiST、Gin索引,分别适用不同的场景,也有各自的限制。比如,如果你要对数组或JSON建立索引,最好建立Gin索引。
数据尽量批量写入,如,使用insert的values(), (), …格式一次插入多条,数据库对这种批量操作有优化,同时减少了与客户端数据往来的时间。一些ORM没有批量插入接口,那么在这样的操作中,就不要使用ORM。这种场景在IoT数据应用中很普遍:许多设备的数据会同时到达,你无须等待就可以一次接收到多条数据,将它们批量入库才是正确做法。如果你担心累积数据的时间会妨碍数据入库的实时性,那么有两个策略可以应用:一是定时N秒和累积K条数据,任意条件满足则入库;二是首次等待数据时使用使用阻塞接收(假设你使用一个MQ缓存你接收到的数据),在取得第一条数据后使用非阻塞接收,直到取得K条数据为止,然后此K条数据入库、再次阻塞接收。这样,你不用担心数据入库时效性,同时也能批量入库。
另一个减少数据交互的方式,是使用函数或存储过程,比如,你要查询一张表,根据查询结果及你提供的参数更新另一张表,那么使用客户端SQL的情况下,你需要两次网络来回,而使用存储过程或函数的话,中间结果不用返回。这在中间结果、往返次数较多时,能得到非常可观的性能提升。PG中不仅可以用SQL写存储过程,还可以用Lua/Python/Tcl/C,这极大提高了存储过程的表现能力和可维护性。不要排斥存储过程,正确地使用它能带来许多收益。
一些成本很高的查询,尤其针对变化不大的数据,并不值得每次都到数据库中运行select,你可以将结果存储在一张临时表、物理视图或外部表中,如通过fdw写入redis。这种性能提高是数量级的。缓存是提速的终极之一。当然,这要结合数据特性、业务流程、技术架构来综合确定。
如果你的数据不需要关系模型,不那么在乎数据完整性、一致性,也不那么依赖时序数据库的特性,那么正适合使用MongoDB/HBase等NoSQL,它们就是为大数据而生,没有ACID(或很弱),没有强模型约束,极大扩展了存储、查询能力。一些低价值密度、不需要实时查询的数据很适合存在NoSQL上。数据量不大但要求查询性能极高时,Redis会是你的好朋友。
而另外一些场景下,使用良好组织的文件可能是更好的选择。比如HDF5,以有结构的二进制形式将数据存储在文件中,体积很小。NASA开发它用来存储科研数据。如果你的数据不需要更新,不需要精细查询,也不需要实时共享给多用户,那么HDF5非常合适。除了内存数据库,有什么能比直接读二进制文件更快(当然,需要考虑磁盘布局)?实际上,Kafka正是凭借磁盘上连续分布的块数据读写方式,实现了极高的吞量。