《Lance 版本管理与时间旅行:快照、回滚与数据复现》

举报
yd_223668617 发表于 2026/11/02 11:06:48 2026/11/02
【摘要】 做数据的人多半都遇到过这种时刻:一份数据集被脚本覆盖写了,等发现时,上一版的数据已经找不回来,只能重新跑一遍上游流程。传统文件格式里,覆盖就是覆盖,Parquet 文件被新文件替换之后,旧数据没有任何挽回余地。Lance 在这一点上的设计不太一样:它内置了版本管理,每一次写入都会自动生成一个新版本,旧版本并不会因为一次 append 或 overwrite 就消失,而是像快照一样保留下来,随...

做数据的人多半都遇到过这种时刻:一份数据集被脚本覆盖写了,等发现时,上一版的数据已经找不回来,只能重新跑一遍上游流程。传统文件格式里,覆盖就是覆盖,Parquet 文件被新文件替换之后,旧数据没有任何挽回余地。Lance 在这一点上的设计不太一样:它内置了版本管理,每一次写入都会自动生成一个新版本,旧版本并不会因为一次 append 或 overwrite 就消失,而是像快照一样保留下来,随时可以打开查看,甚至可以恢复成当前版本。

这篇文章用一个小例子把这套机制走一遍:先写入一份初始数据,再追加一批、再模拟一次“误覆盖”,然后看看如何列出所有版本、如何回到旧版本查看数据、又如何把旧版本恢复为最新版本。理解这几个动作之后,误操作找回数据、按指定版本复现训练数据这类需求,就都有了落地的办法。

环境准备

只需要安装 pylance 本体,再加上构造示例数据用的 pandas 和 numpy:

pip install pylance pandas numpy

示例数据不依赖任何外部数据集,全部在代码里现造。下面统一把数据集写到本地目录 data/users.lance,版本号会随着每次写入往前走,建议读者跟着敲一遍,边写边看版本号的变化。

第一步:写入初始数据,得到版本 1

先构造一份简单的用户表,包含用户编号、姓名和年龄三列,用 create 模式写入:

import pandas as pd
import lance

df_v1 = pd.DataFrame({
    "user_id": [1, 2, 3],
    "name": ["alice", "bob", "carol"],
    "age": [25, 31, 29],
})

lance.write_dataset(df_v1, "data/users.lance", mode="create")

ds = lance.dataset("data/users.lance")
print(ds.version)   # 输出 1,这是数据集的第一个版本

这里有两个点值得注意。第一,mode="create" 表示新建数据集,如果目标路径已经存在一个数据集,这个模式会报错,它适合用在第一次落盘的时候。第二,写入完成后打开数据集,ds.version 给出的就是当前最新版本号。第一次写入是 1,这一点很好记:版本号从 1 开始,每成功写入一次,就递增一次,不需要我们手动维护什么快照文件。

第二步:追加与覆盖,看版本如何递增

日常更新数据最常见的两种方式是追加新行和整体重写。下面先追加两行,再模拟一次“误覆盖”——只写入一行数据,把原来的内容冲掉:

# 追加两行:产生版本 2
df_more = pd.DataFrame({
    "user_id": [4, 5],
    "name": ["dave", "erin"],
    "age": [35, 27],
})
lance.write_dataset(df_more, "data/users.lance", mode="append")

# 整体重写:产生版本 3,原来的 5 行在当前版本里只剩 1 行
df_only = pd.DataFrame({
    "user_id": [99],
    "name": ["temp"],
    "age": [0],
})
lance.write_dataset(df_only, "data/users.lance", mode="overwrite")

ds = lance.dataset("data/users.lance")
print(ds.version)          # 输出 3
print(ds.to_table())       # 当前版本只有 temp 这一行

如果是普通文件,走到这一步就只能认栽了:版本 2 里那 5 行数据已经被冲掉,除非上游还能重跑。但在 Lance 里先别慌,覆盖写只是新增了一个版本 3,版本 1 和版本 2 依然完整地躺在数据集目录里。想确认这一点,可以把所有版本列出来看:

for v in ds.versions():
    print(v)

versions() 返回的是从旧到新的版本清单,可以看到每个版本对应的编号和产生时间。线上排查问题时,先调这个清单,往往就能确认“数据是哪一次写入开始变样的”,定位范围一下就缩小了。

第三步:用 checkout 打开旧版本查看

确认旧版本还在之后,下一步是把当时的数据打开看。Lance 提供了 checkout 方法,传入版本号就能以那个版本的状态打开数据集:

