首页 > 揭秘软件开发中的达摩克利斯之剑
头像
程序员鱼皮
编辑于 2020-12-04 21:11
+ 关注

揭秘软件开发中的达摩克利斯之剑

为什么你的程序总是出现 bug?

凭什么让改 bug 占据了你大部分的时间?

看完本文,保证你能设计出更稳定的程序,摆脱 bug 的缠绕,做项目更安心!


记得我在学校的时候,做的那些项目,不是为了应付课程作业,就是为了参加比赛时展示用,因此对项目的质量要求非常低。

到底有多低呢?

大部分的项目,只要基本的功能可以使用,就算完成了,完全不考虑任何的异常情况。甚至只要能成功运行一次,让我截几张图放到 PPT 或者实验报告里,足够向老师交差或者应付比赛答辩就行。

那项目出现 bug 怎么办呢?

  • 如果测试的时候发现有些功能不可用,那很简单,不管他,直接 PS 一张正常运行的图就行。

  • 如果比赛的时候发现有些功能不可用,那也很简单,把锅甩给 “现场网络不好” 就行。

但是,这些 “小技巧” 在企业中是行不通的,企业级项目必须为企业带来实际的价值,容不得半点马虎和欺骗。

我第一次进入企业实习时,还保留着自己在学校开发项目的狼性 🐺,只要能够完成基本功能就行,保证以最快的速度完成开发。

有一天,当我洋洋得意准备早点下班时,测试同学走过来跟我说。

“喂,你的程序有 bug,这里用户下单怎么金额是负的?”

写个 bug

对于我一个初入职场的小白,这是人生中第一次有人说我的代码有 bug,我有问题,我不对劲。

当时,我脑海的第一个念头竟然是怎么把这个 bug 糊弄过去,而不是怎么去更正!看来我已经养成了非常不好的习惯。

那之后几天,我又连续收到了测试提出的多个 bug,然后将他们一个个改正。如果将这样一个漏洞百出的程序发布上线,带来的损失是不可估量的,现在想想仍心有余悸。

这件事之后,我意识到,在企业中开发项目,不能只追求开发时的效率,还要注重项目的稳定性,否则带来的额外返工时间远比开发时节省的时间要长,而且会影响同事对你的看法。如果将开发时产生的 bug 遗留到线上,后果更是不堪设想!

后来,在字节跳动和腾讯这两家大公司工作后,我进一步认识到了项目稳定性有多重要,并且积累了更多只有在大公司才能学到的提升项目稳定性的经验。

我总结了 10 个开发中通常不会考虑到的风险点,以及 16 个减少风险、提升项目稳定性的方法,分享给大家~

在分享这些之前,先讲个故事。

达摩克利斯之剑

古希腊传说中,达摩克利斯是公元前 4 世纪意大利叙拉古的僭主(古希腊统治者独有的称号)狄奥尼修斯二世的朝臣,他非常喜欢奉承狄奥尼修斯。

他奉承道:“作为一个拥有权力和威信的伟人,狄奥尼修斯实在很幸运。”

于是狄奥尼修斯提议与他交换一天的身份,那他就可以尝试到首领的命运。

在晚上举行的宴会里,达摩克利斯非常享受成为国王的感觉。当晚餐快结束的时候,他抬头才注意到王位上方仅用一根马鬃悬挂着的利剑。他立即失去了对美食和美女的兴趣,并请求僭主放过他,他再也不想得到这样的幸运。

达摩克利斯之剑

这个故事告诉我们什么呢?

  1. 在和平安宁之后,时刻存在着危险与不安。
  1. 当一个人获取多少荣誉和地位,他都要付出同样多的代价。
  1. 地位越高,看似越安全,实则越危险。
  1. 居安思危,对随时可能带来的严重后果,要做到谨慎。

那么这和软件开发又有什么关系呢?下面就让我来揭秘软件开发中的达摩克利斯之剑。

危机四伏

“在和平安宁之后,时刻存在着危险与不安。”

软件开发正是如此,表面上机器是 “死” 的,只会按照人输入的指令或编好的程序来执行,一成不变,听话的很。好像我们写好代码扔到机器上后,就可以高枕无忧。

但真的是这样么?我们真的可以信任机器和程序么?

