当我在2016年开始在WeWork工作时,我很快发现整个架构完全是一团糟......在我设法留在那里的18个月中,我们修复了一大堆混乱。
没有API被记录下来,所以人们只是为他们需要的任何资源建立一个新版本。如果/v2/users ,而且GET很好,但POST需要改变,他们会做一个POST /v3/users ,让大家永远使用GET /v2/users 。或者反之亦然。这一切都没有模式。在同一个客户端代码库中,你可能会遇到一个v1、v2、v3和v6的端点。
API是根据团队认为一个客户需要的东西建立的,但通常最后对其他客户来说是无法使用的,所以看到一个只有一两年历史的服务的v6版本也不是没有。
每个API(和API版本)都会使用它喜欢的任何数据格式和错误格式,这意味着你可能会在同一个代码库中遇到多种错误格式。客户看到 "错误。[对象]"是很常见的。
各种不同的认证系统被广泛使用,其中大部分是非常不安全的,有时端点会意外地没有应用认证。
API经常在2-5秒内做出反应,10秒也不罕见,因为有人说多次HTTP调用是不好的--这意味着巨大的JSON有效载荷与每一个切身相关的信息都被塞进了一次调用。
任何地方都没有HTTP缓存,所以页面加载总是很慢,上游依赖的性能峰值总是比其他地方更明显。
没有人在任何地方使用单一的超时,所以任何服务变慢都会影响到与它对话的任何客户端。
A与B交谈,与C交谈,与D交谈,与E交谈,都是完全同步的。
50多个服务和应用程序都依赖于同样的两个单体,而这两个单体也都依赖于对方,所以如果其中任何一个单体出现问题,它们都会启动一个死亡螺旋,并导致其他所有的事情。
所有的东西都是最低质量的RESTish API,没有一个真正的REST API,然而很多人都确信REST是问题所在。有些人想用GraphQL重写一切,有些人想用所有的gRPC...
同一个客户端的网络版和移动版充满了重复的逻辑,会对同一个用户显示不同的选项,因为有人忘记在if语句中添加第8个条件,没有共享逻辑。
所以...... 😳
有很多工作要做,但把这些工作按正确的顺序进行是很重要的,因为基本上都是单独工作,有80-150名开发人员,他们都只关注紧迫的最后期限,大多不关注质量。你如何选择什么时候做,以什么顺序做?
由于REST是HTTP上的状态机的概念(HATEOAS在这方面很出色),使用实际的REST会解决他们的许多客户端逻辑不匹配的问题。
HTTP/2可以解决他们基于HTTP/1的对"越线 "的担心。
API的演进将有助于避免v13的微小的无稽之谈。
最后,这就是我们的方法:
- 记录现有的混乱局面
- 阻止流血事件发生
- 排出沼泽
- 创建一个风格指南
1.记录现有的混乱局面
我四处询问对使用OpenAPI记录API感兴趣的人。其理由是,你很难解决你看不到的问题,而且如果你不知道哪些API在使用哪些数据格式、版本管理方案、哪些数据看起来像什么,如果你在代码中乱搞或嗅探数据包,那就很难。
有几个团队因为各种原因而选择了OpenAPI。有些人需要文档,但不确定如何去做,有些人认识到需要写新的代码的荒谬性,因为没有人记得旧的代码是如何工作的,但许多人认为这是毫无意义的忙碌。
不管怎么说,我们让一群人创建了OpenAPI描述,从Google Docs中的零散笔记中转换东西,从过时的Postman集合中转换,从集成测试中构思东西,有些人在他们的Java应用中洒下注释,各种各样的。
我们首先挑选了最大的、最重要的、最常用的API,最终其他人也参与进来,想要 "做正确的事情"。
对于有多个API版本的团队,我建议记录他们的API的最新版本,对于有奇怪的方法+资源版本的API (GET /v3/users &POST /v2/users) ,他们应该记录所有最新的方法。
我们的目标不是要立即解决很多问题,而且很容易陷入让一切都变得可爱的困境,但我们必须保持专注,只记录现有的混乱。
为什么呢?好吧,这些问题都已经存在了几个月甚至几年了,在文档方面开始行动,至少可以减缓新版本出现的数量。
当足够多的团队开始使用OpenAPI时,我开始了下一个阶段。
2.控制出血量
我在NewRelic生活了几个月。它无处不在,但完全没有得到充分利用。你打开的每一个API显然都在挣扎,但没有人去看,当他们这样做时,他们看的是平均时间而不是百分比,所以一个看起来不错的应用实际上在挣扎,但没有人看到。没有设置警报。
我在WeWork品牌的HTTP客户端中嵌入了超时逻辑,这些客户端可以直接使用,也可以通过各种SDK使用。起初,最大超时被设置为20秒,因为有一些API端点确实需要20秒的时间来响应。我们修复了弹出的错误,然后将其调整为15秒。然后,我们修复了任何达到该阈值的东西,并将其降低到10秒。这一步花了很长的时间。
从那以后,我挑选了两个最大的上游API,并开始在它们上面工作。有些人说它需要用gRPC重写,有些人建议GraphQL是拯救它的唯一方法,但这个RESTishHTTP/1的巨型有效载荷的混乱有更大的问题。从表面上看,最慢的三个端点是GET /v1/users/{id} 、GET /v2/users/{uuid} 和GET /v3/users/{uuid} ,它们都有自己独特的性能挑战。
与Tom Clark合作--他是Ruby和Postgres优化方面的邪恶天才--我们想出了100种不同的改变,从DB索引到Ruby对象分配和垃圾收集的一切都在这里和那里大获全胜。几周后,我们把最严重的违规端点从2s-10s的范围内降低到1s以下。
这当然会对整个生态系统产生连锁反应。由于生态系统中有如此多的循环引用,让A更快意味着B和C更快,而看到A与B和C对话,意味着A也更快了!"。
NewRelic显示JSON序列化相当慢(被巨大的payloads夸大了),所以人们开始建议切换到gRPC,因为 "Protobuf更快"。这可能有一些好处,但升级所有客户端应用程序的工作将是荒谬的。我们把JSON序列化器换成了Oj,在没有任何客户端需要做任何改变的情况下,我们缩短了大量的时间。
我们做的另一个改变是制作更小、更有针对性、可缓存的端点。我们做了一些你可以点击的端点,而不是包含公司、会员资格、地点和该地点的其他会员资格的用户...
/v3/users/{uuid}/v3/users/{uuid}/profile/v3/users/{uuid}/locale
第一个端点保持不变,充满了无限的信息,但我们增加了部分信息,让那些只想得到一些信息的人能够得到这些信息。这不是一个理想的方法,但它意味着客户可以直接添加?partials=foo ,以获得巨大的性能改进。
这个地区性的端点解决了另一个巨石的性能问题。B会在/users/{uuid} 上点击A来找出locale (en-GB,pt-BR),然后得到489657行JSON,其中一些需要回调到B。这种自我依赖性意味着如果任何一方开始变慢,他们都会崩溃,而这是毫无理由的。将locale信息转移到它自己的端点*,可以加快事情的进展,并*避免了自我依赖。
然后,我们还在这些端点上插入了Cache-Control headers,轻松地在客户端和Fastly上提供HTTP Caching,这一点已经存在,只是被大多数API忽略了。这加快了每一个在HTTP客户端启用HTTP缓存中间件的客户的速度,他们只需改变一行即可。
从字面上看,整个公司最慢的(也是最重要的)API,比下一个最快的API更快,这引起了一些人的注意,这有助于接下来的步骤。
3.排除沼泽
我的希望是,只记录最新的版本将意味着旧的API的客户会开始升级到后来的版本,但这并没有像我希望的那样迅速发生。
我希望API团队能够控制他们的时间。与其在3、4个版本的API中修复错误,不如制定一个 "两个全球版本 "的政策,让v5版本尽快淘汰v1-v4版本并删除v1-v3版本。
以NewRelic为指导,我发现有多个版本的API,其中旧版本会给整个应用程序带来不稳定问题。上面提到的API有v1和v2的废弃版本,使用rails-sunset的日期不同:
# config/initializer/deprecations.rb
SUNSET_MILESTONES = {
drain_the_swamp: DateTime.new(2018, 2, 1),
v2_needs_to_go: DateTime.new(2018, 4, 1),
}
# app/controllers/users_controller.rb
class UsersController
# Deprecate all methods and point them to a blog post
sunset SUNSET_MILESTONES[:drain_the_swamp], link: 'http://example.com/blog/get-them-foos-outta-here'
end
我们积极地废止了v1版本,并与唯一仍在使用它的客户合作。然后我们开始淘汰v2。一直以来,我都在告诉其他API团队负责人做同样的事情,并在我们的SDK中加入Sunset头的嗅探器,这样客户就会开始在他们的日志和测试套件中看到警告,即使他们不知道Sunset 头是什么。
减少API团队问题的表面积意味着他们有更多的时间来做下一步的工作。
4.创建一个风格指南
当你有一个糟糕的API生态系统,有不一致的命名,重叠的术语(账户意味着5种不同的东西),不一致的数据格式,以及无数的其他问题,你需要教育人们如何使API更好。我不会告诉公司的每个人都去读我的书(虽然我确实看到它放在几个桌子上),但我真的很努力地去教育,我成立了一个 "API公会",对团队进行各种培训,与团队领导一对一地讨论问题发生的原因,仔细研究事后报告,找出可以避免的方法,然后把这些都写在一个巨大的风格指南中。
风格指南在Gitbook中开始是一个非常有观点的 "如何在WeWork最好地构建API "的指南。它没有告诉人们所有方法的利弊,并希望人们能做出一个好的决定,而是说 "做X,也许Y,因为这些原因",因为两种方法比无限的独特方法更好。
归纳起来,它说了各种事情,比如:
- 错误格式必须是RFC 7807或JSON:API错误。
- 分页应该是基于光标的,而不是基于页面的。
- 版本管理必须是 "URL中的全局 "或 "API演变"。
- GET方法必须声明其可缓存性(即使是不可缓存的)。
- 内部API可以是任何你想要的范式(gRPC,GraphQL),但部门间或公共API必须是REST。
- 强烈建议在任何新版本中使用JSON:API,但是...。
- 如果使用JSON:API,避免过度依赖
?include=,而偏向于HTTP/2和 "链接"(HATEOAS!)。
然后,我开始着手编写自动化工具,尽可能多地嗅出这些信息,创建了一个名为 "Speccy "的工具,现在已被Spectral取代。
自定义规则集可以用一个简单的DSL创建,当DSL不够用时,你甚至可以用JavaScript创建自定义函数,这意味着可以制定这样的规则:
- 这个端点缺少一个
Cache-Controlheader。 - 这个API描述里有三个不同的版本,请废止最老的版本。
- 我们注意到
?page=,请使用基于游标的分页来代替。 - 这个端点没有列出安全机制。
- 错误的内容类型是
application/json,请使用RFC 7807迁移到application/problem+json。 - 这个JSON响应有300个属性,你能不能把它精简到50个或更少。
所有这些都向在持续集成平台上启用该工具的人发出了错误和警告,这很棘手,因为该API已经在生产中了。我想提前解决更多的问题。
人们会提交他们的新API或新版本的计划,然后自动风格指南会提供一堆反馈,我们就可以问更多有趣的问题,比如:"这些数据来自哪里?
这些数据是从哪里来的?本地数据库还是......你每小时从另一个服务中 "同步 "数千条记录并进行投票?请不要这样做,你会害死它的
在这里了解更多关于自动化风格指南的信息。
这就是我在过去几年里一直在谈论API设计优先的很多原因。我们不得不把很多东西砍在一起,以创建一个涵盖文档、模拟、自动化的工作流程,并在设计时给设计师以反馈。幸运的是,这一切现在都更容易做到了,因为在WeWork扭转局面之后,我加入了Stoplight,重新打造整个API设计优先的工作流程和使之成为可能的工具。
在花时间建立复杂的代码库之前,对这些计划进行规划和推理,并确信它们不会在开发和原型设计阶段在人们没有注意到的情况下发生变化,这确实有助于掌握一个混乱的生态系统,但要做到这一点需要大量的工作。
总结
有很多API设计者和开发者谈论的东西,它们都能解决很多问题,但知道如何和何时使用它们总是很重要:
- API描述(OpenAPI、JSON Schema等)
- 标准API格式
- HATEOAS (REST)
- API的演变
- 断路器
- HTTP/2
- HTTP缓存
- 架构图(GraphQL
- gRPC
在上述情况下,这些东西究竟有多大的帮助,你推举应用这些东西的顺序是什么?
有时gRPC和GraphQL绝对能比RESTish或REST APIs做得更好,但远没有一些人认为的那么频繁。小心那些想重写一切的人。对性能进行改进并发展现有的API会给你带来许多同样的好处,而不会把婴儿和洗澡水一起扔掉。因为你不知道如何使你现有的API工作得更好,而把改变和广泛的学习强加给客户,是一种自私、缓慢和昂贵的行为。
在一个主要是RESTish(HTTP APIs)的公司,我通常建议你尽快在Richardson成熟度模型的高处工作,获得HTTP缓存、"HTTP状态机"(HATEOAS)和进化是解决很多问题的巨大胜利,但这不是我在完全缺乏文档的情况下首先跳进去的事情。
我宁愿看到糟糕但有文档的API,而不是没有人知道如何使用的伟大API。当然,随着时间的推移,通过类型系统(JSON Schema)和链接(HATEOAS),一个伟大的API被很好地记录下来(和自我记录)是最理想的,但这是一步一步来的。
利用HTTP是一个智能分层系统的事实,照亮当前的混乱局面,并慢慢改善它。你可以在不改变API的情况下增加对HTTP/2的支持,然后随着时间的推移,将端点分割成更小的端点,并在其上设置缓存控制头,客户可以在自己的时间内使用这些带有缓存控制的小端点,而不必重写一切,使用完全不同的工具。
无论你做什么,都要建立一个风格指南,集中精力以最快的速度删除旧的代码,并确保新的API/版本在你开始工作之前就已经设计好了。如果你想知道这一点,请在这里阅读关于 "代码优先 "与 "设计优先 "的所有内容,也许你可以一次就把一个完全破碎的生态系统解体。