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

软件系统稳定性设计的秘诀

时间:2023-03-21 13:21:17 科技观察

什么是系统稳定性?控制系统理论认为:当干扰消除时,系统偏离正常状态。如果系统的扰动能够逐渐收敛并最终恢复到正常状态,则系统是稳定的,反之,如果系统的偏差越来越大,则系统不稳定。因此,稳定性是系统抵抗干扰并恢复到平衡状态的能力。对于经典的传递函数软件系统,我们所说的稳定性一般是指BIBO稳定性,即有界输入和有界输出稳定性。如果系统对任何有界输入获得有界输出,则该系统是BIBO稳定的。简而言之,一个稳定的系统需要对各种输入都有预期的输出。随着软件复杂度的增加,稳定性的保证也越来越困难。随着服务规模越来越大,稳定性的重要性越来越高。阿里星电把稳定性比作木桶的地板。如果稳定性有问题,将不会留下任何水。因此,工程师在设计和开发软件时必须坚持底层思维。但是我们的软件需求和计划很少考虑非功能部分,但是软件的结构和实现有非常大的比例是为这个服务的,这可能是软件项目计划经常延期的一个重要原因。如何保证稳定性?虽然理论上没有绝对稳定的系统,但是我们还是可以有所作为,让我们设计开发的系统在生产环境中能够近乎稳定的运行。从大的角度来看,稳定性保障可以分为三个部分:系统规程编码规范、代码提交权限控制CodeReview静态代码扫描、动态代码分析UnitTest、压力测试灰度发布、Rollback、应急预案监控审查、思路故障树分析就是要化繁为简,降低复杂度。无(零)信任和面向失败的设计实践。冗余设计(数据、计算、带宽冗余)快速恢复设计(无状态设计)容错、容灾熔断、隔离限流、损坏服务错误重试策略、避免流量风暴到关键路径、去中心化、避免单点故障负载均衡(负载均衡)避免雷鸣羊群效应看门狗设计安全编码系统纪律通过系统来规范操作和行为,通过纪律来限制框架内每个人的活动,已被证明是确保稳定性和减少错误的有效途径。纪律是关键。只有坚持按制度办,才能不让办法、规定变成空谈。但是,制度和纪律只是划定了质量的底线,只能解决大部分的稳定性问题。很难发现一些隐藏的问题。需要思维方式和实践相结合,不断提升软件质量,更加全面地保障稳定性。思维方式是一个大层次,具有全局指导意义。从众多的指导思想中,我选择了最重要的两点:保持简单和不信任/面向失败的设计,并加以扩展。1.保持简单复杂是稳定的天敌,保持简单,保持稳定。职责单一,功能明确,就是要保持简单。将简单的事情复杂化很容易,但将事情变得简单却令人惊奇。所以保持简单并不是一个低要求。它要求你通过表象了解事物的本质,用最直接、最接地气的方式解决问题。技术生有一个奇怪的习惯。他们喜欢把最近琢磨的东西用到项目中,不然总有锦衣夜行的感觉。我的建议是“学深用浅”。引入复杂性,一方面要权衡利弊,另一方面要警惕损害。我们必须明白,项目开发往往是团队合作。任何复杂性的引入都会对协作者提出更高的要求。门槛符合人性。2.Trustless设计和面向失败的设计Trustless设计也称为零信任设计,与面向失败的设计有相似之处,其本质是一种防御性编程思想。不信任的设计思想假设系统所依赖的上下游不可靠,周围有坏人,攻击无处不在。网络服务需要对客户端请求参数进行严格的校验,不仅校验有效性,还要校验NaN。游戏开发中有一句名言:假设客户端的数据是假的。进程中的大部分函数调用是安全的,结果可预测,但如果跨进程调用(RPC)的可靠性低很多,可能会出现超时、丢包、失败等情况。调用者肯定是知道的,并且处理各种异常情况,是重试吗?如果重试,重试多少次?重试之间的间隔应该如何确定?如何保存和恢复请求的上下文?我们要正确理解不信任设计的内涵,避免过度用力,警惕以面向失败设计为名的无效编程的现实,比如客户端请求数据的严格校验,服务端处理过程中的反复校验,比如空输入parametersforinterfaces,internal调用时重复判断。这样会降低代码密度,混入大量无效代码,损害可读性和执行效率,从本质上违背了“keepitsimple”的原则。修行之术是地方层面的,是实践经验,涉及方方面面,难以一一列举。如果把软件开发比作文章写作,文章的排版就相当于设计层面,设计层面应该是深远的,而用词和句子的选择就相当于实现层面,实现层面应该尽可能微妙。所谓千里堤崩于蚁巢,防渐进尤为重要。1、冗余设计冗余设计是指留有安全余量。冗余包括数据冗余、计算冗余和带宽冗余。数据冗余是指一份数据的多份副本,一主多备。计算冗余,比如一个服务实例的QPS限制是10K,但实际上我们会跑到5K,这样即使流量超速增加,我们还是有响应时间的。2.快速恢复设计(无状态设计)许多互联网服务都是无状态设计。服务实例只是逻辑框,后面跟着分布式一致性数据库。这大大简化了设计。即使一个实例失败,客户也可以很容易地迁移到其他服务实例执行,而有状态的设计要复杂得多,也很难做到。3、容错和容灾容错是指我们的系统必须具有一定的容错能力,也就是当错误发生时,我们必须能够发现、发现、避免,甚至纠正错误。我们必须尽可能长时间地吞下错误。灾备大家都很熟悉,主从设计,远程灾备,目标是应对各种极端情况。4、熔断与隔离熔断机制不仅是软件设计所特有的,在股票市场中也存在。我什至怀疑软件的熔断机制是从股市学来的。隔离本质上就是如果发生了故障,如果不能吞噬,也要隔离,避免错误的传播,千方百计缩小影响范围,相当于被隔离感染新冠。容器化等技术为隔离提供了很好的能力支持。5、限流系统设计要做好资源枯竭和资源不足的准备。如果服务请求超过服务容量,则应使用限流。这应该用作配置或自动执行策略。这类似于地铁的限流。如果你不能处理它,你必须排队。6.有害服务有害服务在我的印象中最早是由腾讯提出来的。意思是如果出现服务能力不足以为所有客户提供服务的异常情况,那么系统应该保证现有客户的服务请求得到满足,而不是让新客户拉现有客户一起死。损失的意思是有损失,有损害。老客户不打扰,新客户降价。这不也是一种无奈的方式吗。7、避免流量风暴的错误重试策略如果设计一个ToC服务,在客户大规模断线的情况下,客户会重连,重连失败再重连。如果重连尝试的频率控制不好,正常的客户端重连可能演变成对服务器的大规模攻击。炸掉一台服务器,干掉另一台,这太可怕了。可以参考内核TCP的重连策略。有最大尝试次数,重试间隔逐渐增加。8.去除关键路径,分散,避免单点故障。企业不需要钥匙先生,钥匙先生会成为瓶颈,软件不能把宝藏放在一个地方。去中心化和去中心化没有什么难理解的。9.负载均衡负载均衡其实就是分担压力。LB应该避免歪斜。LB算法有很多,比如RR,比如consistenthash。每个都有自己的优点和缺点。如果你有兴趣,可以研究一下。LB不限于服务,进程中的多线程可能也需要考虑这个问题。10.避免羊群效应。一只鸟害怕起飞,然后一群鸟害怕起飞。形象是不是很强?有点像破窗效果。蜂拥效应可以参考nginx的处理策略。11、看门狗和心跳机制可以参考内核的看门狗,它其实是一种看门狗机制,检测错误并试图克服错误。12.安全编码安全编码是对专业程序员的基本要求。安全编码有很多规则,有些规则非常详细。这可能与语言有关。如果是和C++相关的,可以参考:C++和C相关的规则比较少,我就罗列一些。比如注意初始化。例如,全局变量没有构造顺序依赖性。例如,谨慎使用强制转换,因为强制转换等同于接管编译器为你做的类型检查。比如理解线程安全函数,理解重入的概念,理解信号机制。比如避免死锁,了解ABBA锁,了解自死锁。例如,提防资源泄漏。例如,处理内存分配失败并理解野指针/悬挂指针。比如要处理好边界,防止越界和溢出。比如内存拷贝要避免内存重叠,理解memmove的目的。例如,了解递归的低效和堆栈的大小限制,避免堆栈爆炸。比如推荐使用STD安全版本函数(_s+n)版本。例如,了解unsigned<0导致无限循环的情况。比如理解浮点数和0比较的问题。比如理解整数数据的溢出和反转。例如,不要返回指向临时变量的引用或指针,了解堆栈帧动态缩放的原理。例如,了解做好检查检查的必要性,包括系统检查和模块检查。总结最后,我们来读一首经典:《系统化思维导论》引用冯·诺依曼的话写道:如果你看一些自动装置,无论是人类设计的还是存在于自然界中,你通常会发现它们的结构在很大程度上是由它们的方式决定的。可以失败,而针对失败采取的防御措施(有点有效),说他们防止失败有点夸张,他们不是失败可预防的,他们只是为了试图达到这种状态而设计的,所以至少大多数失败不是灾难性的。因此,不可能谈及消除故障,或完全消除故障的影响。我们所能做的就是设计一种在大多数故障发生时仍能继续工作的自动装置,一种减轻故障后果而不是治愈故障的装置,大多数存在于人造和自然界的自动装置,其内部原理就这样。本文转载自微信公众号“码砖杂工”,可通过以下二维码关注。转载本文请联系码砖手公众号。