亚马逊AWS官方博客

Amazon S3 深度实践系列之一:S3 CLI深度解析及性能测试

背景

作者在实际的工作当中遇到越来越多关于S3的实践问题,其中被问的最多的也是使用最广泛的工具就是AWS S3 CLI命令行;AWS S3 CLI命令行工具对大文件提供了默认的分段上传下载的能力,同时支持并发上传下载多个文件;AWS S3 CLI 命令行不仅仅提供了高级抽象的cp、sync等操作同时还提供了相对底层的s3api相关的操作以帮助客户应对各种高度定制化的应用场景。

本文通过实验帮助大家更好地理解AWS S3 CLI常用命令的底层原理及在AWS EC2上使用该命令行工具与AMAZON S3交互场景下,影响性能的几个关键要素及几个常见EC2实例类型上的上传下载测试性能情况。

本文是AMAZON S3深度实践系列之一,接着作者会带着大家从AWS CLI探索到Python boto3 S3多线程应用开发实践,再到如何利用前面学到的知识,基于AWS平台利用托管服务构建一个实用的跨区域迁移S3数据的解决方案架构探讨及实践。

基本概念

并发上传vs 分段上传

刚刚使用AWS S3命令行工具的时候,总是混淆分段上传和并发上传的含义;分段上传是指将单个大文件切分成多个小数据块进行上传,而并发上传特指有多个待上传文件或小数据块的情况下,利用多线程同时并发上传多个文件或小数据块。

如下图所示,分段上传首先将文件切分成固定数量的小数据块,再利用多线程并发上传这些小数据块,等 S3收到该文件所有的数据块之后再将它们合并成原始的文件存放在存储桶里。分段上传功能默认会帮你实现并发上传;这样做的好处,显而易见,既可以充分利用网络带宽进行文件处理,同时还可以在网络带宽有限的情况下,减小因网络抖动导致的整个大文件需要重传的问题,即当分段上传中的某个数据块因各种异常没能上传成功时,只需要重新尝试上传该数据块即可。

分段上传vs 断点续传

AWS CLI S3命令行工具默认并没有帮大家实现断点续传的功能,也就是说哪怕我们用cp或sync命令上传一个文件的时候,默认后台会做文件分片进行分段并发上传,但如果由于网络抖动导致其中某些数据块传输失败,那么整个文件又得需要重头开始上传。

但同时我们可以利用 AWS CLI s3api底层接口非常容易地实现断点续传,后面章节我们再展开探讨。

AWS CLI S3 cp命令是如何工作的?

AWS S3 cp 命令是最常用的单文件和多文件复制方法,很多人使用过该命令,但大家知道该命令是如何帮我们执行文件复制的吗?

  • 该命令有帮我们自动做分段上传和下载吗?
  • 分段上传切分文件的依据是什么?每个数据块大小是多大?
  • 这么多数据块,有多线程并发上传吗?我们可以指定线程数量吗?
  • 多个文件的上传下载是如何工作的?

下面我们通过实验来观察和研究下这些问题,整个测试环境基于如下Amazon Linux上如下版本的AWS CLI命令行:

aws-cli/1.11.132 Python/2.7.12 Linux/4.9.51-10.52.amzn1.x86_64 botocore/1.5.95

AWS EC2机型是R4.2xlarge,挂载了一个500GB的gp2测试盘

本地单文件上传场景

第一步,我们首先生成一个48MB的本地测试文件,并通过cp命令上传到S3存储桶,并通过debug开关把详细的日志记录到本地的upload.log文件中,详细命令如下:

$ dd if=/dev/zero of=48Mfile bs=1M count=48
$ aws --debug s3 cp 48Mfile s3://bjslabs/s3/ > upload.log 2>&1
$ ls -lh
总用量 49M
-rw-rw-r-- 1 ec2-user ec2-user 48M 10月 18 00:26 48Mfile
-rw-rw-r-- 1 ec2-user ec2-user 13K 10月 18 00:31 upload.log

第二步,我们通过底层的s3api命令来了解存储到S3上的对象的基本情况,如下所示,通过head-object命令我们可以了解该文件的最后修改时间,对象大小以及一个用于完整性校验的ETag值,那我们如何知道该文件是否是分段上传的呢?

