由于 AWS 并没有像 Google 一样公开出一份 API Design Guide,所以只能根据 API 的模样去逆向工程最初的设计考量。既然上一篇介绍了很多 REST 的缺陷,那么这里也会介绍一下 AWS 是如何处理这类问题的。由于 AWS 在公有云领域无可置疑的领导地位,国内很多的公有云也是跟着 AWS 样子走的,所以如果你看国内一些公有云的 API (如果有)是长得和 AWS 很像的。
NO REST 上一篇说 Google 的设计是很极限的使用了 REST,而 AWS 作为另一个分支的极限,就一点 REST 的样子都没有了。具体作对比来说:
URL 路径里没有到资源的映射,AWS 甚至做到 URL 里根本就没有路径部分,只有域名,资源映射根本是影子都没有
没有 request body,既然路径,方法,header 都不用了,干脆 body 也不要了
AWS 这么设计也是有历史原因,毕竟 AWS 诞生的时候 REST 的概念也刚出来,不可能上来就用,而之后为了兼容性和历史统一就一直使用这种风格了。另外听公司里年级大的一些程序员说 AWS 这种风格叫 SOAP,是当年在企业级应用里十分流行的一种设计,然而作为新世纪的程序员半天也没理解什么是 SOAP,就只能照着 AWS API 的模样来猜一下了。
来看一个创建实例的 API,从这里就能明白 AWS 的设计风格了。
https://ec2.amazonaws.com/ ?Action=RunInstances &ImageId=ami-beb0caec &InstanceType=m1.large &MaxCount=1 &MinCount=1 &KeyName=my-key-pair &NetworkInterface.1.DeviceIndex=0 &NetworkInterface.1. PrivateIpAddresses.1.Primary=true &NetworkInterface.1. PrivateIpAddresses.1.PrivateIpAddress=10.0.2.106 &NetworkInterface.1. PrivateIpAddresses.2.Primary=false &NetworkInterface.1. PrivateIpAddresses.2.PrivateIpAddress=10.0.2.107 &NetworkInterface.1. PrivateIpAddresses.3.Primary=false &NetworkInterface.1. PrivateIpAddresses.3.PrivateIpAddress=10.0.2.108 &NetworkInterface.1.SubnetId=subnet-a61dafcf &AUTHPARAMS
域名 域名分为两部分,产品线名称 + 接入点域名。这里的产品线名称基本和控制台里能看到的是对应的,比如 ec2 的 API 里就包含了实例,子网,Volume,EIP 等这些资源的 API 操作。接入点一般就 amazonaws.com 尽管 AWS 在世界范围内有很多 region 但是接入点都是这一个,应该是通过 DNS 来选则离当地最近的服务器,唯二的例外一个是美国国防部另一个是 AWS 北京采用的是独立的域名接入点。可以看出 AWS API 从设计最初就是希望能够一套方法操纵世界范围内的计算资源。
此外通过产品线来划分域名也可以方便的做到流量分离,如果所有产品的 API 接入点都做成同样的,靠程序进行分离显然会比较麻烦,而且域名划分也可以很好的隔离故障域。
URL 参数 前面说过了 API 的 HTTP 部分没有路径,没有方法,没有 header 和 body,那么就只能靠 URL 参数来传递信息了。所有的 API 参数第一个部分都是 Action=xxx,可以看出来和 REST 围绕资源构造方法的思路不同,AWS API 的设计原则的中心就只有方法。既然不需要围绕资源来构造方法,那么很多 API 的设计就会直白很多。比如之前所说的在 REST 中给实例绑定 eip 这种涉及多个资源操作不好设计 API 的情况,AWS 这里就会简单很多,给实例绑定 eip 的 API 就是 Action=AssociateAddress。同样像删除实例和 List 实例这种 REST 里比较简单的设计,这里也很直白,就是 Action=TerminateInstances, Action=DescribeInstances. 由于所有参数都是通过 HTTP URL 参数传递的,习惯 JSON 传递数据的人可能会有疑问如何传递一个数组或者字典这种带嵌套的复杂数据结构。上面的例子也已经给出了,其实是有方法进行转换的比如上面创建实例 API 例子中的 private_ip 部分换成 JSON 就是: { "private_ips": [ { "primary": true, "private_ip_address":"127.0.0.1" }, { "primary": false, "private_ip_address":"172.31.1.1" } ]} 如果仔细看 API 提供的 Action,可以发现绝大部分 Action 都是复数形式的批量操作 DescribeInstances,TerminateInstances,RunInstances,只有少部分像绑定 IP 这种是单数形式的操作。这样一来 REST 中比较难支持的批量操作,这里在设计最初就考虑支持了。比较跳出 REST 的框架来看,单资源的操作也只是批量操作的一个特殊形式。
另外这样所有信息都放 URL 参数做法额外的一个好处是程序处理起来会容易一些,不必先看路径,再看方法,再看 header 最后看 body 这样从各个地方拼出完整的信息,只需要统一从参数里找就可以。权限相关的处理在这种模式下也会很方便,方法名字直接就出来了,关联的资源 id 也可以从 URL 中直接找到,不用再去解析 body 来拿一堆信息才可以做鉴权。
Name or ID 作为定位资源的唯一标识符,可以用资源的 name 也可以用 id,AWS 的设计中 API 定位一个资源必须要用 ID。用 Name 定位的好处就是 API 看起来对人类比较友好,但问题也会很多。比如需要保证名字的全局唯一性,或者保证名字在某个 namespace 下是唯一的,这就需要很多额外的成本,API 和权限的设计和实现也需要考虑 namespace 会多加一层复杂度。另外当用 name 作为唯一标示的时候,变更名称的操作也会变得复杂。
在 AWS 的体系中,名字是可以任意重复的,在 API 中 Name 只能作为过滤条件出现,是无法保证定位到唯一资源的,所有定位资源都需要用 ID。这里面也有少量例外,比如 LoadBalancer 由于会生成域名,名字本身就是全局唯一的,就可以直接用名字来定位了,而且这类资源也是没有 ID 的。一般认为 id 的问题是格式太长了对人类不友好,看起来也别扭。但其实 ID 没必要像 UUID 那么长,在 AWS 中一般资源的 ID 格式是资源名简称 + 八位十六进制字母,比如 instance 可能是 i-AB098884, volume 是 vol-FF122542, securityGroup 是 sg-AAA223333 这样的话通过资源名先做个隔离,八位十六进制本身能容纳将近 40 亿个名字,对于很多系统来说这个名字空间已经足够了。(然而最近 AWS 新的资源 id 部分已经到 16 位了,天知道他们有多少资源)
特殊字段
RequestID:AWSAPI 返回的 Response 都会带有一个 RequestID 字段,一方面客户如果出了问题可以根据 RequestID 来提工单。另一方面,后端系统也可以通过这个 RequestID 把多个系统串起来,方便定位问题
DryRun:这个字段的意思就是只做参数检查不会真正的区操作资源。有这么个参数的原因大概是因为 AWS API 比较贵…… AWS 上大部分资源都是按小时计费的,比如你在测试代码的时候调用了个创建 instance 的 API,即使你很快就释放也是要交一个小时的使用费用的。如果多做几轮测试,没事再跑个自动化测试,那钱就哗哗的流走了。而像停止删除这类操作虽然不花钱,但是却很危险,万一写代码过程中哪里错了停了所有服务器影响就大了。所以还是用 DryRun 这种来做测试,参数 OK 没啥问题,再真实的跑一下看看实际情况。