关于S3A的一些踩坑和思考

最近工作中架构升级，将原来的 EMR 集群迁移到基于开源的自建集群上，原来使用的一些组件自然也需要改造，其中就包括 s3。在我们的自建集群中，选用的开源 hadoop 中 s3a client（或者 s3a connector，下面简写成 s3a, 意义基本相同）来连接原有的 s3 存储。接下来我就会分享我在用 s3a client 时总结的一些经验

s3 协议的类型

首先介绍一下 s3，s3 全称应该叫AWS S3，又称AWS Amazon Simple Storage Service，是亚马逊公司开发的对象存储服务，广义上讲是一款包含了 Web 服务在内的完整存储产品，狭义上也能特指 AWS 的对象存储。它通过自定的 s3 协议访问存储上的文件，其路径类似：s3://xxx/yyy/zzz

鉴于 AWS 在云市场的占有地位，s3 存储在各个行业也有广泛的应用。然而 s3 协议是 AWS 的私有协议，只有 AWS 产品可用，为了能巩固和扩大 s3 在市场的地位，让用户能任何地方连上 s3 存储，放心地把数据存储到 s3 上，AWS 向 Hadoop 提供了开源版本的 s3 client 来连接 s3，也即是s3n client, 在 s3n 协议上的文件路径类似：s3n://xxx/yyy/zzz

后来 Hadoop 在升级中淘汰了 s3n client, 采用了全新的s3a client，在 s3a 协议下的文件路径类似：s3a://xxx/yyy/zzz