其实,在程序世界中危机四伏,人为因素、环境因素等可能都会对我们的程序产生影响。因此,我们必须时刻坚守软件开发的不信任原则,保持 overly pessimistic(过于悲观),把和程序有关的一切请求、服务、接口、返回值、机器、框架、中间件等等都当做不可信的,步步为营、处处设防。

程序世界里的不信任原则

那为什么写个代码要这么小心翼翼,什么都不信任呢?

大项目的苦衷

“当一个人获取多少荣誉和地位,他都要付出同样多的代价。”

软件开发中,项目价值越大,需要承受的压力也越大,来听听大项目的苦衷吧。

我是一个身价过亿的大项目,每天服务着上千万的用户,帮助他们获得知识与快乐。

我的小伙伴们只看到我身上的光环和荣耀,但是他们看不到我背负的压力和风险,今天终于有机会和大家倾诉我的苦衷了。

记得很多年前,我还是个孩子,只有几个小主人开发我,那段时间,我成长的很快。虽然只有几十个人使用我,但我感到非常轻松和快乐,偶尔偷会儿懒,也不会被人发现。

后来,我的功能越来越多,越来越强大。每天有数之不尽的新面孔来和我打招呼,并享受我提供的服务。渐渐地,更多开发者在我身上留下了印记,我感觉自己正在变得复杂,也开始感受到了压力。我再也找不到机会偷懒,因为我一旦休息,就会让我的主人们损失一比不小的财富。

如今,我已经是一个成熟的大项目了,每天有上千万的用户依赖我,我终于拥有了更大的价值,却也增加了很多烦恼,感受到了更大的危险。

首先,同时服务千万用户,每秒钟都可能会有几十万、甚至几百万个请求需要我来处理,因此我必须每时每刻无休止地高负载工作,且不说休息,哪怕稍微慢了一点,就会遭到用户的投诉,主人们也会因此受到批评。

我的运行,必须依靠很多兄弟们的支撑,因此我必须和兄弟们好好相处,哪怕一个兄弟倒了,我都会受到影响。

在我强大的实力背后,有一颗非常脆弱的心。经历了那么多次的强化和改造,我的功能逐渐变多的同时,也因此被植入了各种框架和插件,体积像滚雪球一般越来越大,不知道什么时候就会爆炸。以至于主人们每次改动我时都要万分谨慎,我的成长也变得十分缓慢。

复杂

然而最让我感到恐惧的,是那些坏家伙们!

他们和正常的用户不同,有的不断制造请求,试图将我击垮。有的绕到我的背后,试图直接控制我。有的对我虎视眈眈,监视并记录我的一举一动。还有的尝试各种非法操作,想从我身上牟取暴利。

作为一个大项目真是太累了,我不知道我还能坚持多久。

真的可信么?

“地位越高,看似越安全,实则越危险。”

如今是一个软件开源和共享的时代,我们在开发项目时,或多或少会使用到网上现有的资源,比如依赖包、工具、组件、框架、接口、现成的云服务等等,这些资源能够大大提升我们的开发效率。

就拿云服务来说,几乎已经成了我们开发必备的资源,以前我们想要做一个网站,可能需要自己买一台物理服务器,然后连通网络,再把项目部署上去。而如今,直接登录大公司的云官网(像腾讯云、阿里云),然后租一台云服务器就行了,非常省事。

图片说明

再说说现在主流的开发框架,以前做一个简单的网站界面可能只会使用 HTMLCSSJavaScript 这三种最基础的技术,而如今,网站的样式和交互越来越复杂,我们不得不使用一些知名的框架来提升开发效率,比如 VueReact

听起来好像没有任何问题,你也根本不会去怀疑什么,因为我们天生带着对大公司,或者说是对名气的信任

但是,你知道么,当你决定使用其他人的资源时,你就已经把项目系统的部分掌控权、甚至可能是半条命,都交出去了。

那么不妨思考一下,你使用的这些资源,真的可信么?

下面 10 个问题,可能改变你对开发的认知。

1. 开发工具可信么?

我们通常是在大而全的开发工具中编写代码,比如 JetBrains IDEA 或者 Vscode。很多刚开始写代码的同学、甚至是一些经验丰富的老手,都对开发工具保持绝对的信任。

比如你在键盘上敲击 a,那编辑器界面上显示的一定是 a

但是,由于内存不足等种种原因,开发工具其实也会抽风

