[关闭]
@levinzhang 2016-08-22T20:04:15.000000Z 字数 7331 阅读 474

一个API,多个门面?

Posted by Guillaume Laforge on Mar 13, 2016

摘要:

在Web API领域出现了一种很有意思的新趋势,那就是各种工程师和公司都在致力于为每个用户的特定需求编写专有的API。在API实现理想的设计之前,现实就会给我们重重一击,因为各种API消费者会有不同且固守的关注点。这样的话,我们可能需要对API进行对应的优化。


在Web API领域出现了一种很有意思的新趋势,那就是各种工程师和公司都在致力于为每个用户的特定需求编写专有的API。假设你的系统不仅要为iOS暴露API、为Android暴露API、为Web站点暴露API、为AngularJS应用前端暴露API,还要为各种机顶盒以及乱七八糟的移动平台或使用你API的第三方公司都暴露各自的API,在API实现理想化的设计之前,现实就会给我们重重一击,因为各种API消费者会有不同且固守的关注点。这样的话,我们可能需要对API进行对应的优化。

体验式API

在InfoQ上,Jérôme Louvel(Restlet的联合创立者和首席geek)曾经采访过Daniel Jacobson(Netflix公司edge工程团队的副主席),他们讨论了Netflix的体验式API(experience API)和临时API(ephemeral API)。Daniel的团队负责处理全球范围内所有的注册请求以及在各种设备上查找和回放视频的流量。借助体验式API的理念,Netflix创建了特殊的API,从而为给定的请求终端产生优化后的响应。通过临时API,Netflix的工程师会迭代式地转换和演化这些体验式API。

体验式API的目标在于解决Netflix所面临的问题,那就是要将它的平台扩展至六千万用户以及数十个具有不同特性的设备。通过这样的专有API,Netflix能够让用户在各种设备上都获得最好的用户体验,并且根据设备的屏幕大小优化带宽,从而实现更少的延迟和数据量消耗。这样的话,能够让Netflix的工程师进展迅速并且独立于核心的后端团队,实现了隔离,他们具有自己的版本模式,独立组织自己的部署工作。

在Netflix,有专门的团队负责这些体验式API,并不是由核心的API团队来负责每个客户端所衍生出的API。其实,并没有太多的公司具有Netflix这样的扩展性,Netflix为所有的API消费者均构建了专有的API,但是这并不意味着在你的环境中采取这种做法也是有效的。对于小型的组织,维护和演化太多的API前端可能并不是高效的做法,甚至是一种反模式,因为这种做法的成本会很高。Netflix必须要构建一个专门的API平台来支持这种方式。

微服务架构

目前的趋势是向基于微服务的架构迁移,API网关或API门面(facade)得到了复苏。架构会分散为多个小的服务,我们需要有前置的服务(front-facing service),它们会负责为用户暴露API。我们可以有很多微服务,因此也可以有多个针对用户的门面,这一点丝毫不足为奇。

网关或门面能够让用户只进行一次调用,而不必要求用户多次调用底层的微服务。这会让API消费者的工作更加简单,并且有助于实现更加智能的网关或门面,它能够充分利用缓存(因为多次调用依然还有必要),应用安全功能(认证、授权)或实现特定的规则(速度限制、IP过滤)。API提供者能够控制消费者如何使用它们的API。

网关(不管是来自供应商的还是像Netflix这样自建的)还会带来一定的附加价值,如边缘服务(edge service,公司中位于DMZ的API基础设施,能够以非常新颖和有趣的方式来使用,比如Netflix使用Zuul实现多个区的弹性)、流水线处理(pipelining)或过滤器链(帮助抽取横切性的关注点并实现企业范围内的模式)。

服务于前端的后端

Sam Newman在一篇文章中,研究了这种消费者专用API的方式,并将其称为“服务于前端的后端(Backends for Frontends,BfF)”模式。这种模式不会为所有的客户端创建通用的API,我们可以拥有多个BfF:一个用于Web前端、一个用于移动客户端(甚至一个用于iOS,另一个用于Android)等等。

