防御者程序员胜于熵

©龙珠。 悟空

程序员辩护者在代码中的任何时间和任何地方都希望出现潜在的问题,并以预先保护自己免受此类问题的方式编写代码。 而且,如果您不能为问题辩护,那么至少要确保将其后果和对用户的影响降到最低。

我回想起好莱坞大片中的FlashForward效果,当主角看到即将来临的灾难并保持极其平静时,因为他事先知道会发生这种情况并对此有所保护。 防御性编程的思想是保护自己免受难以或无法预见的问题的侵害。 安全程序员希望在系统中的任何位置和任何给定时间发生错误,以防止在造成损坏之前进行预防。 但是,目标不是创建一个永不崩溃的系统,这仍然是不可能的。 目标是创建一个在发生任何无法预料的问题时正常崩溃的系统。

让我们更详细地了解“优雅地跌倒”概念中包含的内容。

  • 快速下降。 万一发生意外错误,应立即完成所有操作,尤其是在后续计算困难或可能导致数据损坏的情况下。
  • 整齐地跌倒。 如果发生错误,程序应释放所有资源,删除锁,删除临时和半记录的文件,关闭连接。 等待关键操作完成,否则可能会导致不可预测的结果。 或者以安全的方式使这些操作崩溃。
  • 清楚而美丽地下降。 如果发生故障,错误消息应该简单明了,并包含发生错误的系统上下文中的重要详细信息。 这将帮助负责系统的团队尽快找出问题并解决。

但是你可能有一个问题。

为什么要花时间在将来可能出现的问题上? 现在它们不存在了,代码可以完美地工作了。 此外,问题可能根本不会发生。 毕竟,专业人士不会为了工程而进行工程( YAGNI-您将不需要它)!

最主要的是实用主义


安德鲁·亨特(Andrew Hunt)在“程序员-实用主义者”一书中给出了防御性编程的以下定义-“ 实用主义妄想症”

保护您的代码免受:

  • 自己的错误;
  • 别人的错误;
  • 与您集成的其他系统中的错误和故障;
  • 应用程序所处的铁,环境和平台的错误。

让我们讨论防御性编程的几种战术和战略技术,随后将创建一个可抵抗任何失败的可靠且可预测的系统。

一些技巧似乎是“队长的”,但实际上,许多开发人员甚至没有遵循它们。 但是,如果您遵循简单的实践和方法,则将大大提高系统的稳定性。

不要相信任何人


默认情况下,用户数据不可靠。 用户经常误解我们(系统开发人员)对我们而言显而易见的东西。 预期输入错误,并始终进行检查。

还要检查输入量。 用户可能发送了太多消息。 同时,从业务逻辑的角度来看,这是正确的情况。 但这可能导致处理时间过长。 该怎么办? 例如,如果输入数据量超过某个阈值并且业务的详细信息允许您在后台处理数据,则可以异步运行它。

应用程序设置(例如,配置文件)也会受到其中错误数据的影响。 通常,程序设置以JSON,YAML,XML,INI和其他格式存储。 由于所有这些都是文本文件,因此应该期望某人迟早会更改其中的某些内容,并且您的程序将开始无法正常工作。 它可以是最终用户,也可以是您团队中的某人。

数据库,文件,配置的集中存储,注册表-所有这些位置都可以由其他人访问,并且它们迟早会在此更改某些内容( 墨菲定律 )。

垃圾入口→垃圾入口


如果希望代码完全执行期望的操作,则通过验证并开始处理的输入应该是干净的。

但是,最好进行其他数据验证检查,包括何时开始对其进行处理。 在关键位置(计费,授权,个人和机密数据等),这几乎是强制性要求。 这是必要的,以便在代码中出现错误或输入数据验证器出现问题的情况下,尽快停止执行流程。 检查所有可能的错误情况很难进行质量验证,因此可以使用更简单的方法来验证程序是否仍在正确执行-断言和异常。

健康的偏执狂是所有专业开发人员的特征。 但是寻求最佳平衡并了解何时解决方案已经足够好是非常重要的。

围绕环境的单独配置


问题的常见原因是环境之间的配置分离不足或没有这种分离。

这可能会导致很多问题,例如:

  • 测试环境开始从生产,数据库,队列和其他资源读取和/或写入数据;
  • 测试环境使用带有生产帐户的外部集成和服务;
  • 混合来自不同环境的统计信息,指标,错误;
  • 安全漏洞(开发人员,测试人员和其他团队成员可以使用生产资源);
  • 难以调查的生产错误(例如,由于测试环境开始读取消息,因此队列中的部分消息丢失了)。

这些只是示例,由负责任的配置分离不充分引起的问题的完整列表几乎是无止境的,并且取决于项目的具体情况。

按环境对配置数据进行负责任的分离可以显着降低立即引起与以下各项有关的一类问题的可能性:

  • 安全性
  • 可靠性
  • 支持和部署(DevOps工程师将感谢您)。