比如你想要调用某个函数,通常敲击函数名前几个字母后,开发工具就会自动给你提示完整的函数名,但如果开发工具没有给你提示,你首先怀疑的是这个函数不存在,而不是编辑器没有按预期给出提示。遇到这种情况,可以稍等编辑器一下,或者进一步确认函数是否真的不存在,而不是立刻创建一个新的函数。

又或是项目无法运行,怎么排查都觉得没问题,这时不妨重新启动下开发工具,或者清理一下缓存,说不定项目就能正常运行了!

还有很多非常有意思的情况,比如编辑器一片大红,各种提示错误,但是项目依然能成功运行。

因此,不要绝对相信开发工具。

2. 开源项目可信么?

现在是一个软件开源的时代,在 GitHub 等开源项目平台上能够找到大量优秀的开源项目,好的开源项目甚至可以得到 10 万多个关注,那这些知名的开源项目可信么?

不完全可信!从每个开源项目的 Issues 就能看出这点,而且通常越大的项目,被发现的问题越多,比如 Vue 项目,累积提出并关闭了 8000 多个问题。

Vue 项目的问题

我记得自己有一次使用知名的开源服务器 Tomcat,就遇到了 bug,每次接受到特定的请求都会报错。刚开始我根本没有怀疑是 Tomcat 的问题,而是绞尽脑汁地想自己的代码哪里写错了。后来经过反复的排查和搜索,终于确认了就是 Tomcat 本身的 bug!

虽然开源项目并不完全可信,但是相对于私有项目而言,所有对项目感兴趣的同学可以共同发现项目中的问题,并加以解决,在一定程度上还是能够提高项目的可靠性的。

3. 依赖库可信么?

我们在开发项目时,通常会用到大量的依赖库。直接在官方依赖源(比如 Mavennpm)搜索依赖库,然后使用包管理器,用一行命令或者编写配置文件就能够让其自动安装依赖,非常方便。

但是,这些发布到官方源的依赖库,就可信么?

且不说基本每个开发者都有机会发布依赖库到官方,就算是互联网大公司的依赖库,也未必可信。

给我印象最深刻的就是阿里巴巴的 JSON 序列化类库 fastjson,几乎无人不知、无人不晓,因为其极快的解析速度广受好评。但是,这个库被多次曝光存在高危漏洞,可以让攻击者远程执行命令!一般的开发者根本不会发现这点,从而给项目带来了极大的危害。

因此,在选用依赖库的时候,要做好充分的调研,尽量确认依赖库的安全,并且保证不要和已有的依赖冲突。

4. 编程语言可信么?

Java 是一种强类型语言,具有健壮性。这句话我相信所有学过 Java 的同学都再熟悉不过了。但是,强类型编程语言就一定可信么?

这里可能有同学就要表示怀疑了,如果我们一直使用的最基础最底层的编程语言都存在 bug,那我们怎么去相信建立在这些编程语言上的框架呢?

然而真相是,所有的编程语言都有 bug!而且基本每次编程语言发布新版本时都会对一些历史 bug 进行修正。就 Java 而言,甚至还有一个专门记录 bug 的数据库!

Java Bug 数据库

但是,对于大多数开发者来说,我相信即使在程序中偶然触发了编程语言本身的 bug,也没有足够的自信去质疑,而是直接修改代码来绕过。

确实,质疑编程语言需要一定的基础和知识储备,但是一旦发现了程序中莫名其妙的问题,建议大家不要直接忽略,可以花一些时间去探索研究,说不定你就成功地发现了一个重大的 bug,也能够加深对这门编程语言的理解。

5. 服务器可信么?

服务器是项目赖以生存的宿主,服务器的性能和稳定性将直接影响到项目进程。

无论是个人开发者还是企业,通常都会直接租用大公司提供的云服务器来部署项目,省去了自己搭建和维护的麻烦。

但是大公司的云服务器就可信么?

不完全可信!即使现在的云服务器提供商都承诺自己的服务 SLA(服务级别协议)可以达到 5 个 9(99.999% 一年约宕机 5 分钟),甚至 6 个 9(99.9999% 一年约宕机 30 秒),但是仍然存在一定的风险。

有一个非常有名的案例,在 2013 年,中国最大的社交通讯软件出现大规模的故障,多达几亿用户受到影响。原因竟然是,市政道路建设的一个不注意,把网络光缆挖断了,就导致该软件所在服务器的无法访问。