$ aws s3api head-object --bucket bjslabs --key s3/48Mfile
{
    "AcceptRanges": "bytes",
    "ContentType": "binary/octet-stream",
    "LastModified": "Wed, 18 Oct 2017 00:32:56 GMT",
    "ContentLength": 50331648,
    "ETag": "\"64db0c827ecffa128fa9440d3b04ff18-6\"",
    "Metadata": {}
}

要知道该文件是否是利用了分段上传,我们只需要在以上命令中加一个参数就可以判断,如下所示,如果该文件是利用分段上传的功能,通过head-object查询第一个数据块的基本信息就可以得到该文件一共包含多少个数据块,测试文件48Mfile一共包含6个数据块(PartsCount)。

$ aws s3api head-object --part-number 1 --bucket bjslabs --key s3/48Mfile
{
    "AcceptRanges": "bytes",
    "ContentType": "binary/octet-stream",
    "LastModified": "Wed, 18 Oct 2017 00:32:56 GMT",
    "ContentLength": 8388608,
    "ETag": "\"64db0c827ecffa128fa9440d3b04ff18-6\"",
    "PartsCount": 6,
    "Metadata": {}
}

这里我们可以得知,默认情况下cp命令就会采用分段上传功能。而且从以上命令返回信息我们可以看出来,该文件第一个数据块分片大小是8388608 bytes(ContentLength)即8MB,48MB大小的文件一共被分了6个数据块,所以,可以大胆猜测默认的AWS CLI S3命令行工具默认的文件分片大小就是8MB。

第三步,我们已经判断出cp命令的默认是分段上传,接下来我们通过第一步保存的详细日志(该日志文件比较大,可以点击下载)来分析下,cp命令基本的工作流及它采用的多线程并发上传的具体情况:

通过该实验的日志,我们基本了解了cp命令的基本内部流程,因此,这里面有些参数肯定是可以通过配置来进行修改的,接下来我们来根据官方文档:http://docs.aws.amazon.com/cli/latest/topic/s3-config.html 的说明来试验下,修改相应配置,是否如我们所理解的那样运行,这几个参数是:

接下来我们修改参数并对比下,实验内容为同样大小的本地文件在不同参数条件下,通过cp上传到s3,两次运行的结果对比:

在AWS Configure配置文件中,指定S3的配置参数:

$ cat ~/.aws/config
[default]
region = cn-north-1
[profile dev]
region = cn-north-1
s3 =
  max_concurrent_requests = 5
  multipart_threshold = 10MB
  multipart_chunksize = 6MB

执行profile 为dev的上传测试命令:

$ aws --debug s3 cp 48Mfile s3://bjslabs/s3/ --profile dev > 2>&1

对比upload.log和upload2.log,我们可以看出,multipart_threshold和multipart_chunksize参数是对分段上传的影响是很好理解的,但 max_concurrent_requests参数与单个文件的分段上传的并发线程总数的关系,从日志中只能看出单文件的分段上传的并发线程总数受max_concurrent_requests参数影响,但并发线程的总数上限值还取决于文件分片后进入队列被消费者线程消耗的速度。

感兴趣的读者可以在以上实验的基础上,保持其他参数不变,只修改max_concurrent_requests参数,观察并发线程数的情况,作者在max_concurrent_requests参数值分别为8、15、20、30的情况下观察cp同样的文件的线程数情况如下:

$ aws --debug s3 cp 48Mfile s3://bjslabs/s3/ --profile dev > upload2.log 2>&1

对于单文件上传,我们经过前面的实验有了基本的认识,那我们接下来再看看cp命令在分段上传中途发生网络故障,能否实现类似断点续传的功能效果呢?

整个实验思路如下:

  •  利用dd命令产生一个26GB大小的文件
  • 在cp传送中途强行断开
  • 检查此时在S3桶里面的分片情况
  • 尝试再次上传并观察结果
$ dd if=/dev/zero of=26Gfile bs=1G count=26
$ aws --debug s3 cp 26Gfile s3://bjslabs/s3/ > upload.log 2>&1
$ Ctrl-Z(强行中止)

