恢复Amazon SimpleDB

在解决最近的问题时,我在Amazon Web Services(AWS)平台上测试了许多备用的数据库即服务(DBaaS)。 最后,最适合我的特定用例的是一个经常被忽视的案例: SimpleDB 惊讶吗 我也是。

问题

我正在建立一种机制来为AWS EC2(亚马逊机器映像或AMI)和本地管理程序(在这种情况下为Linux KVM)创建操作系统映像。

Amazon的EC2 API允许使用用户定义的元数据标记AMI。 与其他元数据(例如时间戳)结合使用,这使用户可以识别要从中创建新EC2实例的特定图像。 一个示例查询可能是“查找具有标签Platform=Ubuntu,PlatformVersion=14.04,ImageClass=base的交叉点的最新图像”。 当我们创建新图像时(我们强烈推荐使用Packer),我们会自由地使用这些标签。

当我们的映像构建器为我们的管理程序创建KVM映像时,它将其存储在S3中。 去年11月,AWS添加了一个对象标记功能,该功能允许将任意标记与S3对象相关联,但未能包括索引功能。 没有更多的信息,从其时间戳和标记中标识唯一的S3对象的唯一方法是枚举存储桶中的每个对象-O(n)操作在计算使用率和网络往返次数方面可能非常昂贵。

显而易见的解决方案是创建和维护一个索引,类似于AWS为EC2提供的内部AMI索引。 一个问题是,我们应该使用哪种数据库技术?

拒绝的解决方案I:DynamoDB

DynamoDB是一个非常可扩展的键值数据库。 但是,这不适用于此用例。 DynamoDB查询要求用户(a)识别和查询要检索的特定键,或(b)执行扫描操作,过滤出不合格的结果,然后通过预配置的排序键对它们进行排序。

第一种查询形式与我们尝试做的完全相反:我们想从给定的标记列表中识别键(特别是S3对象URL)。 第二种查询形式很昂贵:执行扫描时,将评估数据库中的所有项目-再次执行O(n)操作。 从用户的角度来看,它的耗时较少,因为扫描和筛选过程是在服务器端(并行使用多台计算机)完成的,但仍然不是特别有效或便宜。

拒绝的解决方案II:RDS

Amazon RDS(关系数据库服务)是AWS产品组合中的另一个DBaaS。 RDS将根据您提供给其API的参数自动为您配置和维护一组SQL服务器。 支持几种SQL服务器类型,但是在我们的实验中,我们选择了MySQL。 (这里的课程同样适用于PostgreSQL或其他RDS实现。)

在MySQL中创建对象和标签表以及适当的索引是相对简单的。

objects表如下所示:

  + ---- + ------------ + ------------------------- + ----- -------------- + 
| id | bucketName | objectName | 时间戳|
+ ---- + ------------ + ------------------------- + ----- -------------- +
| 1 | 我的桶 ubuntu-14.04-base.qcow2 | 20170102T03:04:05 |
| 2 | 我的桶 centos-7-base.qcow2 | 20170203T04:05:06 |
+ ---- + ------------ + ------------------------- + ----- -------------- +

tags表如下所示:

  + ---- + ---------- + ----------------- + ---------- + 
| id | objectId | tagKey | tagValue |
+ ---- + ---------- + ----------------- + ---------- +
| 1 | 1 | 平台| Ubuntu |
| 2 | 1 | PlatformVersion | 14.04 |
| 3 | 1 | ImageClass | 基地
| 4 | 2 | 平台| CentOS |
| 5 | 2 | PlatformVersion | 7 |
| 6 | 2 | ImageClass | 基地
+ ---- + ---------- + ----------------- + ---------- +

从那里开始,在objects表的timestamp列上创建索引,并在tags表的(tagKey,tagValue)列上创建复合索引非常简单。

挑战:查询组成

编写SQL将新对象插入表中相当容易。 但是,查询它们却比我想要的要优雅一些。 当然可以,但是我觉得不是很满意的那种代码。

这是查询表(例如上面的tags表)中的高基数维度时必须实现的逻辑示例。 (此示例在Ruby中。)

  #构建FROM子句: 
#确定我们需要从中选择的表-在这种情况下,
#标签表,为每个标签别名一次(“ tags1” ..“ tagsN”)
#值。 (tags变量只是键/值对的哈希。)
tags_tables = tags.map.with_index(1)做| _,i |
“标记t#{i}”
end.join(',')
  #构建过滤器(WHERE)子句: 
#首先,加入谓词。
join_predicates = tags.map.with_index(1)做| _,i |
“ t#{i} .objectId = objects.id”
end.join('AND')
  #现在,标记谓词。 像优秀的安全小程序员一样, 