除了可用性的不可信之外,可能还有一些安全隐私方面的问题。当然云服务商通常是不会获取用户的数据的,但也没有办法绝对相信他们。毕竟数据的隐私对企业至关重要,这也是为什么大的公司都会搭建属于自己的服务器机房和网络。

机房

6. 数据库可信么?

企业中的大多数业务数据都是存放在数据库中的,通过项目后端程序来操作和查询数据库中的数据。

和服务器一样,我们可以使用软件自己搭建数据库,比如 MySQL,也可以直接租用大公司的云数据库,那么数据库可信么?

其实在企业后端项目中,数据库通常是性能瓶颈,相对比较脆弱,当访问并发量大一点时,数据库的查询性能就会下降,严重时可能整个宕机!即使是大公司提供的云数据库服务,遇到慢查询(需要较长时间的查询)时,可能也无从应对。

数据库中的数据其实也未必可信,有时管理员的一个误操作,不小心删除数据或添加了一条错误数据,可能就会影响用户,造成损失。更有甚者,竟然删库跑路,不讲码德!

删库跑路

因此,不要过于信任数据库,应当使用缓存之类的技术帮助数据库分担压力,并定期备份。否则一旦数据库宕机或数据丢失,带来的损失是不可估量的!

7. 缓存服务可信么?

缓存是开发高性能程序必备的技术,通过将数据库等查询较慢的数据存放在内存中,直接从内存中读取数据,以提升查询性能。有了缓存之后,项目不仅能够支持更多人同时查询数据,还能够保护数据库。

目前比较主流的缓存技术有 RedisMemcached 等,可以自己在服务器搭建,也可以直接租用大公司提供的云缓存服务。

存储键值对的缓存

那么缓存服务是否可信呢?

项目的并发量不是特别大的话,一般的缓存技术就足以支持了,但是如果项目的量级很大,可能缓存也无法承受住压力,严重时就会宕机。而一旦缓存挂掉,大量的查询命令会直接请求数据库,于是数据库也会在瞬间挂掉,严重时还会导致整个项目瘫痪!

因此,在使用缓存时,需要对并发量进行评估,通过搭建集群和数据同步保证高可用性。此外,还要预防缓存雪崩、缓存穿透、缓存击穿等问题,简单解释一下。

缓存雪崩:指大量缓存在同一时间过期,请求都访问不到缓存,全部打到数据库上,导致数据库挂掉。

缓存穿透:持续访问缓存中不存在的 key,导致请求绕过缓存,直接打到数据库上,导致数据库挂掉。

缓存击穿:一个被大量请求高频访问的热点 key 突然过期,导致请求瞬间全部打到数据库上,导致数据库挂掉。

如果不预防这三个问题,即使是租用大公司的缓存服务,也一样吹弹可破。

雪崩

8. 对象存储可信么?

项目中,经常会有用户上传图片或文件的功能,这类数据通常较大,用数据库存储不太方便。虽然我们可以将文件直接存到服务器上,但更好的做法是使用专门的对象存储服务。

可以简单地把对象存储当做一个大的文件夹,我们可以通过它直接上传和下载文件。大的云服务商也都提供了专业的对象存储服务,而无需自己搭建,那么对象存储可信么?

一般情况下,上传到对象存储的文件是不会缺失或丢失的,而且还可以将已上传的数据进行跨园区同步,起到备份的作用。

跨园区同步

但是,记得有一次,上传到对象存储上的文件和源文件竟然不一致,大小足足少了 1M。起初我以为是文件上传到对象存储时,会自动被压缩,但是将对象存储中的文件下载到本地后,发现的确和源文件不一致!虽然出现这种情况的概率极其小,但从那一刻起,我再也不相信对象存储了。

再用自己的真实经历来聊聊对象存储的跨园区同步。因为个人负责的业务比较重要,万一单个机房整体挂掉,可能分分钟是几十万元的损失!因此我为对象存储配置了自动跨园区同步,将文件先上传至广州机房,然后数据会自动同步到上海机房,且运维同学承诺自动同步的延迟不超过 15 分钟。

我相信大部分开发者配置数据同步后也就不管了,相信它一定会自动同步的。结果后面我编写程序去做同步监控、对比数据时,发现经常出现数据未同步的情况,比例高达 10%!

