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

来源 | 程序员鱼皮

作者 | 李鱼皮

为什么你的程序总是出现 bug?
凭什么让改 bug 占据了你大部分的时间?看完本文,保证你能设计出更稳定的程序,摆脱 bug 的缠绕,做项目更安心!


记得我在学校的时候,做的那些项目,不是为了应付课程作业,就是为了参加比赛时展示用,因此对项目的质量要求非常低。到底有多低呢?大部分的项目,只要基本的功能可以使用,就算完成了,完全不考虑任何的异常情况。甚至只要能成功运行一次,让我截几张图放到 PPT 或者实验报告里,足够向老师交差或者应付比赛答辩就行。那项目出现 bug 怎么办呢?

  • 如果测试的时候发现有些功能不可用,那很简单,不管他,直接 PS 一张正常运行的图就行。
  • 如果比赛的时候发现有些功能不可用,那也很简单,把锅甩给 “现场网络不好” 就行。

但是,这些 “小技巧” 在企业中是行不通的,企业级项目必须为企业带来实际的价值,容不得半点马虎和欺骗。我第一次进入企业实习时,还保留着自己在学校开发项目的狼性 🐺,只要能够完成基本功能就行,保证以最快的速度完成开发。有一天,当我洋洋得意准备早点下班时,测试同学走过来跟我说。“喂,你的程序有 bug,这里用户下单怎么金额是负的?”

写个 bug

对于我一个初入职场的小白,这是人生中第一次有人说我的代码有 bug,我有问题,我不对劲。当时,我脑海的第一个念头竟然是怎么把这个 bug 糊弄过去,而不是怎么去更正!看来我已经养成了非常不好的习惯。那之后几天,我又连续收到了测试提出的多个 bug,然后将他们一个个改正。如果将这样一个漏洞百出的程序发布上线,带来的损失是不可估量的,现在想想仍心有余悸。

二八原则 开发 1 天,改 bug 4 天

这件事之后,我意识到,在企业中开发项目,不能只追求开发时的效率,还要注重项目的稳定性,否则带来的额外返工时间远比开发时节省的时间要长,而且会影响同事对你的看法。如果将开发时产生的 bug 遗留到线上,后果更是不堪设想!后来,在字节跳动和腾讯这两家大公司工作后,我进一步认识到了项目稳定性有多重要,并且积累了更多只有在大公司才能学到的提升项目稳定性的经验。我总结了 10 个开发中通常不会考虑到的风险点,以及 16 个减少风险、提升项目稳定性的方法,分享给大家~在分享这些之前,先讲个故事。

达摩克利斯之剑

古希腊传说中,达摩克利斯是公元前 4 世纪意大利叙拉古的僭主(古希腊统治者独有的称号)狄奥尼修斯二世的朝臣,他非常喜欢奉承狄奥尼修斯。他奉承道:“作为一个拥有权力和威信的伟人,狄奥尼修斯实在很幸运。”于是狄奥尼修斯提议与他交换一天的身份,那他就可以尝试到首领的命运。在晚上举行的宴会里,达摩克利斯非常享受成为国王的感觉。当晚餐快结束的时候,他抬头才注意到王位上方仅用一根马鬃悬挂着的利剑。他立即失去了对美食和美女的兴趣,并请求僭主放过他,他再也不想得到这样的幸运。

达摩克利斯之剑

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

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

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

危机四伏

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

软件开发正是如此,表面上机器是 “死” 的,只会按照人输入的指令或编好的程序来执行,一成不变,听话得很。好像我们写好代码扔到机器上后,就可以高枕无忧。但真的是这样么?我们真的可以信任机器和程序么?其实,在程序世界中危机四伏,人为因素、环境因素等可能都会对我们的程序产生影响。因此,我们必须时刻坚守软件开发的不信任原则,保持 overly pessimistic(过于悲观),把和程序有关的一切请求、服务、接口、返回值、机器、框架、中间件等等都当做不可信的,步步为营、处处设防。

