一、先搞懂核心问题:为啥Cloud Spanner灾备要验证数据一致性?

做云数据库的灾备,说白了就是怕主库挂了、或者主库所在的区域出问题,得把业务切到备用的库上继续跑。但切过去之后最要命的是什么?不是业务能不能跑,是跑起来的数据对不对——比如用户刚付的100块钱,切完之后变成90块,或者订单状态乱了,那比业务挂了还麻烦。

Cloud Spanner是谷歌的分布式关系型数据库,它的灾备逻辑跟普通数据库不太一样,自带跨区域的复制能力,还有快照备份的功能。但就算自带这些,也不能保证切换之后数据就一定没问题,比如复制过程中可能丢了没同步完的小批量数据,或者备份恢复的时候出了错,所以必须有一套能反复用的方法,来验证切换后的数据对不对,还要明确哪些情况是我们不用管的,避免做无用功。

二、先铺垫两个基础能力:Cloud Spanner的备份与恢复

要做数据一致性验证,得先搞懂Cloud Spanner自己的两个核心能力,不然没法设计验证流程。

2.1 快照备份:啥时候拍的,就能还原到啥状态

Cloud Spanner的快照备份,是在某个时间点(比如2024-05-20 14:30:00)给整个数据库拍个“照片”,这个照片里的所有数据,跟拍的时候完全一致,哪怕之后主库的数据变了,备份里的也不会变。

举个例子,你在14:30拍了备份,然后14:31有人改了某条订单的状态,那你把备份还原出来的库,那条订单的状态还是14:30的样子。

2.2 复制灾备:实时同步的“热”备用

Cloud Spanner的复制灾备,是主库跟备用库之间实时同步数据,只要主库的数据改了,备用库很快(通常几秒钟)就会同步过去。但这种同步不是“绝对实时”的,会有一点点延迟,比如主库14:30:00改的数据,备用库可能14:30:02才收到。

切换的时候,我们一般会等复制的延迟降到0(或者一个可以接受的很小值)再切,切完之后备用库就变成新的主库了。

三、核心:可复用的对比验证流程

我们要做的,就是不管什么时候做灾备演练,都能用同一套流程来验证切换后的数据对不对,不用每次都重新想怎么验证。这套流程的核心是“用备份当基准,跟切换后的库比”,因为备份是某个时间点的“标准照”,不会变。

3.1 提前准备:演练前拍备份(基准库)

在做灾备演练的前1分钟,给主库拍一个快照备份,然后把这个备份还原成一个单独的“基准库”——这个基准库就是我们的“标准答案”,后面所有的对比都以它为准。

这里要注意,拍备份的时间必须是“演练开始前的最后一刻”,因为备份拍的是当时主库的状态,我们要让这个状态跟演练前主库的状态完全一致。

3.2 做灾备演练:切到备用库

按公司的灾备流程,把业务从主库切到备用库。切完之后,备用库就变成了新的主库,我们叫它“切换后库”。

3.3 核心对比:基准库VS切换后库

这一步是最关键的,我们要对比基准库和切换后库的所有数据是不是一致。但对比不能瞎比,得按规则来,不然比不完。

3.3.1 先对比“静态数据”:不会变的数据

静态数据就是演练前不会变的数据,比如商品的基本信息(商品ID、名称、价格)、用户的基础信息(用户ID、注册时间)。这些数据在演练过程中不会被修改,所以基准库和切换后库的这些数据应该完全一样。

对比的时候,我们可以写一个脚本,把两个库的同一张表的所有数据查出来,然后对比每一条的每一个字段。这里给个具体的例子,用Python写,因为Python比较好懂,而且有专门的Cloud Spanner客户端。

首先明确技术栈:Python 3.9 + Cloud Spanner Client Library(google-cloud-spanner)

然后是脚本,脚本的逻辑是:先连基准库,再连切换后库,然后选一张静态表(比如商品表),把两个库的表数据都查出来,按主键排序,然后逐条对比。

# 导入Cloud Spanner的客户端库
from google.cloud import spanner

# ------------------- 配置部分 -------------------
# 基准库的配置:项目ID、实例ID、数据库ID
BASE_INSTANCE_ID = "spanner-instance-base"
BASE_DATABASE_ID = "spanner-database-base"
# 切换后库的配置:项目ID、实例ID、数据库ID(演练后切换到的库)
TARGET_INSTANCE_ID = "spanner-instance-target"
TARGET_DATABASE_ID = "spanner-database-target"
# 要对比的静态表名
TABLE_NAME = "products"
# 静态表的主键字段(用来排序,保证两个库的查询顺序一致)
PRIMARY_KEY = "product_id"
# ------------------- 配置结束 -------------------

# 初始化两个库的客户端
base_client = spanner.Client()
base_instance = base_client.instance(BASE_INSTANCE_ID)
base_database = base_instance.database(BASE_DATABASE_ID)

target_client = spanner.Client()
target_instance = target_client.instance(TARGET_INSTANCE_ID)
target_database = target_instance.database(TARGET_DATABASE_ID)

# 定义一个函数,查询指定库的指定表的所有数据,按主键排序
def get_table_data(database, table_name, primary_key):
    # 执行SQL查询:选所有字段,按主键排序
    sql = f"SELECT * FROM {table_name} ORDER BY {primary_key} ASC"
    # 执行查询,返回结果
    with database.snapshot() as snapshot:
        results = snapshot.execute_sql(sql)
        # 把结果转成列表,方便对比
        return list(results)