AWS CLI s3api提供了list-parts和list-multipart-uploads两个命令分别可以查看已经完成的分片上传及正在进行的分片上传:

$ aws s3api list-multipart-uploads --bucket bjslabs

list-multipart-uploads 命令会列出指定的bucket桶里面所有的正在进行上的分片信息,如上所示,对我们有帮助的是其中的UploadId的值,在执行list-parts命令时需要传入:

$ aws s3api list-parts --bucket bjslabs --key s3/26Gfile --upload-id OwKKv3NOfXiOq7WwdBt0vYpKGVIXxzrGkxnSwSFGv8Lpwa94xzwj4IDgPvpw9Bp1FBjqUeRf2tEtL.SMCgLPhp23nw4Ilagv7UJDhPWQ0AalwwAC0ar4jBzfJ08ee4DKLd8LroSm0R7U_6Lc8y3HgA-- > parts.info 2>&1

打开parts.info文件可以看到所有的已经上传好的分片信息,包含每个分片的最后修改时间,大小,ETag以及PartNumber:

接下来,我们看看如果再次运行同样的cp命令,会帮我们进行断点续传吗?判断逻辑有两点(1)两次的UploadId是否一致(2)同一个PartNumber的数据块的最后修改时间是否一致。先记录好目前的这两个值:

UploadId:

OwKKv3NOfXiOq7WwdBt0vYpKGVIXxzrGkxnSwSFGv8Lpwa94xzwj4IDgPvpw9Bp1FBjqUeRf2tEtL.SMCgLPhp23nw4Ilagv7UJDhPWQ0AalwwAC0ar4jBzfJ08ee4DKLd8LroSm0R7U_6Lc8y3HgA—

选取PartNumber=1的数据块,该数据块最后修改时间是:

"2017-10-18T14:43:54.000Z"

重新执行一边cp命令并记录结果:

$ aws --debug s3 cp 26Gfile s3://bjslabs/s3/ > upload2.log 2>&1
$ Ctrl-Z(强行中止)
$ aws s3api list-multipart-uploads --bucket bjslabs > processing.info 2>&1

从结果我们发现,有两个正在上传的数据分片,一个是我们前一次命令产生的,一个是最近一次cp命令被中断产生的。

$ aws s3api list-parts --bucket bjslabs --key s3/26Gfile --upload-id OwKKv3NOfXiOq7WwdBt0vYpKGVIXxzrGkxnSwSFGv8Lpwa94xzwj4IDgPvpw9Bp1FBjqUeRf2tEtL.SMCgLPhp23nw4Ilagv7UJDhPWQ0AalwwAC0ar4jBzfJ08ee4DKLd8LroSm0R7U_6Lc8y3HgA-- > parts2.info 2>&1
$ aws s3api list-parts --bucket bjslabs --key s3/26Gfile --upload-id 7P10pMiJ.Tj.xsogV7JeG99G4Ev6kV_5SqsdcEBKXzVi9Kg1SgvcWkTmay0wpB2WYsdnXtsFyofRIjOMfu9hZnh6DXmggVzSpyiKbAgw0qSyZDHVt5OdkcqpfX52uHpM5tc9BQUkIVD3dWu29xUeyg-- > parts3.info 2>&1

我们会发现执行两次cp命令对同一个文件,会帮我们保留两个UploadId及其对应的已经上传的分段数据,根据complete-multipart-upload的文档说明,我们知道,分段上传在合并分段数据的时候,是根据UploadId进行合并的,两个不同的UploadId说明AWS CLI cp命令不会智能帮我们判断已经上传的分段之后开始续传我们的文件分片即没有直接支持断点续传。

一个文件如果经过分段上传了一部分数据但没有传完的情况下,已经传完的数据块和正在进行上传的数据块占用的存储是需要收费的,因此,我们需要清除掉这些无用的数据块,一种办法是通过命令,另外一种方式可以利用AMAZON S3的生命周期管理定期自动删除掉这样的数据块。

