对象版本控制和并发控制

概览

启用了版本控制的存储分区会维护对象的非当前版本,并提供恢复意外删除的数据或检索旧数据版本的方法。您可以随时为存储分区启用或停用版本控制。关闭版本控制后,现有对象版本会保留在原位,并且每次上传新版本时存储分区都只会覆盖对象的现行版本。

无论您是否对存储分区启用了版本控制,每个对象都会有两个关联的正整数字段:

  • 世代,该字段在对象内容被覆盖时会更新。
  • 元数据世代,用于标识元数据的世代。该字段从 1 开始,每次更新给定内容世代的元数据(例如,ACL 或 Content-Type)时,此字段都会更新;在世代编号发生更改时会重置。

在这两个整数中,处理版本控制的数据时只会使用世代。世代和元数据世代都可以与并发控制(将在后面的部分中介绍)结合使用。

如需在 gsutil 中使用对象版本控制,您可以使用一种嵌入对象世代的存储网址(我们称为特定于版本的网址)。例如,无版本的对象网址:

gs://bucket/object

可能有两个版本,其中包含以下特定于版本的网址:

gs://bucket/object#1360383693690000
gs://bucket/object#1360383802725000

以下部分介绍了如何使用版本控制和并发控制。

对象版本控制

您可以使用“versioning get”和“versioning set”命令在存储分区上查看、启用和停用对象版本控制。例如:

gsutil versioning set on gs://bucket

将为指定的存储分区启用版本控制。如需了解详情,请参阅 gsutil help versioning

如需查看启用了版本控制的存储分区中的所有对象版本及其 generation.metageneration 信息,请使用 gsutil ls -a:

gsutil ls -a gs://bucket

您还可以指定要为其查找特定于版本的网址的特定对象,也可以使用通配符:

gsutil ls -a gs://bucket/object1 gs://bucket/images/*.jpg

创建其他对象版本时,世代值会形成单调递增的序列。因此,最新对象版本始终是特定对象的 gsutil ls 输出中列出的最后一个版本。例如,如果存储分区包含以下三个版本的 gs://bucket/object:

gs://bucket/object#1360035307075000
gs://bucket/object#1360101007329000
gs://bucket/object#1360102216114000

则 gs://bucket/object#1360102216114000 是最新版本,gs://bucket/object#1360035307075000 是最早可用的版本。

如果您使用 gsutil 指定无版本的网址,则只能对对象的现行版本执行操作,例如:

gsutil cp gs://bucket/object ./dir

或者:

gsutil rm gs://bucket/object

使用 * 和 ** 等通配符时也是如此。它们仅对匹配对象的现行版本执行操作。例如,以下命令会移除现行版本并为存储分区中的每个对象创建一个非当前版本:

gsutil rm gs://bucket/**

如需对特定对象版本执行操作,请使用特定于版本的网址。例如,假设上述 gsutil ls -a 命令的输出为:

gs://bucket/object#1360035307075000
gs://bucket/object#1360101007329000

在这种情况下,命令:

gsutil cp gs://bucket/object#1360035307075000 ./dir

将检索到对象第二新的版本。

请注意,特定于版本的网址不能作为 gsutil cp 命令的目标(尝试此操作会导致出错),因为向版本控制的对象写入数据始终会创建新版本。

另请注意,某些 shell 将“#”视为特殊字符(例如,启用了 extendedglob 选项的 zsh)。如果您使用的 shell 将“#”视为特殊字符,则需要用英文引号括起参数,例如:

gsutil cp 'gs://bucket/object#1360035307075000' ./dir

如果某对象已被删除,则它不会显示在正常的 gsutil ls 列表中(即不带 -a 选项的 ls)。如需恢复已删除的对象,您可以运行 gsutil ls -a 来查找可用版本,然后将某个特定于版本的网址复制到无版本的网址,例如:

gsutil cp gs://bucket/object#1360101007329000 gs://bucket/object

请注意,执行此操作将创建一个新的对象版本,这可能会产生额外费用。您可以通过删除旧的特定于版本的对象来清理额外的副本:

gsutil rm gs://bucket/object#1360101007329000

或者,您可以使用 gsutil mv 命令来合并这两个步骤:

gsutil mv gs://bucket/object#1360101007329000 gs://bucket/object

如果您在启用了版本控制的存储分区中移除对象的现行版本,则非当前版本将会保留:

gsutil rm gs://bucket/object

如果您移除对象特定于版本的网址(即使是现行版本),则该版本会被永久删除:

gsutil rm gs://bucket/object#1360101007329000

如果您要删除对象的所有版本,请使用 gsutil rm -a 选项:

gsutil rm -a gs://bucket/object

如果您要移除存储分区中的所有对象的所有版本(以及存储分区本身),请使用 rm -r 选项(-r 表示 -a 选项):

gsutil rm -r gs://bucket

