先简单介绍一下 API,就是相当于前端比如 Web 访问到后端的服务接口,这中间有一个隔离,适配给外部各端进行访问,隔离是起到安全性的考虑,还有一个协议转换的考虑。
当然,基于这一块我们还有很多其他的考虑。在饿了么初期发展阶段,我们的很多 Web API 层都是手写的,即多数应用服务后端,都自己写 Web API,单独部署,提供给前端 HTTP API 调用。
当时业务高速发展,为了快速应对,有一些业务逻辑会放在 Web API 层,甚至在 Web API 层也会访问数据库,进行数据库操作。
在 API 层直接访问数据库会导致安全性的一些问题,这个是不允许的。前端访问后端,这个 HTTP API 接口是什么风格?
有 Restful ,还有 JSON-RPC ,需要统一考虑,不然对前端开发的体验不一致。
另外 HTTP API 文档存在过时,不能反映代码实现的变化,比如代码改了,文档没有改。
最后,前端同学和后端同学同时开发,但开发的节奏可能不太一样。比如前端在进行开发,但后端可能会被插入更加紧急的项目,就不能及时完成当前项目的 API。
这样可能会延误前端的开发,产生前后端开发不同步,导致前端资源和后端资源有个相互等待,会导致开发体验的不顺畅,效率也不高。
基于这些情况,公司考虑是否可以统一形成一个 API 框架,于是我们调研了各个部门,发现他们都有这个需求,希望有一个统一的 API 框架开发,目前也有一些独立写的 Web API 层。
如下图,是调研的一些需求:
从图中,我们能看到一个很重要的功能就是 HTTP 到 SOA 的服务映射,还有认证与鉴权,比如公司的 SSO,包括用户、饿了么的一些鉴权,API 部署与运行。
API Orchestration,这个相当于是 API 的拼接和剪裁的概念,可以调用多个 API,取各个 API 返回结果的一部分,重新组合成新的结果,返回给前端。
这个应用在一个前端 API 调用,可以实际调用多个后端 API,组装一个返回结果给前端,减少前端调用后端多次,提高前端用户的体验。
另外,因为后端已经有了很多基础服务的接口,新业务开发不需要后端再提供接口,只需要在这些接口上进行组合、裁剪就可以了。
此外还有 API 文档、API 测试、Mock API、限流、反爬(就是说接口暴露在公网,存在爬虫爬取这些接口的情况,我们怎么保护接口等等)以及灰度。
我们的 API Everything 做这些事情时,把后端的 SOA 服务通过 API 的方式,安全可靠地提供给各种端(Web/APP)来使用。
调研出来以后,需要考虑以什么原则去考虑产品和系统设计。
API Everything 是一个基础框架,我们首先考虑到基础框架的稳定性是第一位。
哪怕你不能满足所有功能需求,但你很稳定,接入 API Everything 框架的各个应用系统就不会歇菜,不会出问题,服务才能在稳定的基础上提升,才有可能增加新的功能和特性,也就是说稳定压倒一切。
然后是性能,包括吞吐量(响应速度)、性能高了,就会节省硬件资源;高可用性,避免出现单点故障。还有容错性,对外各种依赖,要考虑外部依赖歇菜怎么办,怎么降级。
还有,API Everything 接了后端的应用系统,外部流量进来,不能冲击到后端应用系统。如何让这个系统更健壮,怎么保护自己,怎么保护接入的应用系统等等。
其次,在这个基础之上考虑 DevOps 怎么弄,提供接入方自助 DevOps,还有各种指标查看、监控告警、排错手段、查看 log/trace/exception、自助扩容等。
最终希望这一块做到对接入方是透明的,可以自动扩容。API Everything 框架引起的问题,由我们解决,接入应用方出现的问题,由接入方解决,要有非常清晰的边界界定。
一般问题可以自动解决,现场日志自动保留,尽可能自愈,并且接入方知道发生了什么情况。
另外,怎么让我们的研发或者应用开发端更“懒”?就是把经常要做的事情自动化。
比如接入一个应用方,要进行各种配置,比较繁琐也容易出错,那我们是不是可以进行自动化接入、自动化配置呢?
刚才也谈到代码和文档不同步怎么办,所以我们不写文档,在代码里面写文档。
比如:在 Java 代码里写 Java doc 注释,我们就把这些注释抽出来,作为 API 文档的一部分;另外,我们也提供一些标注,帮助完成文档。
用户体验也是考虑的一大因素,因为技术产品基本上由工程师直接进行开发,追求完成功能,多数没有考虑用户体验,导致用起来操作别扭。
所以在这方面要充分吸取教训,把使用者的体验考虑起来,能点一下就不用点两下,不能把技术复杂性暴露给用户去理解、去操作,让用户用起来很爽,简单不去操作是一个目标。
这个框架涉及到很多配置,散放在不同系统,我们的想法是在一个配置里面全把它搞定,不要让用户理解这个是 API Everything 框架的哪一部分管理的,要到哪个系统去操作。
另外就是满足不同的功能需求,比如接入不同的协议等,这是我们对整个产品方案在原则上的一些考虑。
生命周期
从 API 这边出发,可以看到 API 的生命周期是从 API 开发开始的,这个过程中会有文档、Mock,开发完了是管理,也就是授权谁能访问,有些是不是可以灰度。
API 管理之后是 API 网关服务,就是运行态服务,对 API 进行协议转换,比如将 HTTP 协议转换成 SOA,调用后台的 SOA 服务,最后进入 API 运维,就是对 API 进行监控管理和部署扩容。
根据产品系统设计原则,结合 API 的生命周期,我们规划了下面几个产品,如下图:
比如说开发支持这一块,就是 API Portal,运行支持这块就是 Stargate Cluster。还有质量保证,就是 API Robot,通过自动化回归测试来保证 API 的质量。
另外要考虑到在开发的过程中,怎么让前后端同步,这里面很重要的一点是怎么样 Mock API 的数据,让前后端分离开发?这就产生了 Mock Server,于是根据规划形成了这四个产品
但这四个产品之间是什么样的关系呢?这里从系统上的交互来考虑。
如上图,我们从底下看,前端的应用(比如到达圈前端),通过 HTTP 访问,到达 Nginx Cluster,然后转向 SOA 服务。
灰色的路径就是到达圈管理后台应用,灰色再上去就到达圈服务,另外还有红色这条路径,前端 URL 可以通过加入 query string 访问 Mock Server。
Stargate Cluster 收到这个 querystring,就不会发给后端 SOA 服务,会路由到指定的 Mock Server。前端会通过 Mock 的方式完成前端的开发。
API Portal 负责 API 文档这块,文档对应的是部署到哪个环境,在 API Portal 里有显示。
在饿了么有如下几个环境:
-
开发的 Alpha 测试环境,这个是提供给开发使用的。
-
Beta 环境,这个是提供给测试来验证是否可以上线的环境,只有通过了 Beta 测试,才能上线。这个 Beta 环境也用来和其他团队进行联调。
-
Prod 环境,用于线上生产。在 API Portal 上就能知道当前部署的应用、对外提供的 API 文档具体是什么。这是 API Portal 通过访问 Stargate Cluster 部署信息获得的。
API Robot 从 API Portal 中获取 API 定义,通过定义发测试请求给 Stargate Cluster。
饿了么内部服务是 SOA 架构,服务间有相互依赖的情况,需要进行测试、联调。
有时候发现我们 SOA 服务依赖对方 SOA 服务,对方还没有开发完成,我们想测试自己开发 的 SOA 服务怎么办?
这时我们就可以用 Mock Server,Mock 对方的 SOA 服务,写一下 Mock Case,完成我们自己的开发测试。
刚才说代码即文档,所以可能要规范一下怎样写代码,注释和标注写完了,就可以自动化从这些注释和标注中抽出文档,形成 API 文档。
Web API 这一块不需要手工写,目前我们自动生成对应 Web API 的代码,然后自动再部署。
部署就是监听 SOA 服务部署消息,收到了部署消息,就自动生成的 Web API 代码并且自动部署。
而 Mock,我们也是自动生成的,创建 Mock Case 的时候,就自动生成相应的数据,这些数据可以自己改。还有 API 的监控告警,每个应用接入,自动进行全链路监控。
Stargate Cluster 技术架构
如上图,这是其中一个产品 Stargate Cluster 的技术架构。
从上面看,ELESS 是我们构建系统,我们会监听它的构建消息,当有构建过来的时候,调用 base.stargate_core 服务。
该服务实现非常简单,就是把 Stargate Cluster 需要的信息保存到 MaxQ 里面,MaxQ 是饿了么自己开发的一个 MQ 产品,目前在大量应用。
最后 Stargate Cluster 运营管理服务是从 MaxQ 取消息进行处理。
为什么会有这样的考虑?因为之前我们在 Stargate Cluster 运营管理服务里经常有迭代开发,经常增加一些功能(比如异地多活),这些功能不断地迭代、增加,需要经常部署,处于一个不太稳定的情况。
我们想任何这种构建和部署消息不能丢,于是就有了 stargate_core 这个服务,这个服务非常简单,不进行功能上的迭代,保持不变,这样就比较可靠,作用就是把构建和部署消息放在 MaxQ 里头。
而 stargate 运营管理不断开发、然后重启,这个过程中至少 MaxQ 里的数据不会丢,重启完了也可以消费,继续进行部署。我们是基于变化和不变化的隔离去考虑系统可靠性的结果。
Stargate Cluster 基于 Docker 部署
谈谈为什么能自动部署?其实挺有意思的,我们这边用的是 Docker 环境。
从底下开始看,可以看到,比如 SOA 服务部署了,通过 ELESS(饿了么发布系统 ),我们获得部署消息,放到 MaxQ 里。
然后我们从 MaxQ 拿出消息,调用 AppOS(饿了么自研的 Docker 平台),启动相应的 Docker 实例。
实例上运行着这个 SOA 服务对应的 Web API 代码,Docker 实例启动时,调用 Navigator(饿了么自研的 Nginx 管理平台),将该 Docker 实例的 IP 注册到 Nginx 上,这样外部流量就打到这些 Docker 上了。
在新 Docker 成功启动之后,Stargate Cluster 就会调用 AppOS 将之前版本的 Docker 实例给销毁掉,通过 Navigator 将对应在 Nginx 上的 IP 也删除掉,这就是完成自动部署。
这是我们自动部署的一些信息,从图中可以看到左上角基本上是 SOA 服务部署的 Push Seq,下面是 PushSeq Used by Client。
这两个 Push Seq 一致,就说明 SOA 服务部署时,Stargate Cluster 将其对应的 Web API 端也自动部署了,两边用的版本(Push Seq)是一致的。同时,包括部署的一些版本信息我们也都会保存下来。
API Portal – 自动化文档
讲完 Stargate Cluster,我们再来看看 API Portal 这一块。
API Portal 在系统部署时,获取部署的源文件,自动解析这些代码,然后根据代码里面的注释和标注自动生成 API 文档,这个文档以 Swagger 方式存储。
刚开始我们以 Swagger 的原生界面去展示这个界面,前端开发不太习惯这个界面,于是我们就用前端喜欢的,即作为各种表格进行展示来给他们使用。上面的表格展示方式,就是因为这个开发出来的。
那 Swagger 原生的界面还需要吗?API Portal 上有一个功能, Try it Out(就是试一试),具体就是后端开发采用这个功能看看后端 API 吐的数据长得怎么样,不对就修改后端 API,直到满意为止。
前端也使用这个功能,看看没有实际数据又是怎么样的。Try it Out 功能就采用了 Swagger 原生的界面,后端反而比较喜欢这个页面,因此保留下来。这样前后端的用户体验,也都能满足,大家各取所需。
下面是一个 Swagger 文档界面:
Mock Server 是什么?
看看图中这个场景,SOA Client 要对 Service Provider 进行测试。
Service Provider (就是被测试的对象 Server Under Test )依赖外部的服务,比如 Server Cluster 1,但是外部的服务因为其他情况不能用,我们就用 Mock Server 来模拟 Server Cluster 1。
这样依赖问题解决了,SOA Client 对 Service Provider 就能正常进行测试了。
使用 Mock Server 还可以解决,依赖服务需要返回特定的场景,但又不好操作,这样通过 Mock Server,写不同的 Mock Case 进行返回,就比较方便。
实际情况下 SOA 环境里面有很多依赖关系,但对方的接口没有确认或者环境不好怎么办?
我们通过 Mock Server 把这些服务全部屏蔽掉,这样只需要测试我们要测试的服务就好,这对 SOA 环境解决依赖问题是挺有效的。
Mock Server—自动解析
饿了么有 Maven 私服,各个 SOA 服务之间通过 Maven 私服上的 API 进行调用。
上图是我们的 Mock Server 界面,从图中可以看到左边是输入依赖服务的接口 Maven 依赖,基本上操作就是延续之前 SOA 调用的一些流程,把外部依赖填到上面的框里头,在 Mock Server 指定依赖的接口。
于是 Mock Server 就能从 Maven 私服去拉这些依赖,自动分析它到底有哪些类,分析好这个类,包括这个类里面有哪些方法都全部显示出来。
上图右边的方法就是自动分析,黄色标识的那个方法,就是想进行 Mock,点击下右边的加号,就创建了 Mock Case。这个 Mock Case 名字就是测试任意参数。
自动生成 Mock Case
Mock Case 是说当 Mock Server 接收的数据,即请求的参数和 Mock Case 里设定的 Input 匹配,那么这个 Mock Case 里设定的 Output 就作为响应返回。
刚才创建了一个 Mock Case 叫做测试任意参数, Mock Case 的值是根据分析的 Model(数据定义),自动生成。
比如 Input "type" : Integer,我们没有加入 enum[234567] 的时候,就意味着只要是请求里包含任何整数,该 Mock Case 就会被命中,返回 Output 的内容。
如果加入了 enum [234567],那么只有请求参数里是 234567,该 Mock Case 才会命中。
Output 支持函数,上面 Preview 就可以看到执行 Output 的表达式之后是什么结果,当该 Mock Case 被命中时,这个 Preview 的结果就作为响应返回。
前后端开发分离
后端开发根据 PRD,通过在代码接口里添加标注和注释,就完成了 API 定义。
把代码 check in,构建系统知道这个变化后,然后把这个变化通知到 API Portal,就自动生成了 API 文档,后端在 API Portal 上通知前端,双方通过API Portal 讨论确认 API 文档。
前端根据这个文档,通过 API Portal 上提供的 Mock,完成前端的开发,更复杂的交互需要构造后端 API 产生不同的数据。
前端通过构造不同的 Mock Case,完成这些复杂的开发。开发完成之后,就去进行其他事情,在后端完成 API 开发之后,通知前端一起进行联调。
在以前,前后端不分离的话就会相互等待,前端开发等后端实现 API,吐出数据之后再进行,对前端开发体验不好,现在这样前端就可以一气呵成把它开发完成,不用等候后端了。
现在,后端这边也开发完了,说一个时间大家联调一下就 OK 了,这就是整个前后端开发分离的流程。
应用实践
这个流程在我们配送范围的项目里已经应用起来了,这里我们给出了一个迭代的统计,这个迭代可以在开发的过程中就看到使用的情况。
比如原来估计工时 5 天,现在 3 天就做完了,后端也节省一些时间,以前他们说,不然就等前端调用后告诉他们什么样子,或者他自己写一些测试脚本去看是什么样的数据。
这方面他就写了 API,通过 API Portal 就能看到后端 API 吐的数据是否正确,这样的话后端开发也减少了开发时间。
还有,后端以前要写 Web API 代码,还要进行部署,现在也不用写了,也不用部署了。
以前联调时间都比较长,是两天时间,因为那时联调是包括开发时间,他不确定数据对不对。
所以有些处理还没有写,等他看到数据以后再去开发出来,时间就稍微长一些。
联调的时间,只需半个小时,看这个事情通不通就好了,整个开发体验还是不错的,总体来看,整个开发时间减少了 50% 左右。
问题真的解决了吗?
有了这些产品,我们看最早提到的那些问题是不是都解决了。假如今天写业务,我们是通过统一的方式介入,把它下沉到 SOA,因此业务也不会分散到各个地方。
我们在定义 API 时,也给了一个缺省的 API 生成方式,就是使用 Json RPC,自动将方法签名等作为 URL。
另外也提供了 Mock 服务,帮助前后端开发分离,还有就是代码即是文档这一块。然而这些问题都解决了,真的解决了吗?
其实还远远没有,还有更多事情要解决:
-
全链路环节比较多,出现问题时如何快速定位?
-
故障发生时:能够自动把现场保留下来吗?能够执行基本分析,把分析的结果保存下来吗?能够自愈吗?
-
能够快照后台服务及数据,通过 Docker 环境,通过之前记录的 traffic,自动化完成回归测试吗?
-
采用 Async Web,提高性能?采用 GO?
-
当有一些业务需求:现有 API 相互组合就可以完成这个需求,还是需要开发?需要智能分析所有 API 业务属性吗?需要面向业务开发提供搜索和推荐?
我们一直在想,面向开发、测试、运维角色,如何提供一个更好的服务?也就是说怎么样更懒、更自动化。
比如在 API 的运维上,我们能不能做到对于接入用户方完全无感知?还有就是我们的 API Everything 团队,面对多个接入方同时进行,我们应怎样处理,自动化?
其实我们不希望太多重复的事情需要重复去做,而是考虑怎么通过工具把我们自己给解放出来,去做更多自动化的事情,首先解放自己,才能拯救用户。
最后我们也希望从根上去思考问题,去解决问题。