因此,不能完全相信对象存储,虽然大部分情况下大公司的对象存储服务很可靠,但不能确保万无一失。尤其是同步备份的场景下,是否真的同步成功了,又有多少同学关心过呢?不妨写个程序去验证和保障。

9. API 接口可信么?

在开发中,我们经常会调用其他系统提供的 API 接口来轻松实现某种功能。比如查询某地的天气,可以直接调用其他人提供的天气查询接口,而无需自己编写。我们也可以提供 API 接口给其他人使用,尤其是在微服务架构中,各服务之间都是以接口调用的形式实现交互协作的。

几乎所有的 API 接口提供者都会说自己的接口有多安全、请放心使用,那么 API 接口真的可信么?

其实,API 接口是最不可信的资源!

首先,API 接口的提供方可以是任何开发者,很难通过他们的一面之词来确定接口的稳定性和安全性。

即使这个接口性能很高、也很安全,但是你并不了解有多少人和你在同时使用这个接口,也许只有你,又也许是 100 万个其他的开发者呢?在这个竞争条件下,接口的 qps(query per second 每秒查询数)还能达到预期么?接口返回时长真的不会超时么?

更有甚者,偷偷地把 API 接口改动了,却没有给调用者发送通知,这样接口的调用方全部都会调用失败,严重影响项目的运行!

因此,我们在调用第三方 API 接口时,一定要慎重、慎重、再慎重!

此外,如果我们是 API 接口的提供者,也要注意保护好自己的 API 接口,避免同时被太多的开发者调用,导致接口挂掉。

API 存在复杂的调用关系

10. Serverless 可信么?

如果说服务器不可信,那我们干脆就不租服务器了,直接租用大公司提供的 Serverless 服务来作为项目的后台不就行了?

Serverless 指无服务器架构,并不是真的不需要服务器,而是将项目接口的部署、运维等需要对服务器的操作交给服务商去做,让开发者无需关心服务器,专心写代码就好。

docker 容器

听起来非常爽,那 Serverless可信么?

使用 Serverless,虽然能够大大提升开发和运维效率,但是其相对服务器等资源而言,更不可信!

首先,Serverless 本身就是部署在服务器上的,难免会受到服务器的影响。

其次,Serverless 服务不会长期保持应用的状态,而是随着请求的到来而启动,存在冷启动时期,虽然也有很多相关的优化和解决方案,但仍无法精确地保证接口的性能,尤其是在高并发场景下,性能往往达不到预期。

最重要的是,当你选择使用 Serverless 服务时,你就和某云服务提供商绑定了,后续想要迁移是非常困难的!试想一下,你项目的所有功能都交给别人来维护,真的是好事么?一旦云服务提供商改造了架构或接口,你的代码也要随之改动,而这种改动却不是由自己控制的!

当然,Serverless 具有非常多的优点,也是云计算技术发展的必然趋势,只是希望大家在使用前,考虑到那些可能的风险,并做好应对措施。

云计算时代

总结:正是因为我们太过信任那些名气大、看似安全的资源,所以其背后的危险才更难以被察觉,带来的后果往往也更致命!

防御性编程

“居安思危,对随时可能带来的严重后果,要做到谨慎。”

在软件开发中,虽然项目表面上能够正常运行,但风险无处不在,因此我们要学习防御性编程思想。把自己当成一个杠精,不要相信任何人,尽力去发现程序中的风险,积极防御。

下面给大家分享 16 个防御性编程的方法,学习之后,能够大大减少程序中的风险。

祈祷性编程

1. 编程习惯

要减少程序中的风险,首先要养成良好的编程习惯。

首先,在写代码时,一定要保持良好的心态,不要仓促或者以完成任务的心态去写代码。如果仅仅是为了完成需求,那么很有可能不会注意到代码中的风险,甚至是发现了风险也懒得去修补,这样确实能够节约开发的时间,但是后面出现问题后,你还是要花费更多的时间去排查、沟通和修复 bug。拔苗助长,适得其反。

在写代码时,如果在一个地方多次使用相同且复杂的变量名或字符串,建议不要手动去敲,而是用大家最喜欢的 “复制粘贴”,防止因为手误而导致的 bug。

复制粘贴一把梭