程序世界里的不信任原则

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

大项目的苦衷

“当一个人获取多少荣誉和地位,他都要付出同样多的代价。”软件开发中,项目价值越大,需要承受的压力也越大,来听听大项目的苦衷吧。

我是一个身价过亿的大项目,每天服务着上千万的用户,帮助他们获得知识与快乐。我的小伙伴们只看到我身上的光环和荣耀,但是他们看不到我背负的压力和风险,今天终于有机会和大家倾诉我的苦衷了。记得很多年前,我还是个孩子,只有几个小主人开发我,那段时间,我成长得很快。虽然只有几十个人使用我,但我感到非常轻松和快乐,偶尔偷会儿懒,也不会被人发现。后来,我的功能越来越多,越来越强大。每天有数之不尽的新面孔来和我打招呼,并享受我提供的服务。渐渐地,更多开发者在我身上留下了印记,我感觉自己正在变得复杂,也开始感受到了压力。我再也找不到机会偷懒,因为我一旦休息,就会让我的主人们损失一笔不小的财富。如今,我已经是一个成熟的大项目了,每天有上千万的用户依赖我,我终于拥有了更大的价值,却也增加了很多烦恼,感受到了更大的危险。

首先,同时服务千万用户,每秒钟都可能会有几十万、甚至几百万个请求需要我来处理,因此我必须每时每刻无休止地高负载工作,且不说休息,哪怕稍微慢了一点,就会遭到用户的投诉,主人们也会因此受到批评。我的运行,必须依靠很多兄弟们的支撑,因此我必须和兄弟们好好相处,哪怕一个兄弟倒了,我都会受到影响。在我强大的实力背后,有一颗非常脆弱的心。经历了那么多次的强化和改造,我的功能逐渐变多的同时,也因此被植入了各种框架和插件,体积像滚雪球一般越来越大,不知道什么时候就会爆炸。以至于主人们每次改动我时都要万分谨慎,我的成长也变得十分缓慢。

复杂

然而最让我感到恐惧的,是那些坏家伙们!他们和正常的用户不同,有的不断制造请求,试图将我击垮。有的绕到我的背后,试图直接控制我。有的对我虎视眈眈,监视并记录我的一举一动。还有的尝试各种非法操作,想从我身上牟取暴利。作为一个大项目真是太累了,我不知道我还能坚持多久。

真的可信么?

“地位越高,看似越安全,实则越危险。”如今是一个软件开源和共享的时代,我们在开发项目时,或多或少会使用到网上现有的资源,比如依赖包、工具、组件、框架、接口、现成的云服务等等,这些资源能够大大提升我们的开发效率。就拿云服务来说,几乎已经成了我们开发必备的资源,以前我们想要做一个网站,可能需要自己买一台物理服务器,然后连通网络,再把项目部署上去。而如今,直接登录大公司的云官网(像腾讯云、阿里云),然后租一台云服务器就行了,非常省事。再说说现在主流的开发框架,以前做一个简单的网站界面可能只会使用 HTMLCSSJavaScript 这三种最基础的技术,而如今,网站的样式和交互越来越复杂,我们不得不使用一些知名的框架来提升开发效率,比如 Vue 和 React。听起来好像没有任何问题,你也根本不会去怀疑什么,因为我们天生带着对大公司,或者说是对名气的信任。但是,你知道么,当你决定使用其他人的资源时,你就已经把项目系统的部分掌控权、甚至可能是半条命,都交出去了。那么不妨思考一下,你使用的这些资源,真的可信么?下面 10 个问题,可能改变你对开发的认知。1. 开发工具可信么?我们通常是在大而全的开发工具中编写代码,比如 JetBrains IDEA 或者 Vscode。很多刚开始写代码的同学、甚至是一些经验丰富的老手,都对开发工具保持绝对的信任。比如你在键盘上敲击 a,那编辑器界面上显示的一定是 a。但是,由于内存不足等种种原因,开发工具其实也会抽风。比如你想要调用某个函数,通常敲击函数名前几个字母后,开发工具就会自动给你提示完整的函数名,但如果开发工具没有给你提示,你首先怀疑的是这个函数不存在,而不是编辑器没有按预期给出提示。遇到这种情况,可以稍等编辑器一下,或者进一步确认函数是否真的不存在,而不是立刻创建一个新的函数。又或是项目无法运行,怎么排查都觉得没问题,这时不妨重新启动下开发工具,或者清理一下缓存,说不定项目就能正常运行了!还有很多非常有意思的情况,比如编辑器一片大红,各种提示错误,但是项目依然能成功运行。