此外,优良作法是将秘密数据(密钥,令牌,密码)存储在专门设计用于存储和处理秘密的单独位置。 这样的系统可安全地加密数据,具有管理访问权限的灵活方法,并且还可以让您快速更改密钥(如果密钥已被破坏)。 在这种情况下,您无需更改代码并重新部署应用程序。 这对于处理金融交易,机密或个人数据的系统尤其重要。

记住级联效应


大型复杂系统崩溃的常见原因是级联效应。 系统某个部分的功能发生故障或降级,并且与此相关的其他子系统逐个失效。 级联直到整个系统完全无法访问。

一些保护性技巧:

  • 使用带有随机元素的渐进(指数)超时;
  • 为连接超时和套接字超时设置合理的值;
  • 可以预见在个别服务失败的情况下会发生故障。 最好暂时降低某些功能,完全禁用服务,但不要冒险破坏整个系统。 但是,请想象一下,在这种情况下,用户会看到一条易于理解且不令人恐惧的消息,并且支持和开发团队会尽快发现问题。

快速报告问题


所有系统失败。 有时,其中发生一些奇怪的事情,创作者期望“每十年一次”。 集成和外部API会定期变得不可用或响应不正确。 对于所有此类情况进行回退通常是困难,漫长或根本不可能的。 提前预料这种情况并尽快报告。 登录到ERROR级别或监视系统-理所当然。 向运行状况检查添加其他验证甚至更好。 要将消息从代码发送到Slack,Telegram,PagerDuty或其他可以立即将问题通知您的团队的服务是理想的。

但重要的是要清楚地了解何时可以直接发送邮件。 仅当错误,可疑或非典型情况与业务流程相关联,并且团队中的特定个人或一群人尽快收到通知并可以做出响应就很重要。

所有其他技术问题和偏差均应通过标准方式进行处理-监视,警报,记录。

缓存常用和/或最新数据


程序和人有一个共同点-他们倾向于重用经常使用或最近遇到的数据。 在高负载的系统中,您应该始终记住这一点,并将数据缓存在系统中最热的位置。

缓存策略高度依赖于项目和数据的细节。 如果数据是可变的,则需要使缓存无效。 因此,请提前考虑如何执行此操作。 并且还要考虑如果过时的数据出现在缓存中,缓存失灵等情况可能带来的风险。

用廉价的操作代替昂贵的操作


使用字符串是任何程序中最常见的操作之一。 如果这不是最佳方法,则可能是昂贵的操作。 在不同的编程语言中,使用字符串的细节可能有所不同,但是您应该始终记住这一点。

在具有大型代码库的大型应用程序中,通常会发现许多年前编写的代码可以正常运行,但在性能方面不是最佳的。 通常,数据结构从数组/列表到哈希表的平凡变化会带来严重的提升(即使仅在代码中位于本地)。

有时,您可以通过重写算法以使用按位运算来提高性能。 但是,即使在极少数情况下需要证明其合理性,代码也非常复杂。 因此,在做出决定时,请考虑代码的可读性以及需要支持的事实。 其他棘手的优化也是如此:几乎总是这样的代码变得难以阅读且难以维护。 如果您仍然决定进行棘手的优化,请不要忘记编写注释来描述您希望此代码执行的操作以及以这种方式编写代码的原因

同时,应该以健康的实用主义来对待优化:

  • 如果您(作为开发人员)需要花费几秒钟或几分钟的时间-马上就可以这样做;
  • 如果更多,则只有在您100%确定其必要性时,才立即进行操作。 在所有其他情况下,推迟它,编写TODO代码,收集更多信息,与同事协商等都是有意义的。

过早的优化是万恶之源(Donald Knuth)

用较低级别的语言重写


这是一个极端的措施。 与高级语言相比,低级语言几乎总是更快。 但是这种解决方案需要付出代价-开发这样的程序需要更长的时间并且更加困难。 有时,用低级语言重写系统的关键部分,可以大大提高生产率。 但是存在副作用-通常,此类解决方案无法跨平台使用,并且其支持成本更高。 因此,请谨慎决策。

实地不是战士


最后,我想指出一件事,也许是最重要的一件事。 只有在所有团队成员都遵守并让每个人都了解由谁负责情况以及在紧急情况下需要做什么时,我们在前几段中考虑的措施才有效。 解决问题后,重要的是与所有感兴趣的人举行会议(Post Mortem),并找出出现此问题的原因以及今后可以采取哪些措施来防止此问题发生,这一点很重要。 在许多情况下,技术和过程都需要改变。 有了每一个新的Post Mortem,您的系统将变得更加可靠,团队将变得更加经验丰富且具有凝聚力,并且宇宙中的熵将略微减少;)

本文部分使用了“ 为什么防御编程是鲁棒编码的最佳方法”中的资料 (Ravi Shankar Rajan)。

Source: https://habr.com/ru/post/zh-CN472922/


All Articles