谈 API 的撰写 - 总览 里我们谈到了做一个 API 系统的基本思路和一些组件的选型,今天谈谈架构。

部署

首先要考虑的架构是部署的架构。部署的方案往往会深刻影响着系统的结构。我们需要问自己一个问题:从宏观上看,这个系统我们希望如何进行部署?

很多 API 系统是这样部署的(方案一):

(load balancer 和 nginx proxy (web server) 可能是同一个 cluster。这里逻辑上把他们划分开来。)

这是很典型的做法,所有的 API 在一套系统里部署,简单,高效,比较容易上手。然而,随着时间的推移,功能的复杂,这样的系统会越来越不堪重负。比如说我们做一个内容发布平台的 API 系统(类似于知乎日报),起初我们可能只需要内容相关的 API,渐渐地你要加入统计(tracking)相关的 API,然后我们又需要用户行为相关的 API,它们各自访问不同的数据源,行为方式也大不相同(内容相关的 API 可以做 cache,而统计和用户行为相关的 API 不能 cache)等等,当这些逻辑结构各异的 API 被揉进一个系统里时,这个系统会越来越难以维护。

所以,这样的部署方案会演进成下面的部署方案(方案二):

我们把 API 按照功能做了拆分,load balancer / nginx proxy 之后是一个个 API application。它们有自己的 load balancer / nginx proxy,可以按照每个 API application 本身的规模进行 scale up / down。比如说内容相关的 API,访问量(折合成运算量)是用户相关的 API 的 5 倍,那么,部署的时候我们可以把资源按照 5:1 的比例部署;再比如在高峰期整个系统负载过大,可以把统计 API 关掉,在 proxy 侧直接返回 503,把节省的资源配置到其他地方。

这里谈到的部署方案刻意忽略了一些细节,比如说日志如何收集和管理,服务本身的监控和信息的收集(APM)等没有提及。它们是部署方案中的关键环节,但毕竟本文不是专门讲部署的,故而忽略。

显而易见地,方案一和方案二的软件架构也会有所不同。方案二中,两个 API application 间的访问会通过 RPC(也可以使用 HTTP,但效率略低)完成,而方案一种,可能直接就是一个 function call 或者直接访问对方的数据库。方案二是一种分治的思想,把大的问题变成一条公共路径上若干相似的小问题的解决。

Pipeline

接下来的文章中,我们以方案二为蓝本,描述一个 API application 的架构。之前我们提到了这些目标:

  • A well defined pipeline to process requests

  • REST API done right (methods, status code and headers)

  • Validation made easy

  • Security beared in mind

  • Policy based request throttling

  • Easy to add new APIs

  • Easy to document and test

  • Introspection

除了后面三个,其他都和 API 处理的 pipeline 有关。我们知道,一个 API 的执行,从 request 到 response,整个 pipeline 能够划分成几个阶段:request -> pre-processing -> processing -> post-processing -> response。其中,"processing" 指的是 API 路由真正执行的代码。好的架构应该尽可能把 API 执行路径上的各种处理都抽象出来,放到公共路径(或者叫中间件,middleware)之中,为 API 的撰写者扫清各种障碍,同时能够促使 API 更加标准化。

下图是我构思的一个 pipeline,它并不是最好的,但最能反映我的思想:

