为什么你的程序总是出现各种意想不到的 bug? (二)

文章类型:行业资讯    发表2022-01-11   文章编辑:怒熊网络 · 一站式互联网+技术服务商!   阅读:302

<!--接上文https://www.nuxiong.com/news_show_6544.html-->

防御性编程

“居安思危,对随时可能带来的严重后果,要做到谨慎。”
在软件开发中,虽然项目表面上能够正常运行,但风险无处不在,因此我们要学习防御性编程思想。把自己当成一个杠精,不要相信任何人,尽力去发现程序中的风险,积极防御。
下面给大家分享 16 个防御性编程的方法,学习之后,能够大大减少程序中的风险程序bug

1. 编程习惯

要减少程序中的风险,首先要养成良好的编程习惯。
首先,在写代码时,一定要保持良好的心态,不要仓促或者以完成任务的心态去写代码。如果仅仅是为了完成需求,那么很有可能不会注意到代码中的风险,甚至是发现了风险也懒得去修补,这样确实能够节约开发的时间,但是后面出现问题后,你还是要花费更多的时间去排查、沟通和修复 bug。拔苗助长,适得其反。
在写代码时,如果在一个地方多次使用相同且复杂的变量名或字符串,建议不要手动去敲,而是用大家最喜欢的 “复制粘贴”,防止因为手误而导致的 bug。
此外,我们在代码中应该加强对返回值的检查,并且选择安全的语法和数据结构,避免使用被废弃的语法。不同的编程语言也有不同的最佳编程习惯,比如在 Java 语言中,应该对所有可能为 NULL 的变量进行检查,防止 NPE(NULL Pointer Error 空指针异常),在开发多线程程序时,选用线程安全的 ConcurrentHashMap 而不是 HashMap 等等。还可以利用 Assert(断言)来保证程序运行中的变量值符合预期。
推荐使用一个自带检查功能的编辑器来书写代码,在我们编写代码时会自动检查出错误,还能给出好的编码风格的建议,能够大大减少开发时的风险。此外,在代码提交前,一定要多次检查代码,尤其是那些复制粘贴过来的文件,经常会出现遗漏的修改。提交代码后,也可以找有经验的同事帮忙阅读和检查下代码(代码审查),进一步保证没有语法和逻辑错误。

2. 异常处理

程序的运行风云变幻,同一段代码在不同情况下也可能会产生不同的结果,甚至是异常。因此很多主流的编程语言中都有异常处理机制,比如在 Java 中,先用 try 捕获异常、再用 catch 处理异常、最后用 finally 释放资源和善后。
在编程时,要合理利用异常处理机制,来防御代码中可能出现的种种问题。通常在异常处理中,我们会记录错误日志、执行错误上报和告警、重试等。
比如不信任数据库,那就在查询和操作数据时添加异常处理,一旦数据库抽风导致操作失败,就在日志中记录失败信息,并通过邮件、短信等告警方式通知到开发者,就能第一时间发现问题并排查。必要时还可以实现自动重试,省去一部分人工操作。

3. 请求校验

所有的请求都是不可信的,哪怕是在公司内网,也有可能因为一些失误,导致发出了错误的请求。
因此我们编写的每个接口,在实现具体的业务逻辑前,一定要先对请求参数加上校验,下面列举几种常见的校验方式:
  1. 参数类型校验:比如请求参数应该是 Integer 整型而不是 Long 长整数类型。

  2. 值合法性校验:比如整数的范围大于等于 0、字符串长度大于 5,或者满足某种特定格式,比如手机号、身份证等。

  3. 用户权限校验:很多接口需要登录用户或者管理员才能调用,因此必须通过请求参数(请求头)来判断当前用户的身份,被一个普通用户下载了 VIP 付费电影肯定是不合理的!

       4. 流量控制
上面提到,所有的请求都是不可信的,不仅仅是请求的值,还有请求的量和频率。对于所有接口,都要限制它的调用频率,防止接口被大量瞬时的请求刷爆。对于付费接口,还要防止用户对接口的请求数超过原购买数。
此外,还有一种容易被忽视的情况,假如你的接口 A 中又调用了其他人的接口 B,也许你的接口 A 自身的逻辑能够承受每秒 1000 个请求,但是你确定接口 B 可以承受么?
因此,需要进行流量控制,不仅仅是预防接口被刷爆,还可以保护内部的服务和调用。
什么,你说你的接口很牛逼,每秒能抗 100 万个请求,也没有调用其他的服务,那我就找 100 万 + 1 个人同时请求你的接口,看你怕不怕!
常用的流量控制按照不同的粒度可分为:
  1. 用户流控:限制每个用户在一定时间内对某个接口的调用数。

  2. 接口流控:限制一定时间内某个接口的总调用数。

  3. 单机流控:限制一定时间内单台服务器上的项目所有接口的总调用数。

  4. 分布式流控:限制一定时间内项目所有服务器的总请求数。