ds_old = lance.dataset("data/users.lance").checkout(2)
print(ds_old.to_table())   # 版本 2 的 5 行数据完整都在

需要强调的是,checkout 打开的是一个只读视角:适合查看、校验、把旧数据读出来另存或重新计算,但不应该指望在这个视角上继续写数据。另外,查看完旧版本之后,如果想回到最新版本,不需要重新指定版本号,直接不带参数调用 checkout() 即可:

ds_latest = lance.dataset("data/users.lance").checkout()
print(ds_latest.version)   # 回到最新版本,即 3

这种“在版本之间来回切”的能力,就是常说的时间旅行。它的价值不只在救火:做问题复盘时,把数据集切到出问题之前的那一版,和出问题之后的那一版做字段级对比,比凭日志猜测要可靠得多。

第四步:用 restore 把旧版本恢复为最新

只查看旧版本往往不够,如果那次误覆盖的数据已经在被下游使用,就需要把旧版本正式恢复成当前版本。这一步用 restore() 完成。它的用法有个固定顺序:先 checkout 到目标版本,再在那个视角上调用 restore():

ds = lance.dataset("data/users.lance")
ds.checkout(2)     # 先切到想恢复的版本 2
ds.restore()       # 把版本 2 的内容恢复为最新版本

ds_now = lance.dataset("data/users.lance")
print(ds_now.version)        # 输出 4,而不是回到 2
print(ds_now.to_table())     # 5 行数据已经回来

这里最容易误解的一点是:restore() 并不是把版本号倒回去,也不是把版本 3 从历史上抹掉。它的做法是以旧版本的内容为基础,再生成一个全新的版本,也就是例子里的版本 4,内容与版本 2 相同。这样设计的好处是历史始终完整:版本 3 那次误操作仍然有记录,日后复盘能看清发生了什么;同时当前数据又回到了正确状态。理解成一句话就是:checkout 是“看过去”,restore 是“把过去的内容带到现在,并留下一笔新记录”。

两个典型场景

第一个场景就是上面演示的误覆盖找回。脚本写错路径、过滤条件写反、调度任务重复执行,这类事故在数据管道里并不少见。有了版本管理,应急动作从“重跑上游、等几小时”变成“先 checkout 核对、再 restore 恢复”,几分钟就能先止损,之后再慢慢查根因。

第二个场景是机器学习里的训练数据复现。模型效果突然变差时,最难回答的问题往往是“这次训练到底用的哪一版数据”。如果在训练脚本里把当时数据集的版本号记录进实验日志,事后只要 checkout 到那个版本,就能拿到与当时一模一样的数据重新跑实验,把数据因素和代码因素分开验证。对需要审计的业务来说,“数据 + 版本号”也比“数据 + 大概的日期”要严谨得多,因为一天之内数据可能被写过很多次,只有版本号能精确定位到那一份快照。

常见坑

第一,把 checkout 当成 restore。只 checkout 不 restore,下游读到的仍然是最新版本的内容,排查的人看了旧数据以为已经恢复了,实际线上用的还是错的那一版。恢复动作一定要以 restore 成功为准。

第二,restore 之前忘了先 checkout。restore 恢复的是“当前 checkout 到的那个版本”,如果跳过 checkout 直接调用,恢复的可能并不是你想要的那一版。稳妥的做法是 restore 之后再用 to_table() 抽查几行,确认内容无误。

第三,版本号不做记录。版本管理能保留历史,但前提是你知道要找哪一版。建议把每次关键任务使用的版本号写进日志或任务参数里,尤其是训练任务;等出了问题再靠时间去猜版本,排查成本会高很多。

第四,以为 overwrite 等于彻底删除。overwrite 只是让最新版本换成新数据,旧版本仍然可以 checkout 出来。如果数据涉及合规删除要求,不能认为覆盖写就等于数据销毁,需要另行确认处理方式,不要把回滚能力和删除能力混为一谈。

小结

Lance 的版本管理可以归纳为三件事:写入即快照,append 和 overwrite 都只会新增版本;checkout 负责回到任意旧版本只读查看,checkout() 不带参数则回到最新;restore 负责把某个旧版本的内容生成一个新的最新版本,历史不被改写。用好这三件事,数据管道就多了一张安全网:写错了能找回,做实验能复现,出了问题能对着版本清单一步步查。对于数据会被反复更新、又经不起丢失的场景,这套机制值得在建模之初就纳入考虑,而不是等第一次事故之后再补。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。