SoundCloud采用了BfF模式,具有针对iOS平台、Android平台、Web站点以及嵌入式Web的API前端。与在Netflix的场景类似,如果有专门的团队负责这些前端的话,这种技术能够达到最佳的效果。如果你只有一个团队负责后端及其API的话,那么最好不要用大量不同消费者的API变种来加重他们的负担。

回到微服务方面,BfF对于迁移也是很有意义的:当从单体架构迁移至微服务时,某个BfF可以调用单体应用的功能,而其他的BfF则可以调用新的微服务,请遵循Strangler模式,按照这种模式,我们可以渐进式从遗留代码转移到新的演进方案上。

单体应用是非常复杂的,很容易积累技术债,会同时混合太多的关注因素,而微服务能够让我们每次只聚焦一个特定的关注点。但是,微服务架构也有它的不足之处。我们需要对其进行运维,它们之间需要协作,它们可能会按照与大型单体应用不同的节奏进行演化。在这样一个分布式系统中,维护所有服务之间的一致性并不简单。众多微服务之间的通信可能会引入额外的等待时间,这是服务通信的延迟所造成的。微服务的数据副本和反规范化(denormalization)也会带来一定的影响,它们可能会使数据的管理和一致性复杂化。微服务并不是免费的午餐,你可以阅读Vijay Alagarasan的文章了解微服务反模式的更多信息以及Tareq Abedrabbo所撰写的“微服务的七宗罪”。

采用体验式API或BfF的决定性因素可以归结为它们是否有专门的团队来维护。如果你们是很小的组织,只有一个团队来负责后端以及面向前端或Web的API,那么维护这么多的变种将会更加复杂(设想一下它们的维护成本),但是如果你们的组织足够大,那么多个团队能够更加容易地承担这些前端API的任务,并按照他们自己的节奏来演化这些API。

API作为团队沟通的模式

尽管公司是以团队的形式来组织的,但是我看到在越来越多的场景下,开发人员会被分为前端开发人员(Web或移动)以及后端开发人员,其中后端开发者会负责实现Web或移动设备所需的API。Web API成为了项目交付的中心点:API是一种契约,将不同的团队关联了起来,能够让他们高效协作。

如果我们开发的API要给别人使用的话,很重要的一点就是不要破坏这种契约。通常,有些框架和工具能够让我们根据代码库生成API定义——例如,通过注解驱动的方式,在端点、查询参数等内容上添加注解。但有时候,即便你自己的测试用例依然能够通过,很小的代码重构也可能会破坏契约。你的代码库没有问题,但重构可能会破坏API消费者的代码。为了更加高效的协作,可以考虑采用API契约优先的方式,确保你的实现依然能够遵循共享的协议:API定义。目前,有不少可用和流行的API定义语言,如SwaggerOpen API specification)、RAMLAPI Blueprint。你可以选择一个最适合你的。

采用API定义的方式有多项优势。首先,因为我们的实现需要遵循API定义,所以破坏兼容性会更加困难。其次,API定义对工具化非常有利。通过API定义,我们可以生成客户端SDK,这样的话API的消费者就可以将其集成到他们的项目中,实现对API的调用,甚至可以作为skeleton,用来生成服务的初始实现。我们还可以创建API的mock,这样在底层API构建的时候,开发可以调用这些mock,从而避免协调API生产者和消费者之间不同的开发周期。每个团队都可以按照自己的节奏开展工作!但是,它的好处并不局限于代码和兼容性,还涉及到文档。API定义语言还会帮助我们对API实现文档化,它们会生成很漂亮的文档,展现了各种端点、查询参数等等,并且(有时)还会提供可交互的控制台,通过它,我们可以很容易地发起对API的调用。

为不同的消费者提供不同的负载

采用API契约优先的方式当然会有所帮助,并且会提供很多的收益,但是如果不同的客户端有不同的API需求的话,该怎么办呢?具体来讲,如果我们无法奢侈到有专门的团队来负责不同的API门面的话,那么该如何让API满足所有API消费者的需求呢?