当然,除了上面提到的几种方式外,流控可以非常灵活,也有很多优秀的限流工具。比如 Java 语言 Guava 库的 RateLimiter 令牌桶单机限流、阿里的 Sentinel 分布式限流框架等。

5. 回滚

有时,我们对项目的操作可能是错误的,可能是人工操作,也可能是机器操作,从而导致了一些线上故障。这时,可以选择回滚。
回滚是指撤销某个操作,将项目还原到之前的状态,这里介绍几种常见的回滚操作。
数据回滚
有时,我们想要批量插入数据,但是数据插入到一半时,程序突然出现异常,这个时候我们就需要把之前插入成功的数据进行回滚,就好像什么都没发生过一样。否则可能存在数据不一致的风险。
最常见的方式就是使用事务来处理数据库的批量操作,当出现异常时,执行数据库客户端的回滚方法即可。
配置回滚
如果将项目的配置信息,比如数据库链接地址,写死到代码中,一旦配置错了或者地址发生变更,就要重新修改代码,非常麻烦。
比较好的方式是将配置发布到配置中心进行管理,让项目去动态读取配置中心的配置。如果不小心发布了错误的配置,可以直接在配置中心进行回滚,将配置还原。
发布回滚
没有人能保证自己的代码正确无误,很多时候,项目在测试环境验证时没有发现任何问题,但是一上线,就漏洞百出。这就说明我们最新发布的代码是存在问题的。
这时,最简单的做法就是进行版本回滚,将之前能够正常运行的代码重新打包发布。大公司一般都有自己的项目发布平台,能够使用界面一键回滚,自动发布以前版本的项目包。

6. 多级缓存

上面提到,缓存对项目是非常重要的,不仅是提升性能的利器,也是数据库的保护伞。
但如果缓存挂掉怎么办呢?
有两种方案,第一种是为缓存搭建集群,从而保证缓存的Redis 集群。
但是一切都不可信,集群也有可能挂掉!
那么可以用第二种方案,一级缓存挂掉,我们就再搞一个二级缓存顶上!
通常,在高并发项目中,我们会设计多级缓存,即分布式缓存 + 本地缓存。当请求需要获取数据时,先从分布式缓存(比如 Redis) 中查询,如果分布式缓存集体宕机,那就从本地缓存中获取数据。这样,即使缓存挂掉,也能够帮助系统支撑一段时间。
这里可能和一些多级缓存的设计不同,有时,我们会把本地缓存作为一级缓存,缓存一些热点数据,本地缓存找不到值时,才去访问分布式缓存。这种设计主要解决的问题是,减少对分布式缓存的请求量,并进一步提升性能,和上面的设计目的不同。

7. 服务熔断和降级

每年的双十一,我们会准时守着屏幕上的抢购页面,只为等待那一个 “请稍后再试!”
我们的项目其实远比想象的要脆弱,很多服务经常因为各种原因出现问题。比如搞活动时,大量用户同时访问会导致对项目服务的请求增多,如果项目顶不住压力,就会挂掉。
为了防止这种风险,我们可以采用服务降级策略,如果系统实在无法为所有用户提供服务,那就退而求其次,给用户直接返回一个 “友好的” 提示或界面,而不是强行让项目顶着压力过劳死。
配合服务熔断技术,可以根据系统的负载等指标来动态开启或关闭降级。比如机器的 CPU 被占用爆满时,就开启降级,直接返回错误;当机器 CPU 恢复正常时,再正常返回数据、执行操作。
Hystrix 就是比较有名的微服务熔断降级框架。

8. 主动检测

上面提到,即使是大公司的同步服务,也可能会出现同步不及时甚至是数据丢失的情况。因此,为了进一步保证同步成功、数据的准确,我们可以主动检测
比如编写一个定时脚本或者任务,每隔一段时间去检查原地址和目标地址的数据是否一致,或者通过一些逻辑来检查数据是否正确。当然也可以在每次数据同步结束后都立即去检测,更加保险。

9. 数据补偿

当检测出数据不一致后,我们就要进行数据补偿,比如将没有同步的数据再次进行同步、将不一致的数据进行更新等。
除了用来解决主动检测出的数据不一致,数据补偿也被广泛用于业务设计和架构设计中。
比如调用某个接口查询数据失败后,停顿一段时间,然后自动重试,或者从其他地方获取数据。又如消息队列的生产者发送消息失败时,应该自动进行补发和记录,而不是直接把这条消息作废。
数据补偿的思想本质上是保证数据的最终一致性,数据出错不可怕,知错能改就是好孩子。这种思想也被广泛应用于分布式事务等场景中。

10. 数据备份

数据是企业的生命,因此我们必须尽可能地保证数据的安全和完整。
很多同学会把自己重要的文件存放在多个地方,比如自己的电脑、网盘上等等。同样,在软件开发中,我们也应该把重要的数据复制多份,作为副本存放在不同的地方。这样,即使一台服务器挂了,也可以从其他的服务器上获取到数据,减少了风险。

