当前位置: 首页 > 科技观察

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

时间:2023-03-11 20:49:08 科技观察

作者|KislayVerma汇编|同时,采用分布式架构的组织也发现他们需要考虑分布式故障带来的额外复杂性,这往往超出了实际的业务逻辑。虽然分布式计算的谬误有据可查,但对组织而言并非易事。因此,构建一个大规模、可靠的分布式系统架构成为一个难题。作为必然结果,当我们将网络交互的复杂性引入混合时,在其他非分布式系统中看起来不错的代码有可能成为一个大问题。在生产代码中摸索了几年,遇到了各种故障模式并发现了导致它们的原因后,我逐渐能够识别一些更常见的故障模式。不同公司之间存在差异,使用不同的语言栈(取决于内部基础设施和工具的成熟度),但可以从问题的原因中总结出一些共同的经验。下面是我从这些经验中总结出的一些codereviewguidelines,可以形成一个checklist,用于审查分布式环境下系统间通信相关的代码。虽然此清单中提到的问题并不适用于所有情况,但它们涵盖了代码审查的基础知识,并且可以按照这些问题来完成问题,标记缺失的项目以供在此过程中进一步讨论。这种方法对于发现系统中的问题非常有效。从这个意义上说,大多数问题都可以通过这个“无需思考的清单”来发现。如何调用远程系统1.远程系统出现故障怎么办?无论系统设计得多么仔细,它都会失败——这在生产中已得到证实。由于代码错误、基础设施问题、流量激增、管理不善的系统等原因,可能会发生故障,从而导致故障。调用方如何处理故障将决定整个架构的弹性和健壮性。定义错误处理路径:错误处理路径必须在代码中明确定义,不要让系统在最终用户面前崩溃。这里需要明确的向用户指出错误,例如:一个设计良好的错误页面,一个带有错误信息的异常日志,一个带有fallback机制的断路器。制定恢复计划:考虑代码中的每个远程交互并弄清楚如何恢复被中断的工作。考虑价格问题:工作流是否需要有状态才能从故障点触发?您是否将所有失败的有效请求发布到重试队列/数据库表并在远程系统恢复时重试请求?是否有脚本来比较两个系统的数据库并以某种方式使它们同步?在部署系统之前,是否有明确的系统恢复计划?2.当远程系统变慢时会发生什么?这种情况比彻底失败更困难,因为我们不知道远程系统是否正常工作。因此需要检查以下事项来处理这种情况。如果我们使用像Istio这样的服务网格技术,其中一些问题可以很容易地解决,而无需更改应用程序代码。尽管如此,我们还是应该注意这些问题。为远程系统调用设置超时:这包括远程API调用、事件发布和数据库调用的超时。我在很多代码中都发现了这个问题,所以需要检查远程系统是否设置了合理的超时时间,避免调用者在系统无响应时等待,浪费资源的情况。超时重试:网络和系统都不是100%可靠的,重试对于系统恢复是非常必要的。重试机制将消除系统交互中的许多“问题”。如果可能,在重试中使用某种补偿机制(固定的、指数的)。在重试机制中加入一点抖动(这里的抖动可以理解为随机重试,比如设置随机重试时间3-5s重试一次,避免所有调用者一起连续重试被调用者,导致负载的增加calleeincreased),可以给被调用系统一些喘息的空间,可以保证调用者在负载下获得更好的调用成功率。重试的另一面是幂等性,我们将在本文后面介绍。使用断路器:有些应用程序没有预先打包此功能,但我看到公司在内部编写了自己的包装器。如果你有这个需求,一定要实现它,投资断路器会让你受益。它提供了一个清晰的框架,用于在出现错误时定义回退策略。不要将超时视为请求失败-超时不是失败,而是一种不确定的情况,应该以某种方式处理。因此,需要建立一个明确的机制,让系统在超时的情况下进行同步。处理机制可以是简单的协调脚本,也可以是有状态的工作流,也可以通过死信队列来实现(消息被拒绝,消息TTL过期,队列达到最大长度)。不要在事务中调用远程系统——当远程系统的访问速度变慢时,数据库连接仍会保持很长时间。如果继续访问,由于速度问题无法完成系统访问,则不会释放数据库连接。也就是说,数据库连接会被用完,最终导致系统中断。使用智能批处理:如果您正在处理大数据请求,您可以将远程调用(API调用、数据库读取)一个一个地进行批处理,以消除网络开销。每个批次的大小越大,整体延迟就越大,可能失败的工作单元就越多。因此需要优化批量大小以提高性能和容错能力。如何面对调用者的请求所有的API都必须保证幂等性:幂等性就是实现调用者API的超时重试功能。只有API能够支持无副作用的安全重试,调用方才能安心使用重试功能。这里的API指的是同步API和任何消息传递接口——调用者可以向API发送相同的消息两次(或者代理可以发送两次)。明确定义响应时间和吞吐量SLA并遵守定义的规则:在分布式系统中,快速失败比让调用者等待要好得多。诚然,吞吐量SLA很难实现(分布式限速是一个难题),但我们需要确保SLA为主动调用中的故障做好准备。另一个重要方面是了解下游系统的响应时间以确定系统的最快速度。定义和限制批处理API:如果你暴露批处理API,你应该明确定义批处理的最大数量。这个数量需要受到SLA的限制,即需要遵守SLA的规则定义。预先考虑可观察性:可观察性意味着能够分析系统的行为,而无需查看API或组件的内部结构。预先考虑您关心的系统指标和需要收集的数据可以帮助您回答以前没有问过的问题。然后测试系统并获得这些数据。这样做的一个强大机制是识别系统的域模型,并在域中发生某些事件时发布事件。(例如,收到请求ID123,为请求123返回响应-注意使用这两个“域”事件如何产生一个称为“响应时间”的新指标。将原始数据转换为预先确定的聚合)。总的原则是尽量使用缓存:网络是多变的,所以尽量使用缓存,把最新的数据放在里面。当然,也可以使用远程缓存机制(如Redis服务器运行在单独的服务器上),但至少缓存可以将数据带入控制域,减少系统的负载。考虑单元故障:如果一个API或一条消息代表多个工作单元(批次),那么单元故障意味着什么?如果有效载荷失败一次,这意味着什么?或者对于个别单位独立成功或失败意味着什么?如果部分成功,API会返回成功代码还是失败代码?这意味着一个API调用多个工作单元,其中工作单元可以是组件或API。有可能调用多个工作单元时,其中一个工作单元失败,或者一些工作单元成功。这个时候,作为调用这些工作单元的最外层API,就需要考虑是成功还是失败了。如果失败如何返回失败信息。在系统边缘隔离外部域对象:不允许其他系统的域对象以复用的名义在系统中使用。这将加剧我们的系统与其他系统的实体建模的耦合,并且每当其他系统发生变化时,我们的系统将进行大量重构。我们应该始终构建我们自己的实体表示并将外部有效负载转换为我们系统内的此模式,然后在我们的系统中使用它。安全性在每个边缘清理输入:在分布式环境中,系统的任何部分都可能受到损害(从安全角度来看)。因此,进入系统的数据在系统边界被“清理”,假设进入系统的数据可能不干净或不安全。Nevercommitcredentials(凭证):从不将凭证(数据库用户名/密码或API密钥)提交到代码库。虽然向代码库提交凭据对某些人来说是家常便饭,但我们需要改掉这个坏习惯。始终遵循“凭据必须始终从外部加载到系统(保证安全存储)”的规则。译者介绍崔浩,社区编辑,资深架构师。他拥有18年的软件开发和架构经验,以及10年的分布式架构经验。他曾经是惠普的技术专家。乐于分享,撰写了多篇阅读量超过60万的热门技术文章。《分布式架构原理与实践》作者。