在InfoQ最近的一篇文章中,Jean-Jacques Dubray阐述了他为什么 停止使用MVC框架。在这篇文章的引言中,他阐述了移动或前端开发人员如何频繁地要求适合于他们UI需求的API,而不管底层业务理念的数据模型是什么。Dubray所描述的状态-行为-模型(SAM)模式能够很好地支持BfF方式。SAM是一个崭新的、反应型函数式的模式,它清晰地将业务逻辑与显示效果分开,进而简化了前端的架构,尤其是将后端API与视图进行了解耦。因为state和model与action和view进行了分离,所以action能够专门服务于给定的前端或根本不进行展现:这取决于你会将光标置于什么位置。我们还可以从中心后端或它们的门面中生成状态表述或视图。

Web站点或单页应用可能需要展现产品的详细视图并且还要包含它的评论,但是移动设备则很可能只展现产品的详情和它的评分,并且允许移动用户在点击的时候再去加载评论。根据UI的不同,流程、可用的行为、详情等级以及查询到的实体可能都会有所差异。通常,在移动设备上,我们都希望减少API调用获取数据的次数,这是因为连接性和带宽的限制,我们希望返回的负载恰好只包含所需的内容,没有额外的信息。但是,这一点对于Web前端来说就没有那么重要,通过异步调用的方式,我们完全可以按照懒加载的方式加载更多的内容和资源。不管是在哪种场景下,API显然都需要快速响应,并具有很好的服务等级协议。但是,当我们要为不同的消费者提供多个自定义的API时,这方面有什么可选方案吗?

特定端点、查询参数和字段过滤

一种基本的方式就是提供不同的端点(/api/mobile/movie/api/web/movie),甚至是更为简单的查询参数(/api/movie?format=full/api/movie?format=mobile),不过我们可能还有更为优雅的方案。

类似于查询参数,我们的API还可以让用户决定想要哪些字段,从而自定义返回的负载,比如:/api/movie?fields=title,kind,rating/api/movie?exclude=actors

通过使用字段过滤的方式,我们还可以确定是否要在响应中包含相关的资源:/api/movie?includes=actors.name

自定义MIME媒体类型

作为API的实现者,我们还有其他的可选方案。我们可以根本不提供任何的自定义的功能!消费者要么接受我们提供的API,要么将API包装到他们自己的门面中,在这个门面中,他们可以构建想要的自定义功能。因为我们都是非常友善的人,所以会给他们提供多个profile:在媒体类型方面,我们可以发挥创造性,根据消费者所请求的媒体类型不同,返回更加精简或更加丰富的负载。例如,如果你看一下GitHub API的话,可能会注意到这样的类型:application/vnd.github.v3.full+json

通过使用“full” profile,API会提供完整的负载和相关的实体,你还可以使用“mobile”或“minimal”变种的profile。

API消费者在发起调用时,就可以请求最适合其使用场景的媒体类型。

Prefer头信息

Irakli Nadareishvili曾经写过一篇文章,介绍了API中客户端优化的资源表述,提到了一个鲜为人知的头信息域:Prefer头信息(RFC 7240)。

与自定义媒体类型类似,客户端可以使用Prefer头信息请求特定的profile:使用Prefer: return=mobile能够让API响应自定义的负载并且会带上Preference-Applied: return=mobile的头信息。注意的是,当使用Prefer头信息的时候,需要对应地使用Vary头信息。

作为API开发人员,如果我们要负责确定支持哪种类型的负载,那么你可能会喜欢自定义媒体类型、Prefer头信息或专门的端点。如果你想要客户端能够更加灵活地确定要检索哪些字段或关联关系的话,那么你可能会更中意字段过滤或查询参数的方案。

GraphQL

与其React视图框架一起,Facebook为开发人员引入GraphQL。在这里,消费者能够完全控制会接受到什么样的负载结果,包括字段及关联关系。消费者在发起请求时,指定了返回的负载应该是什么样子的:

{
  user(id: 3500401) {
    id,
    name,
    isViewerFriend,
    profilePicture(size: 50)  {
      uri,
      width,
      height
    }
  }
}

API所响应的负载应该会像如下所示:

{
  "user" : {
    "id": 3500401,
    "name": "Jing Chen",
    "isViewerFriend": true,
    "profilePicture": {
      "uri": "http://someurl.cdn/pic.jpg",
      "width": 50,
      "height": 50
    }
  }
}

