数据库内核月报 - 2017 / 06

MySQL · 引擎特性 · 从节点可更新机制

背景

主从集群,指由一个主数据库实例和多个从数据库实例组成,其中主数据库实例提供读写功能支持,而从数据库不提供对外服务或只提供只读功能支持,但也有从数据库提供读写功能支持,下面就这几种集群架构做详细的解读,并就如何实现从节点可更新机制进行探讨。

主从集群概述

主从集群的实现方式主要有以下几种:

  • 基于磁盘镜像的主备集群
  • 基于Proxy中间件的主从(多主)集群
  • 基于共享磁盘的主从集群
  • 基于日志重放(物理日志或逻辑日志)的主从集群

一、基于磁盘镜像的主备集群

基于磁盘镜像的主备集群是最早出现的一种高可用解决方案,对数据库系统没有特殊要求,不需要额外的功能支持,利用原有单机数据库系统即可搭建,主要利用磁盘镜像来实现主备之间数据的同步。

原理是利用磁盘镜像的机制,当主机数据更新后,在写入磁盘的同时将数据同步到备机的磁盘上,主机缓冲区中的数据并不能同步到备机。当主机down机后,除了缓冲区数据未同步之外,磁盘上的数据还可能因为部分写而导致数据错误。所谓部分写是由于IO的特性导致的。

通常磁盘IO以扇区为单位一次性读写,但真正的数据页通常大于一个扇区(512字节),因此在down机时有可能只写了数据页的一部分,如一个数据页面大小为16K,down机时只写了此数据页的2K。后面会专门介绍数据库解决部分写的方法。

它的缺点也比较明显,主备机实例不能同时启动,只有当主机down机后,备机启动,并执行恢复操作,然后提供对外服务。因此,切换时间较长,而且备机的资源无法充分利用,适用于主机硬件损坏导致无法恢复的场景,也可用于计划检修等场景。

二、基于Proxy中间件的主从(多主)集群

基于proxy中间件可以搭建多主或主从集群,主要原理是利用proxy代理对应用请求进行分类、转发,当是写请求时,将写请求转发到集群中的每一台服务器;当是读请求时,将读请求转发到任意一台服务器即可。

基于proxy中间件搭建的集群,在数据库层面并不保证多机之间的数据一致性,而是由proxy来间接实现一致性,proxy通过检查发给所有服务器的写请求的执行结果,当任一服务器执行失败,就反向执行写请求,以保证多节点之间数据的一致性,这种一致性只是弱一致性,在数据库中也称为事务补偿机制。此外,也可以利用数据库本身提供的XA协议(如果数据库支持的话),但为了提高一致性,会损失性能和可用性。

它的主要缺点是无法保证事务的强一致性,当一个节点down机后,其它节点后续的写请求无法在down机服务器上执行,会造成down机服务器的数据丢失。

三、基于共享磁盘的主从集群

基于共享磁盘的主从集群是目前比较常见的集群架构之一,原理是通过主从服务器读取共享磁盘的数据来进行数据同步。因为数据库服务器的缓存,所以除了从共享磁盘读取数据之外,还要进行日志的内存重放,以更新缓存的数据。

共享磁盘架构的主要问题是缓存的同步,当主机提交事务时,同时将日志发到从机,或将日志刷写到磁盘后通知从机读取,然后从机重放日志,更新缓冲区的数据,但从机禁止将缓冲区数据写回到磁盘,只有主机可以将数据写回到磁盘。另外一个问题是部分写的问题,后面会专门讨论。

根据主机提交事务的时机,可以实现异步、近同步、实时同步等不同模式。异步指主机提交事务时,将日志发给从机,但不关心是否发送成功即完成事务提交。因为有网络延迟、重放日志延迟等,主从之间的数据并不是实时同步的。通常日志发送采用专门的进(线)程来实现,从机的数据有可能delay主机很久,这要实际的生产环境有很大关系。

近同步指从机收到日志即向主机返回成功,主机即可完成事务提交,但此时从机尚未日志重放,会有稍许日志重放的延迟。

共享磁盘架构的主要的问题在于必须有集群文件系统的支持,依赖于集群文件系统来保证节点间的数据一致性,并且在数据库层面也要有分布式锁来防止写写冲突和读写冲突。

四、基于日志重放(物理日志或逻辑日志)的主从集群

基于日志重放的主从集群是最常见的集群构架,并且也有很多独立于数据库的产品。日志大体有两种,一种是逻辑日志,一种是物理日志。物理日志通常都比较小,易于保存和传输,但重放的代价比较大,对从机资源的消耗也比较大,mysql的主从集群就是典型的以逻辑日志为基础的主从集群。与之相反的是物理日志,通常物理日志都比较大,但物理日志重放的代价都比较小,对从机资源的消耗也比较小,但对网络资源的消耗比较大。

逻辑日志重放代价上最有利的场景一个应用场景就是全表更新;而物理日志重放代价上最有利的场景更新全表扫描中的一条记录。因此,不能简单的说,哪种日志更好,必须以实际的应用场景来具体分析。

数据库系统中解决部分写的常见方法