#我们正在对查询进行参数化,而不是对值进行内部赋值。
tag_predicates = tags.map.with_index(1)做| _,i |
“((t#{i} .tagKey =?AND t#{i} .tagValue =?)”
end.join('AND')
tag_values = @ source_image_tags.map {| k,v | [k,v]}。flatten
  #最后,构建语句并执行它: 
语句= client.prepare(
“ SELECT objects.bucketName,objects.objectName” \
“ FROM对象,#{tags_tables}” \
“在#{tag_predicates}和#{join_predicates}的地方” \
“按时间戳DESC LIMIT 1排序”)
 结果= statement.execute(* tag_values) 

(如果您知道这样做的一种更优雅且性能相同的方式,我很乐意收到您的来信。)

那么,即使解决方案可行,为什么我仍拒绝该解决方案?

一个看似简单的解决方案导致了脚手架的堆积

使用RDS,常识和最佳实践是将您的实例放置在VPC(虚拟私有云)中。 这样可以确保无法从公共Internet访问数据库。

诚然,此表不包含任何专有价值很高的数据。 但是可用性很重要,我想降低数据库可能被恶意参与者篡改或破坏的可能性,该恶意参与者以某种方式获取了我们的密码,或者恶意参与者会造成大量空闲连接到我们的数据库并拒绝我们的服务(MySQL具有最大并发连接限制)。

因此,我将其分配给了VPC。 好吧 不完全的。

将RDS实例放入VPC会牺牲可访问性以确保安全。 现在,我们要解决许多挑战:

Lambda的乐趣

为了使索引器正常工作,我们依赖于Amazon S3在将对象添加到图像存储桶中或从图像存储桶中删除对象时调用Lambda函数的功能。

简而言之,Lambda函数是打包脚本或Java类中的函数,遵循特定的调用约定。 您上传了代码包,然后AWS在指定条件下在其一台计算机上执行该包中的功能。

默认情况下,Lambda函数在VPC外部运行,而没有对自己VPC内部资源的任何特权访问。 但是由于索引更新程序功能需要访问RDS实例,因此我们需要在VPC中运行它们。 幸运的是,我们可以配置Lambda函数来做到这一点。

但是,在VPC中运行Lambda功能存在巨大的警告-它们无法访问公共Internet。 通常这很好,但是我们的索引器需要的不仅仅是访问RDS服务器的能力。 我们需要它来访问:

  • Amazon S3本身,获取对象标签; 和
  • AWS Key Management Service(KMS),以解密数据库的密码。

不幸的是,AWS没有为VPC内的大多数服务提供API终端节点。 他们有公共地址。 因此,我们必须找到一种方法来让Lambda函数发挥作用,也可以吃掉它:提供对RDS实例的访问, 允许其访问公共Internet以与S3和KMS进行通信。

我们最初的VPC设计相当简单:它具有一个RFC1918子网(10.0.0.0/16);它具有一个RFC1918子网。 互联网网关; 以及指向网关的默认路由。 对于EC2实例,这很好用,我们可以选中“分配公共IP”框。 但是,对于Lambda而言并非如此。

为了使Lambda函数在VPC中运行时必要的API能够正常工作,我们必须添加一些脚手架。 这包括:

  • 一个单独的“专用”子网(10.1.0.0/16);
  • 我们原始子网(10.0.0.0/16)中的NAT网关;
  • 我们新的专用子网的单独路由表;
  • 上面的路由表中的路由表条目将默认网关设置为NAT网关。

所有这些只是为了使我们的Lambda函数起作用。 再次肯定可以,但是我们的堆栈似乎变得越来越大,越来越复杂。

打破骆驼背的稻草

然后,我们意识到人们可能希望在其个人工作站上运行测试套件:如果他们可以从S3中获取KVM映像,将其转换为VirtualBox设备,然后在本地运行测试该怎么办? 这对于开发人员的生产率将非常方便。

但是问题是,我们没有将开发人员连接到VPC的简单易用的方法。 如果没有到VPC中RDS实例的网络路由,他们将无法查询索引以帮助他们找到正确的基础映像。

一种可能的选择是使用VP中的公共IP地址创建堡垒主机,然后要求开发人员设置SSH端口转发。 在这一点上,用户体验开始显得严峻。 所以我在寻找其他选择。

SimpleDB解决方案

事实证明,几乎每个人都忘记了AWS产品系列中的一个数据库服务:Amazon SimpleDB。 它不是花哨的东西,也不是为海量数据而构建的,但是它确实具有我们需要的属性:

  • 任意“列”(实际上是属性)来存储和搜索我们的键值对
  • 易于访问,但受IAM(身份和访问管理)保护
  • 类似于SQL的查询语言,但不需要我们构造标签表的叉积。
  • 低价运行:每月免费 25个机器小时; 其后每机器小时收取14美分(不包括转移成本,这也是微不足道的)

我们尝试了一下,效果很好。 还记得上面的复杂查询构建器吗? 现在看起来像这样(再次是Ruby):

  simpledb = Aws :: SimpleDB :: Client.new 
  #请参阅以下有关缺少准备好的查询支持的注释 
where_clause = tags.map {| k,v | “#{k} ='#{v}'”} .join('AND')
  resp = simpledb.select( 
select_expression:“从图像中选择存储桶名称,对象名称” \
“ WHERE#{where_clause}” \
“ INTERSECTION _timestamp不为空” \
“ ORDER BY _timestamp DESC”)

SimpleDB不像许多SQL客户端那样支持准备好的语句和占位符,但是由于SELECT是唯一可以使用它执行的类似SQL的操作,因此它固有地不受SQL注入攻击的影响。

结论

如果您需要简单的标签数据库,请不要忽略Amazon SimpleDB。 它并没有大张旗鼓,但它可能是您完成工作所需的工具。 对于小批量用例,它实际上是免费的。