GraphQL会作为一个查询,同时还会作为回应该请求的描述。GraphQL让API的消费者能够完全控制返回的内容,提供了最高级别的灵活性。

在规范方面,还有一种类似的方式,那就是OData,它能够让我们通过$select、$expand和$value参数来自定义负载。但是OData有点落伍,处在被抛弃的边缘,前段时间Netflix和eBay已经宣布不再支持OData,而其他的参与者,如微软和SalesForce对它依然提供支持。

超媒体API

最后一个要讨论的可选方案就是超媒体API。当提到超媒体API的时候,你通常会想到那些额外的超链接会让响应变得凌乱,它们可能会让负载的大小成倍增加。对于移动设备来说,负载的大小和调用的次数确实值得关注。尽管如此,非常重要的一点在于,我们要通过HATEOAS(超媒体作为应用状态引擎)来思考超媒体,它是一个经常被忽视的REAT API核心原则。它与API所提供的功能有关。消费者可以访问相关的资源,但是这些超媒体关系所提供的链接也可以作为不同的可选profile,比如:

{
    "_links": {
        "self": { "href": "/movie/123" },
        "mobile": { "href": "/m/movie/123" },
    }
}

另外,有些超媒体方式能够完全接受嵌入式关联实体的理念。Hydra、HAL和SIREN提供了嵌入子实体的功能,所以我们在获取一部电影的信息的时候,也能以嵌入式列表的形式列出它的所有演员。

在一篇关于如何选择超媒体格式的文章中,Kevin Sookocheff给出了一个样例,展示了如何访问“玩家的朋友列表”,在这个资源访问中,嵌入了这些好友的实际表述而不仅仅是这些资源的链接,因此能够减少对每个好友资源的访问:

{
  "_links": {
    "self": { "href": 
        "https://api.example.com/player/1234567890/friends" 
    },
    "size": "2",
    "_embedded": { 
      "player": [
        { 
          "_links": { 
            "self": { "href":
                "https://api.example.com/player/1895638109" },
            "friends": { "href": 
                "https://api.example.com/player/1895638109/friends" }
          },
          "playerId": "1895638109",
          "name": "Sheldon Dong"
        },
        { 
          "_links": { 
            "self": { "href": 
              "https://api.example.com/player/8371023509" },
            "friends": { "href": 
                "https://api.example.com/player/8371023509/friends" }
            },
            "playerId": "8371023509",
            "name": "Martin Liu"
      }
    ]
  }
} 

小结

Web API所面临的消费者种类在持续增加,他们有着不同的需求。微服务架构会鼓励我们针对这些需求部署细粒度的API门面(也就是所谓了体验式API或BfF模式),但是如果你要满足太多不同消费者的需求的话,这可能会成为一种反模式,如果你只有一个很小的团队来应对所有前端的话,那么这种问题会更加严重。

一定要进行必要的权衡!在准备采用某种方式之前,你需要学习可选方案的成本并考虑你是否能够支持这些方案。创建API的不同变种是有成本的,对于实现API的人和消费API的人来说均是如此,这取决于所采用的策略。此外,在发布API给消费者之后,你可能需要重新思考和重构这个API,因为在设计阶段,你可能没有充分考虑这些特定的设备或客户需求。

如果你有专门的团队来负责这些API门面,那么这是一种可行的方案。如果你没有这么奢侈的团队的话,有一些其他的方式来为消费者提供自定义负载,而且能够避免引入复杂性,这包括一些简单的技巧,如字段过滤或Prefer头信息,也包括全面的解决方案,如自定义媒体类型或GraphQL这样的规范。

但是,我们也不一定非得这样大动干戈,可以采用一种中间方案:一个主要的、完整的API,再加上一个或两个针对移动设备的变种,这样的话,很可能就已经满足了所有消费者的需求。再考虑增加一些字段过滤功能,这样每个人都会对你的API表示满意!

关于作者

Guillaume Laforge 是Restlet的产品领导者,他是API开发的倡导者,同时还担任Apache Groovy编程语言项目的VP/主席。

查看英文原文:One API, Many Facades?

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注