$ aws s3api abort-multipart-upload --bucket bjslabs --key s3/26Gfile --upload-id OwKKv3NOfXiOq7WwdBt0vYpKGVIXxzrGkxnSwSFGv8Lpwa94xzwj4IDgPvpw9Bp1FBjqUeRf2tEtL.SMCgLPhp23nw4Ilagv7UJDhPWQ0AalwwAC0ar4jBzfJ08ee4DKLd8LroSm0R7U_6Lc8y3HgA--
$ aws s3api abort-multipart-upload --bucket bjslabs --key s3/26Gfile --upload-id 7P10pMiJ.Tj.xsogV7JeG99G4Ev6kV_5SqsdcEBKXzVi9Kg1SgvcWkTmay0wpB2WYsdnXtsFyofRIjOMfu9hZnh6DXmggVzSpyiKbAgw0qSyZDHVt5OdkcqpfX52uHpM5tc9BQUkIVD3dWu29xUeyg--

S3下载到本地单文件场景

该场景下,我们关注的点主要是,对于在S3桶里面的对象,cp命令都会自动帮我分段分片下载吗?

首先尝试通cp直接下载上一个章节通过cp命令上传到S3桶里面的对象:

$ aws --debug s3 cp s3://bjslabs/s3/ . > download.log 2>&1

从日志文件里面可以看出,cp命令确实是默认采用了分段下载,调用GetObject接口,在Header里面设置range值并通过多线程并发下载;似乎非常完美,但等等,我们还忘了一个场景,假如文件上传到S3桶的时候没有使用分段上传呢?我们试验一下:

  • 还是利用本地dd出来的48Mfile的文件
  • 修改AWS CLI S3的参数,将multipart_threshold改到50MB,这样对于小于50MB的文件上传就不会采用分段上传
  • cp上传该文件并确认该文件没有被分段上传
  • cp 下载,看看是否是分段下载

第一步,修改aws configure配置文件:

第二步,通过cp上传该文件,并通过head-object命令发现该文件没PartsCount 信息即没有被分片分段上传(因为前面我们设置了自动分片的最小文件大小是50MB,该文件48MB小于50MB)

$ aws s3 cp ./48Mfile s3://bjslabs/s3/48Mfile_nonparts --profile dev
upload: ./48Mfile to s3://bjslabs/s3/48Mfile_nonparts
$ aws s3api head-object --part-number 1 --bucket bjslabs --key s3/48Mfile_nonparts
{
"AcceptRanges": "bytes",
"ContentType": "binary/octet-stream",
"LastModified": "Wed, 18 Oct 2017 15:52:34 GMT",
"ContentLength": 50331648,
"ETag": "\"f6a7b2f72130b8e4033094cb3b4ab80c\"",
"Metadata": {}
}

第三步,通过cp下载该文件,并分析日志文件

$ aws --debug s3 cp s3://bjslabs/s3/48Mfile_nonparts . > download2.log 2&>1

$ aws s3api head-object --part-number 1 --bucket bjslabs --key s3/48Mfile_nonparts
{
"AcceptRanges": "bytes",
"ContentType": "binary/octet-stream",
"LastModified": "Wed, 18 Oct 2017 15:52:34 GMT",
"ContentLength": 50331648,
"ETag": "\"f6a7b2f72130b8e4033094cb3b4ab80c\"",
"Metadata": {}
}

透过日志,我们可以看到,虽然我们上传该文件是没有使用多文件上传,但利用cp命令在默认的S3参数的情况下也会自动分片分段下载我们的对象。

本地目录与S3批量上传下载场景

前面两个小节,我们通过实验深度探索了,单文件通cp操作的一些细节;本小节我们一起来看看,在本地批量上传文件到S3及从S3批量下载文件的场景。对于性能这块我们放到后面章节,本小节主要探讨:

1.    max_concurrent_requests参数对于文件并发上传下载的影响

2.    cp命令中的一些高级特性

第一步,随机生成20个100MB的测试数据文件,并准备aws configure 配置文件,修改最大并发数的参数值,保持其它参数默认,并通过不同的profile指定不同的max_concurrent_requests的参数值:

$ seq 20 | xargs -i dd if=/dev/zero of={}.data bs=1M count=100
$ vi ~/.aws/config

第二步,跑测试命令,记录详细日志:

