微服务设计原则——易维护

2024-08-06 08:20:56 浏览数 (3)

1.充分必要

不是随便一个功能都需要开发个接口。

虽然一个接口应该只专注一件事,但并不是每个功能都要新建一个接口。要有充分的理由和考虑,即这个接口的存在十分有意义和价值。无意义的接口不仅浪费开发人力,还使服务变得臃肿,增加维护成本。

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

2.单一职责

每个 API 应该只专注做一件事情。

就像我们开发人员一样,要么从事后台开发,要么从事前端开发,要么从事服务器运维开发。公司一般不会让一个人包揽所有的开发工作,因为这让员工的职责不够单一,不利于员工在专业领域的深耕,很容易成为万金油。对公司的影响是因员工对专业知识掌握的不够深,导致开发出的软件质量得不到保证。

让接口的功能保持单一,实现起来不仅简单,维护起来也会容易很多,不会因为大而全的冗杂功能导致接口经常出错。

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

3.内聚解耦

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

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

4.开闭原则

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

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

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

此外,在设计模式中模板方法模式和观察者模式都是开闭原则的极好体现。

5.统一原则

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

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

6.用户重试

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

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

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

这种情况下我们该怎么做,既让接口实现起来简单,也能满足需求呢?

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

秉着尽可能地由用户重试的原则,我们应该将写入群的黑名单存储放到第一步,踢出群放到第二步。并且踢出群作为非关键逻辑,允许失败。即使踢出群失败,用户有重试的机会,可以后面手动将该用户踢出群。

由用户重试,我们的接口在实现上将变得简单。

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

7.最小惊讶

代码尽可能避免让读者蒙圈。

最小惊讶原则(Principle of Least Astonishment)指的是系统或服务的设计应尽量避免让用户或开发者感到意外或困惑,确保系统的行为符合用户的直觉和预期。

最小惊讶原则同样适用于代码层面。

遵循最小惊讶原则有助于提高服务的为可维护性,因为其更容易让人理解。

代码不仅要写给机器看,也要写给人看。很多时候,一段代码需要一群人共同维护,如果你在里面杂七杂八地加了很多不易于别人理解的奇技淫巧,会降低了代码可读性,不利于维护。

只需根据需求来设计并实现,切勿过度设计一个复杂无用、华而不实的服务。能用简单的方法去实现,就别把它搞复杂。目的是为了实现功能、解决问题,而不是炫技。使用一些常见,久经考验的实现方式比一些炫技复杂难理解的实现方式更容易让人接受。

比如 Golang 中将 []byte 转成 string。

代码语言:javascript复制
// Good
// 直观且安全
x := []byte("Hello World!")
y := string(x)

// Bad
// 虽然性能好,但是不常用且不安全
import "unsafe"

x := []byte("Hello World!")
y := *(*string)(unsafe.Pointer(&x))

8.避免无效请求

不要传递无效请求至下游。

无效请求下游应及早检测发现并拒绝,可能会引发相关入参无效的告警,混淆视听且骚扰。我们应避免传递无效请求至下游,避免浪费带宽和计算资源。

换位思考,谁都不想浪费力气做无用功。

9.入参校验

自己收到的请求要做好入参校验,及早发现无效请求并拒绝,然后告警。发现垃圾请求后推动上游不要传递无效请求至下游。

此时,我们是上游的下游,做好入参校验,避免做无用功。

10.设计模式

适当的使用设计模式,让我们的代码更加简洁、易读、可扩展。

设计模式(Design Pattern)是一套被反复使用、多人知晓、分类编目、代码设计经验的总结。使用设计模式可以带来如下益处。

  • 简洁。比如单例模式,减少多实例创建维护的成本,获取实例只需要一个 Get 函数。
  • 易读。业界经验,多人知晓。如果告知他人自己使用了相应的设计模式实现某个功能,那么他人便大概知晓了你的实现细节,更加容易读懂你的代码。
  • 可扩展。设计模式不仅能简洁我们的代码,还可以增加代码的可扩展性。比如 Go 推崇的 Option 模式,既避免了书写不同参数版本的函数,又达到了无限扩增函数参数的效果,增加了函数扩展性。