我们详细说说这个 pipeline 下的每个组件:

  • throttling:API 应该有最基本的访问速度的控制,比如,对同一个用户,发布 tweet 的速度不可能超过一个阈值,比如每秒钟 1 条(实际的平均速度应该远低于这个)。超过这个速度,就是滥用(abuse),需要制止并返回 429 Too many requests。throttling 可以使用 leaky bucket 实现(restify 直接提供)。

  • parser / validation:接下来我们要解析 HTTP request 包含的 headers,body 和 URL 里的 querystring,并对解析出来的结果进行 validation。这个过程可以屏蔽很多服务的滥用,并提前终止服务的执行。比如你的 API 要求调用者必须提供 X-Client-Id,没有提供的,或者提供的格式不符合要求的,统统拒绝。这个步骤非常重要,如同我们的皮肤,将肮脏的世界和我们的器官隔离开来。

  • ACL:除了基本的 throttling 和 validation 外,控制资源能否被访问的另一个途径是 ACL。管理员应该能够配置一些规则,这些规则能够进一步将不合法 / 不合规的访问过滤掉。比如说:路径为 "/topic/19805970" 的知乎话题,北京时间晚上10点到次日早上7点的时间端,允许在中国大陆显示。这样的规则可以是一个复杂的表达式,其触发条件(url)可以被放置在一个 bloom filter 里,满足 filter 的 url 再进一步在 hash map 里找到其对应的规则表达式,求解并返回是否允许显示。

  • normalization:顾名思义,这个组件的作用是把请求的内容预处理,使其统一。normalization 可以被进一步分为多个串行执行的 strategy,比如:

    • paginator:把 request 里和 page / sort 相关的信息组合起来,生成一个 paginator。

    • client adapter:把 API client 身份相关的信息(device id,platform,user id,src ip,...)组合成一个 adapter。

    • input adapter:输入数据的适配。这是为处女座准备的。很多时候,输入数据的格式和语言处理数据的格式不一样,这对处女座程序员是不可接受的。比如说 API 的输入一般是 snake case(show_me_the_money),而在某些语言里面(如: javascript),约定俗成的命名规则是 showMeTheMoney,所以把输入的名称转换有利于对代码有洁癖的程序员。

  • authentication:用户身份验证。这个不多说,主要是处理 "Authorization" 头。对于不需要验证的 API,可以跳过这一步。做 API,身份验证一定不要使用 cookie/session based authentication,而应该使用 token。现有的 token base authentication 有 oauth, jwt 等。如果使用 jwt,要注意 jwt 是 stateless 的 token,一般不需要服务器再使用数据库对 token 里的内容校验,所以使用 jwt 一定要用 https 保护 token,并且要设置合适的超时时间让 token 自动过期。

  • authorization:用户有了身份之后,我们进一步需要知道用户有什么样的权限访问什么样的资源。比如:uid 是 9527 的用户对 "POST /topic/"(创建一个新的话题),"PUT /topic/:id"(修改已有的话题)有访问权限,当他发起 "DELETE /topic/1234" 时,在 authorization 这一层直接被拒绝。authorization 是另一种 ACL(role based ACL),处理方式也类似。

  • conditional request:在访问的入口处,如果访问是 PUT/PATCH 这样修改已有资源的操作,好的 API 实现会要求客户端通过 conditional request(if-match / if-modified)做 concurrent control,目的是保证客户端要更新数据时,它使用的是服务器的该数据的最新版本,而非某个历史版本,否则返回 412 precondition failed。

  • preprocessing hook:稍后讲。

  • processing:API 本身的处理。这个一般是 API 作者提供的处理函数。

  • postprocessing:稍后讲。

  • conditional request:在访问的出口处,如果访问的是 GET 这样的操作,好的 API 实现会支持客户端的 if-none-match/if-not-modified 请求。当条件匹配,返回 200 OK 和结果,否则,返回 304 Not Modified。304 Not Modified 对客户端来说如同瑰宝,除了节省网络带宽之外,客户端不必刷新数据。如果你的 app 里面某个类别下有五十篇文章,下拉刷新的结果是 304 Not Modified,客户端不必重绘这 50 篇文章。当然,有不少 API 的实现是通过返回的数据中的一个自定义的状态码来决定,这好比「脱裤子放屁」—— 显得累赘了。

  • response normalization:和 request 阶段的 normalization 类似,在输出阶段,我们需要将结果转换成合适的格式返回给用户。response normalization 也有很多 strategy,比如:

    • output adapter:如果说 input adapter 是为有洁癖的程序员准备的,可有可无,那么 output adapter 则并非如此。它能保持输出格式的一致和统一。比如你的数据库里的字段是 camel case,你的程序也都是用 camel case,然而 API 的输出需要统一为 snake case,那么,在 output adapter 这个阶段统一处理会好过每个 API 自己处理。

    • aliasing:很多时候,你获得的数据的名称和定义好的 API 的接口的名称并不匹配,如果在每个 API 里面单独处理非常啰嗦。这种处理可以被抽取出来放在 normalization 的阶段完成。API 的撰写者只需要定义名称 A 需要被 alias 成 B 就好,剩下的由框架帮你完成。

    • partial response:partial response 是 google API 的一个非常有用的特性(见:https://developers.google.com/+/web/api/rest/#partial-response ),他能让你不改变 API 实现的情况下,由客户端来决定服务器返回什么样的结果(当前结果的一个子集),这非常有利于节省网络带宽。

  • serialization:如果 API 支持 content negotiation,那么服务器在有可能的情况下,优先返回客户端建议的输出类型。同一个 API,android 可以让它返回 application/msgpack;web 可以让它返回 application/json,而 xbox 可以获得 application/xml 的返回,各取所需。

  • postserialization:这也是个 hook,在数据最终被发送给客户端前,API 调用者可以最后一次 inject 自己想要的逻辑。一般而言,一些 API 系统内部的统计数据可以在此收集(所有的出错处理路径和正常路径都在这里交汇)。

多说两句 response normalization,如果在这一层做得好,很多 API 里面啰啰嗦嗦处理的事情都能被处理的很干净。你只需要一套严格测试过的代码,就可以让所有的 API 在输出时大为受益。比如:

在经过 response normalization:

  • output adapter 把 camel case 变成 snake case,所以 errorName -> error_name

  • aliasing(如果定义了 error_name -> err_name)把 error_name 转换为 err_name

  • 如果客户端访问时只想要 err_name / err_msg,那么 partial response 只返回这两个域

返回结果如下:

这样的一个 pipeline 从具体的 API 的行为中抽象化出了一个 API 处理的基本流程,并且很容易在几个 hook 处进行扩展。

以上的描述基本上和语言,框架无关。回到 node 和 restify 本身,我们会发现,有些事情并不好处理。比如说,在 restify 里,一个路由的 action 往往就会直接调用 res.send() 发送数据,那么,post-processing 的各种行为如何能够注入?如果是从头开始构建一个框架,那么,pipeline 里的每个组件返回一个 Promise 或者 Observable,将其串联起来就可以了,但在 restify 里,你无法这么干。对于这样一个具体的问题,我采用的方法是使用 python 中 wraps 类似的方式:

然后通过监听 'beforeSend','afterSend' 两个事件来起到注入逻辑的效果。这样虽说是个 hack,但是是眼下可能最好的解。

在 node.js 这样的异步系统里还要注意,event emit 的监听函数如果是异步的,处理起来的顺序可能并非如你所愿,为此,我开发了一个 eventasync 库,对 node.js 的 event emitter 做 monkey patch,使其支持 async listerner。

接口

理顺了 pipeline,整个架构基本就清晰了,接下来要考虑提供一个什么样的接口让 API 的写作能够高效。restify 提供的接口:

虽然很简单,但是很难满足我们对于 pipeline 的需求,比如说,validation。如何做 validation 只能是某个 API 的作者来做决策,框架来收集这些决策信息并在 pre-processing 阶段执行。所以,我们必须在路由初始化之前收集这一信息;此外,还有很多信息,如一条路由是否需要 authentication,如何做 alias,这些信息都需要 API 的撰写者提供给框架,而框架来收集。所以,作为一个框架,我们需要一个更好的 interface 提供给 API 的撰写者。这是我的 proposal:

这个接口包含几重信息:

  • 路由接受 POST method

  • 路由的 path 是 /logout

  • 路由有一个很详细的 markdown 撰写的文档(还记得我们的需求是:easy to document 么?)

  • 其接受一个参数为 (req, res, next) 的 action function(也可以是多个)

  • 其对 body 提供一个 joi validator(除 body 外,也可以对 header,param 和 query 做 validation)

  • 使用这个 API 需要 authentication,调用完毕后要记录 audit trail

通过这样一个接口,我们把 API 系统区隔为「编译时」和「运行时」。这个接口写出来的 API,更像是一个等待编译的源文件。在 API 系统启动的时候,会经历一个「编译」的过程,把所有的 route 汇总起来,生成 restify 认识的路由形式,同时,收集里面的各种信息(比如 validator,authentication),供框架的各个 middleware 使用。

不要小看这样一个接口上的改变和「编译时」/「运行时」的区分,它除了可以让 API 的各个信息无缝地和 pipeline 对接,还能够实现我们期望的 introspection:

(通过 route 生成的 swagger 文档,供 API 使用者使用)

(通过 route 生成的 cli 文档,供 API 开发者 introspection)

相信通过这个接口,你能够更好地理解 David Wheeler 的那句:

All problems in computer science can be solved by another level of indirection.

转载:陈天 程序人

谈 API 的撰写 - 架构的更多相关文章

  1. 谈 API 的撰写 - 子系统

    在做一个系统时,有一些子系统几乎是必备的:配置管理,CLI,以及测试框架. 配置管理 我们先说配置管理.一个系统的灵活度,和它的配置管理是离不开的.系统中存在的大量的预置的属性(下文简称 proper ...

  2. 谈 API 的撰写 - 总览

    背景 之前团队主要的工作就是做一套 REST API.我接手这个工作时发现那些API写的比较业余,没有考虑几个基础的HTTP/1.1 RFC(2616,7232,5988等等)的实现,于是我花了些时间 ...

  3. 浅谈API网关(API Gateway)如何承载API经济生态链

    序言 API经济生态链已经在全球范围覆盖, 绝大多数企业都已经走在数字化转型的道路上,API成为企业连接业务的核心载体, 并产生巨大的盈利空间.快速增长的API规模以及调用量,使得企业IT在架构上.模 ...

  4. 谈一下关于CQRS架构如何实现高性能

    CQRS架构简介 前不久,看到博客园一位园友写了一篇文章,其中的观点是,要想高性能,需要尽量:避开网络开销(IO),避开海量数据,避开资源争夺.对于这3点,我觉得很有道理.所以也想谈一下,CQRS架构 ...

  5. 浅谈HTML5单页面架构(二)——backbone + requirejs + zepto + underscore

    本文转载自:http://www.cnblogs.com/kenkofox/p/4648472.html 上一篇<浅谈HTML5单页面架构(一)--requirejs + angular + a ...

  6. 浅谈大型web系统架构

    动态应用,是相对于网站静态内容而言,是指以c/c++.php.Java.perl..net等服务器端语言开发的网络应用软件,比如论坛.网络相册.交友.BLOG等常见应用.动态应用系统通常与数据库系统. ...

  7. 转:浅谈大型web系统架构

    浅谈大型web系统架构 动态应用,是相对于网站静态内容而言,是指以c/c++.php.Java.perl..net等服务器端语言开发的网络应用软件,比如论坛.网络相册.交友.BLOG等常见应用.动态应 ...

  8. AngularJS进阶(二十五)requirejs + angular + angular-route 浅谈HTML5单页面架构

    requirejs + angular + angular-route 浅谈HTML5单页面架构 众所周知,现在移动Webapp越来越多,例如天猫.京东.国美这些都是很好的例子.而在Webapp中,又 ...

  9. QPS从0到4000请求每秒,谈达达后台架构演化之路(转载)

    https://blog.csdn.net/czbing308722240/article/details/52350219 QPS从0到4000请求每秒,谈达达后台架构演化之路   达达是全国领先的 ...

随机推荐

  1. 核苷酸(evolution)

    核苷酸(evolution) 题目描述 生物课是帕特里克最讨厌的课程,没有之一. 相比做一些无聊而又无趣的遗传题,他更喜欢其他所有的科目. 包括英语. 但是今天不同.他被一个关于RNA感染DNA的题目 ...

  2. 5-Dalvik垃圾收集机制Cocurrent GC

    Dalivik垃圾回收收机制Cocurrent GC简介和学习计划 导语: 在C/C++中,开发者需要手动地管理在堆中分配的内存,但是这往往导致很多问题. 1. 内存分配之后忘记释放,造成内存泄漏. ...

  3. IPFS

    http://www.r9it.com/20190412/ipfs-private-net.html IPFS指令集中文版(一) https://www.jianshu.com/p/ce74b32d2 ...

  4. WPF 自动选择dll,以SQLite为例

    在学习sqlite的过程中,发现它的dll是区分32位和64位的,起初觉得很恼火,但是仔细看了下, 发现让程序自行选择dll其实也不是一件很麻烦的事情,如下: 1>创建一个sqlite数据 2& ...

  5. 三个div向左浮动不在同一行,向右浮动在同一行的解决办法

    前几天在写代码的时候发现了一个问题,问题的大致描述如下: 在一个大的div中,同一行有三个小的div,当三个小的div均向左浮动时,会出现换行问题,均向右浮动时却在同一行. 解决这个问题的方法是在:在 ...

  6. 解决 Mac OS X 下 IntelliJ IDEA、jEdit 等 Java 程序中文标点输入无效的方法

    Mac OS X 下基于 Java 的程序(如 IntelliJ IDEA.jEdit 等)会出现中文标点输入无效的问题,在中文输入法状态,可以输入中文字,但输入中文标点最后上去的是英文标点.查阅了相 ...

  7. FZU2187 回家种地(矩形面积并)

    矩形面积并(只覆盖一次的面积)的裸题.好久没写代码debug了我太久,太辛酸了. #pragma warning(disable:4996) #include <iostream> #in ...

  8. SHELL判断服务是不是正在运行

    使用SHELL脚本进行检查服务开启情况 #!/bin/bash #需要首先安装 yum install nmap -y #检查指定端口是否开启 function checkPortStatus() { ...

  9. springBoot 程序入口

    入口类要放在首个package 这样它能扫到所有的包 @SpringBootApplication @EnableScheduling public class App { public static ...

  10. 在Android 7.0以上PopupWindow.showAsDropDown()不起作用问题

    最近优化界面,调用PopupWindow.showAsDropDown()发现,窗口并没有在控件下方显示. 有网友说是7.0以上系统的问题,可以自定义PopupWindow,重写showAsDropD ...