[转]一张“无脑”清单告诉你分布式系统代码有多复杂

https://mp.weixin.qq.com/s/abohPG3ACJD34BsGszvrNA

开篇

微服务架构在当今的软件工程领域被广泛采用。同时,采用分布式架构的组织也发现需要考虑分布式故障的附加复杂性,而这种复杂性往往超出实际业务逻辑。

虽然分布式计算的谬误是有据可查的,但对于组织而言并不是一件容易的事情。因此,构建大规模、可靠的分布式系统架构就成为一个难题。作为推论,当我们将网络交互的复杂性引入其中时,在原先非分布式系统中看起来很好的代码就有可能成为一个大问题。

在生产代码中摸爬滚打几年后,遭遇了各种故障模式并且发现导致故障的根源之后,我逐渐能够识别一些更常见的故障模式。由于不同公司以及使用不同的语言堆栈之间存在差异(取决于内部基础设施和工具的成熟度),但是可以从产生问题的原因中总结出一些具有共性的经验。

下面就是我从这些经验中总结出来的一些代码审查指南,这个指南可以形成一份清单,并用来审查分布式环境中与系统间通信相关的代码。虽然这份清单上提到的问题并不适用所有情况,但它们覆盖了代码审查的基本面,可以按照这个清单将问题走查一遍,在此过程中标记缺失的项目以供进一步讨论,利用这种方式发现系统中的问题是非常行之有效的。从这个意义上来说,可以通过这个“无脑清单”来发现大多数问题。

图片

如何调用远程系统

1、当远程系统发生故障时会发生什么?

无论系统设计的多么谨慎,它都会出现故障 – 这是在生产中被印证的事实。故障的发生可能源于代码错误,基础设施问题,流量激增,系统疏于管理等,总之结果是引发故障。调用者如何处理故障将决定整个架构的弹性和健壮性。

定义错误处理路径:必须在代码中明确错误处理路径,而不是让系统在最终用户面前崩溃。这里需要向用户明确指出错误,例如:设计良好的错误页面、带有错误信息的异常日志,以及带有回退机制的断路器等。

制定恢复计划:考虑代码中的每一次远程交互,并弄清楚如何恢复被中断的工作。思考如下价格问题:工作流程是否需要有状态才能从故障点触发?是否将所有失败的有效请求发布到重试队列/数据库表,并在远程系统恢复时重试请求?是否有脚本来比较两个系统的数据库并以某种方式使它们同步?在部署系统之前,是否有一个明确的系统的恢复计划?

2、当远程系统变慢时会发生什么?

这种情况比彻底失败更难办,因为我们不知道远程系统是否在工作。因此需要检查以下事项从而处理这种情况。如果我们使用类似 Istio 的服务网格技术,其中一些问题可以轻松搞定而不需要修改应用程序代码。即便如此,我们也应该关注这些问题。

为远程系统调用设置超时:这包括远程 API 调用、事件发布和数据库调用的超时时间。我在很多代码中发现过这个问题,因此需要检查远程系统是否设置了合理的超时时间,从而避免该系统在无响应时调用者因为等待而浪费资源的情况发生。

超时重试:网络和系统并不是100%可靠的,重试对于系统恢复是非常必要的。重试机制会消除系统交互中的许多“问题”。如果可能,在重试中使用某种补偿机制(固定的、指数的)。在重试机制中添加一点抖动(这里的抖动可以理解为随机重试,例如设置随机的重试时间3-5s重试一次,避免所有调用者一起不断地对被调用者进行重试,导致被调用者的负载增大),这样做可以给被调用系统一些喘息的空间,通过能够保证调用者在负载下获得更好的调用成功率。重试的另一面是幂等性,我们将在本文后面介绍。

使用断路器:一些应用程序并没有预先打包这个功能,但我看到公司内部会编写自己的包装器。如果你有这个需求,一定要实现它,对断路器的投入会让你获益。它会提供明确的框架来定义错误情况下的回退策略。

不要把超时当作请求失败来处理——超时不是失败,而是一种不确定的场景,应该通过一种处理方式来应对这种不确定性。因此需要建立明确的处理机制,允许系统在发生超时的情况下进行同步。处理机制可以是简单的协调脚本,也可以是有状态的工作流,或者是通过死信队列(消息被拒绝、消息TTL过期、队列达到最大长度)实现。