(Hadoop 最初推出的 s3 client，也采用 s3 协议，即文件路径类似：s3://，但是年代久远，早已废弃，这里就不提了)

总结一下三种文件协议的区别

s3 协议是 AWS 最初也是私有的协议，广泛用在 AWS 的产品中，例如 EMR，EC2 等等。如果你们公司使用的是 AWS 全家桶，基本都是通过 s3 协议访问 s3

s3n 和 s3a 是 hadoop 上基于 AWS s3 SDK 开发的开源 s3 client，能够摆脱 AWS 产品的限制，在自建的服务中自由访问 s3 存储，适用于仅使用了 AWS 的 s3 存储服务，需要和多方产品交互的场景

在性能上，三者差别不大，考虑到 s3 由 AWS 自己维护，在版本迭代的速度上可能要快于开源的 s3n 和 s3a

在兼容性上，作为 s3 协议的派生，原则上 s3n 和 s3a 都是兼容 s3 协议的；而 s3n 和 s3a 之间一般情况也是兼容（即原来采用的 s3n 协议，后来升级到了 s3a），但是 s3n 和 s3a 之间在目录创建上存在一些区别（下一 part 会提及），在一些场景中需要注意

s3a 在目录处理上的不同

实际上很多人并不认同 s3 为文件系统，因此 s3 是对象存储，和传统的文件系统有较大的区别。在官方的文档也说：

Amazon S3 is not a filesystem, it is an object store.

两者的一个重要区别就是对目录概念的解释。

传统的 Unix 风格的文件系统，例如 HDFS，其文件系统的组成是目录文件树，即生成的目录都是 “一直存在” 的，无论目录内是否存在文件。

s3 “文件系统”，由于其底层是通过对象存储（或者称之为块存储），其目录是 “虚拟” 的，举个例子：如果两个对象有相同的路径前缀：a/b/file1 和 a/b/file2，那么 s3 会认为这里存在目录 a/b/

两者在这方面的差异就会导致在实际使用中可能出现各种坑

在具体场景的区别

让我们再细分一下场景：
创建目录

HDFS 等目录文件树会创建一个空目录，可以往里面添加文件和目录，也可以在任意时候（无论目录中有没有文件或其他目录）通过 ls 来发现这个目录

实际上，要是 s3 也能做到这一点，那它也可以被视为实现了目录文件树，可惜它做不到。由于目录是 s3 是通过前缀来识别目录，因此当目录中没有任何文件时，需要通过目录标记（Directory Marker，后面简称 DM）来标记目录。当目录中创建了文件后，就会删除这个 DM；反之，当目录删除成为空目录时，又会添加这个 DM

在 s3a 中，会在空目录的场景中生成以 path_name+/ 为名称的文件来作为 DM，例如执行mkdir(s3a://bucket/a/b) 会创建一个标记对象 a/b/

在更老的 s3n 中，则是以path_name_$folder$方式作为 DM

相信从这里就能看出一丝不妙了吧，当新老版本的产品在某些约定上存在分歧时，往往会出问题。简单举个例子，当你用aws s3命令创建了目录，但是却用 s3a 去连接时，往往会找不到该目录，因为在aws s3工具中创建的目录没有创建 s3a 的 DM，因此在 s3a 协议中找不到目录。同理，当一个集群从低版本的 hadoop 升级到高版本时，也需要格外留意空目录的存在问题，因为有可能升级的同时将 s3n 升级到了 s3a, 识别的 DM 发生变化，就检查不到原来的空目录。

创建文件

在一般目录文件树的文件系统中，只需要按照目录所在路径，创建单一文件即可

在 s3 中，创建文件的操作可能会伴随着一系列 DM 的删除操作, s3a 需要在一个请求中包含删除所有父级 DM 的请求

删除目录和文件

HDFS 目录文件树的删除目录操作和删除文件操作基本相同，其删除的语义也更符合一般逻辑

在 s3 中，由于 DM 的存在，当删除文件 / 目录的时候如果父级目录变成了空目录，则需要将 DM 补充添加

存在的问题

s3 对目录的处理虽是无奈之举，但也确实成为了许多问题的源头

s3n 和 s3a 上采用的 DM 不同，意味着 s3a 所在的 hadoop 版本无法向下兼容

用 s3a 创建或删除文件的时候一般会需要删除或创建一批 DM，这会导致实际的请求量较大。而且，s3 中每个对象的读写视为一次操作，因此可能会带来较大的开销（结合 s3 的读写限制，这是 s3 性能不佳的主要原因，具体内容下节讨论）

在使用 list 操作的时候，由于每个请求中列出的对象的数量是父目录的数量，因此目录层级越深，请求越长。

在正式版本的 s3 桶中，即使没有对象没删除，逻辑删除的标记也会写进索引里，这对使大型目录的查询变慢

s3a 的性能问题

在 s3a 的官方文档上，展示了 s3a 和 HDFS 的一些不同，我将它贴了过来：

总的来说，以下几点原因导致了 s3a 的性能问题

由于桶分片导致 IOPS 被限制。

不同类型的 AWS EC2 虚拟机可能进行网络 IO 进行不同的限制。

对象和数据越多，目录重命名和复制操作花费的时间就越长。rename () 的性能更加缓慢。

在读取 s3 时使用 seek () 操作会强制新的 HTTP 请求。这可能会增大使读取 Parquet/ORC 文件的开销。

另外，还需要注意的是，由于 AWS s3 在读写上做出了频率限制，按照一般约定，当对 s3 的一个分区每秒超过读 5500 次或写 3500 次时，s3 就会拒绝请求，显示错误 503
可见，若是使用 s3 进行大量读写还是存在着不小的挑战，在大数据领域中使用 s3 还是更多看重的是它的计费成本和性价比。如果你的场景追求性能，那我的建议是：快跑！

解决方法
当然，实际场景下很多选择往往不是我们能左右的，假如我们必须要在一些 OLAP 或者其他大规模数据处理场景中使用 s3，这里依然会有一些优化建议。

使用 s3a committer。hadoop 内置了多种 s3a commiter 来优化 s3 文件的提交，其核心思路就是利用 s3 的 multipart Upload 机制来加速文件上传，同时不同的 committer 也有不同的优化思路，具体可以参考这篇文档

一些参数的调优，例如合理配置线程数和连接数，加大块读取的大小等等
更多优化的场景和细节，可以查看官方文档

总结

总的来说，s3 本身在某些场景还是很有优势的，作为 AWS 生态的重要一环，人们更看重它的安全可靠，物美价廉，以及与 AWS 其他云产品之间联动带来的叠加优势。但我们也要承认在不适当的场景中使用 s3 依然会不小的副作用。在实际生产中，也可能存在各种妥协因素，无法做不到釜底抽薪，但我们至少可以了解产品特性，尽最大可能优化性能。
因此我就简单写了一些 s3a 常见的可能会踩坑的点以及如何采取措施，其实大多来自于官方文档的整理，希望对你有所帮助～

参考文档

Hadoop-AWS module: Integration with Amazon Web Services

Experimental: Controlling the S3A Directory Marker Behavior

Maximizing Performance when working with the S3A Connector

Committing work to S3 with the “S3A Committers”