此外,我们在代码中应该加强对返回值的检查,并且选择安全的语法和数据结构,避免使用被废弃的语法。不同的编程语言也有不同的最佳编程习惯,比如在 Java 语言中,应该对所有可能为 NULL 的变量进行检查,防止 NPE(NULL Pointer Error 空指针异常),在开发多线程程序时,选用线程安全的 ConcurrentHashMap 而不是 HashMap 等等。还可以利用 Assert(断言)来保证程序运行中的变量值符合预期。

推荐使用一个自带检查功能的编辑器来书写代码,在我们编写代码时会自动检查出错误,还能给出好的编码风格的建议,能够大大减少开发时的风险。此外,在代码提交前,一定要多次检查代码,尤其是那些复制粘贴过来的文件,经常会出现遗漏的修改。提交代码后,也可以找有经验的同事帮忙阅读和检查下代码(代码审查),进一步保证没有语法和逻辑错误。

编辑器语法检查和提示

2. 异常处理

程序的运行风云变幻,同一段代码在不同情况下也可能会产生不同的结果,甚至是异常。因此很多主流的编程语言中都有异常处理机制,比如在 Java 中,先用 try 捕获异常、再用 catch 处理异常、最后用 finally 释放资源和善后。

在编程时,要合理利用异常处理机制,来防御代码中可能出现的种种问题。通常在异常处理中,我们会记录错误日志、执行错误上报和告警、重试等。

比如不信任数据库,那就在查询和操作数据时添加异常处理,一旦数据库抽风导致操作失败,就在日志中记录失败信息,并通过邮件、短信等告警方式通知到开发者,就能第一时间发现问题并排查。必要时还可以实现自动重试,省去一部分人工操作。

异常啦

3. 请求校验

所有的请求都是不可信的,哪怕是在公司内网,也有可能因为一些失误,导致发出了错误的请求。

因此我们编写的每个接口,在实现具体的业务逻辑前,一定要先对请求参数加上校验,下面列举几种常见的校验方式:

  1. 参数类型校验:比如请求参数应该是 Integer 整型而不是 Long 长整数类型。
  1. 值合法性校验:比如整数的范围大于等于 0、字符串长度大于 5,或者满足某种特定格式,比如手机号、身份证等。
  1. 用户权限校验:很多接口需要登录用户或者管理员才能调用,因此必须通过请求参数(请求头)来判断当前用户的身份,被一个普通用户下载了 VIP 付费电影肯定是不合理的!

4. 流量控制

上面提到,所有的请求都是不可信的,不仅仅是请求的值,还有请求的量和频率。对于所有接口,都要限制它的调用频率,防止接口被大量瞬时的请求刷爆。对于付费接口,还要防止用户对接口的请求数超过原购买数。

此外,还有一种容易被忽视的情况,假如你的接口 A 中又调用了其他人的接口 B,也许你的接口 A 自身的逻辑能够承受每秒 1000 个请求,但是你确定接口 B 可以承受么?

因此,需要进行流量控制,不仅仅是预防接口被刷爆,还可以保护内部的服务和调用。

什么,你说你的接口很牛逼,每秒能抗 100 万个请求,也没有调用其他的服务,那我就找 100 万 + 1 个人同时请求你的接口,看你怕不怕!

DDOS 分布式拒绝服务攻击

常用的流量控制按照不同的粒度可分为:

  1. 用户流控:限制每个用户在一定时间内对某个接口的调用数。
  1. 接口流控:限制一定时间内某个接口的总调用数。
  1. 单机流控:限制一定时间内单台服务器上的项目所有接口的总调用数。
  1. 分布式流控:限制一定时间内项目所有服务器的总请求数。

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

Sentinel 流控面板

5. 回滚

有时,我们对项目的操作可能是错误的,可能是人工操作,也可能是机器操作,从而导致了一些线上故障。这时,可以选择回滚。

回滚是指撤销某个操作,将项目还原到之前的状态,这里介绍几种常见的回滚操作。

数据回滚

有时,我们想要批量插入数据,但是数据插入到一半时,程序突然出现异常,这个时候我们就需要把之前插入成功的数据进行回滚,就好像什么都没发生过一样。否则可能存在数据不一致的风险。

最常见的方式就是使用事务来处理数据库的批量操作,当出现异常时,执行数据库客户端的回滚方法即可。

配置回滚

如果将项目的配置信息,比如数据库链接地址,写死到代码中,一旦配置错了或者地址发生变更,就要重新修改代码,非常麻烦。