请注意,如果您继续上传到启用了版本控制的存储分区中的同一对象,则要创建的对象的旧版本数量不会受到限制。您应负责删除要保留的版本之外的版本。

复制版本控制的存储分区

您可以使用以下命令,在两个版本控制的存储分区之间复制数据:

gsutil cp -r -A gs://bucket1/* gs://bucket2

使用版本控制的存储分区运行时,此命令将复制每个对象版本。在 gs://bucket2 中创建的副本具有不同的世代编号(因为在创建对象副本时分配了新的世代),但对象排序顺序将保持一致。例如,gs://bucket1 可能包含:

% gsutil ls -la gs://bucket1 10  2013-06-06T02:33:11Z
53  2013-02-02T22:30:57Z  gs://bucket1/file#1359844257574000  metageneration=1
12  2013-02-02T22:30:57Z  gs://bucket1/file#1359844257615000  metageneration=1
97  2013-02-02T22:30:57Z  gs://bucket1/file#1359844257665000  metageneration=1

执行复制操作后,gs://bucket2 可能包含:

% gsutil ls -la gs://bucket2
53  2013-06-06T02:33:11Z  gs://bucket2/file#1370485991580000  metageneration=1
12  2013-06-06T02:33:14Z  gs://bucket2/file#1370485994328000  metageneration=1
97  2013-06-06T02:33:17Z  gs://bucket2/file#1370485997376000  metageneration=1

请注意,对象版本的顺序相同(正如所看到的,两个列表中的大小序列相同),但世代编号(以及时间戳)在 gs://bucket2 中为较新。

并发控制

如果您要使用 Cloud Storage 构建应用,则可能需要谨慎执行并发控制。通常情况下,gsutil 本身不用于此用途,但可以通过 gsutil 编写脚本来执行并发控制。

例如,假设您要使用 gsutil 实现“滚动更新”系统,在该系统中,定期作业会计算一些数据并将其上传到云端。每次运行时,该作业都会从上次运行时计算的数据开始,然后计算新值。为使此系统稳健可靠,您需要多台可运行作业的机器,这会提高两个同时运行的作业尝试同时更新一个对象的可能性。这会导致以下潜在的竞争情况:

  • 作业 1 计算要写入的新值
  • 作业 2 计算要写入的新值
  • 作业 2 写入新值
  • 作业 1 写入新值

在这种情况下,作业 1 读取的值在写入更新后的对象时不再是当前值,并且此时写入将导致过时的数据(或根据应用,导致损坏的数据)。

为防止出现这种情况,您可以找到所创建的对象特定于版本的名称,然后使用相应网址中包含的信息在后续 gsutil cp 命令中指定 x-goog-if-generation-match 标头。您可以通过两个步骤完成此操作。首先,在上传时使用 gsutil cp -v 选项来获取所创建的对象特定于版本的名称,例如:

gsutil cp -v file gs://bucket/object

可能会输出:

Created: gs://bucket/object#1360432179236000

您可以从此对象中提取生成值,然后构建如下所示的后续 gsutil 命令:

gsutil -h x-goog-if-generation-match:1360432179236000 cp newfile \
    gs://bucket/object

此命令会请求 Cloud Storage 尝试上传新文件,但如果上传时处于活跃状态的新文件的世代与指定的世代不匹配,则请求会失败。

如果您使用的命令更新了对象元数据,则需要找到对象的当前元世代。为此,请使用 gsutil ls -a 和 -l 选项。例如,命令:

gsutil ls -l -a gs://bucket/object

将输出如下所示的信息:

  64  2013-02-12T19:59:13Z  gs://bucket/object#1360699153986000  metageneration=3
1521  2013-02-13T02:04:08Z  gs://bucket/object#1360721048778000  metageneration=2

借助此信息,您可以使用以下命令请求在该对象的旧版本上设置 ACL,如此一来除非是“数据+元数据”的当前版本,否则该命令将失败:

gsutil -h x-goog-if-generation-match:1360699153986000 -h \
  x-goog-if-metageneration-match:3 acl set public-read \
  gs://bucket/object#1360699153986000

如果不添加这些标头,则更新只会覆盖现有 ACL。请注意相比之下,“gsutil acl ch”命令会自动使用这些标头,因为它会执行“读取-修改-写入”周期来修改 ACL。

如果您想体验世代和元数据世代的工作原理,请尝试以下操作。首先,上传一个对象;然后使用 gsutil ls -l -a 列出该对象的所有版本以及每个版本的元数据世代;然后重新上传该对象并重复运行 gsutil ls -l -a。您应该会看到两个对象版本,每个版本的元数据世代都为 1。现在尝试设置 ACL,然后重新运行 gsutil ls -l -a。您应该会看到最新的对象世代的元数据世代现在为 2。

了解详情

如需详细了解如何使用版本控制和前提条件,请参阅 https://cloud.google.com/storage/docs/object-versioning