不要在事务中调用远程系统——当远程系统访问速度变慢时,依旧会长时间保持数据库连接,如果访问持续而因为速度的问题一直无法完成系统的访问,会导致数据库的连接也无法释放,也就将数据库连接用完,最终造成系统中断的后果。

使用智能批处理:如果处理大量数据请求,可以逐个进行批量远程调用(API 调用、数据库读取)从而消除网络开销。每个批量处理的量越大,整体延迟就会越大,可能失败的工作单元也会越多。因此需要针对性能和容错性优化批量大小。

如何面对调用方请求

所有 API 必须保证幂等性:幂等性是为了实现调用方 API 的超时重试功能。只有 API 能够支持安全重试且不会有副作用时,调用者才能安心使用重试功能。这里的 API 是指同步 API 和任何消息传递接口——调用者可能会发布两次相同的消息(或者代理可能会发送两次)给到该 API。

图片

明确定义响应时间和吞吐量 SLA 以及遵守定义的规则:在分布式系统中,快速失败比让调用者等待要好得多。诚然,吞吐量 SLA 很难实现(分布式速率限制一个难题),但我们需要确保 SLA 在主动呼叫失败时做好准备。另一个重要方面是了解下游系统的响应时间,以确定系统最快的速度。

定义和限制批处理 API:如果公开批处理 API ,则应明确定义最大批处理的数量,这个数量需要受到 SLA 的限制,也就是需要遵守 SLA 的规则定义。

预先考虑可观察性:可观察性意味着能够分析系统的行为,而无需通过查看API或组件的内部来实现。预先考虑你关心的系统指标以及需要收集的数据,帮助你回答以前未提出的问题。再对系统进行检测并获得这些数据。执行此操作的一个强大机制是识别系统的域模型,当域中发生某个事件时进行发布事件的操作。(例如收到请求id 123,返回请求 123 的响应——注意如何使用这两个“域”事件会导出一个称为“响应时间”的新指标。将原始数据转换到预先确定的聚合中)。

一般性原则

尽量使用缓存:网络变化无常,因此尽可能多地使用缓存,并不断讲最新的数据保存其中。当然,有可能会使用远程缓存机制(例如,Redis服务器运行在单独的服务器上),但至少通过缓存的方式可以将数据带入控制域并减少系统的负载。

考虑单元故障:如果一个 API 或一条消息代表多个工作单元(批处理),那么需要思考单元故障意味着什么?如果有效载荷都失败一次意味着什么?又或者单个单元独立成功或失败意味着什么?部分成功呢,API 是否响应成功或失败代码?

这里的意思是一个 API 调用多个工作单元,这里的工作单元可以是一个组件或者是一个 API 。有可能在调用多个工作单元的时候,其中一个工作单元失败了,或者有的工作单元成功了,这个时候作为最外层调用这些工作单元的 API 来说要考虑好是成功还是失败,如果失败如何返回失败信息。

在系统边缘隔离外部域对象:不允许以重用的名义在系统中使用其他系统的域对象。这将会加剧我们的系统与其他系统的实体建模的耦合,在其他系统发生更改时,我们的系统都会进行大量重构。我们应该始终构建自己的实体表示并将外部有效负载转换为此我们系统内的模式,然后我们的系统中使用它。

安全性

在每个边缘清理输入:在分布式环境中,系统的任何部分都可能受到损害(从安全角度来看)。因此,在系统边界处会对进入系统的数据进行“消毒”处理,这里有一个假设就是这些进入系统的数据有可能不是干净或安全的。

永远不要提交凭证(Credentials):永远不要将凭证(数据库用户名/密码或 API 密钥)提交到代码库。虽然提交凭证到代码库对于某些人来说是常规操作,但我们需要摒弃这种陋习。始终遵守“凭证必须始终从外部(有安全存储保证)加载到系统”的规则。

译者介绍

崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。曾任惠普技术专家。乐于分享,撰写了很多热门技术文章,阅读量超过60万。《分布式架构原理与实践》作者。

原文链接:
https://kislayverma.com/programming/code-review-checklist-for-distributed-systems/

发表评论

邮箱地址不会被公开。 必填项已用*标注


*