比较好的方式是将配置发布到配置中心进行管理,让项目去动态读取配置中心的配置。如果不小心发布了错误的配置,可以直接在配置中心进行回滚,将配置还原。

发布回滚

没有人能保证自己的代码正确无误,很多时候,项目在测试环境验证时没有发现任何问题,但是一上线,就漏洞百出。这就说明我们最新发布的代码是存在问题的。

这时,最简单的做法就是进行版本回滚,将之前能够正常运行的代码重新打包发布。大公司一般都有自己的项目发布平台,能够使用界面一键回滚,自动发布以前版本的项目包。

6. 多级缓存

上面提到,缓存对项目是非常重要的,不仅是提升性能的利器,也是数据库的保护伞。

但如果缓存挂掉怎么办呢?

有两种方案,第一种是为缓存搭建集群,从而保证缓存的高可用。

Redis 集群

但是一切都不可信,集群也有可能挂掉!

那么可以用第二种方案,一级缓存挂掉,我们就再搞一个二级缓存顶上!

通常,在高并发项目中,我们会设计多级缓存,即分布式缓存 + 本地缓存。当请求需要获取数据时,先从分布式缓存(比如 Redis) 中查询,如果分布式缓存集体宕机,那就从本地缓存中获取数据。这样,即使缓存挂掉,也能够帮助系统支撑一段时间。

这里可能和一些多级缓存的设计不同,有时,我们会把本地缓存作为一级缓存,缓存一些热点数据,本地缓存找不到值时,才去访问分布式缓存。这种设计主要解决的问题是,减少对分布式缓存的请求量,并进一步提升性能,和上面的设计目的不同。

多级缓存设计

7. 服务熔断和降级

每年的双十一,我们会准时守着屏幕上的抢购页面,只为等待那一个 “请稍后再试!”

我们的项目其实远比想象的要脆弱,很多服务经常因为各种原因出现问题。比如搞活动时,大量用户同时访问会导致对项目服务的请求增多,如果项目顶不住压力,就会挂掉。

为了防止这种风险,我们可以采用服务降级策略,如果系统实在无法为所有用户提供服务,那就退而求其次,给用户直接返回一个 “友好的” 提示或界面,而不是强行让项目顶着压力过劳死。

配合服务熔断技术,可以根据系统的负载等指标来动态开启或关闭降级。比如机器的 CPU 被占用爆满时,就开启降级,直接返回错误;当机器 CPU 恢复正常时,再正常返回数据、执行操作。

Hystrix 就是比较有名的微服务熔断降级框架。

Hystrix

8. 主动检测

上面提到,即使是大公司的同步服务,也可能会出现同步不及时甚至是数据丢失的情况。因此,为了进一步保证同步成功、数据的准确,我们可以主动检测

比如编写一个定时脚本或者任务,每隔一段时间去检查原地址和目标地址的数据是否一致,或者通过一些逻辑来检查数据是否正确。当然也可以在每次数据同步结束后都立即去检测,更加保险。

主动检测

9. 数据补偿

当检测出数据不一致后,我们就要进行数据补偿,比如将没有同步的数据再次进行同步、将不一致的数据进行更新等。

除了用来解决主动检测出的数据不一致,数据补偿也被广泛用于业务设计和架构设计中。

比如调用某个接口查询数据失败后,停顿一段时间,然后自动重试,或者从其他地方获取数据。又如消息队列的生产者发送消息失败时,应该自动进行补发和记录,而不是直接把这条消息作废。

数据补偿的思想本质上是保证数据的最终一致性,数据出错不可怕,知错能改就是好孩子。这种思想也被广泛应用于分布式事务等场景中。

10. 数据备份

数据是企业的生命,因此我们必须尽可能地保证数据的安全和完整。

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

数据备份

11. 心跳机制

接口可是个复杂多变的家伙,如果我们的项目依赖其他的接口来完成功能,那么最好保证该接口一直活着,否则可能会影响项目的运行。

举个例子,我们在使用银行卡支付时,肯定需要调用银行提供的接口来获取银行卡的余额信息,如果这个接口挂了,获取不到余额,用户也就无法支付,也就损失了一笔收入!

因此,我们需要时刻和重要的接口保持联系,防止他们不小心死了。可以采用心跳机制,定时调用该接口或者发送一个心跳包,来判断该接口是否仍然存活。一旦调用超时或者失败,可以立刻进行排查和处理,从而大大减少了事故的影响时长。

