微服务接口设计原则
发布日期:2021-06-29 19:18:05 浏览次数:2 分类:技术文章

本文共 4064 字,大约阅读时间需要 13 分钟。

在这里插入图片描述

文章目录

0.前言

现如今后台服务大部分以微服务的形式存在,每个微服务负责实现应用的一个功能模块。而微服务是由一个个接口组成,每个接口实现某个功能模块下的子功能。

在这里插入图片描述

以一个IM应用为例,那么它的功能架构可能是下面这样的。

在这里插入图片描述

所以如果是后台开发的同学,经常需要实现一个后台微服务来提供相应的功能。

服务是以接口的形式提供服务,在实现服务时,我们要将一个大的功能拆分成一个个独立的子功能来实现,每一个子功能就是我们要在服务中实现的一个接口。

有时一个服务会有很多接口,每个接口所要实现的功能可能会有关联,那么这就非常考验设计服务接口的功底,让服务变得简单可靠。

业界已经有很多比较成熟的实践原则,可以帮助我们设计实现出一个可靠易维护的服务。

服务接口设计原则并没有严格的规范,下面介绍个人认为最为重要的几个原则。

1.单一职责

每个API接口应该只专注一件事。

这样会让接口的功能单一,实现起来简单,维护起来容易,降低了接口因功能冗杂而出错的概率。

比如读写分离和动静分离的做法都是单一职责原则的具体体现。如果一个接口干了两件事情,就应该把它分开,因为修改一个功能可能会影响到另一个功能。

2.充分必要

不是随便一个功能就要有个接口。

虽然一个接口应该只专注一件事,但并不是每一个功能都要新建一个接口。要有充分的理由和考虑,即这个接口的存在是十分有意义和价值的。无意义的接口不仅增加了维护的难度,更重要是对于程序的可控性的大大降低,接口也会十分臃肿。

相关功能我们应该考虑合成为一个接口来实现。

3.内聚解耦

一个接口要包含完整的业务功能,而不同接口之间的关联要尽可能的小。

这样便降低了对其他接口的依赖程度,如此其他接口的变动对当前接口的影响也会降低。一般都是通过消息中间件 MQ 来完成接口之间的耦合。

4.开闭原则

指对扩展开放,对修改关闭。

这句话怎么理解呢,也就是说,我们在设计一个接口的时候,应当使这个接口可以在不被修改的前提下被扩展,换句话说就是,应当可以在不修改源代码的情况下改变这个接口的行为。

比如 IM 应用中,当用户输入用户简介时有个长度限制,我们不应该将长度限制写死在代码,可以通过配置文件的方式来动态扩展,这就做到了对扩展开放(用户简介长度可以变更),对修改关闭(不需要修改代码)。

5.无状态服务

我们应该尽可能地使微服务是一个无状态的服务。

状态即数据。如果某一调用方的请求一定要落到某一后台节点,使用服务在本地缓存的数据(状态),那么这个服务就是有状态的服务。

我们以前在本地内存中建立的数据缓存、Session缓存,到现在的微服务架构中就应该把这些数据迁移到分布式缓存中存储,让业务服务变成一个无状态的计算节点。迁移后,就可以做到按需动态伸缩,微服务应用在运行时动态增删节点,就不再需要考虑缓存数据如何同步的问题。

6.统一原则

接口要具备统一的命名规范、统一的出入参风格、统一的异常处理流程、统一的错误码定义、统一的版本规范等。

统一规范的接口有很多优点,自解释、易学习,难误用,易维护等。

7.简单可靠

可靠性只有靠不断追求最大程度的简化而得到。

乏味是一种美德。与生活中的其他东西不同,对于软件而言,“乏味”实际上是非常正面的态度。我们不想要自发性的和有趣的程序;我们希望这些程序按设计执行,可以预见性地完成商业目标。与侦探小说不同,缺少刺激、悬念和困惑是源代码的理想特征。

因为工程师也是人,他们经常对于自己编写的代码形成一种情感依附,这些冲突在大规模清理源代码的时候并不少见。一些人可能会提出抗议,“如果我们以后需要这个代码怎么办?”,“我们为什么不只是把这些代码注释掉,这样稍后再使用它的时候会更容易。”,“为什么不增加一个功能开关?”,这些都是糟糕的建议。源代码控制系统中的更改反转很容易,数百行的注释代码则会造成干扰和混乱;那些由于功能开关没有启用而没有被执行的代码,就像一个定时炸弹等待爆炸。极端地说,当你指望一个Web服务7*24可以用时,某种程度上,每一行新代码都是负担。

法国诗人Antoine de Saint-Exupéry曾写道,“不是在不能添加更多的时候,而是没有什么可以去掉的时候,才能达到完美”。这个原则同样适用于软件的设计和构建。API设计是这个规则应该被遵循的一个清晰的例子。书写一个明确的、简单的API是接口可靠的保证。我们向API消费者提供的方法和参数越少,这些API就越容易理解。在软件工程上,少就是多!一个很小的,很简单的API通常也是一个对问题深刻理解的标志。

