This work is licensed under a Creative Commons Attribution 3.0
Unported License.
http://creativecommons.org/licenses/by/3.0/legalcode
本文档的目的是描述启用object_post_as_copy = false在 Swift 中再次作为合理的部署配置的要求,同时不牺牲由object_post_as_copy = true.
启用的功能。为了简洁起见,我们将使用术语“fast-POST”来指代通过设置object_post_as_copy = false启用的操作模式,并使用“POST-as-COPY”来指代object_post_as_copy = true.
当前使用 fast-POST 会产生以下限制
解决方案是实现 fast-POST,使得对对象的 POST 将触发容器更新。
这将需要将当前来自 PUT(或 DELETE)的所有容器更新语义扩展到 POST,并同样涵盖所有失败场景。特别是,来自 POST 的容器更新必须是可序列化的(在日志事务意义上,请参阅 Container-server),以便可以通过 POST 进行的乱序元数据更新,以及通过 PUT 和 DELETE 进行的数据更新,可以在容器数据库中复制和调和。
此外,新的 ssync 复制引擎对 fast-POST 的操作测试较少。某些行为尚不清楚。目前看来,ssync 与 fast-POST 具有以下限制
如果可能,或者作为后续工作(请参阅 Updates to ssync),ssync 应该保留同步 .meta 和 .data 文件更新的语义差异。
Swift 客户端 API 描述 Swift 允许对象的“元数据”通过 POST 动词“更新”。
客户端 API 还描述容器列表包含每个对象的以下特定元数据项:名称、大小、哈希(Etag)、last_modified 时间戳和 content-type。如果这些元数据值中的任何一个被更新,则客户端期望容器列表中该对象的命名条目应反映其新值。
例如,如果对象在 t0 上传,大小为 s0,etag 为 m0,然后在 t1 之后,具有相同名称的对象成功存储,大小为 s1,etag 为 m1,则容器列表应最终反映该对象在 t1 时 last_modified 的新值 s1 和 m1。
这两种 API 功能都可以通过以下方式满足
有理由认为,存储在容器中的某些对象元数据项不应允许通过 POST 更改 - 对象名称、大小和哈希应被视为 POST 不允许修改对象的主体(从 etag 和大小都是派生的)时不变的。
然而,有理由认为 content-type 应该允许在 POST 上更改。也有理由认为,容器列表报告的对象的 last_modified 时间应等于最近的 POST 或 PUT 的时间戳。
如果允许在 POST 上更改 content-type,则必须更新容器列表,以满足客户端 API 期望,但当前实现缺乏对由 POST 触发的容器更新的支持
object-server POST 路径不会发出容器更新请求,也不会存储异步待处理请求。
container-server 的 PUT /object 路径对对象行的部分更新没有语义 - 这意味着没有办法在不创建新记录来替换旧记录的情况下更改对象的 content-type。但是,由于 POST 仅描述对象的转换,而不是完整的更新,因此对象服务器无法可靠地提供生成新容器记录所需的整个对象状态,而该状态是在单个时间戳下进行的。
例如,处理 POST 的对象服务器可能没有最新的对象大小和/或哈希,因此不应在 POST 的时间戳下将这些项目包含在容器更新中。
后端容器复制过程同样不支持部分更新的对象记录的复制。
因此,使用 fast-POST 模式更新对象元数据会导致对象状态和容器列表之间出现不一致:在对对象的 HEAD 请求返回的 Last-Modified 标头将反映上次 POST 的时间,而容器列表中的值将反映上次 PUT 的时间。
此外,container-sync 进程无法检测到对象状态已被 POST 更改,因为它依赖于每次对象更改时在容器数据库中创建新行。
代码考古学似乎支持 POST-as-COPY 操作模式的主要动机是允许在不使用客户端使用 PUT 重新上传整个对象的情况下修改 content-type,并启用 container-sync 以同步对象元数据更新。
以下提议的更改有助于实现 Swift 内部所有跟踪对象状态的服务最终达到一致对象元数据视图的属性,该属性具有三个组成部分
由于这些组件中的每一个都可以在不同的节点上不同的时间设置,因此对象的状态必须包含三个时间戳,全部或部分可能相等
我们断言,为了保证最终一致性,Swift 内部进程必须独立跟踪每个元数据组件的时间戳。这三个时间戳中的一些或全部通常会相等,但 Swift 进程不应断言这种相等性,除非可以从客户端请求生成的状态推断出来。
无需更改 - 代理服务器已在后端对象 POST 请求中包含容器更新标头。
容器服务器“PUT /<object>”路径将被修改为支持将三个时间戳值包含在存储在待处理文件中并最终传递到数据库 merge_items 方法的更新项中。
merge_items 方法将被修改,以便更新对象的所有现有行与对象更新合并,以生成编码每个元数据组件及其各自时间戳的最新的新行,即该行将编码三个元组
(data-timestamp, size, name, hash)
(content-type-timestamp, content-type)
(metadata-timestamp)
这需要存储两个额外的的时间戳,这将通过将所有三个时间戳编码到现有 created_at 列(多个时间戳编码)中存储的单个字符串中,或通过向 objects 表添加新列来实现。请注意,每个对象将继续在数据库表中只有一行。
容器列表代码将被修改为使用对象的 metadata 时间戳作为报告的 last-modified 值。
注意
有了这个提议,新的容器数据库行不一定存储与单个对象更新一起发送的所有属性。每新行现在由更新和任何现有行中的最新的元数据组件组成。
有了提议的更改,.meta 文件现在可能包含 content-type 值,该值设置的时间与另一个可变元数据不同。与 Updates to ssync 不同,基于 rsync 的复制过程无法查看对象文件的内容。复制过程因此无法区分具有相同名称但可能包含不同 content-type 和 content-type-timestamp 值的两个 meta 文件。
因此,必须修改 .meta 文件的命名,以便文件名指示 metadata-timestamp 和 content-type-timestamp。当前的提议是使用 content-type-timestamp 和 metadata-timestamp 的编码作为 .meta 文件名。具体来说
- 如果 .meta 文件包含 content-type 值,则其名称应为 metadata-timestamp 的编码,后跟(较早或相等)的 content-type-timestamp,并带有 .meta 扩展名。
- 如果 .meta 文件不包含 content-type 值,则其名称应为 metadata-timestamp,并带有 .meta 扩展名。
在 替代方案 中讨论了 .meta 文件命名的其他选项。
hash_cleanup_listdir 函数将被修改,以便根据文件名进行词法排序来确定是否应删除特定的 meta 文件,不再基于文件名进行判断 - 文件名将被分解为 content-type-timestamp 和 metadata-timestamp,并且将保留具有最新值的文件(或两个文件)。
此外,DiskFile 实现必须更改为在文件名指示一个包含最新的 content-type,另一个包含最新的 metadata 时,保留并在对象目录中读取最多两个 meta 文件。
多个 .meta 文件仅在处理下一个 PUT 或 POST 请求之前存在。在 PUT 上,所有旧的 .meta 文件都被删除 - 它们的内容已过时。在更新的 POST 上,读取多个 .meta 文件并合并其内容,获取最新的用户元数据和 content-type。合并的元数据被写入单个更新的 .meta 文件,并且所有旧的 .meta 文件都被删除。
例如,考虑一个对象目录,在 rsync 之后具有以下文件(排序)
t0_offset.meta - unwanted
t2.data - wanted, most recent data-timestamp
enc(t6, t2).meta - wanted, most recent metadata-timestamp
enc(t4, t3).meta - unwanted
enc(t5, t5).meta - wanted, most recent content-type-timestamp
如果 POST 在 t7 发生,具有新的用户元数据但没有新的 content-type 值,则处理 post 后的目录内容将是
t2.data
enc(t7, t5).meta
请注意,当对象从两个 .meta 文件合并 content-type 和 metadata-timestamp 时,它正在重建已经传播到容器服务器的相同状态。对象服务器不需要响应复制事件发送容器更新(即当前行为没有变化)。
此外,我们应该努力枚举对 ssync 进行的必要更改,以支持保留 POST 和 PUT 之间的语义差异。例如
当前的 ssync 实现似乎表明它最初的目的是针对默认的 POST-as-COPY 配置进行优化,并且它无法像 rsync 复制那样很好地处理 fast-POST 的一些边缘情况。由于 ssync 仍然被描述为实验性的,改进 ssync 支持不应是解决 rsync 部署中 fast-POST 当前限制的要求。然而 ssync 仍在积极开发和改进中,并且仍然是许多其他努力改进和增强 Swift 的关键组成部分。对 fast-POST 的完全 ssync 支持应该是将 fast-POST 设置为默认值的要求。
容器同步需要能够发现对象已更改,以及能够请求该对象的能力。
由于通过 fast-POST 的每次对象更新都会触发容器更新,因此容器数据库中将为每个对象的更新添加一行(和时间戳)(就像今天的 POST-as-COPY 一样!)
数据库中的 metadata-timestamp 将反映对象的完整版本和元数据转换。可以使用 X-Backend-Timestamp 验证检索到的对象的精确版本。
X-Newest 应该更新为使用 X-Backend-Timestamp。
注意
我们应该修复同步守护进程,不要使用 row['created_at'] 值来设置发送到对等容器的对象的 x-timestamp,而是使用正在同步的对象的 X-Timestamp。
如果需要,多个时间戳 t0、t1 ... 将被编码到单个时间戳字符串中,其形式为
<t0[_offset]>[<+/-><offset_to_t1>[<+/-><offset_to_t2>]]
其中
编码三个单调递增时间戳的示例是
1234567890.12345_2+9f3c+aa322
编码三个相等时间戳的示例是
1234567890.12345_2
即与 t0 的缩短形式相同。
编码两个时间戳的示例,其中第二个时间戳较旧的是
1234567890.12345_2-9f3c
请注意,编码时间戳的词法排序不一定能产生任何时间顺序。
在以下示例中,我们尝试列举各种需要就如何实现序列化或合并乱序元数据更新做出决策的故障条件。
这些示例使用当前编码多个时间戳的提案 多重时间戳编码 在 .meta 文件名和容器数据库 created_at 列中。为了简单起见,我们使用 t2-t1 作为简写来表示这种形式中时间戳 t2 和 t1 的编码,但请注意,-t1 部分实际上是一个时间差,而不是 t2 时间戳的绝对值。
(.meta 文件名的确切格式仍在讨论中。)
考虑一个在时间 t1 PUT 的对象的初始状态
Obj server 1,2,3: /t1.data {etag=m1, size=s1, c_type=c1}
Cont server 1,2,3: {ts=t1, etag=m1, size=s1, c_type=c1}
所有服务器最初一致,在时间 t2 成功 fast-POST 修改对象的 content-type。一切顺利时,我们的对象服务器将最终处于一致状态
Obj server 1,2,3: /t1.data {etag=m1, size=s1, c_type=c1}
/t2+t2.meta {c_type=c2}
该提案是让 fast-POST 触发一个容器更新,该更新是 .data 文件中的现有元数据和新的 content-type 的组合
Cont server 1,2,3: {ts=t1+t2+t2, etag=m1, size=s1, c_type=c2}
注意
即使 content-type 未更新,也将为每个 POST 发出容器更新,以确保容器列表的 last-modified 时间与对象状态一致,并确保为容器同步创建新行。
现在考虑一些故障场景...
在这种情况下,只有一部分对象节点会收到元数据更新
Obj server 1,2: /t1.data {etag=m1, size=s1, c_type=c1}
/t2+t2.meta {c_type=c2}
Obj server 3: /t1.data {etag=m1, size=s1, c_type=c1}
正常的对象复制会将元数据更新 t2 复制到故障对象服务器 3,使其状态与其它对象服务器保持一致。
由于故障对象节点不会更新其各自的容器服务器,因此该容器服务器也会过时
Cont server 1,2: {ts=t1+t2+t2, etag=m1, size=s1, c_type=c2}
Cont server 3: {ts=t1, etag=m1, size=s1, c_type=c1}
在服务器 3 上进行复制时,行合并会将 t2 的 content-type 更新与现有行合并,以创建一个与服务器 1 和 2 相同的行。
如果容器服务器在对象服务器处理 POST 时离线,则对象服务器会将更新记录的 async_pending 存储在与 PUT 和 DELETE 相同的方式中。
如果对象不存在,POST 将返回 404 并且不会处理请求
Obj server 1,2: /t1.data {etag=m1, size=s1, c_type=c1}
/t2+t2.meta {c_type=c2}
Obj server 3: 404
对象复制后,对象服务器应具有相同的文件。这不需要更改 rsync 复制。ssync 复制将被修改为发送带有 t1(包括 content-type=c1)的 PUT,然后发送带有 t2(包括 content-type=c2)的 POST,即 ssync 将复制健康服务器收到的请求。
如果一个对象服务器有一个较旧的 .data 文件,那么与其容器更新一起发送的组合时间戳将与其它节点的不同
Obj server 1,2: /t1.data {etag=m1, size=s1, c_type=c1}
/t2+t2.meta {c_type=c2}
Obj server 3: /t0.data {etag=m0, size=s0, c_type=c0}
/t2+t2.meta {c_type=c2}
对象复制后,对象服务器应具有相同的文件。这不需要更改 rsync 复制。ssync 复制将被修改为发送带有 t1 的 PUT,即 ssync 将复制故障服务器错过的请求。
假设容器服务器 3 也已过时,容器行将被更新为
Cont server 1,2: {ts=t1+t2+t2, etag=m1, size=s1, c_type=c2}
Cont server 3: {ts=t0+t2+t2, etag=m0, size=s0, c_type=c2}
在服务器 3 上进行容器复制时,行合并会将后来的数据时间戳 t1 应用到现有行,以创建一个与服务器 2 和 3 匹配的新行。
假设容器服务器 3 也已更新,容器行将被更新为
Cont server 1,2: {ts=t1+t2+t2, etag=m1, size=s1, c_type=c2}
Cont server 3: {ts=t1+t2+t2, etag=m1, size=s1, c_type=c2}
请注意,在这种情况下,行合并应用了更新中的 content-type,但忽略了比现有数据库行中的值更旧的不可变元数据。
如果没有任何节点具有 t1 .data 文件可以处理客户端请求时的 POST,则元数据可能仅应用于具有陈旧 .data 文件的节点
Obj server 1,2: /t0.data {etag=m0, size=s0, c_type=c0}
/t2+t2.meta {c_type=c2}
Obj server 3: /t1.data {etag=m1, size=s1, c_type=c1}
对象复制最终将使对象服务器保持一致。
容器也可能类似地不一致
Cont server 1,2: {ts=t0+t2+t2, etag=m0, size=s0, c_type=c2}
Cont server 3: {ts=t1, etag=m1, size=s1, c_type=c1}
在服务器 3 上进行容器复制时,行合并会将 t2 的 content-type 更新应用于现有行,但会忽略数据时间戳和不可变元数据,因为服务器 3 上的现有行具有较新的数据时间戳。
在容器服务器 1 和 2 上进行复制时,行合并将应用来自服务器 3 的数据时间戳和不可变元数据更新,但会忽略 content-type 更新,因为它们具有较新的 content-type 时间戳。
如果初始状态已经包含元数据更新,则 content-type 可能会被覆盖
Obj server 1,2,3: /t1.data {etag=m1, size=s1, c_type=c1}
/t2+t2.meta {c_type=c2}
在这种情况下,容器也会反映元数据的 content-type
Cont server 1,2,3: {ts=t1+t2+t2, etag=m1, size=s1, c_type=c2}
当在 t3 发生另一个 POST 时,它包含 content-type 更新,对象服务器的最终状态将完全覆盖上次元数据更新
Obj server 1,2,3: /t1.data {etag=m1, size=s1, c_type=c1}
/t3+t3.meta {c_type=c3}
如果初始状态已经包含元数据更新,则 content-type 可能会被覆盖
Obj server 1,2,3: /t1.data {etag=m1, size=s1, c_type=c1}
/t2+t2.meta {c_type=c2}
在这种情况下,容器也会反映元数据的 content-type
Cont server 1,2,3: {ts=t1+t2+t2, etag=m1, size=s1, c_type=c2}
当在 t3 发生另一个 POST 时,它不包含 content-type 更新,对象服务器会将当前 content-type 记录与其新的元数据合并,并存储在一个新的 .meta 文件中,该文件的名称指示它包含在两个不同时间修改的状态
Obj server 1,2,3: /t1.data {etag=m1, size=s1, c_type=c1}
/t3-t2.meta {c_type=c2}
容器服务器更新现在将编码三个时间戳,这将导致容器服务器上的行合并将元数据时间戳应用于其现有行并为对象创建一个新行
Cont server 1,2,3: {ts=t1+t2+t3, etag=m1, size=s1, c_type=c2}
如果以前的 content-type 更新在所有节点上不一致,那么在 t3 不包含 content-type 值的后续元数据更新将导致节点之间的元数据集合不同
Obj server 1,2: /t1.data {etag=m1, size=s1, c_type=c1}
/t3-t2.meta {c_type=c2}
Obj server 3: /t1.data {etag=m1, size=s1, c_type=c1}
/t3.meta
更糟糕的是,如果后续 POST 未在所有节点上成功处理,我们最终可能会在没有单个节点具有完全最新的元数据的情况下
Obj server 1,2: /t1.data {etag=m1, size=s1, c_type=c1}
/t3-t2.meta {c_type=c2}
Obj server 3: /t1.data {etag=m1, size=s1, c_type=c1}
/t4.meta
使用 rsync 复制,每个对象服务器最终将具有一致的文件集,但将有两个 .meta 文件
Obj server 1,2,3: /t1.data {etag=m1, size=s1, c_type=c1}
/t3-t2.meta {c_type=c2}
/t4.meta
当打开 diskfile 时,将读取两个 .meta 文件以检索最新的 content-type 和最新的可变元数据。
使用 ssync 复制,不一致的节点将交换 POST,最终将在每个节点上获得一致的单个 .meta 文件
Obj server 1,2,3: /t1.data {etag=m1, size=s1, c_type=c1}
/t4-t2.meta {c_type=c2}
编码 content-type 时间戳,然后是元数据时间戳(即相对于提案反转顺序)。这将导致编码始终具有正偏移量,这与容器更新中使用的 enc(data-timestamp, content-type-timestamp, metadata-timestamp) 形式一致。但是,采用建议的编码顺序可确保具有某些比数据文件更新的内容的文件始终在词法上排序在数据文件之前,从而减少了 diskfile 代码(例如 hash_cleanup_listdir)中的抖动,并且对于人类检查来说可能更直观(“t2-offset.meta 保存在具有 t1.data 的目录中,因为 t2 晚于 t1”,而不是“t0+offset 保存在具有 t1.meta 的目录中,因为 t0 和偏移量之和晚于 t1”)。
使用带有“正常”部分是 content-type 时间戳,偏移量是到元数据时间戳的时间差的两个向量时间戳。
(作者理解,在这种方式中使用时间戳偏移量来表示元数据时间戳是安全的,因为容器协调器绝不会使用时间戳偏移量来对具有相同外部时间戳的 object PUT 和 DELETE 施加内部顺序。)
这原则上与建议的选项相同,但可能导致文件名不太紧凑,并且可能与两个向量时间戳混淆。
使用元数据时间戳和 .meta 文件内容的哈希值来形成 .meta 文件的名称。时间戳部分允许清理早于 .data 或 .ts 文件的 .meta 文件,而哈希部分区分包含不同的 Content-Type 和/或 Content-Type 时间戳值的 .meta。在复制期间,所有有效的 .meta 文件都保存在对象目录中(最坏情况下的数量限制为对象环中的副本数)。当 DiskFile 加载元数据时,将读取所有 .meta 文件,并将最新的值合并到元数据字典中。写入合并的元数据字典后,可以删除所有贡献的 .meta 文件。
此选项更通用,因为它允许其它元数据项目也具有单独的时间戳(无需在 .meta 文件名中编码无限数量的时间戳)。因此,它支持其它潜在的新功能,例如可更新的对象 sysmeta 和可更新的用户元数据。当然,任何此类功能都超出了提案的范围。
POST-as-COPY 有一些限制使其不适合某些工作负载。
此外,由于 COPY 首先暴露给客户端,因此确定客户端始终可以显式地实现语义行为。
我们可以通过要求到达对象服务器的每个 POST 都包含 content-type 来简化 .meta 文件的管理,从而消除维护单独的 content-type 时间戳的需要。不需要维护多个元文件。容器更新仍然需要在对象 POST 期间发送,以使容器服务器与对象状态同步。容器服务器仍然需要修改以将 content-type 和元数据时间戳与现有行合并。
要求在每个 POST 中包含 content-type 对客户端来说是不合理的要求,但可以通过代理服务器使用带有 X-Newest = True 的 HEAD 请求检索当前的 content-type 并将其插入到后端 POST 中来实现,当客户端 POST 中缺少 content-type 时。
但是,此方案违反了我们的断言,即任何内部过程都不应假定对象的某个时间戳等于另一个时间戳。在这种情况下,代理正在强制 content-type 时间戳与由于传入 POST 请求而产生的元数据时间戳相同。在故障条件下,代理可能会读取陈旧的 content-type 值,将其与最新的元数据时间戳关联,并因此错误地覆盖更新的 content-type 值。
如果作为此替代方案的进一步发展,代理还使用带有 X-Newest 的 HEAD 读取“当前”content-type 值及其时间戳,并将这两项添加到后端对象 POST 中,那么我们将回到对象服务器需要在 .meta 文件中维护单独的 content-type 和元数据时间戳。
此外,如果在 POST 期间系统中最新的 content-type 不可用,它将被丢失,更糟糕的是,如果最新的值与数据文件关联,则没有明显的方法可以在不执行此规范中描述的合并的情况下正确提升其数据时间戳值。
这基本上是 fast-POST 和 POST-as-COPY 今天所做的事情。当对象元数据在 t3 更新时,转换对象的 x-timestamp 为 t3。但是,fast-POST 从未更新容器列表中的 last-modified。
在 fast-POST 的情况下,它可以将 t3 元数据更新异步地应用于 t1 .data 文件,因为它限制了元数据更新,这些更新不包括需要合并到容器更新中的更改。
我们希望能够更新 content-type 并因此更新容器列表。
对于 POST-as-COPY 来说,它可以这样做是因为应用于 .data 文件 t0 的元数据更新被认为“比” .data 文件 t1“更新”。应用于 t0 数据文件在 t3 的转换记录存储在容器中,而“更新”的 t1 .data 文件的记录是无关紧要的。
这表明 .data 文件时间戳将是偏移量,并且合并 t3_t0 和 t3_t1 将优先选择 t3_t1。但是,合并 t3_t0 和 t1 将优先选择 t3_t0(就像 POST-as-COPY 今天所做的那样)。unlink old 方法需要更新,以用于 rsync 复制,以确保 t3_t0 元数据文件“保护”t0 数据免受“更新”的 t1 .data 文件的影响。
通常认为在 POST-as-COPY 期间发生陈旧读取导致数据丢失的情况很少见,同样的侥幸心理同样适用于此针对容器更新的快速 POST 实现的提议规范。区别在于,此实现会丢弃元数据更新,并优先选择最新的 .data 文件。
该替代方案被否决,虽然可行但不太理想。
待定
无
无
无
如果容器列表中报告的最后修改时间更改为 POST 的时间而不是 PUT 的时间,则可能需要修改 API 文档(当前 POST-as-COPY 操作和快速 POST 操作之间存在不一致)。
在成功实施此提案后,我们可能需要弃用 POST-as-COPY。
无
对象服务器和容器同步需要新的和修改过的单元测试。探测测试将有助于验证行为。
无