心跳检测

12. 冗余设计

在系统资源和容量评估时,我们要做一些冗余设计,比如数据库目前的总数据量有 1G,那么如果要将数据库的数据同步到其他存储(比如 Elasticsearch)时,至少要多预留一倍的存储空间,即 2G,来应对后面可能的数据增长。业务的发展潜力越大,冗余的倍数也可以越多,但也要注意不要过分冗余,毕竟资源也是很贵的啊!

其实,冗余设计是一种重要的设计思想。当我们设计业务或者系统架构时,不能只局限于当前的条件,而是要考虑到以后的发展,选择一种相对便于扩展的模式。否则之后项目越做越大,每一次对项目的改动都步履维艰。

13. 弹性扩缩容

梦想还是要有的,说不定突然,我们原先只有 100 人使用的小项目突然就火了,有几十万新用户要来使用。

但是,由于我们的项目只部署在一台服务器上,根本无法支撑那么多人,直接挂掉,导致这些用户非常扫兴,再也不想用我们的项目了。

梦想破碎了

这也是常见的风险,我们可以使用弹性扩缩容技术,系统会根据当前项目的使用和资源占用情况自动扩充或缩减资源。

比如当系统压力较大时,多分配几台机器(容器),当系统压力较小时,减少几台机器。这样不仅能够有效应对突发的流量增长,还能够在平时节约成本,并省去了人工分配调整机器的麻烦。

14. 异地多活

前面提到,服务器是不可信的,别说一个服务器挂掉,由于一些天灾人祸,整个机房都有可能集体挂掉!

和备份不同,异地多活是指在不同城市建立独立的数据中心,正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的服务,即同时有多个 “活” 的服务。

而某个地方业务异常的时候,用户能够访问其他地方正常的业务系统,从而获得正确的服务。

如此一来,即使广州的机房跨了,咱还有上海的,上海的跨了,咱还有北京的。

同时活着的服务越多,系统就越可靠,但同时成本也越高、越复杂,因此几乎都是大公司才做异地多活。千万不要让正常情况下的投入大于故障发生的损失!

15. 监控告警

项目的运行不可能一直正常,但是我们不可能 24 小时盯着电脑屏幕来监视项目的运行情况吧?又不能完全不管项目,出了 bug 等着用户来投诉。

因此,最好的方式是给业务添加监控告警,当程序出现异常时,信息会上报到监控平台,并第一时间给开发者发送通知。还可以通过监控平台实时查看项目的运行情况,出了问题也能更快地定位。

Grafana 监控平台

16. 线上诊断和热修复

既然程序世界一切都不可信,危险无处不在,那么干脆就做最坏的打算,假设线上程序一定会出 bug。

既然防不胜防,那就严阵以待,在 bug 出现时用最快的速度修复它,来减少影响。

通常,我们要改 bug,也需要经历改动代码、提交代码、合并代码、打包构建、发布上线等一系列流程。等流程走完了,可能系统都透心凉了。

为提高效率,我们可以使用线上诊断和热修复技术。在出现 bug 时,先用线上诊断工具轻松获取项目的各运行状态和代码执行信息,提升排查效率。发现问题后,使用热修复技术直接修改运行时的代码,无需重新构建和重启项目!

Java 中,我们可以使用阿里开源的诊断工具 Arthas,同时支持线上热修复功能。也可以自己编写脚本来实现,但是相对复杂一些。

Arthas Logo


看到这里,肯定有同学会吐槽,怎么写个程序要考虑那么多和功能无关的问题。本来五分钟就能写完的代码,现在可能一个小时都写不完!

超凶

其实,并不是所有的项目都要做到绝对的安全(当然我们也做不到),而是我们应该时刻保持居安思危的思想,把防御性编程当做自己的习惯。

实际情况下,要根据项目的量级、受众、架构、紧急程度等因素来综合评估将项目做到何种程度的安全,而不是过度设计、杞人忧天。

让我们把时间慢下来,在开发前先冷静思考,预见并规避风险,不要让达摩克利斯之剑落下。

更多好文欢迎点击我的头像查看简介,可以多和鱼皮交流呀,共同进步!
有帮助的话求个赞 + 收藏叭!

全部评论

(9) 回帖
加载中...
话题 回帖

推荐话题

近期热帖

近期精华帖

热门推荐