通过优化 S3 读取来提高效率和减少运行时间

作者|BhalchandraPandit
译者|平川
策划|Tina
概述
本文将介绍一种提升S3读取吞吐量的新方法 , 我们使用这种方法提高了生产作业的效率 。 结果非常令人鼓舞 。 单独的基准测试显示 , S3读取吞吐量提高了12倍(从21MB/s提高到269MB/s) 。 吞吐量提高可以缩短生产作业的运行时间 。 这样一来 , 我们的vcore-hours减少了22% , memory-hours减少了23% , 典型生产作业的运行时间也有类似的下降 。
虽然我们对结果很满意 , 但我们将来还会继续探索其他的改进方式 。 文末会有一个简短的说明 。
动机
我们每天要处理保存在AmazonS3上的数以PB计的数据 。 如果我们看下MapReduce/Cascading/Scalding作业的相关指标就很容易发现:mapper速度远低于预期 。 在大多数情况下 , 我们观测到的mapper速度大约是5-7MB/s 。 这样的速度要比awss3cp这类命令的吞吐量慢几个数量级 , 后者的速度达到200+MB/s都很常见(在EC2c5.4xlarge实例上的观测结果) 。 如果我们可以提高作业读取数据的速度 , 那么作业就可以更快的完成 , 为我们节省相当多的处理时间和金钱 。 鉴于处理成本很高 , 节省的时间和金钱可以迅速增加到一个可观的数量 。
S3读取优化
问题:S3A吞吐量瓶颈
如果我们看下S3AInputStream的实现 , 很容易就可以看出 , 以下几个方面可以做些改进:
单线程读:数据是在单线程中同步读取的 , 导致作业把大量时间花在通过网络读取数据上 。
多次非必要重新打开:S3输入流是不可寻址的 。 每次执行寻址或是遇到读取错误时 , 总是要重复打开“分割(split)” 。 分割越大 , 出现这种情况的可能性越高 。 每次重新打开都会进一步降低总体的吞吐量 。
解决方案:提高读取吞吐量
通过优化 S3 读取来提高效率和减少运行时间
文章图片
图1:S3读取器的预取+缓存组件*
架构
为了解决上述问题 , 我们采取了以下措施:
我们将分割视为是由固定大小的块组成的 。 默认大小是8MB , 但可配置 。
每个块在异步读取到内存后 , 调用者才能访问 。 预取缓存的大小(块的数量)是可配置的 。
通过优化 S3 读取来提高效率和减少运行时间】调用者只能读取已经预取到内存中的块 。 这样客户端可以免受网络异常的影响 , 而我们也可以有一个额外的重试层来增加整体弹性 。
每当遇到在当前块之外寻址的情况时 , 我们会在本地文件系统中缓存预取的块 。
我们进一步增强了这个实现 , 让生产者-消费者交互几乎不会出现锁 。 根据一项单独的基准测试(详情见图2) , 这项增强将读吞吐量从20MB/s提高到了269MB/s 。
顺序读
任何按照顺序处理数据的消费者(如mapper)都可以从这个方法中获得很大的好处 。 虽然mapper处理的是当前检索出来的数据 , 但序列中接下来的数据已经异步预取 。 在大多数情况下 , 在mapper准备好处理下一个数据块时 , 数据就已经预取完成 。 这样一来 , mapper就把更多的时间花在了有用的工作上 , 等待的时间减少了 , CPU利用率因此增加了 。
Parquet文件读取更高效
Parquet文件需要非顺序读取 , 这是由它们的磁盘格式决定的 。 我们最初实现的时候没有使用本地缓存 。 每当遇到在当前块之外寻址的情况时 , 我们就得抛弃预取的数据 。 在读取Parquet文件时 , 这比通常的读取器性能还要差 。
在引入预取数据的本地缓存后 , 我们发现Parquet文件读取吞吐量有明显的提升 。 目前 , 与通常的读取器相比 , 我们的实现将Parquet文件读取吞吐量提升了5倍 。