11.禁用 flag 标识

为什么接口不要使用 flag 标识,因为这会使接口变得臃肿,违背单一职责,最终难以维护。

这里说下,我们为什么会使用 flag 标识。

有时,我们需要提供一个读接口供上游调用查询相关信息。如主调 A 需要信息 a,主调 B 需要信息 b,主调 C 需要信息 c,主调 D 需要信息 a 和 b。如果为每个主调获取信息都提供单独的接口,那么接口会变得很多。为了减少接口的数量,我们很容易想到给接口增加多个 flag 参数,每个主调在调用接口时携带不同的 flag,表明需要获取哪些信息,然后接口根据入参 flag 获取对应的信息。比如主调 A 调用时将 flag_a 置为 true,主调 B 将 flag_b 置为 true,主调 C 将 flag_c 置为 true,主调 D 将 flag_a 和 flag_c 置为 true。

在项目前期或者 flag 数量较少的情况下,接口功能不是很多时,一般不会暴露出问题。一但开了这个口子,随着需要不同信息主调的增多,接口会不停的增加 flag,最终导致接口变得庞大臃肿,不仅难以阅读维护,还会使接口性能低下。

所以,我们应该禁用 flag 标识,尽可能地保证接口功能单一。

回到上面提到的场景,不适用 flag 标识,我们改如何是好呢?

我们应该坚持单一职责的原则,将信息进行原子分割,每个原子信息作为一个独立的接口对外提供服务。如果需要多个原子信息,我们可以增加一个 proxy 层,以独立接口将需要的相关原子信息汇聚组合。这么做你可能会问,接口变多了,会导致服务难以维护。不用担心,如果服务接口数量过多,我们应该对服务进行拆分。

还是以上面提及的例子为例,接口禁用 flag 前后组织形式对比如下:

12.分页宜小不宜大

对于查询 API 来说,当查询结果集包含成千上万条记录时,返回所有结果是一个挑战,它给服务器、客户端和网络带来了不必要的压力,于是便有了分页接口。

通常我们通过 page 和 size(offset 和 limit)或 after_id 和 limit 实现分页。page 与 size 适合数据总量小的浅分页查询,after_id 和 limit 适合数据总量大的深分页查询。

在设计分页接口时,页大小需要设置上限。这是因为如果没有上限,客户端可以请求任意大的页大小,从而可能导致服务器性能问题,例如一次请求返回过多数据,导致服务器响应变慢,网络传输时间变长,甚至可能引起系统崩溃等问题。

为了防止这种情况的发生,通常会在设计分页接口时设置一个最大页大小限制。当客户端请求的页大小超过最大限制时,应该向客户端返回一个错误提示,告知客户端页大小超过最大限制,建议客户端减小页大小,以保证服务器和客户端的正常运行。

那么页大小设为多少合适呢?

常见的页大小有 10,20,50,100,500 和 1000。如何选择页大小,我们应该在满足特定业务场景需求下,宜小不宜大。

太大的页,主要有以下几个问题:

  • 影响用户体验。页太大,加载会比较慢,用户等待时间会比较长。
  • 影响接口性能。页太大,会增加数据的拉取编解码耗时,降低接口性能。
  • 浪费带宽。很多场景下,用户在浏览的过程中,不会看完一页中的所有数据,返回太大的页是一种浪费。
  • 扩展性差。随着业务的发展,接口在页大小不变的情况下,返回的页数据可能会越来越大,导致接口性能越来越差,最终拖垮接口。

页大小多少合适,没有标准答案,需要根据具体的业务场景来定。但是要坚持一点,页宜小不宜大。如果页大小能用 10 便可满足业务需求,就不要用 20,更不要用 50。

参考文献

SOLID - wikipedia

1 人点赞