随着分布式和云计算应用的增加,调试本质上变得越来越困难。本文分享了一种情况,在这种情况下,您会期望库能够防止不同的 API 版本。然而,事实并非如此,它导致了意外的行为,而且非常难以调试。这可能是一个有用的例子,说明有时为了系统地找到问题的根本原因,有必要剥离抽象层。
S3(简单存储解决方案)API 是一种行业标准,它提供了以编程方式与云存储交互的能力。许多云提供商将其作为与对象存储交互的方式之一来实现。有不同的供应商可供选择,这有助于避免供应商锁定。此外,有不同的实现可供选择意味着您可以选择最适合您和您的团队的流行标准的开源实现。
然而,正如我们所了解的,API 版本的差异可能会导致意外的问题。本文利用这些差异来说明故障排除过程。
Konveyor Crane
Crane 是 Konveyor 社区 的一部分,该社区致力于解决与应用程序现代化和可移植性相关的问题,以进一步推广 Kubernetes 的采用。 Crane 允许用户将部署在 OpenShift 3 上的应用程序(以及相关数据)迁移到 OpenShift 4。在幕后,它使用 Velero 来编排迁移。 Velero 使用对象存储来执行备份和还原操作。
Velero 如何存储数据
Velero 可以配置为使用对象存储中的存储桶作为备份存储位置(备份数据存储在此处)。 Velero 将备份组织在名为 <prefix>/backups
的目录中(其中 prefix
是可配置的)。在 backups
目录下,Velero 为每个备份创建一个单独的目录,例如 <prefix>/backups/<backup-name>
。
此外,为了确保在对象存储中创建的备份在集群中可用并且可用于还原,Velero 会列出 backups
下所有目录的前缀列表。它使用 ListObjectsV2 S3 API 来实现这一点。 ListObjectsV2 API 与 ListObjects API 的不同之处在于它处理分页的方式。
API 差异如何产生 Bug
这两个 API 版本之间的差异很微妙。首先,客户端会在他们发送到 S3 服务器的请求中看到差异。当请求 ListObjectV2 时,客户端会发送类似这样的内容
GET /?list-type=2&delimiter=Delimiter&prefix=Prefix
HTTP/1.1
Host: Bucket.s3.example.objectstorage.softlayer.net
x-amz-request-payer: RequestPayer
x-amz-expected-bucket-owner: ExpectedBucketOwner
对于 ListObjects,请求看起来非常相似,但缺少 list-type=2
GET /?delimiter=Delimiter&marker=Marker&prefix=Prefix
HTTP/1.1
Host: Bucket.s3.example.objectstorage.softlayer.net
x-amz-request-payer: RequestPayer
x-amz-expected-bucket-owner: ExpectedBucketOwner
对于忽略 list-type=2
参数的服务器,很容易使用 ListObject 响应类型来响应基本的 ListObjectsV2 调用。
API 版本响应类型之间有趣的差异在于分页的实现方式。两个版本都共享响应中的一个通用字段 isTruncated
;这表示服务器是否在其响应中发送了一组完整的键。在 ListObjectsV2 中,此字段与 NextContinuousToken
字段一起使用以获取下一页(以及因此下一组键),并迭代直到 isTruncated
字段为 false。但是,在 ListObjects API 中,而是使用 NextMarker
字段。这两种实现方式存在细微的差异。
我们的观察
当我们观察 Velero 调试日志时,我们发现总共找到了 555 个备份对象。但是,当我们针对同一个存储桶运行 s3cmd 命令以列出对象时,它返回了 788 个。在查看 s3cmd 命令行界面 (CLI) 的调试日志后,我们发现 s3cmd 可以使用 ListObjects 与服务器通信。我们还注意到,s3cmd 调试日志第一页上的最后一个字段是 Velero 在其列表中看到的最后一个字段。这立即敲响了警钟,表明使用 ListObjectsV2 API 时分页未正确实现。
在 ListObjectsV2 API 中,NextContinuousToken
字段用于将客户端带到下一页,并且 aws-go-sdk
中的 ListObjectV2Pages
方法在其实现中使用了此字段。逻辑是:如果 NextContinuousToken
字段为空,则不存在更多页面,因此设置 LastPage=true
。
考虑到服务器可能会在 ListObjectV2Pages API 调用上发送没有设置 NextContinuousToken
的 ListObject 响应,很明显,如果响应是使用 ListObject 响应进行分页,则 ListObjectsV2Pages 将仅读取第一页。这正是发生的情况,并通过使用 示例程序 在调试器中观察到这一点得到了验证。
只需将 Velero 的实现更改为使用 ListObjectsPages 方法(该方法使用 ListObjects API),Velero 就能够报告 788 的备份计数,这与 s3cmd CLI 一致。
由于这种语义差异,客户的迁移工作受阻。根本原因源于正在使用的库,而分析解决了客户的问题。
结论
这个案例研究表明,像 S3 API 这样被广泛采用的东西的实现也可能存在 Bug,并且可能以意想不到的方式引起问题。
要跟踪 Konveyor 开发团队如何解决现代化和迁移问题的技术分析,请查看我们的工程 知识库。有关 Konveyor 工具的更新,请加入 konveyor.io 的社区
评论已关闭。