在数据库系统中,数据通常以固定大小的页面来保存,页面大小也通常是512字节的整数倍,512字节是通常磁盘一个扇区的大小,扇区是一次磁盘IO的最小单位。SSD盘通常扇区大小为4K。在数据库系统中,一般情况下,数据页面的大小都会远大于512字节,随着大数据的到来,8K,16K页面已经很常见。当数据页面比较大时,要将一个数据页面刷写到磁盘上就需要多次IO,但这多次IO之间并不是原子操作,在执行过程中很可能因为各种原因导致中断,如断电,磁盘损坏等,造成的后果就是磁盘上的数据页面前面部分已经更新,后半部分仍是旧的数据,也就是部分写。出现部分写后的数据页是无法使用的,典型的牛唇不对马嘴。使用raid条带化也可能会导致同样的问题。

在数据库系统实现过程中,必须如何发现并解决这个问题。首先我们来说一说单机系统中的解决方法。

首先数据库要能检测到这个问题,实现的方法也比较简单,在页面头上记录一个标志,如时间戳,然后在页面尾上也记录一个相同的标志,当读取数据页面后,只要检查一下标志是否相同即可证明页面是否存在部分写。需要注意的是每次页面更新后,标志也必须同时进行修改,不能使用原有页面的标志。还有一些代价更大的方法,如生成整个页面的摘要,然后记录到页头中,除了可以检查页面的部分写之外,还能防止页面被恶意篡改,但这对系统资源的消耗比较大。一种热衷的办法是计算部分页面数据的摘要,但要包含页面头和页面尾,这样就可以能检测页面的部分写,也能部分达到防止篡改的目的。

在实现页面部分写检测的功能之后,我们还要解决如何这个问题,我们的目的是能得到正确的数据,而不只是发现错误。当数据库发现页面错误之后,可以采用的办法是通过日志重放来恢复数据。如果要从系统最初的状态重头开始进行日志重放,那么代价太大,或者有些场景下就是不可能实现的。为了加速这个过程,数据库引用checkpoint来解决这个问题。当checkpoint时,当前所有缓冲区中被修改的页面都要被刷写到磁盘上,执行成功后,记录checkpoint日志,以后就可以从这个点进行恢复。这与部分写有什么关系呢?数据库在更新每个缓冲区的数据后,都会记录日志,日志内容记录了当前修改的内容,最重要的一点是在记录日志时,如果发现当前的缓冲区是在checkpoint之后的首次修改时,会做一个当前页面的快照,并将其记录到磁盘上,在postgresql中是将其记录到redo日志中,另外一些数据库是记录到一个专门的日志中,如mysql的double write。当这个缓冲区被多次修改后,若在将其刷写到磁盘的过程中出现的断机等异常情况,造成部分写后,系统重新启动读取页面时发现页面有部分写,那么会将之前记录到磁盘的快照覆盖出现部分写的数据页面,然后在此基础上进行日志重放,从而达到恢复部分写的结果。

在基于共享磁盘的主从集群中也会存在相同的问题,有可能是主要写了一半,而从机此时来读取数据,就会读取到错误的数据。利用前面时间戳或摘要的方法,可以轻易解决检测部分写的问题。如何读到正确的数据呢?一种简单易用的方法就是重读,当检测到数据页面错误时,重试几次就好了,就能解决绝大多数的问题了,如果总是读取不正确,就要考虑是不是磁盘坏掉了。报错给DBA也是一个不错的选择。如果非要数据库来解决了,也是可以的,同样采用在快照上重放日志就可以实现数据的恢复,但代价比较大,运行过程中进行数据页恢复的设计也比较复杂。

从节点可更新机制

通常在主从集群中,主节点是可以读写的,而从节点通常只能提供只读功能,能不能实现在从节点也可以读写支持呢?当然可以,常见的方法有如下几种:

  • SQL转发
  • 全局锁
  • 主机延迟裁定

1.SQL转发

从节点接受应用请求后,经过词法语法分析后,若发现是更新类的操作,如DML,DDL等,将其转发给主节点,再将主节点执行后的返回结果转发回给应用程序。从节点本身实际并不真正执行更新操作,它依赖于主从之间的同步机制来更新本地缓存的数据。若不是实时同步更新,可能会导致应用程序在从节点更新,又在从节点读取到更新前的脏数据(历史数据)。若是更新较多的应用场景,从节点并不能承担分流的任务,主节点的负载不会有明显降低。

2.全局锁

如果集群实现了全局锁,从节点就可以真正在本地更新数据,但需要有额外的同步机制将从节点的数据同步到主节点。全局锁的实现可以使用集中式锁管理,也可以使用分布式锁管理,可以根据需要和应用场景来选择实现。
全局锁虽然解决了从节点的更新问题,但会对整个系统的性能造成较大影响,因为每次对数据页面的访问都要加全局锁,增加了大量额外的网络开销。并且还要实现从节点到主节点的数据同步机制。

3.主机延迟裁定

从节点收到应用程序的更新请求后,直接在备机执行syntax解析、SQL优化、执行等,在提交之前,向主节点发送裁定请求,以判定此写操作是否可以提交。向主节点发送的内容至少应包括更新前的数据快照、修改后的数据等,主节点收到裁定请求,比较更新前的数据快照是否与当前的数据相同,若相同则说明在从机修改数据之前此份数据没有被修改过,是在最新数据版本上进行的更新,则将更新的内容应用到主节点,如果需要记录日志,同时将日志刷写到磁盘。如果更新前的数据快照与当前的数据不相同,则说明在从机修改数据之前,主机已经修改过数据,但尚未同步到从机,因此必须回滚事务。