即使在运行在亚马逊 EC2 上的虚拟集群中,S3 的工作速度也比 HDFS 慢。
这是因为它们是截然不同的系统,正如您所见
功能 | HDFS | 通过 S3A 连接器的 S3 |
---|---|---|
通信 | RPC | HTTP GET/PUT/HEAD/LIST/COPY 请求 |
数据本地性 | 本地存储 | 远程 S3 服务器 |
复制 | 多个数据节点 | 上传后异步 |
一致性 | 一致的数据和列表 | 从 2020 年 11 月开始一致 |
带宽 | 最佳:本地 IO,最差:数据中心网络 | 服务器和 S3 之间的带宽 |
延迟 | 低 | 高,尤其是对于“低成本”目录操作 |
重命名 | 快速、原子 | 通过 COPY 和 DELETE 进行慢速伪重命名 |
删除 | 快速、原子 | 文件删除速度快,目录删除速度慢且非原子性 |
写入 | 增量 | 分块写入;在写入器关闭之前不可见 |
读取 | seek() 速度快 | seek() 速度慢且开销大 |
IOPS | 仅受硬件限制 | 调用者被限制为 S3 存储桶中的分片 |
安全性 | Posix 用户 + 组;ACL | AWS 角色和策略 |
从性能角度来看,需要记住的关键点是
rename()
的性能低下会在作业的提交阶段、DistCP
等应用程序以及其他地方显现出来。总体而言,尽管 S3A 连接器使 S3 看起来像文件系统,但它并不是,而且一些尝试保留隐喻的做法“非常不理想”。
要最有效地使用 S3,需要小心。
S3A 文件系统支持矢量化读取 API 的实现,客户端可以使用该 API 提供要读取的文件范围列表,并返回与每个范围关联的未来读取对象。有关完整的 API 规范,请参阅 FSDataInputStream。
可以配置以下属性,以根据客户端要求优化矢量化读取。
<property> <name>fs.s3a.vectored.read.min.seek.size</name> <value>4K</value> <description> What is the smallest reasonable seek in bytes such that we group ranges together during vectored read operation. </description> </property> <property> <name>fs.s3a.vectored.read.max.merged.size</name> <value>1M</value> <description> What is the largest merged read size in bytes such that we group ranges together during vectored read. Setting this value to 0 will disable merging of ranges. </description> <property> <name>fs.s3a.vectored.active.ranged.reads</name> <value>4</value> <description> Maximum number of range reads a single input stream can have active (downloading, or queued) to the central FileSystem instance's pool of queued operations. This stops a single stream overloading the shared thread pool. </description> </property>
S3A 文件系统客户端支持输入策略的概念,类似于 Posix fadvise()
API 调用。这可以调整 S3A 客户端的行为,以针对不同的用例优化 HTTP GET 请求。
sequential
读取文件,可能有一些短的前向寻址。
在单个 HTTP 请求中请求整个文档;通过跳过中间数据,支持在预读范围内的前向寻址。
这提供了最大的顺序吞吐量,但向后寻址的开销非常大。
批量读取文件的应用程序(DistCP、任何复制操作)应使用顺序访问,读取 gzip .gz
文件中的数据也应如此。由于“正常”fadvise 策略从顺序 IO 模式开始,因此很少需要明确请求此策略。
random
针对随机 IO 进行了优化,特别是 Hadoop PositionedReadable
操作——尽管 seek(offset); read(byte_buffer)
也受益。
不是请求整个文件,而是将 HTTP 请求的范围设置为 read
操作中所需数据长度的范围(如果需要,则向上舍入到 setReadahead()
中设置的预读值)。
通过降低关闭现有 HTTP 请求的成本,这对于通过一系列 PositionedReadable.read()
和 PositionedReadable.readFully()
调用访问二进制文件的 IO 非常高效。顺序读取文件成本高昂,因为现在必须发出许多 HTTP 请求才能读取文件:每个 GET 操作之间都有延迟。
随机 IO 最适合具有大量查找特征的 IO
PositionedReadable
API 读取数据。read()
调用或小 read(buffer)
调用。在创建文件系统实例时,必须在配置选项 fs.s3a.experimental.input.fadvise
中设置所需的 fadvise 策略。也就是说:它只能在每个文件系统基础上设置,而不能在每个文件读取基础上设置。
<property> <name>fs.s3a.experimental.input.fadvise</name> <value>random</value> <description> Policy for reading files. Values: 'random', 'sequential' or 'normal' </description> </property>
HDFS-2744,扩展 FSDataInputStream 以允许 fadvise 提议添加一个公共 API 来设置输入流上的 fadvise 策略。一旦实现,这将成为用于配置输入 IO 策略的支持机制。
normal
(默认)normal
策略从以 sequential
模式读取文件开始,但如果调用者在流中向后查找,它将从顺序切换到 random
。
此策略本质上识别了列式存储格式(例如 Apache ORC 和 Apache Parquet)的初始读取模式,这些模式查找文件末尾,读取索引数据,然后向后查找以选择性地读取列。与随机策略相比,第一次查找可能很昂贵,但总体过程比使用 random
策略顺序读取文件或使用 sequential
策略读取列式数据要便宜得多。
Hadoop MapReduce、Apache Hive 和 Apache Spark 都将工作写入 HDFS 和类似的文件系统。当使用 S3 作为目标时,由于使用复制和删除来模拟 rename()
,因此速度很慢。
如果提交输出需要很长时间,则是因为您正在使用标准的 FileOutputCommitter
。
您的问题似乎是性能,但这是潜在问题的症状:S3A 伪造重命名操作的方式意味着无法在输出提交算法中安全地使用重命名。
修复:使用其中一个专用的 S3A 提交器。
每个与单个存储桶交互的 S3A 客户端(作为单个用户)都有自己专用的开放 HTTP 1.1 连接池以及用于上传和复制操作的线程池。默认池大小旨在平衡性能和内存/线程使用。
您可以通过设置属性来获得更大的(重复使用)HTTP 连接和线程池以进行并行 IO(尤其是上传)
属性 | 含义 | 默认值 |
---|---|---|
fs.s3a.threads.max |
AWS 传输管理器中的线程 | 10 |
fs.s3a.connection.maximum |
HTTP 连接的最大数量 | 10 |
我们建议对执行大量 IO 的进程使用较大的值:DistCp
、Spark 工作程序等。
<property> <name>fs.s3a.threads.max</name> <value>20</value> </property> <property> <name>fs.s3a.connection.maximum</name> <value>20</value> </property>
但是,请注意,如果每个查询处理不同的 s3 存储桶集或代表不同的用户执行操作,则执行许多并行查询的进程可能会消耗大量资源。
fs.s3a.block.size
上传数据时,会以选项 fs.s3a.block.size
设置的块进行上传;默认值为“32M”,表示 32 兆字节。
如果使用较大的值,则在上传开始之前会缓冲更多数据
<property> <name>fs.s3a.block.size</name> <value>128M</value> </property>
这意味着向 S3 发出的 PUT/POST 请求更少,从而降低了 S3 限制客户端的可能性
上传大文件时,块会保存到磁盘,然后排队等待上传,多个线程并行上传不同的块。
可以通过将选项 fs.s3a.fast.upload.buffer
设置为 bytebuffer
或对于堆上存储 array
来在内存中缓冲块。
缓冲到它时很容易用尽内存;选项 fs.s3a.fast.upload.active.blocks"
存在,用于调整一次写入到 S3 的单个输出流可以排队的活动块的数量。
由于每个缓冲块的大小由 fs.s3a.block.size
的值确定,因此块越大,越有可能用尽内存。
DistCP 可能很慢,特别是如果未针对 S3 工作调整操作的参数和选项。
为了加剧这个问题,DistCP 总是对正在处理的存储桶施加重负载,这将导致 S3 限制请求。它将限制:目录操作、新数据上传和删除操作等
-numListstatusThreads <threads>
:设置为高于默认值 (1)。-bandwidth <mb>
:用于限制每个工作进程的上传带宽-m <maps>
:限制映射器的数量,从而限制对 S3 存储桶的负载。使用 -m
选项添加更多映射器并不能保证更好的性能;它可能只会增加发生的限制量。具有更高每个映射器带宽的较少数量的映射器可能更有效。
DistCp 的 -atomic
选项将数据复制到目录中,然后将其重命名到适当位置,这是复制发生的地方。这是一个性能杀手。
-atomic
选项。-append
操作;避免使用。-p
S3 没有 POSIX 样式的权限模型;这将失败。如 前面所述,对 fs.s3a.threads.max
和 fs.s3a.connection.maximum
使用较大的值。
确保存储桶使用 sequential
或 normal
fadvise 查找策略,即 fs.s3a.experimental.input.fadvise
未设置为 random
通过将 -numListstatusThreads
设置为较高的数字来并行执行列表。确保 fs.s3a.connection.maximum
等于或大于所使用的值。
如果使用 -delete
,将 fs.trash.interval
设置为 0 以避免将已删除的对象复制到回收站目录。
不要将 fs.s3a.fast.upload.buffer
切换到内存中的缓冲区。如果一个 distcp 映射器内存不足,它将失败,并且有导致整个作业失败的风险。保留默认值 disk
更安全。
可能有用的是以更大的块上传;在 HTTP 连接使用方面,这更高效,并且降低了针对 S3 存储桶/分片进行的 IOP 速率。
<property> <name>fs.s3a.threads.max</name> <value>20</value> </property> <property> <name>fs.s3a.connection.maximum</name> <value>30</value> <descriptiom> Make greater than both fs.s3a.threads.max and -numListstatusThreads </descriptiom> </property> <property> <name>fs.s3a.experimental.input.fadvise</name> <value>normal</value> </property> <property> <name>fs.s3a.block.size</name> <value>128M</value> </property> <property> <name>fs.s3a.fast.upload.buffer</name> <value>disk</value> </property> <property> <name>fs.trash.interval</name> <value>0</value> </property>
fs -rm
hadoop fs -rm
命令可以将文件重命名为 .Trash
,而不是删除它。使用 -skipTrash
来消除该步骤。
这可以在属性 fs.trash.interval
中设置;虽然默认值为 0,但大多数 HDFS 部署都将其设置为非零值,以降低数据丢失的风险。
<property> <name>fs.trash.interval</name> <value>0</value> </property>
Amazon S3 使用一组前端服务器来提供对基础数据的访问。选择使用哪个前端服务器是通过负载均衡 DNS 服务来处理的:当查找 S3 存储桶的 IP 地址时,根据前端服务器的当前负载来选择要返回给客户端的 IP 地址。
随着时间的推移,前端的负载会发生变化,因此那些被认为“负载较轻”的服务器也会发生变化。如果 DNS 值被缓存一段时间,您的应用程序最终可能会与过载的服务器通信。或者,在发生故障的情况下,尝试与不再存在的服务器通信。
而且,出于小程序时代的历史安全原因,JVM 的 DNS TTL 默认值为“无限”。
为了更好地与 AWS 配合,将与 S3 配合使用的应用程序的 DNS 生存时间设置为较低的值。请参阅 AWS 文档。
有关此示例的介绍,请参阅 HADOOP-13871。
curl
curl -O https://landsat-pds.s3.amazonaws.com/scene_list.gz
nettop
监视进程连接。当对某个特定的 S3 存储桶(或其中的分片)发出大量请求时,S3 将响应 503 “节流”响应。只要整体负载降低,就可以从节流中恢复。此外,由于在对对象存储进行任何更改之前发送,因此本质上是幂等的。出于此原因,客户端将始终尝试重试节流请求。
可以独立于其他重试限制配置节流请求的重试次数限制和尝试之间的指数间隔增加。
<property> <name>fs.s3a.retry.throttle.limit</name> <value>20</value> <description> Number of times to retry any throttled request. </description> </property> <property> <name>fs.s3a.retry.throttle.interval</name> <value>500ms</value> <description> Interval between retry attempts on throttled requests. </description> </property>
如果客户端因 AWSServiceThrottledException
故障而失败,增加间隔和限制可能会解决此问题。但是,这是 AWS 服务因客户端数量和请求速率过大而过载的标志。将数据分散到不同的存储桶中和/或使用更平衡的目录结构可能会有帮助。请参阅 AWS 文档。
使用 SSE-KMS 加密读取或写入数据会强制 S3 调用 AWS KMS 密钥管理服务,该服务带有自己的 请求速率限制。对于一个帐户,所有密钥及其所有用途的默认值为 1200/秒,对于 S3 来说,这意味着:所有使用 SSE-KMS 加密数据的存储桶。
如果你在像 distcp
复制这样的较大规模操作中看到很多节流响应,减少尝试使用存储桶的进程数量(对于 distcp:使用 -m
选项减少映射程序的数量)。
如果你正在读取或写入文件列表,如果你可以随机化列表,使其不会按简单的排序顺序处理,则可以减少 S3 数据特定分片上的负载,从而可能增加吞吐量。
S3 存储桶受到来自所有同时客户端的请求的节流。不同的应用程序和作业可能会相互干扰:在进行故障排除时考虑这一点。将数据分区到不同的存储桶可能有助于在此处隔离负载。
如果你正在使用使用 SSE-KMS 加密的数据,那么这些限制也会适用:这些限制比 S3 数字更严格。如果你认为自己达到了这些限制,你可能会增加这些限制。请参阅 KMS 速率限制文档。
如果你正在编写应用程序以通过 Hadoop API 与 S3 或任何其他对象存储一起使用,以下是一些最佳实践。
使用 listFiles(path, recursive)
代替 listStatus(path)
。递归的 listFiles()
调用可以在单个 LIST 调用中枚举路径的所有依赖项,而不管路径有多深。相比之下,在客户端中实现的任何目录树遍历都会发出多个 HTTP 请求来扫描每个目录,一直到最底层。
缓存 getFileStats()
的结果,而不是重复请求它。这包括使用 isFile()
、isDirectory()
,它们只是 getFileStatus()
的包装器。
依赖于在操作的源不存在时引发 FileNotFoundException
,而不是在有条件地调用操作之前实现自己的文件探测。
rename()
避免任何将数据上传到临时文件然后使用 rename()
将其提交到最终路径的算法。在 HDFS 上,这提供了一个快速的提交操作。对于 S3、Wasb 和其他对象存储,你可以直接写入目标,因为你知道在关闭写入之前文件不可见:写入本身是原子的。
如果源不存在,rename()
操作可能会返回 false
;这是 API 中的一个弱点。考虑在调用 rename 之前进行检查,如果/当新的 rename() 调用公开时,切换到它。
delete(path, recursive)
请记住,如果路径不存在,delete(path, recursive)
是一个空操作,因此在调用它之前无需检查路径是否存在。
delete()
通常用作清理操作。对于对象存储,这很慢,如果调用者期望立即响应,可能会导致问题。例如,一个线程可能会阻塞很长时间,以至于其他活动检查开始失败。考虑生成一个执行程序线程来执行这些后台清理操作。
默认情况下,S3A 使用 HTTPS 与 AWS 服务通信。这意味着与 S3 的所有通信都使用 SSL 加密。此加密的开销会显著降低应用程序的速度。配置选项 fs.s3a.ssl.channel.mode
允许应用程序触发某些 SSL 优化。
默认情况下,fs.s3a.ssl.channel.mode
设置为 default_jsse
,它使用 Java 安全套接字扩展实现 SSL(这是运行 Java 时的默认实现)。但是,有一个区别,在 Java 8 上运行时,GCM 密码将从启用的密码套件列表中删除。在 Java 8 上运行时,GCM 密码已知存在性能问题,有关详细信息,请参阅 HADOOP-15669 和 HADOOP-16050。需要注意的是,GCM 密码仅在 Java 8 上禁用。GCM 性能在 Java 9 中得到了改进,因此,如果指定了 default_jsse
并且应用程序在 Java 9 上运行,那么与使用香草 JSSE 运行相比,它们应该不会看到任何区别。
fs.s3a.ssl.channel.mode
可以设置为 default_jsse_with_gcm
。此选项在 Java 8 上的密码套件列表中包括 GCM,因此它等效于使用香草 JSSE 运行。
实验性功能
从 HADOOP-16050 和 HADOOP-16346 开始,fs.s3a.ssl.channel.mode
可以设置为 default
或 openssl
以启用 HTTPS 请求的原生 OpenSSL 加速。OpenSSL 使用原生代码实现 SSL 和 TLS 协议。对于通过 HTTPS 读取大量数据的用户,OpenSSL 可以提供比 JSSE 更显著的性能优势。
S3A 使用 WildFly OpenSSL 库将 OpenSSL 绑定到 Java JSSE API。此库允许 S3A 使用 OpenSSL 透明地读取数据。wildfly-openssl
库是 S3A 的可选运行时依赖项,它包含用于将 Java JSSE 绑定到 OpenSSL 的原生库。
WildFly OpenSSL 必须加载 OpenSSL 本身。可以使用系统属性 org.wildfly.openssl.path
来执行此操作。例如,HADOOP_OPTS="-Dorg.wildfly.openssl.path=<path to OpenSSL libraries> ${HADOOP_OPTS}"
。有关更多详细信息,请参阅 WildFly OpenSSL 文档。
当 fs.s3a.ssl.channel.mode
设置为 default
时,S3A 将尝试使用 WildFly 库加载 OpenSSL 库。如果它不成功,它将回退到 default_jsse
行为。
当 fs.s3a.ssl.channel.mode
设置为 openssl
时,S3A 将尝试使用 WildFly 加载 OpenSSL 库。如果它不成功,它将抛出一个异常,并且 S3A 初始化将失败。
fs.s3a.ssl.channel.mode
配置fs.s3a.ssl.channel.mode
可以按如下方式配置
<property> <name>fs.s3a.ssl.channel.mode</name> <value>default_jsse</value> <description> If secure connections to S3 are enabled, configures the SSL implementation used to encrypt connections to S3. Supported values are: "default_jsse", "default_jsse_with_gcm", "default", and "openssl". "default_jsse" uses the Java Secure Socket Extension package (JSSE). However, when running on Java 8, the GCM cipher is removed from the list of enabled ciphers. This is due to performance issues with GCM in Java 8. "default_jsse_with_gcm" uses the JSSE with the default list of cipher suites. "default_jsse_with_gcm" is equivalent to the behavior prior to this feature being introduced. "default" attempts to use OpenSSL rather than the JSSE for SSL encryption, if OpenSSL libraries cannot be loaded, it falls back to the "default_jsse" behavior. "openssl" attempts to use OpenSSL as well, but fails if OpenSSL libraries cannot be loaded. </description> </property>
fs.s3a.ssl.channel.mode
的支持值
fs.s3a.ssl.channel.mode 值 |
说明 |
---|---|
default_jsse |
在 Java 8 上使用不带 GCM 的 Java JSSE |
default_jsse_with_gcm |
使用 Java JSSE |
默认值 |
使用 OpenSSL,如果无法加载 OpenSSL,则回退到 default_jsse |
openssl |
使用 OpenSSL,如果无法加载 OpenSSL,则失败 |
命名约定是为了保持与 HADOOP-15669 的 ABFS 支持向后兼容。
随着进一步的 SSL 优化,将来可能会将其他选项添加到 fs.s3a.ssl.channel.mode
。
为了让 OpenSSL 加速工作,类路径上必须有兼容版本的 wildfly JAR。这在已发布的 hadoop-aws
模块的依赖项中没有明确声明,因为它不是必需的。
如果找不到 wildfly JAR,网络加速将始终回退到 JVM。
注意:过去 wildfly JAR 和 openSSL 版本之间存在兼容性问题:版本 1.0.4.Final 与 openssl 1.1.1 不兼容。另一个复杂之处在于,hadoop-azure-datalake
中使用的 azure-data-lake-store-sdk
JAR 的旧版本包含 1.0.4.Final 类的未着色副本,即使明确在类路径上放置了较新版本,也会导致绑定问题。
创建并初始化 S3A 文件系统实例时,客户端会检查提供的存储桶是否有效。这可能会很慢。您可以通过如下配置 fs.s3a.bucket.probe
来忽略存储桶验证
<property> <name>fs.s3a.bucket.probe</name> <value>0</value> </property>
注意:如果存储桶不存在,当对文件系统执行操作时,此问题将浮出水面;您将看到 UnknownStoreException
堆栈跟踪。
应用程序通常通过 FileSystem.get()
或 Path.getFileSystem()
从共享缓存中请求文件系统。对于每个用户,缓存 FileSystem.CACHE
将缓存给定 URI 的一个文件系统实例。对 URI(例如 s3a://landsat-pds/
)的缓存 FS 的所有 FileSystem.get
调用都将返回该单个实例。
文件系统实例是为缓存按需创建的,并且将在请求实例的每个线程中完成。这在任何同步块之外完成。一旦任务具有已初始化的文件系统实例,它将在同步块中将其添加到缓存中。如果现在缓存已经为该 URI 具有一个实例,它将缓存副本还原到该实例,并关闭刚刚创建的 FS 实例。
如果文件系统需要时间才能初始化,并且许多线程正在尝试并行检索同一 S3 存储桶的文件系统实例,则除了一个线程之外,所有其他线程都将执行无用功,并且可能会无意中在共享对象上创建锁争用。
有一个选项 fs.creation.parallel.count
,它使用信号量来限制可以并行创建的 FS 实例的数量。
将其设置为较低数字将减少浪费的工作量,代价是限制可以同时为不同的对象存储/分布式文件系统创建的文件系统客户端的数量。
例如,值为四会对 s3a://landsat-pds/
存储桶的连接器浪费实例化数量设置上限。
<property> <name>fs.creation.parallel.count</name> <value>4</value> </property>
这也意味着,如果四个线程正在创建此类连接器,则所有尝试为其他存储桶创建连接器的线程最终也会被阻塞。
考虑在运行应用程序时对此进行试验,在这些应用程序中,许多线程可能会尝试同时与同一慢速初始化对象存储进行交互。