嵌入式软件的问题分析
1 问题背景
一切为了进度,软件开发的首要目标就是以最快的速度满足客户需求,“快”是第一要素,但是短期指标;可复用性、扩展性等长期指标被忽略,导致后期的维护、功能增减调整都非常困难。一个小的业务需求会牵一发而动全身,一个小的故障修复可能引入更多的问题。整个系统包袱越来越沉重,软件的质量和开发周期越来越不可控。
排除软件开发人员的水平和项目进度的原因,主要影响因素还包括软件架构,和软件缺陷的修复能力。对于量产软件,架构问题是先天性的,后期很难大改,只能前期预防;软件缺陷问题是无法避免的,只能期望快速修复。
2 软件架构问题
2.1 软件架构的特点
1. 承载力
正如一艘船最多能装多少人,从软件方面来说是软件架构能承载多少业务或功能需求,当然,这需要架构师一开始架构系统的时候,就需要有一定的预见性。但也没必要为了极小概率事件增加过多的冗余。
2. 易用性
易用性决定了软件的整体开发效率,好的架构会让团队成员容易上手,子系统容易对接,开发效率高,各模块和子系统的编写只需要关注系统的设计和编码工作,其他模块间通信方面的事情架构可以提供很好的兼容。
3. 扩展性
一个水杯除了用来喝水,也可用来喝酒,适应不同场景,在一定范围内满足不同的需求,是非常有必要的。软件架构也是这样,要新增更多的功能就要具备更高的扩展性。可扩展性的关键就在于新增部分不能影响其他,如果增删导致系统整体使用异常,那么这个架构的可扩展性就很差。
4. 伸缩性
伸缩性就是设计的方案或系统是否可以根据需求适配不同数量的功能或子系统,在我们设计的软件系统中,架构的可伸缩性决定了架构的可适配性,例如,当硬件资源不足时,可以调整配置如flash的空间分配,支持减少一些服务但仍能正常运行。
5. 容错性
软件运行中的异常,如用户的非法操作,或者软件本身的小缺陷导致整个系统无法使用,那这个架构容错性就很差。软件中的一些缺陷无法避免,但是我们应尽量保证这个缺陷的影响范围最小。倘若出现系统无法使用的情况,应该有备份方案,比如自动重启或者自动恢复数据等功能,也应该能够让开发人员及时知道问题的发生,以及问题所在的位置并记录错误信息。
在架构设计中,以上五项基本能力缺一不可,某项能力的突出并不能带动其他项,如果某一项能力比较弱,随着时间的推移,问题会越来越大,甚至系统崩溃。就像木桶原理那样,一个木桶的容量不是取决于最长的那根木板,而是取决于最短的那根。
2.2 如何规划软件架构
2.2.1 必须熟悉业务
软件是为业务服务的,业务才是“目的”,软件系统是为了达成业务系统目标的手段和方法。适应当前的业务需求是基础,充分考虑和预测未来的业务扩展,根据业务的扩展性来设计软件的扩展性。如果可预见未来没有扩展重大新业务的需求,那么相应的软件架构就没有必要采用高扩展的软件架构。比如嵌入式的传感器数据采集小设备,就没有必要把云计算等,业务范围不沾边的技术点放到其中。软件架构必须以服务业务为核心思想,不熟悉当前软件业务、和未来业务的扩展的架构师是很难设计出好的软件架构。
2.2.1 借鉴业内成熟的架构
不照搬,并不意味着不要借鉴。借鉴业内成熟的软、硬件架构是相对稳妥、高效的做法。以业内的架构为基础,根据自身业务的特点,进行适配、裁剪和增加新的功能。熟悉业内常规的、成熟的、最新的软件架构是架构师的一项基本功。但熟悉并不是意味着必须立即在目标系统中实施这些软件架构。
2.2.3 采用设计模块
设计模式(Design pattern)代表了最佳实践,设计模式是软件开发人员在开发过程中对一般问题的解决方案;是一套被反复使用的、多数人知晓的代码设计经验的总结,经过相当长的一段时间的试验和错误总结出来的。
使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性,合理地运用设计模式可以完美地解决很多问题。每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。
用设计模式构建一个新的软件模块时,短期会让人感觉有多此一举的味道;但中长期来看,设计模式能够克服“坏”架构的特征。学习这些模式有助于经验不足的开发人员通过一种简单快捷的方式来学习软件设计。尽管设计模块通常被有经验的面向对象的软件开发人员所采用,但是嵌入式软件C语言也可以借鉴,参考《嵌入式软件的设计模式(上)》,《嵌入式软件的设计模式(下)》。
2.2.4 合理的横向和纵向切分
横向切分 :从硬件、驱动、组件到业务层,软件分层隔离。如数据通信:PHY/MAC/IP/TCP/应用层
纵向切分 :根据业务处理流程的环节纵向切分,不同的环节为不同的模块,不同的业务功能为不同的模块,如socket网络、GNSS卫星定位。
2.2.5 按树形结构组织
按照树形结构的方式组织软件系统,不同的大功能拆分为小功能,文件夹内套文件夹的实现形式,命名上统一,方便按功能快速找到对应的源码。
2.2.6 降低模块之间的耦合度
耦合性是一种软件度量,是指一程序中模块及模块之间信息或参数依赖的程度;内聚性是一个和耦合性相对的概念,一般而言低耦合性代表高内聚性,反之亦然。
2.2.7 降低模块与模块之间通信
一个软件内模块与模块之间的通信,构成了一个内部的通信网,避免内部模块的通信采用网状结构,这种解决方案是设计模式中的中介者模式。
2.3 重构和演进架构
架构不能一成不变,要随着业务需求的演进而升级重构,一成不变的架构是危险的,总有一天架构成为业务演进的最严重的制约因素。
这种需要实际开发中除完成既定的项目外,预留部分人力进行架构升级维护,持续小改动,不定期根据业务的需求进行架构的重构,未雨绸缪。
3 软件问题的分析与解决
嵌入式软件由于调试手段的限制、部署场景的多样化、软硬件问题混合在一起、外部环境因素的影响等因素,导致软件经常会遇到一些非常难以解决的问题。
3.1 解题思想
熟悉软件的业务流程:从业务的角度发现问题、复现问题并解决问题。
熟悉软件的总体架构:软件架构是解决难题问题的基本框架,基于软件架构解决问题不会陷入到局部细节,导致修复一个问题的同时产生新的问题,不会犯原则性、方向性错误。
熟悉软件代码的实现:熟悉代码的细节,能够更好、更快的在蛛丝马迹中找到证据和突破点,甚至在问题还没有收敛前,提供一种收敛的方向,引领问题的解决,对代码的熟悉程度直接关系到解决问题的速度。
3.2 调试手段和信息不足相关问题
3.2.1 现场偶发性、难复现性引发的问题
一些偶发性现象级问题,甚至导致系统偶发性的重启,无法复现,设备重启之后,故障消失后,再也很难复现。
1、分析日志文件
从log中寻找异常提示,是应对不可重复性、偶发性故障最基本的手段。在系统某处发生异常时,一定会在log中留下蛛丝马迹,可以请客户协助提供串口日志,在log文件中查找问题。或者设备自己内部记录log,但嵌入式设备由于存储空间的限制,可能先前过于久远的信息,就会被新的信息被覆盖,针对这种情况,就需要定期清除无效日志。有些异常会导致系统重启,而重启之后,就会导致异常信息被正常重启的信息覆盖,这就需要系统能够支持log的备份。不管怎么样,log为定位现场问题提供了最基本的、最主要的信息来源。一个完善的log机制,对于定位现场问题非常有帮助。如果不满足,可能首要任务是先完善日志功能。
2、回退软件版本,紧急消除现场问题
有些现场问题,虽然偶发事件,但发生后影响严重,客户无法接受。针对这种情况,在解决问题之前,可以先把软件降级,降级到相对稳定,没有严重故障的版本。
3、比较相邻版本之间的代码改动
如果不容易复现的故障,确认在升级了某个软件版本之后才出现的,而其他现场条件都没有变化,且分析log也无法发现异常点。此时,一种高效的解决此问题的方法,就是比较两个版本之间的代码的改动。
代码改动比较少,分析代码比较容易;如果代码改动比较多,就需要根据用户描述的现象,结合前后代码的改动模块,初步分析最可能是哪个模块引起的,这种往往需要对系统架构较深刻的理解。在众多修改模块中,分析最有可能关联的代码模块的改动,然后逐一排查 。分析代码的改动与出现的现象之间可能的关联关系,对开发人员个人的技术素养和方**有较高的要求 。比较相邻版本之间的代码改动,针对某些棘手的现场问题,有时候确实是一个非常有效的手段。
4、问题复现
虽然常规来说现场很难复现,但可以人为的修改软件、构建或增加模拟数据,人为创造或触发条件,增加故障复现的几率。在设计触发条件时,需要围绕用户描述的现场故障现象来设计触发条件,观察是否能否复现,且表现一致。 |