浅尝 Amazon Glacier with PHP


作者:郑凯

先打个广告,我所在的公司 FunPlus 需求各种职位,我所在的组也在招后端程序员,主攻数据分析挖掘方向,配合数学系毕业的 PM 和 LOL 打到电信1钻1的 PM 来让我们的游戏产生更多价值,待遇方面我就说一句,不是每个公司都会给所有员工配马库斯椅子的,有兴趣的可以跟我联系,信箱 zhengkai@gmail.com

前几天说到存储大量数据用什么方案,觉得 10T 以下还好说,一台 PC 插一堆 2T 硬盘就完了,如果数据多了就得用专业存储设备了,最后同事 Felix 推荐用 Amazon Glacier,觉得有点意思,研究了一下,目前已经开始用了,说一下这两天的摸索过程

先解释一下 Glacier 的工作方式,我认为恰当的形容是网络版磁带机,提供比 S3 更难用、也更廉价的存储方案(事实上在 S3 的价格页上就顺便列举了 Glacier 的价格,略高于 S3 的 1/3)。随时可以上传文件,但是下载必须提前申请,等待 3-5 小时后,就可以下载了,有效期 24 小时,时间超过之后无法下载,想再下就得再重新申请。由此可见,只适合存储长期不动、偶尔会用一下的大文件。

官方 SDK 里只有 Java 和 .Net 版,但其实 AWS 现在的 SDK 已经很全了,PHP 版 SDK 是有的,不知道为什么没列进去。我是用的 Composer 版,很快就可以按着例子来测试了。

Glacier 用起来的麻烦之一是,文件不能像 S3 那样保持目录结构,不是你告诉 Glacier 文件名,而是上传成功后 Glacier 告诉你文件名(ArchiveId),这是一个 138 字节长、像 base64 一样混乱的字符串,每个文件都有这么一个串,你需要自己保存好(当然弄丢了也可以找回,下面会说)。上传前的原始文件名可以保存在 ArchiveDescription 里,当然也可以在 ArchiveDescription 里保存一些格式化过的信息(有例子是说里面存了一大堆 tag:xx 的文本)

文件上传后,可以通过 listVaults 查看传了多少文件、占了多少空间(本来 aws console 里可以看的,但是我们公司只有 DevOPS 才能看到,我没这个权限,最初找问题的时候他只能通过 QQ 截图给我看),这里有个坑就是,你查看的状态不是实时的,你只能看到几个小时前的统计数据。这个问题浪费了我几个小时的时间。

如果你想开始下载文件,这个操作称之为 initiateJob,有两种方式,Type 参数分别为 archive-retrieval 和 inventory-retrieval,前者是你提交一个 ArchiveId 申请下载那个文件,而后者其实是申请文件列表,你指定一个时间段和格式(CSV 或 JSON),可以得到一个相应格式的文件,里面装有那个时间段里所有文件的时间、文件大小、ArchiveId、ArchiveDescription 等信息,我测试的时候取了某个时间段,有 5500 个文件,最终得到的文件列表是一个 1.8MB 的 JSON 文件。

时间段看例子是个 UTC 时间,用 PHP 构造的话大体是这样

<?php
function fdate($sDate) {
  return gmdate('Y-m-d\TH:i:s\Z', strtotime($sDate));
}

$oResult = $oClient->initiateJob([
  'vaultName' => 'exampleVault',
  'Type' => 'inventory-retrieval',
  'InventoryRetrievalParameters' => [
      'StartDate' => fdate('2014-06-02'),
      'EndDate' => fdate('2014-06-04'),
  ],
]);

按我理解 inventory-retrieval 只是一种弥补手段,这些信息应该在上传的时候自己就保存,不然下载的时候先申请 inventory-retrieval 再按 ArchiveId 下载文件,光申请等待的时间就是两遍、10个小时左右。

开始申请下载后,可以通过 listJobs 观察下载状态,刚添加的 job 是 InProgress,等可以下载了就会变成 Succeeded。如果你要下载上万的文件,就真的需要提交上万个 job(看起来有点傻,我也不太确认,如果有别的方法请不吝赐教),好在 listJob 是可以翻篇的,每个 listJob 默认返回一千个结果,如果还有更多会返回个 Marker,再将其作为参数重发 listJob 就会得到下一页。可以 initiateJob 之后,就不断循环 listJob 只取 StatusCode == Succeeded 的 job

同时我也没太搞明白 listJob 是否收费,因为下载完成如果要短信提醒是需要收费的,但你可以没完没了的循环 listJob,官方的价格表上说

UPLOAD and RETRIEVAL Requests     $0.050 per 1,000 requests

但是这个 RETRIEVAL 是 initiateJob 还是也包括 listJob 就不得而知了,但是,确实没多少钱,而且多等两个小时再查也不是什么大事,这个问题也就不深究了。但肯定不包括下载行为,因为下载是 GETJOBOUTPUT

总之,不管上传还是下载,都至少需要个轻量级的数据库来自己存储额外信息,我自己用 flock 锁住一个文本文件当数据库了。gist 上放了份代码,仅供参考,地址 https://gist.github.com/zhengkai/92651a8c3353a89111a3(因为是几个文件合并到一起的,我甚至都不确定它是否能运行,仅供参考,需要确保 composer 已经把 aws/aws-sdk-php 装上了),目前在用类似的代码每天将 gzip 后大约 50G 的数据往 Glacier 上传。

尝试 PHP SDK 的过程中,为了确认我的操作是否正确,我额外找了一个命令行工具 glacier-cli,它也同样使用了一套自己的数据库来让操作更直观一些(比如你可以一直操作原始文件名,它来负责管理 ArchiveID ),Python 写的,使用前需要安装依赖库

sudo easy_install boto iso8601 sqlalchemy

另外有些功能没提到,如文件可以追加,下载的时候也可以按段下载,但是已经远离了磁带机的现代程序员大多想的是按时间切割文件,我也在考虑将已经按小时分段的文件在上传的时候按日期合并为每天一个文件,因为如果将来要下载,最小的颗粒度至少是天,甚至是按月的。分析的话,可能会同时开一堆 EC2 并行分析,因为 AWS 同网内流量是不收费的,如果硬要下载到别处分析的话,可能流量钱都比 EC2 的钱要多。