11. 心跳机制

接口可是个复杂多变的家伙,如果我们的项目依赖其他的接口来完成功能,那么最好保证该接口一直活着,否则可能会影响项目的运行。
举个例子,我们在使用银行卡支付时,肯定需要调用银行提供的接口来获取银行卡的余额信息,如果这个接口挂了,获取不到余额,用户也就无法支付,也就损失了一笔收入!
因此,我们需要时刻和重要的接口保持联系,防止他们不小心死了。可以采用心跳机制,定时调用该接口或者发送一个心跳包,来判断该接口是否仍然存活。一旦调用超时或者失败,可以立刻进行排查和处理,从而大大减少了事故的影响时长。

12. 冗余设计

在系统资源和容量评估时,我们要做一些冗余设计,比如数据库目前的总数据量有 1G,那么如果要将数据库的数据同步到其他存储(比如 Elasticsearch)时,至少要多预留一倍的存储空间,即 2G,来应对后面可能的数据增长。业务的发展潜力越大,冗余的倍数也可以越多,但也要注意不要过分冗余,毕竟资源也是很贵的啊!
其实,冗余设计是一种重要的设计思想。当我们设计业务或者系统架构时,不能只局限于当前的条件,而是要考虑到以后的发展,选择一种相对便于扩展的模式。否则之后项目越做越大,每一次对项目的改动都步履维艰。

13. 弹性扩缩容

梦想还是要有的,说不定突然,我们原先只有 100 人使用的小项目突然就火了,有几十万新用户要来使用。
但是,由于我们的项目只部署在一台服务器上,根本无法支撑那么多人,直接挂掉,导致这些用户非常扫兴,再也不想用我们的项目了。
这也是常见的风险,我们可以使用弹性扩缩容技术,系统会根据当前项目的使用和资源占用情况自动扩充或缩减资源。
比如当系统压力较大时,多分配几台机器(容器),当系统压力较小时,减少几台机器。这样不仅能够有效应对突发的流量增长,还能够在平时节约成本,并省去了人工分配调整机器的麻烦。

14. 异地多活

前面提到,服务器是不可信的,别说一个服务器挂掉,由于一些天灾人祸,整个机房都有可能集体挂掉!
和备份不同,异地多活是指在不同城市建立独立的数据中心,正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的服务,即同时有多个 “活” 的服务。
而某个地方业务异常的时候,用户能够访问其他地方正常的业务系统,从而获得正确的服务。
如此一来,即使广州的机房垮了,咱还有上海的,上海的垮了,咱还有北京的。
同时活着的服务越多,系统就越可靠,但同时成本也越高、越复杂,因此几乎都是大公司才做异地多活。千万不要让正常情况下的投入大于故障发生的损失!

15. 监控告警

项目的运行不可能一直正常,但是我们不可能 24 小时盯着电脑屏幕来监视项目的运行情况吧?又不能完全不管项目,出了 bug 等着用户来投诉。
因此,最好的方式是给业务添加监控告警,当程序出现异常时,信息会上报到监控平台,并第一时间给开发者发送通知。还可以通过监控平台实时查看项目的运行情况,出了问题也能更快地定位。

16. 线上诊断和热修复

既然程序世界一切都不可信,危险无处不在,那么干脆就做最坏的打算,假设线上程序一定会出 bug。
既然防不胜防,那就严阵以待,在 bug 出现时用最快的速度修复它,来减少影响。
通常,我们要改 bug,也需要经历改动代码、提交代码、合并代码、打包构建、发布上线等一系列流程。等流程走完了,可能系统都透心凉了。
为提高效率,我们可以使用线上诊断和热修复技术。在出现 bug 时,先用线上诊断工具轻松获取项目的各运行状态和代码执行信息,提升排查效率。发现问题后,使用热修复技术直接修改运行时的代码,无需重新构建和重启项目!
Java 中,我们可以使用阿里开源的诊断工具 Arthas,同时支持线上热修复功能。也可以自己编写脚本来实现,但是相对复杂一些。
看到这里,肯定有同学会吐槽,怎么写个程序要考虑那么多和功能无关的问题。本来五分钟就能写完的代码,现在可能一个小时都写不完!
其实,并不是所有的项目都要做到绝对的安全(当然我们也做不到),而是我们应该时刻保持居安思危的思想,把防御性编程当做自己的习惯。
实际情况下,要根据项目的量级、受众、架构、紧急程度等因素来综合评估将项目做到何种程度的安全,而不是过度设计、杞人忧天。
让我们把时间慢下来,在开发前先冷静思考,预见并规避风险,不要让达摩克利斯之剑落下。
<END>
声明:本文以上文章来源于程序员鱼皮,作者李鱼皮,如作者不愿意转载请联系网站删除。