为什么不能运行?为什么能运行?

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

2. 开源项目可信么?

现在是一个软件开源的时代,在 GitHub 等开源项目平台上能够找到大量优秀的开源项目,好的开源项目甚至可以得到 10 万多个关注,那这些知名的开源项目可信么?不完全可信!从每个开源项目的 Issues 就能看出这点,而且通常越大的项目,被发现的问题越多,比如 Vue 项目,累积提出并关闭了 8000 多个问题。

Vue 项目的问题

我记得自己有一次使用知名的开源服务器 Tomcat,就遇到了 bug,每次接受到特定的请求都会报错。刚开始我根本没有怀疑是 Tomcat 的问题,而是绞尽脑汁地想自己的代码哪里写错了。后来经过反复的排查和搜索,终于确认了就是 Tomcat 本身的 bug!虽然开源项目并不完全可信,但是相对于私有项目而言,所有对项目感兴趣的同学可以共同发现项目中的问题,并加以解决,在一定程度上还是能够提高项目的可靠性的。

3. 依赖库可信么?

我们在开发项目时,通常会用到大量的依赖库。直接在官方依赖源(比如 Maven 和npm)搜索依赖库,然后使用包管理器,用一行命令或者编写配置文件就能够让其自动安装依赖,非常方便。但是,这些发布到官方源的依赖库,就可信么?且不说基本每个开发者都有机会发布依赖库到官方,就算是互联网大公司的依赖库,也未必可信。给我印象最深刻的就是阿里巴巴的 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 长整数类型。
  2. 值合法性校验:比如整数的范围大于等于 0、字符串长度大于 5,或者满足某种特定格式,比如手机号、身份证等。
  3. 用户权限校验:很多接口需要登录用户或者管理员才能调用,因此必须通过请求参数(请求头)来判断当前用户的身份,被一个普通用户下载了 VIP 付费电影肯定是不合理的!

4. 流量控制

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

DDOS 分布式拒绝服务攻击

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

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

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

Sentinel 流控面板

5. 回滚

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

6. 多级缓存

上面提到,缓存对项目是非常重要的,不仅是提升性能的利器,也是数据库的保护伞。但如果缓存挂掉怎么办呢?有两种方案,第一种是为缓存搭建集群,从而保证缓存的高可用。

但是一切都不可信,集群也有可能挂掉!那么可以用第二种方案,一级缓存挂掉,我们就再搞一个二级缓存顶上!通常,在高并发项目中,我们会设计多级缓存,即分布式缓存 + 本地缓存。当请求需要获取数据时,先从分布式缓存(比如 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

看到这里,肯定有同学会吐槽,怎么写个程序要考虑那么多和功能无关的问题。本来五分钟就能写完的代码,现在可能一个小时都写不完!其实,并不是所有的项目都要做到绝对的安全(当然我们也做不到),而是我们应该时刻保持居安思危的思想,把防御性编程当做自己的习惯。实际情况下,要根据项目的量级、受众、架构、紧急程度等因素来综合评估将项目做到何种程度的安全,而不是过度设计、杞人忧天。让我们把时间慢下来,在开发前先冷静思考,预见并规避风险,不要让达摩克利斯之剑落下。


暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