软件的简单性是可靠性的前提条件。当我们考虑如何简化一个给定的任务的每一步时,我们并不是在偷懒。相反,我们是在明确实际上要完成的任务是什么,以及如何容易地做到。我们对新功能说“不”的时候,不是在限制创新,而是在保持环境整洁,以免分心。这样我们可以持续关注创新,并且可以进行真正的工程工作。

8.用户重试

接口失败时,应该尽可能地由用户重试。

失败不可避免,因为接口无法保证100%成功。一个简单可靠的异常处理策略便是由用户重试,而不是由后台服务进行处理。

还是 IM 应用为例,有这样的需求场景。群管理员需要拉黑用户,被拉黑的用户要先剔出群,且后续不允许加入群。那么拉黑由一个独立的接口来完成,需要两个操作。一是将用户剔出群,二是将用户写入群的黑名单存储。此时两个操作无法做到事务,也就是我们无法保证两个操作要么同时成功,要么同时失败。这种情况下我们该怎么做,既让接口实现起来简单,要能满足需求呢?

我们如果将用户剔出群放到第一步,那么可能会存在踢出群成功,但是写入群的黑名单存储失败,这种情况下提示用户拉黑失败,但却把用户给踢出了群,对用户来说,体验上是个功能bug。

秉着用户尽可能地由用户重试的原则,我们应该将写入群的黑名单存储放到第一步,踢出群放到第二步。并且踢出群作为非关键逻辑,允许失败,因为者可以让用户手动将该用户踢出群,这就给了用户重试的机会,并且我们的接口在实现上也变得简单。

如果要引入消息队列存储踢出群的失败日志,让后由后台服务消费重试来保证一定成功,那么实现上将变得复杂且难以维护。不是非常重要的操作,一定不要这么做。

9.兜底降级

我们大部分服务都是如下的结构,既要给使用方使用,又依赖于他人提供的第三方服务,中间又穿插了各种业务、算法、数据等逻辑,这里面每一块都可能是故障的来源。

在这里插入图片描述
如果第三方服务挂掉怎么办?我们业务也跟着挂掉?显然这不是我们希望看到的结果,如果能制定好兜底降级方案,那将大大提高服务的可靠性。

比如我们做个性化推荐服务时,需要从用户中心获取用户的个性化数据,以便代入到模型里进行打分排序,但如果用户中心服务挂掉,我们获取不到数据了,那么就不推荐了?显然不行,我们可以在cache里放置一份热门商品以便兜底;

又比如做一个数据同步的服务,这个服务需要从第三方获取最新的数据并更新到mysql中,恰好第三方提供了两种方式:1)一种是消息通知服务,只发送变更后的数据;2)一种是http服务,需要我们自己主动调用获取数据。我们一开始选择消息同步的方式,因为实时性更高,但是之后就遭遇到消息迟迟发送不过来的问题,而且也没什么异常,等我们发现一天时间已过去,问题已然升级为故障。合理的方式应该两个同步方案都使用,消息方式用于实时更新,http主动同步方式定时触发(比如1小时)用于兜底,即使消息出了问题,通过主动同步也能保证一小时一更新。

10.批量处理

如果调用方需要调用我们接口多次才能进行一个完整的操作,那么这个接口设计就可能有问题。

比如获取数据的接口,如果仅仅提供 getData(int id) 接口,那么使用方如果要一次性获取 20 个数据,它就需要循环遍历调用我们接口 20 次,不仅使用方性能很差,也无端增加了我们服务的压力,这时提供一个批量拉取的接口getDataBatch(List<Integer> idList)显然是必要的。

对于批量接口,我们也要注意接口的吞吐能力,避免长时间执行。

还是以获取数据的接口为例:getDataList(List<Integer> idList),假设一个用户一次传 1w 个id进来,那么接口可能需要很长的时间才能处理完,这往往会导致超时,用户怎么调用结果都是超时异常,那怎么办?限制长度,比如限制长度为 100,即每次最多只能传 100 个id,这样就能避免长时间执行,如果用户传的 id 列表长度超过 100 就报异常。

加了这样限制后,必须要让使用方清晰地知道这个方法有此限制,尽可能地避免用户误用。

有三种方法:

(1)改变方法名,比如getDataListWithLimitLength(List<Integer> idList)
(2)在接口说明文档中增加必要地注释说明;
(3)接口很明确地抛出超长异常,很直白地告知调用方。

11.过载保护

如果是高并发场景使用地接口,那么需要做好流量控制,防止服务过载引发雪崩。

相信很多做过高并发服务的同学都碰到类似事件:某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。

如何应对这种情况?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。

控制流量,一般使用地算法有漏桶算法和令牌桶算法。

12.避免使用锁

高并发和高性能系统中使用锁,往往带来的坏处要大于好处。

在并发编程中,锁带解决了安全性问题,同时也带来了性能问题,因为锁让并发处理变成了串行操作,所以如无必要,尽量不用显式使用锁。

锁和并发,貌似有一种相克相生的关系。

在这里插入图片描述

参考文献

[1]

[2]
[3]
[4]

转载地址:https://dablelv.blog.csdn.net/article/details/114093138 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Golang 接口相等比较注意要点
下一篇:一文读懂什么是数据库事务

发表评论

最新留言

关注你微信了!
[***.104.42.241]2024年04月17日 22时05分19秒