前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >软件系统的稳定性

软件系统的稳定性

作者头像
张逸
发布2018-03-07 16:09:27
7.3K0
发布2018-03-07 16:09:27
举报
文章被收录于专栏:斑斓斑斓

软件系统的稳定性,主要决定于整体的系统架构设计,然而也不可忽略编程的细节,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃。

大约是五年前,我在QCon Beijing聆听了Michael T. Nygard的演讲。当时他的演讲题目为《失败来临的前兆》,我当时作为该Track主持人,还依稀记得Nygard伟岸的身躯,重量级的牛人气息扑面而来。时光荏苒,白云苍狗,五年时间匆匆而过,思之不禁让我感慨。

△ Michael T. Nygard

在QCon Beijing 2010的演讲

我正是在那时听闻Nygard的大作Release It!,此书获得了2008年度Jolt大奖的提名,在Nygard的个人网站上,提及他写作此书的动机:

这本书凝聚了我多年来与生产系统打交道的经验。我经常因为某些本该24x7运作的系统宕机,而在半夜三点受到惊扰。 关于系统设计和架构的书籍往往只告诉你怎样满足功能需求,的确这类书籍对你在QA面前过关会有很大帮助。然而这本书中的重点将放在怎样才能使一款软件成为真正的产品这个话题上。如果你不想整日被电子紧箍所束缚,这本书应该正是你所想要的东西。

显然,我们低估了此书的价值,在国内较少看到对此书的推介。或者是因为此书相对狭窄的读者范围,又或者因为中文版的“千呼万唤始出来”。它本来可以很好地成为架构师,尤其是关注产品质量的设计者最有力的武器,无奈,被藏于深山,就这般被略过了。时至今日,Release It!对架构师而言,仍然具有非常大的参考价值。它可能成为经典。

△ Michael T. Nygard大作Release It!

除了Release It!,在《架构之美》一书中,Michael Nygard还贡献了一篇精彩的文章《记忆留存》。我在对该书的书评中这样写道:

本章在整本书中,也是工程实证主义表现得最为浓烈的一章。Michael完整地介绍了Creation Center项目的架构过程。作者通过明确事实,发现重要问题,然后识别关注点的方式来剖析架构,理解需求到功能实现的映射。 作者的讲述都是经验之谈,因而总是显得言之有物。例如在讲解架构模块中的渲染管道时,提出了一种“快速失败”模式,遵循“快速失败、大声失败”的设计哲学。之所以运用这一原则,是因为系统的核心功能是生产照片。这样的生产过程不允许因为软件的原因而导致生产线停下来。这就决定了渲染管道的设计,必须在最早的过程中进行验证。 整章内容让我唯一感到恼怒的就是篇幅太短,许多步骤、技巧以及设计思想都是点到即止,终究有些隔靴搔痒的感觉。

我在阅读Release It!一书时,对于某些章节仍嫌晦涩难懂,若不脑洞大开就很难吃透;诸多模式也让我有眼花缭乱之感。倘若能像蠹鱼啮书一般细细品味每一字句,必能有更多收获。下文就是我当时阅读此书的一小步印迹。

软件系统的稳定性,主要决定于整体的系统架构设计,然而也不可忽略编程的细节,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃。

这正是我阅读Release It!的直接感受。究其原因,一方面是程序员对代码质量的追求不够,在项目进度的压力下,只考虑了功能实现,而不用过多的追求质量属性;第二则是对编程语言的正确编码方式不够了解,不知如何有效而正确的编码;第三则是知识量的不足,在编程时没有意识到实现会对哪些因素造成影响。

例如在Release It!一书中,给出了如下的Java代码片段:

△ 代码片段,需单击放大或横向阅读

这一小段代码是造成Airline系统崩溃的罪魁祸首。

程序员充分地考虑了资源的释放,但在这段代码中他却没有对多个资源的释放给予足够的重视,而是以释放单资源的做法去处理多资源。在finally语句块中,如果释放Statement资源的操作失败了,就可能抛出异常,因为在finally中并没有捕获这种异常,就会导致后面的conn.close()语句没有执行,从而导致Connection资源未能及时释放。最终导致连接池中存放了大量未能及时释放的Connection资源,却不能得到使用,直到连接池满。当后续请求lookupByCity()时,就会在调用connectionPool.getConnection()方法时被阻塞。这些被阻塞的请求会越来越多,最后导致资源耗尽,整个系统崩溃。

Release It!的作者Michael T. Nygard对Java中同步方法的使用也提出了警告。

同步方法虽然可以较好地解决并发问题,在一定程度上避免出现资源抢占、竟态条件和死锁的情况。但它的一个副作用同步锁可能导致线程阻塞。这就要求同步方法的执行时间不能太长。

Java的接口方法不能标记synchronized关键字,当我们在调用封装好的第三方API时,基于“面向接口设计”的原理,可能调用者只知道公开的接口方法,却不知道实现类事实上将其实现为同步方法,这种未知性就可能存在隐患。

假设有这样的一个接口:

△ 代码片段,需单击放大或横向阅读

如果接口方法get()的实现如下:

△ 代码片段,需单击放大或横向阅读

这段代码很简单,当调用者试图根据id获得目标对象时,首先会在Cache中寻找,如果有就直接返回;否则通过create()方法获得目标对象,然后再将它存储到Cache中。create()方法是该类定义的一个非final方法,它执行了DB的查询功能。

现在,假设使用该类的用户对它进行了扩展,例如定义RemoteAvailabilityCache类派生该类,并重写create()方法,将原来的本地调用改为远程调用。问题出现了。由于此时的create()方法是远程调用,当服务端比较繁忙时,发出的远程调用请求可能会被阻塞。由于get()方法是同步方法,在方法体内,每次只能有一个线程访问它,直到方法执行完毕释放锁。现在create()方法被阻塞,就会导致其他试图调用RemoteAvailabilityCache对象的get()方法的线程随之而被阻塞,进而可能导致系统崩溃。

当然,我们可以认为这种扩展本身是不合理的。但从设计的角度来看,它并没有违背Liskove替换原则。从接口的角度看,它的行为也没有发生任何改变,仅仅是实现发生了变化。如果不是同步方法,则一个调用线程的阻塞并不会影响到其他调用线程,问题就可以避免了。

这里的同步方法本身是合理的,因为只有采取同步的方式才能保证对Cache的读取是支持并发的。书中给出这个例子,无非是要说明同步方法潜在的危险,提示我们在编写代码时,需要考虑周全。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2015-03-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 逸言 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档