顺序分析dev1.log到dev30.log日志文件,可以观察到Thread数量变化,最大编号分别为Thread-7,Thread-9,Thread-14,Thread-18,Thread-26,Thread-36; 为了观察最大的线程数量,作者增加测试了max_concurrent_requests分别为100,1000的结果:

由上表可见,当我们逐渐增大max_concurrent_requests参数值的时候,实际的并发线程数是随着线性增长的,直到一个合理的线程数量,此案例里面256个8MB的数据分片,AWS CLI cp命令使用到309个线程就足够速度消费添加到队列里面的上传任务。

同理,我们从S3批量下载的情况,执行如下命令:

$ aws --debug s3 cp s3://bjslabs/s3/data1000/ ./data1000 --recursive --profile dev1000 > dev1000.log 2>&1
$ aws --debug s3 cp s3://bjslabs/s3/data100/ ./data100 --recursive --profile dev100 > dev100.log 2>&1

从日志文件中分析出,max_concurrent_requests为100或1000时,cp命令并发下载的线程数最大编号为107和275。


对于批量操作,不可避免有时会有选择的上传和下载某些特征的文件,cp 命令可以通过include和exclude参数进行文件模式匹配,看以下几个例子:

通过以下命令仅仅下载所有 ”.data” 结尾的文件:

$ aws s3 cp s3://bjslabs/s3/data1000/ ./data1000 --recursive --profile dev1000 --exclude “*” --include “*.data”

通过以下命令上传当前目录中,除子目录 “data100” 之外的所有文件到存储桶:

$ aws s3 cp ./ s3://bjslabs/s3/data1000/ --recursive --profile dev1000 --exclude “data100/*”

S3到S3的复制场景

首先,我们先来看一个同区域的文件复制案例,执行如下命令并记录详细日志:

$ aws --debug s3 cp --source-region cn-north-1 s3://bjslabs/s3/data100/1.data s3://bjslabs/s3_2/ --profile dev100 > sameregion100.log 2>&1

查看日志文件,可以了解两点(1)默认也是支持分段并发的(2)每个分段是调用的upload-part-copy 接口执行复制操作的:

而如果不是S3到S3的复制的话,比如前面两个场景,源是本地目标地址是S3的话则cp命令最终会调用upload-part 方法进行上传,这两个命令有什么区别吗?

参见upload-part-copyupload-part在线文档,仔细对比如下两个命令最终的REST请求,可以看到,UploadPartCopy请求只需要将源数据指向源的对象(x-amz-copy-source)以及相应的数据范围(x-amz-copy-source-range);但UploadPart请求中必须要讲该分段的数据包含进去;也就是可以推断,S3到S3的复制不需要经过我们执行该命令的机器进行数据中转;

样例请求(UploadPartCopy):

样例请求(UploadPart):

本章小结

本章节,我们深度解析了cp命令在不同场景下的数据传输行为,基本的一些结论见下表;其中,S3到S3的复制,从底层API可以分析出不需要经过运行命令的机器进行中转,这样节约进出执行cp命令的机器流量;同时我们s3api提供了很多底层S3底层接口,基于这些接口,可以方便地在分段上传的基础上实现断点续传。

AWS S3 CLI上传下载性能测试

本章继续和大家一起来探讨下影响AWS S3 CLI进行数据传输的性能的基本因素以及实际场景下基于AWS EC2的一些数据传输性能测试情况。

同区域不同S3桶数据的复制性能测试

测试环境:

  • BJS区域
  • R4.2xlarge 跑cp命令
  • AWS CLI S3参数max_concurrent_requests为1000
  • 测试方法,脚本跑10次取平均时间

测试结果如下,时间单位是秒:

总体平均时间:(29+29+28+6+29+5+6+6+6+29)/10=17.3秒,root.img的大小为8.0GB,AWS北京区域不同桶之间的数据平均传输速度为473.52MB/s,最高速度可以达到1.6GB/s,最低282.48MB/s。

为了验证该场景下对网络的影响,我们截取了这阶段的网络方面的监控,我们观察到这段时间,该测试机的网络输出和网络输入有几个波峰,但峰值最高没超过12MB,从侧面验证了我们的前面的判断,即cp在S3到S3复制的场景下,数据不会经过命令行运行的机器转发,而是直接由S3服务直接完成:

