一篇文章帮你梳理清楚API设计时需要考虑的几个关键点
微服务架构提倡有许多职责单一的小服务组成,这些服务之间互相交互。然而这就造成了一系列的问题,比如:服务之间如何发现彼此?是否采用统一的协议?如果一个服务无法与其他服务通信会怎样?我会在接下来的内容里讨论部分相关话题。
通信协议
随着服务数量越来越多,在服务间使用标准化通信方法愈加重要。由于服务不一定使用相同语言编写,通信协议的选择必须不依赖具体语言和平台。此外还要同时考虑同步和异步通信。
首先,聊聊传输协议。
HTTP是同步通信的最佳选择。HTTP客户端几乎已得到所有语言支持,很多云平台都内建了HTTP负载均衡器,该协议本身内建了用于缓存、持久连接、压缩、身份验证以及加密所需的机制。最重要的是,围绕该协议有一个稳健成熟的工具生态体系可供使用:缓存服务器、负载均衡器、优秀的浏览器端调试器,甚至可对请求进行重播的代理。
协议较为繁琐是HTTP的不足之一,它需频繁发送纯文本头字段(Header),并频繁建立和终止连接。相比HTTP生态系统已经带来的巨大价值,我们可以辩解说接受这些不足是一个合理的权衡。然而,时下已经有了另外一个更好的选项:HTTP/2。通过对头字段进行压缩并用一个持久连接实现多路复用请求(Multiplexing request),该协议有效解决了上述问题,同时维持与老版本客户端的向后兼容性。HTTP目前依然实用,未来一样很好用。
话虽如此,如果已达到一定规模,通过降低内部传输的开销可对底线造成较显著的改善,那么也许更适合用其他传输方式。
对于异步通信,需要实施发布订阅模式。为此有两个主要方法:
使用消息代理(Broker):所有服务将事件推送至该代理,其他服务可订阅需要的事件。这种情况下将由消息代理定义所用传输协议。由于一个集中化的代理很容易造成单点故障,一定要确保此类系统具备容错性和横向伸缩能力。
使用服务所交付的Webhook:服务可暴露出一个供其他服务订阅事件使用的端点,随后该服务会将事件以Webhook形式(例如在主体中包含序列化消息的HTTP POST)提供给已订阅的目标服务。此类Webhook的交付应由服务所管理的异步工作进程发送。这种方式可避免单点故障并获得固有的横向伸缩能力,同时这样的功能也可直接构建在服务模板中。
企业服务总线(ESB)或消息传递设施呢?
一个重量级的消息传递基础设施的存在通常会鼓励将业务逻辑脱离服务本身而进入消息层。这种做法会导致服务的内聚性降低,并增加了额外一层,从而会降低服务内聚力,并增加了额一层,随着使用时间延长可能无意间导致复杂度逐渐提高。与一个服务有关的任何业务逻辑都应属于该服务,并由该服务的团队负责管理。强烈建议坚守智能的服务+哑管道这样的原则,以确保不同团队维持自治力。
接着再谈谈序列化格式。
这方面有两个主要的竞争者:
JSON:RFC 7159定义的一种纯文本格式。
Protocol Buffers:谷歌创建的一种基于二进制连接格式的接口描述语言。
对于“服务异常”的定义
正如需要自动化的监控和警报机制,确定所有服务有统一的异常定义,也是一个好主意。
对HTTP传输协议来说这一点很简单。服务通常可生成200、300以及400系列的HTTP状态代码。任何500错误代码或超时通常可认定服务出现故障。这些代码也可用于反向代理和负载均衡器,如果这些组件无法与后端实例通信,通常会抛出502(Bad Gateway)或503(Service Unavailable)错误。
API的设计
好的API必须易用且易理解,可在不暴露底层实现细节的情况下提供完成任务所需的信息,这些信息数量恰恰满足需求,不多不少。同时API的演化只会对现有用户造成最少量影响。API的设计更像是一种艺术而非科学。
由于已选择HTTP作为传输协议,为了释放HTTP的全部潜力,还要将HTTP与REST配合使用。RESTful API提供了资源丰富的端点,可通过GET、POST以及PATCH等动词操作。我之前写的一篇有关RESTful API设计的文章详细介绍了对外API的设计,这篇文章的大部分内容也适用于微服务API的设计。
但是为什么服务API必须是面向资源的?
这样可以让不同服务的API实现一致性并更简洁。借此可通过更易于理解的方式检索或搜索内容,无须寻找修改资源某一特定属性所需的方法,可直接针对资源使用PATCH(部分更新)。这样可减少API上的端点数量,有助于进一步降低复杂度。
由于大部分现代化公开API都是RESTful API,因此有丰富的工具可供使用。例如客户端库、测试自动化工具,以及自省代理(Introspecting proxy)。
服务发现
在服务实例变化不定的环境中,用硬编码指定IP地址的方式是行不通的,需要通过某种发现机制让服务能相互查找。这意味着对于到底有哪些可用服务必须具备“权威信息来源”。此外还要通过某种方式借助这个权威来源发现服务实例之间的通信,并对其进行均衡。
服务注册表
服务注册表可以作为信息的权威来源。其中包含有关可用服务的信息,以及服务网络位置。考虑到该服务本身的一些关键特质(是一种单一故障点),该服务必须具备极高容错能力。
发现和负载均衡
创建可用的注册表只解决了问题的一半,还需要实际使用这种注册表才能让服务以动态的方式相互发现!此时主要有两种方法:
智能服务器:客户端将请求发送至已知负载均衡器,负载均衡器可通过注册表得知可用实例。这是一种传统做法,但可用于通过负载均衡器端点传输的所有流量。服务器端负载均衡器通常是云平台的标配。
智能客户端:客户端通过服务注册表发现实例清单并决定要连接哪个实例。这样就无须使用负载均衡器,并能提供一个额外收益:让网络流量的分散更均匀。Netflix借助Ribbon采用了这种方式,并通过该技术提供了基于策略的高级路由功能。若要使用这种方式,需要通过特定语言的客户端库实现发现和均衡功能。
使用负载均衡器和DNS实现更简单的发现机制
在大部分云平台上,获得最基本服务发现功能最简单的办法是为每个服务添加一条指向负载均衡器的DNS记录。此时负载均衡器的已注册实例清单将成为服务注册表,DNS查询将成为服务发现机制。运行状况异常的实例会自动被负载均衡器移除,并在恢复运行后重新加入。
去中心化的交互
去中心化交互可更好地满足我们的要求:弱耦合,高内聚,每个服务自行负责自己的界限上下文。所有这些特征最终都可提高团队的自治能力。通过服务监控所有相互协调的其他服务所发出的事件,这种方法也可用被动方式对工作流整体的状态进行追踪。
版本控制
变化是不可避免的,重点在于如何妥善管理这些变化。API的版本控制能力,以及同时对多个版本提供支持的能力,这些都可大幅降低变化对其他服务团队造成的影响。这样大家将有更多时间按自己的计划更新代码。每个API都应该有版本控制机制!
那么多个版本到底该如何维护?
所有受支持的版本应共存于同一份基准代码和同一个服务实例中。此时可使用结构版本化(Versioning scheme)确定请求的到底是哪个版本。可行的情况下,老的端点应当更新以将修改后的请求中继至对应新端点。虽然同一个服务中多版本共存的局面不会降低复杂度,但可避免无意中增加复杂度,导致本就复杂的环境变得更复杂。
限制一切
一个服务如果超负荷运转,那么让它直接快速的失败,要好过拖累其他服务。所有类型的请求需要对不同情况下的使用进行一定的限制。此外还要通过某种方法,按照需要提高对使用情况的限制。这样可确保服务稳定,而负责服务的团队也将有机会对使用量的进一步激增做好规划。
虽然此类限制对不能自动伸缩的服务最重要,但对于可自动伸缩的服务最好也加以限制。你肯定不希望以“惊喜”的方式了解到设计决策中所包含的局限!然而对可自动伸缩的服务进行的限制可略微放宽一些。
为了帮助服务团队获得自助服务管理能力,限制机制的管理界面可包含在服务模板中,或在平台层面上通过集中化服务的方式提供。
连接池
请求量突然激增会使得服务对下游服务造成极大压力,这样的压力还会顺着整个链条继续向下传递。连接池有助于在请求量短时间内激增时“抚平”影响。通过合理设置连接池规模,即可即可对在任意时间内向下游发出的请求数量做出限制。
可为每个需要通信的服务设置一个独立连接池,借此将下游服务中存在的故障隔离在系统的特定位置。
.. 别忘了要快速失败
如果无法从池中获得连接,此时最好能快速失败,而不要无限期堵塞。这个速度决定了其他服务要等你等多久。故障本身对团队来说也是一种预警,并会导致一些很有用的疑问:是否需要扩容了?是否下游服务中断了?
更短的超时
设想一下这样的场景:一个服务接到大量请求开始超负荷并变慢,进而对该服务的所有调用都开始变慢。这种问题会持续对上游造成影响,最终用户界面开始显得迟钝。用户请求得不到预期回应,开始四处乱点期待着能自己解决问题(遗憾的是这种事情经常发生),这种做法只会让问题进一步恶化。这就是连锁故障。很多服务会在出现故障的同时发出警报,相信我,你绝对不想就这种问题获得第一手的亲身体验。
由于有多个服务相互支撑并可能出现故障,此时确定问题的根源成了一个充满挑战的工作。故障是服务本身的内部问题造成的,还是因为某个下游服务?这种场景很适合为下游API的调用使用较短超时值。超时值使得多个服务不会“缓慢地”逐渐进入故障状态,而是可以让一个服务真正发生故障时其他服务能快速故障,并从中判断出问题根源。
因此仅使用默认的30秒超时值还不够好。要将超时值设置为下游服务认为合理的时间。举例来说,如果预计某个服务的响应时间为10 – 50毫秒,那么超时值只要大于500毫秒就已经不合适了。
容忍不相关的变更
服务API会逐渐演化。需要与API的使用方进行协调的变更,其发布速度会远低于无须这种协调的变更。为了将耦合程度降至最低,服务应当能容忍与之通信的服务中所产生的不相关变更。这其实意味着如果服务中加入了字段,或改动/删除了不再使用的字段,不应该导致与该服务通信的其他服务出现故障。
如果所有服务都能容忍不相关变更,就可在无须任何协调的情况下对API进行额外改动。对于比较重大但依然不相关的变更,也只需要使用该服务的团队运行自己的测试工具确认一切都能正常工作即可。
断路开关
与故障资源进行的任何通信企图都会产生成本。消耗端需要使用资源尝试发起请求,这会用到网络资源,同时也会消耗目标端的资源。
断路开关可防止发起注定会失败的请求。该机制的实现非常简单:如果到某个服务的请求出现较多失败,添加一个标记并停止在接下来一段时间里继续向这个服务发请求。但同时也要定期允许发起一个请求,借此确认该服务是否重新上线,确认上线即可取消这样的标记。
断路开关的逻辑要封装在服务模板所包含的客户端库中。
关联ID
一个用户发出的请求可能引起多个服务执行操作,因此对某一特定请求的影响范围进行调试可能会很难。此时一种简化该过程的方法是:在服务请求中包含一个关联ID。关联ID是一种唯一标识符,可用于区分每个服务传递给任意下游请求的请求来源。通过与集中化日志机制配合使用,可轻松看到请求在整个基础架构中的前进路径。
该ID可由面向用户的聚合服务,或由任何需要发出请求,但该请求并非传入请求直接导致的意外结果的服务生成。任何足够随机的字符串(例如UUID)都可用作这个用途。
维持分布式一致性
在最终一致的世界里,服务可通过订阅事件馈送源(Feed)的方式与其他服务同步数据。
虽然听起来很简单,但魔鬼往往隐藏在细节里。数据库和事件流通常是两个不同系统,这使得你非常难以用原子级方式同时写入这两个系统,进而难以确保最终一致性。
可以使用本地数据库事务封装数据库操作,同时将其写入事件表。随后事件发布程序会从事件表读取。但并非所有数据库都支持此类事务,事件发布程序可能要从数据库提交日志中读取信息,但并非所有数据库都能暴露此类日志。
... 或者就保持不一致的状态,稍后再修复吧
分布式系统很难实现一致性。就算以分布式一致性为核心特性的数据库系统也要很多额外操作才能实现。与其打这样一场硬仗,其实也可以考虑使用某种尽可能足够好的同步解决方案,并在事后通过专门的过程找出并修复不一致的地方。
这种方式也能实现最终一致性,只不过“不一致的窗口期”可能会略微长于通过复杂的方式跨越不同系统(数据库和事件流)实现一致性时的窗口期。
每块数据都应该有一个单一数据源(Single source of truth)
就算要跨越多个服务复制某些数据,也应该让一个服务始终成为任何其他数据的单一数据来源。对数据的所有更新需要在这个数据源上进行,同时这个数据源也可在未来用于进行一致性验证时的记录来源。
如果某些服务需要强一致怎么办?
首先需要复查服务边界是否正确设置。如果服务需要强一致,通常将数据共置在一个服务(以及一个数据库)这样的做法更合理,这样可用更简单方式提供事务保障。
如果确认服务边界设置无误但依然需要强一致,则要检查一下分布式事务,这种机制很难妥善实现,同时可能会在两个服务间产生强耦合。建议将其作为最后的手段。
身份认证
所有API请求需要进行身份认证。这样服务团队才能更好地分析使用模式,并获得用于管理不同使用模式下对请求进行限制所需的标识符。
这种标识符是服务团队为使用该服务的用户提供的,具备唯一性的API密钥。必须具备某种颁发和撤销此类API密钥的方法。这些方法可内建于服务模板,或通过集中化身份认证服务在平台层面上提供,这样还可让服务团队以自助服务的方式管理自己的密钥。
自动重试
在能够“快速失败”后,还需要能以自动方式对某些类型的请求进行重试。对于异步通信这一能力更为重要。
故障后的服务恢复上线后,如果有大量其他服务正在同一个重试窗口内重试,此时很容易给系统造成巨大压力。这种情况也叫惊群效应(Thundering herd),使用随机化的重试窗口可轻松避免这种问题。如果基础架构没有实施断路开关,建议将随机化重试窗口与指数退避(Exponential backoff)配合使用以便让请求进一步分散。
遇到持久的故障又该怎么办?
有时候故障可能是格式有误的请求造成的,并非目标服务故障所致。这种情况下无论重试多少次都不会成功。当多次重试失败后,应将此类请求发送至一个死信队列(Dead queue)以便事后分析。
仅通过暴露的API通信
服务间的通信只能通过已确立的通信协议进行,不能有例外。如果发现有服务直接与其他服务的数据库通信,肯定是哪里做错了。
另外要主意:如果能对服务通信方式做出通用假设(Universal assumption),就能更容易地为防火墙后的服务组件提供更稳妥的保护。
-END-
欢迎关注“互联网架构师”,我们分享最有价值的互联网技术干货文章,助力您成为有思想的全栈架构师,我们只聊互联网、只聊架构,不聊其他!打造最有价值的架构师圈子和社区。
本公众号覆盖中国主要首席架构师、高级架构师、CTO、技术总监、技术负责人等人 群。分享最有价值的架构思想和内容。打造中国互联网圈最有价值的架构师圈子。
长按下方的二维码可以快速关注我们
如想加群讨论学习,请点击右下角的“加群学习”菜单入群。
一篇文章帮你梳理清楚API设计时需要考虑的几个关键点相关推荐
- 一篇文章帮你搞定JVM中的堆
文章目录 一篇文章帮你搞定JVM中的堆 堆的核心概述 堆的内存细分 设置堆内存大小与OOM OOM(OutOfMemory)举例 年轻代与老年代 图解对象分配过程 MinorGC,MajorGC,Fu ...
- 一篇文章带你快速理解JVM运行时数据区 、程序计数器详解 (手画详图)值得收藏!!!
受多种情况的影响,又开始看JVM 方面的知识. 1.Java 实在过于内卷,没法不往深了学. 2.面试题问的多,被迫学习. 3.纯粹的好奇. 很喜欢一句话:"八小时内谋生活,八小时外谋发展. ...
- 小程序开发教程,字节跳动Android三面凉凉,一篇文章帮你解答
开篇 说一下我大概的情况.渣本毕业,工作已经有快3年了,从高中就开始玩小破站.无论是学习还是日常放松都是在b站.大学主学的软件技术专业,所以,入职bilibili是我大学时期给自己定的小目标. 在学校 ...
- 美容仪器设计时需要考虑女性的需求
医疗技术的进步促进了美容行业的发展,护肤和美容已成为当前消费的热点.虽然现在很多男性开始关注美容,但更多的美容客户是女性,在进行美容仪器设计时,我们需要考虑女性的需求! 女性的产品设计并不简单,女性对 ...
- 一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生
来源:微信公众号[编程新说] 曾经的VIP服务 在网络的初期,网民很少,服务器完全无压力,那时的技术也没有现在先进,通常用一个线程来全程跟踪处理一个请求.因为这样最简单. 其实代码实现大家都知道,就是 ...
- 我想谈谈关于Android面试那些事,一篇文章帮你解答
开头 通常作为一个Android APP开发者,我们并不关心Android的源代码实现,不过随着Android开发者越来越多,企业在筛选Android程序员时越来越看中一个程序员对于Android底层 ...
- 【干货分享】一篇文章帮你搞定前端高频面试题
前言 如今前端技术日新月异.对于前端开发人员来说,不仅需要掌握最新的前沿技术,还需要保持对基础知识的熟练掌握.而面试则是进入优秀企业的必经之路.在面试中,高频面试题的掌握是获得成功的关键.本文将为大家 ...
- Affiliate Marketing是什么?一篇文章帮你彻底搞清楚!
经常有人私信问我,什么是Affiliate Marketing,也就是国内常说的联盟营销.之前简单介绍过:一种常见的网络带货模式,你有流量给商家带货,成交后商家给你结算佣金.可能解释地太过笼统,今天我 ...
- 一篇文章教你详细搭建API接口自动化测试框架
目录 1 需求整理 1.1 实现目的 1.2 功能需求 1.3 其他要求 1.4 适用人员 1.5 学习周期 1.6 学习建议 2 详细设计 2.1 需求分析 2.2 技术栈 3 框架设计 3.1 框 ...
- 一篇文章帮你搞懂什么是“最小可行性产品”(MVP),以及如何实现!
从"概念"到"最小可行性产品" "最小可行性产品"这个词虽然诞生已久,但不同的人对其的理解不尽相同,也算是目前科技领域中最被误用的术语之一. ...
最新文章
- python自学视频-师傅带徒弟学Python:第一篇Python基础视频课程
- 华硕笔记本节能证书_新标准兼顾性能与续航 笔记本换代哪些型号值得买?
- mysql导入存储过程报错_mysql导入建立存储过程或函数报错This function has none of DETERMINISTIC, NO SQL解决办法...
- Elasticsearch Painless Script详解
- 三:大型网站的核心架构要素
- 地址null一个简单的第三人称汽车驾驶系统
- Transformer在图像复原领域的降维打击!ETH提出SwinIR:各项任务全面领先
- 基于粒子群和麻雀搜索的LMS自适应滤波算法 - 附代码
- cdc2016年cypher资源_CDC最新Cypher!Ty.简直叼爆
- jq 获取父元素html,jq获取父级元素_使用jquery获取父元素或父节点的方法
- 2020美赛F奖论文(一):摘要、绪论和模型准备
- 程序员都知道的二维码扫码登录的底层原理
- 《局域网技术与组网工程实验》学习笔记
- 软硬结合——写给硬件开发工程师的全栈入门实战
- A7 ~ A11处理器(iphone5s~iphoneX) 14.0 ~ 14.8.1免越狱安装Trollstore教程
- Android 8.1输入法配置
- 离校毕业生刚去新的陌生城市,需要提高警惕的几点
- MAAS+JUJU+CONJURE-UP全自动部署OPENSTACK
- 在Ubuntu中运行Exe程序
- Delegate.Combine