# 获取两个库的商品表数据
base_data = get_table_data(base_database, TABLE_NAME, PRIMARY_KEY)
target_data = get_table_data(target_database, TABLE_NAME, PRIMARY_KEY)

# 对比两个数据列表
if base_data == target_data:
    print(f"静态表 {TABLE_NAME} 数据一致!")
else:
    # 如果不一致,找出不一样的地方
    print(f"静态表 {TABLE_NAME} 数据不一致!")
    # 遍历每一条数据,找差异
    for i, (base_row, target_row) in enumerate(zip(base_data, target_data)):
        if base_row != target_row:
            print(f"第{i+1}条数据不一致:")
            print(f"基准库数据:{base_row}")
            print(f"切换后库数据:{target_row}")
            break
    # 如果两个列表长度不一样,说明有数据丢了或者多了
    if len(base_data) != len(target_data):
        print(f"两个库的表行数不一样!基准库行数:{len(base_data)},切换后库行数:{len(target_data)}")

这个脚本很简单,核心就是把两个库的同一张表的数据按同样的顺序查出来,然后直接对比列表是否相等。如果相等,说明静态数据没问题;如果不相等,就会打印出具体的差异在哪里。

3.3.2 再对比“动态数据”:演练前修改过的数据

动态数据就是演练前可能被修改的数据,比如订单的状态、用户的余额。这些数据的对比要注意,不能直接跟基准库比,因为基准库是演练前拍的,而演练前可能有业务在改数据,这些数据在主库和备用库的同步是有延迟的。

比如演练前10秒,用户改了订单状态,主库改了,但备用库还没同步完,这时候拍的基准库是主库的状态,而切换后的库(原来的备用库)还没同步到这个修改,所以直接比的话就会有差异,这是正常的,不是问题。

那动态数据怎么比?我们要加一个“时间窗口”的限制:只对比演练前10分钟(或者你能接受的同步延迟时间)之前修改过的动态数据。因为10分钟之前修改的数据,肯定已经同步到备用库了,不会有延迟。

具体怎么实现?比如订单表有个“update_time”字段,记录这条数据最后修改的时间。我们在对比的时候,只对比update_time在“演练开始时间 - 10分钟”之前的订单数据。

修改上面的脚本,对比动态表(比如orders表)的时候,SQL语句改成:

SELECT * FROM orders WHERE update_time < '2024-05-20 14:20:00' ORDER BY order_id ASC

这里的'2024-05-20 14:20:00'就是演练开始时间(比如14:30:00)减去10分钟的时间。这样对比出来的差异才是真的问题,不是因为同步延迟导致的正常差异。

3.4 明确验证边界:哪些不用验证?

很多人做验证的时候,会把所有数据都比一遍,浪费很多时间,其实有些情况是不用验证的,这就是验证边界:

  1. 演练前10分钟之内修改的动态数据:因为同步延迟,这些数据可能没同步到备用库,切完之后会同步,所以不用比。
  2. 演练过程中产生的新数据:比如切完之后业务又产生了新的订单,这些数据本来就是切换后库的新数据,不用跟基准库比。
  3. 日志类数据:比如操作日志,这些数据的修改时间如果是在演练前10分钟之内,也不用比,因为日志本来就是动态的,延迟同步很正常。

明确了这些边界,我们就不用做无用功,只需要对比应该对比的部分。

四、应用场景、优缺点、注意事项

4.1 应用场景

这套方法适合所有用Cloud Spanner做跨区域灾备的公司,尤其是对数据一致性要求高的行业,比如金融(支付、银行)、电商(订单、库存)、医疗(患者数据)。不管是每月一次的常规灾备演练,还是紧急情况下的真实灾备切换,都能用这套方法来验证数据。

4.2 技术优缺点

优点:

  1. 可复用:不管什么时候做演练,流程都是一样的,不用每次重新设计。
  2. 准确:用备份当基准,避免了用“当前主库”当基准的误差(因为当前主库可能还在改数据)。
  3. 高效:明确了验证边界,不用对比所有数据,节省时间。

缺点:

  1. 依赖Cloud Spanner的备份能力:如果备份失败,就没有基准库,验证就做不了。
  2. 对比大表的时候会慢:如果表有几百万条数据,查询和对比的时间会比较长,需要优化(比如按主键分批次对比)。
  3. 动态数据的时间窗口需要提前定义:如果时间窗口设得太小,可能会把正常的延迟当成问题;设得太大,可能会对比到不该对比的数据。

4.3 注意事项

  1. 拍备份的时间必须准确:一定要在演练开始前的最后一刻拍,不然基准库的状态跟演练前主库的状态不一致,对比就没有意义。
  2. 备份要能正常还原:拍了备份之后,最好先试还原一次,确保备份是好的,不然演练的时候才发现备份坏了,就麻烦了。
  3. 同步延迟的时间要提前测:比如你平时测的Cloud Spanner的跨区域同步延迟最大是5分钟,那你的时间窗口就设成10分钟,留够余量。
  4. 脚本要提前测试:对比脚本要在非演练环境先测几次,确保能正常运行,不然演练的时候脚本出问题,验证就做不了。

五、总结

Cloud Spanner的灾备验证,核心就是“用备份当基准,对比应该对比的数据”。我们先拍一个基准备份,然后切到备用库,再对比静态数据和时间窗口内的动态数据,同时明确哪些不用对比,这样就能快速、准确地验证切换后的数据一致性。

这套方法的关键是“可复用”和“明确边界”,不用每次都重新想怎么验证,节省时间,也能避免漏测或者误测。只要按这套流程做,就能保证灾备切换后的业务数据是对的,不会出大问题。