S3桶数据到AWS EC2下载性能测试

测试环境(针对下面表格的场景一):

  • BJS区域
  • R4.2xlarge 跑cp命令,
  • EC2挂500GB的gp2 ssd磁盘
  • AWS CLI S3参数max_concurrent_requests为1000
  • 测试方法,脚本跑10次取平均时间

测试结果如下,时间单位是秒:

总体平均时间:(67+64+66+65+65+65+66+65+64+65)/10=65.2秒,root.img的大小为8.0GB,该测试场景的数据平均传输速度为125.64MB/s,下载速率比较平稳。

在继续试验之前,我们总结下,影响EC2虚机下载S3数据的速度的几个因素:

  • 磁盘的吞吐率(SSD还是实例存储还是HDD)
  • EBS带宽,是否EBS优化(EBS本身也是通过网络访问,是否有EBS优化为EBS I/O提供额外的专用吞吐带宽,否则和EC2网络共享带宽性能)
  • S3服务的带宽(通常我们认为S3服务不是性能瓶颈)

我们已经测试了R4.2xlarge的下载性能,接下来我们选择几个典型的虚机类型分别进行测试,由于测试时间跨度比较大,测试场景中的EC2操作系统层没有任何调优,测试结果仅供参考。所有场景测试都针对一块盘,进行同一个8GB的文件下载,下载10次,根据时间算平均下载速率。

通过简单的测试我们发现,

1.    虽然随着机型增大比如从R4.xlarge 到R4.4xlarge,同样是单块SSD磁盘的情况,磁盘就成为整个下载速度的瓶颈,因为R4.4xlarge EBS优化的专用吞吐量理论值可以达到437MB/s,而简单测试下来的下载速度最高到130MB/s左右;

2.    SSD磁盘在整个测试场景中,是最优的选择,而且SSD盘的大小无需到3TB就可以有很好的性能表现

3.    实例存储在没有预热(dd整个盘)的情况下表现很一般

4.    st1盘6TB左右的测试盘,下载测试性能不如500GB的SSD盘

5.    3334GB的SSD盘的表现跟比预期要差很多

6.    测试场景中EC2下载S3的平均速度没有超过150MB/s

以上表格中的虽然我们测试了不同系列的不同规格的机型,但是我们可以通过修改实例类型非常方便地切换,因此监控上我们可以看出测试时间跨度中,我们的机器资源使用情况。

整个测试过程中R4系列机器的CPU的利用率总体还算正常,在15%到60%之间浮动:

整个测试过程中500GB的gp2磁盘的写入带宽变化如下图所示:

整个测试过程中3334GB的gp2磁盘的写入带宽变化如下图所示:

整个测试过程中6134GB的st1磁盘的写入带宽变化如下图所示:

AWS S3 CLI的一些限制

S3 CLI提供的分段上传算法有些限制在我们实际使用的时候,特别要关注比如分段总大小不能超过10000,每个分段的数据块最小为5MB,分段上传最大的对象为5TB等等。详细情况请参考AWS官方文档

下一篇将要探讨和解决的问题

在了解AWS S3 CLI命令底层原理和影响基本性能的基本因素之后,我们接下来会继续来探讨,如何利用S3的底层API实现S3中的数据的跨区域可靠、低成本、高效传输。

总结

本文和大家一起深度研究了AWS S3 cp命令在各种场景中的底层实现原理,同时利用实验,总结了关于AWS EC2下载S3数据的基本性能测试结果,期待更多读者可以实践出更多的性能优化方法。随着大数据分析的蓬勃发展,存放在S3上的数据越来越多,本系列主要会和大家一起深度探讨S3数据复制迁移的最佳实践。

作者介绍:

薛军

AWS 解决方案架构师,获得AWS解决方案架构师专业级认证和DevOps工程师专业级认证。负责基于AWS的云计算方案架构的咨询和设计,同时致力于AWS云服务在国内的应用和推广,在互联网金融、保险、企业混合IT、微服务等方面有着丰富的实践经验。在加入AWS之前已有接近10年的软件开发管理、企业IT咨询和实施工作经验。