目录
前言
在嵌入式软件中,由于调试手段的限制、部署场景的多样化与复杂化以及、软硬件问题混合在一起、外部环境因素的影响以及内存空间的限制等因素,导致嵌入式软件经常会遇到一些非常难易解决的问题,本文对个人的历史经验进行了总结。
第1章 解题思想
-
思路:
全面、胆大地假设,小心地求证。 -
团队合作:
大多数时候,团队力量 大于个人英雄主义,善于
领导和发挥
团队的力量(不同技术领域的专家)
-
熟悉软件的业务流程:
从业务的角度发现问题、解决问题,一切软件是为业务服务的。 -
熟悉软件的总体架构:
软件架构是解决难题问题的基本框架,基于软件架构解决问题不会陷入到局部细节中无法自拔或迷失方向,基于软件架构解决问题,不会犯原则性、方向性错误。
-
熟悉软件代码的实现:
熟系代码的细节,能够更好、更快的在蛛丝马迹中找到证据和突破点,甚至在问题还没有收敛前,提供一种收敛的方向,引领问题的解决。对代码的熟悉程度直接关系到解决问题的速度。 -
方法论:用系统化、结构化思维方法解决问题
[架构之路-51]:架构师 – 用系统化、结构化思维解决复杂难搞的软件故障问题 – 马克思主义哲学在软件系统中的应用_文火冰糖的硅基工坊的博客-CSDN博客
备注:
简单的功能性故障的解决,不在本文探讨的范围。
第2章 调试手段和信息不足相关问题
2.1 现场偶发性、难复现性引发的问题
@故障描述
在工程中,经常遇到一些偶发性现象级问题,客户或测试发现 ,目标系统偶然会出现一些与预想或期望不一致的现象,有时候甚至导致系统偶发性的重启或偶发性的暂停服务。
客户试图上报故障,描述故障信息或描述复现方法时发现,该故障是偶发性的,无法复现,设备重启之后,故障消失后,再也很难复现。
客户自己也不知道复现的方法和步骤,甚至无法获得目标系统出异常或故障时候的上下文。
@解决之道
(1)分析log文件(日志文件)
分析log,从log中寻找异常提醒,是应对不可重复性、偶发性故障
最基本
的手段。
在系统某处发生异常时,一定会在log中留下蛛丝马迹:
- 有些很直观:根据log可以直接找到问题所在。
- 有些很隐蔽:只有对log非常敏感或代码非常熟悉的人,才能感知到log中的异常。
- 有些别淹没:在一个大型的复杂系统中,问题的现象与出异常的根源往往不在同一个地方,比如OAM上报的异常,异常的根源很可能在其他地方,因此,让OAM的从log中发现其他组件的异常是一件很困难的事,除非问题非常严重,否则,不太可能所有的组件team都从log中查找异常,这就导致,有些异常信息,由于故障定位的人的技术知识或业务知识的不完备,导致真正的异常信息被淹没在log中,这种现象在很多团队存在。
- 异常信息被覆盖:任何嵌入式系统的log信息,由于存储空间的限制,最远时间的log信息取决于存储空间的大小,log信息输出频度,因此,过于久远的信息,就会被新的信息被覆盖,也就是异常信息的根源信息被覆盖掉了。针对这种情况,就需要客户那边有一种监控机制,一旦发现异常,就需要收集log。
- 异常信息重启后备正常信息覆盖:有些异常会导致系统重启,而重启之后,就会导致异常信息被正常重启的信息覆盖。这就需要系统能够支持log的备份,最好被备份5次重启的log信息。
不管怎么样,log为我们定位现场问题提供了最基本的、最主要的信息来源。
一个完善的log机制,对于定位现场问题非常有帮助。
(2)回退软件版本,紧急消除现场问题
有些现场问题,虽然偶发事件,但发生后,影响程度比较高,客户不可接受。
针对这种情况,在真正发现问题之前,可以先把软件降级到之前的版本,降级到相对稳定,没有严重故障的版本。
(3)比较相邻版本之间的代码改动
如果不容易复现的故障,是在升级了某个软件版本之后才出现的,而其他现场条件都没有变化。
且分析log也无法发现异常点,此时,一种高效的解决此问题的方法,就是比较两个版本之间的代码的改动。有时候,代码改动比较少,分析代码比较容易;有时候,代码改动比较多,这时候,就需要根据用户描述的现象,结合前后代码的改动模块,初步分析最可能是哪个模块引起的,这种往往需要对系统架构较深刻的理解。
一旦确定了,在众多修改模块中,最有可能的
有关联
的模块,就分析最有可能关联的代码模块的改动,然后逐一排查 。
分析代码的改动与出现的现象之间可能的关联关系,甚至可以根据代码推测代码中可能的bug。
当然,这对开发人员个人的技术素养和方法论有较高的要求 。
不过,比较相邻版本之间的代码改动,针对某些棘手的现场问题,有时候,确实是一个非常有效的手段。
(4)试图在实验室复现
虽然常规来说:现场很难复现,实验室复现更难。但在实验室复现问题,对解决棘手问题非常有帮助,是一种常见的解决复杂问题的手段,因为,一旦复现,无论是收集目标系统的log,还是检查系统状态,还是发现触发故障的条件,都非常有帮助。
在实验室复现,还有一个好处:
就是可以在实验室,人为的构建或增加模拟数据,增加业务流量,人为创造或设计触发条件,增加故障复现的几率。
在设计触发条件时,需要围绕用户描述的现场故障现象来设计触发条件,观察是否能否复现。
一旦复现,就可以反向推测,客户现场可能是类似(不一定就是)原因。
历史经验表明,这种方法,对于解决客户关注度高但在用户现场又难以复现的问题,是非常有帮助的。
(5)分析代码
根据用户描述的现象,硬分析代码,是一种通用的方法,放四海皆准的方法。
熟悉自身代码的逻辑关系,是基本功。
(6)打开debug log
如果常规的log,无法展现故障的异常,就需要打开debug log。
在现场打开debug log也是有负面作用的:
- log过多,影响客户现场软件的性能。
- log过多,旧的log很快就被覆盖,即使出现故障,也会被海量的正常log覆盖。
这种方法,需要可能实时监控,一旦出来问题,立即保存当前的log。
工程实践中,实施难度大。
(7)打patch
对有可能出问题的地方,但现场代码有没有log,可以打一些patch,增加新的log信息。
另外,还可以根据客户描述的故障现象,人为地增加以下监控代码,用于监控软件内部的状态,并把这些信息存放到log或其他地方。
然后,把增加了patch的软件放到客户现场进行进一步观察。当下次异常偶发出现的时候,我们额外关注的信息就可以被捕获到。
2.2 客户提供的信息不准确、甚至是错误信息、误导性信息
@问题描述
大量的历史案例表明,客户对现场信息的描述准确性,取决于客户自身技术能力大小。
由于客户技术能力大小千差万别,经验表明,很多客户描述的现场信息,经常是不准确的,甚至是完全相反的,有时候,信息之间是自相矛盾的,跟有甚至,客户把自己的猜测当成事实来描述。
客户对现象的描述,有时候,还带有强烈的个人倾向和推测,并非完全的客观。
@解决之道
- 要对客户现象进行基本的判断。
- 千万不能完全听从客户对问题的描述,还需要根据log和软件系统的设计,反向推测客户提供的信息的准确性和可能性大小。
-
要跟客户
反复确认
故障的现象,
用自己的专业性
,
引导客户表达更专业性、更准确性的故障现象信息
。及时纠正客户对现象错误的描述,或者说完全不相关的描述。 - 分析客户提供的信息,哪些可能是相关的,哪些可能是完全无关的,对于无关的信息,需要进行剔除,以免被误导。
当然,要做到这一点的前提是自己对系统的架构、软件实现比较熟悉以及较深的技术功底,才能有足够的自信判断哪些客户提供信息的准确性 。
2.3 log信息不足或被覆盖的问题
@问题描述
经常会出现这样的一种信息,开发人员从log中无法获取有用的信息。
或是log确实没有有用的信息,或是技术人员的能力所限。
@解决之道
-
增加log文件的大小
-
增加log文件的个数
-
时间点:
尽可能在故障出现的时间点附近抓取log,以免故障信息被常规信息覆盖。 -
技术能力:
提升技术人员分析log的能力,相同的log,不同的技术人员,从获取有价值信息的程度是不同的。 -
提升技术人员对自身代码的熟悉程度:
log是代码逻辑的反应,对代码逻辑越是熟悉,从log中发现有价值信息的能力越大。 -
跨团队分析log:
大型复杂系统是有无数个跨组织的团队共同完成的,因此,如果技术人员自身发现不了故障或有效信息,技术人员需要得到其他团队的帮助,请求他们分析log,观察其他组件是否有异常信息。或寻求熟悉整个系统的专家的协助。
2.4 现象与真正的原因不在一起的问题
@故障描述
大多时候,解决软件故障时,是可以做到头痛医头,脚痛医脚。
然后,有些时候,头痛的原因并不在“头”,而在“脚”。这就需要知道“头痛”与“脚”的某种关联关系。
在工程时间中,经常遇到案发现场与抛尸现场不在同一处的故障问题。
解决这样的问题,对技术人员的综合技能的要求非常高,因为这个问题,不再是局部问题,而是发散或漫游到调查该问题的技术人员不熟悉的其他的软件组件领域。
即使对于熟悉整个系统的人而言,也是一个难点,因为,一开始,问题的现象与问题的原因之间的路径是发散的,没有一个确切的路径。
要找到问题的原因并加以解决,是一个艰难的过程。
怎么办?
@解决之道
- 其实,在发现根本原因之前,一开始并不知道出问题的地方与真正的原因是否在一起。
-
首先,必须以故障的表面现象作为
锚点
,作为出发点。为后续进一步的调查立一个基点。 - 根据,现象找到出问题的代码,根据代码和log分析代码的表面原因。
-
如果,确实是本处代码的问题,直接在此解决即可。即
头痛医头,脚痛医脚。
-
很多情形时,真正的原因不在显示异常的地方:或收到输入信息、或收到了异常的事件、或收到了异常的、或自身状态机的问题等。这时候,就需要问自己:“输入的信息是合理的吗”, “此时为什么会有这样的事件或消息?”, 代码在我这里修改是合理的吗, 符合系统整体的业务逻辑需求吗?有时候,由于复杂系统的程序员没有系统的视角,常以为消除故障了
表面现象
就是解决了问题。很多时候,站在系统的视角,可以从多个层面加以解决,比如,告警信息的消除,可以从OAM层面解决,可以也可从RF/L1/L2/L3的层面解决,也可以从FPGA的角度加以解决。即使在OAM内部,也有多个功能模块组成,因此,可以从规则过滤模块解决,也可从前端模块解决,也可以从后端模块解决。具体在哪儿解决最合理,这就需要有系统和结构的视角。详细可以参考:
[架构之路-51]:架构师 – 用系统化、结构化思维解决复杂难搞的软件故障问题 – 马克思主义哲学在软件系统中的应用_文火冰糖的硅基工坊的博客-CSDN博客
总之:需要多问“为什么”。
2.5 报错点发生在第三方库或软件模块内部
@故障描述
有时候,软件保障报错的地方是在第三方库,而第三方库有没有源代码或不熟悉第三方,怎么办呢?
@解决之道
- 如果第三方代码没有源代码,则把这个问题上报给第三方,让第三方给出内部出错的原因。
- 如果第三方库有源代码的话,可分析第三方代码。
- 如果没有,可以考虑升级最新的库,看是否有同样的问题。
- 增加代码,打印或检查传入第三方库函数库的参数是否正确,是否合法,大多数时候,是我们错误地传入了不合适的参数给第三方库。
- 有时候,第三方软件(不一定是库),并不仅是API函数,而是一个独立的线程或进程,我们的自己模块是通过消息与第三方的软件,这时候,需要检查发送给第三方库的消息中的数据结构以及所填写的数据是否符合要求。
- 检查使用第三方的时序是否正确,在软件系统中,时序是一个非常重要,同样的函数,同样的代码,如果时序不对,也会导致代码逻辑紊乱。
2.6 软硬件结合导致的无法定位的问题
@故障描述
在嵌入式系统中,有时候会出现硬件异常,导致软件寄存器的状态异常,软件检测到异常后,软件的状态或逻辑就会处理这种异常,然后当这些信息报给硬件人员时,硬件人员很难根据有限的信息判断,硬件到底怎么了,硬件的故障在哪里。遇到这样的问题,通常软件和硬件就会反复的踢皮球。然后,客户看到的异常是在软件这边,怎么办呢?
这里还有一个商业上的问题:如果客户感受到是硬件的问题,设备需要回收的;如果处理不好,客户有可能要求厂家全部回收所有的硬件,这会给厂家造成很大的经济损失。怎么办?
@解决之道
- 郑重的把问题提交硬件团队,让他们来确认硬件的故障,如果真是硬件有问题,且对客户的业务存在大的影响,就需要主动撤回硬件。
- 由于硬件团队,对于现场的设备,通常并没有手段来判断是否真是硬件问题的,软件团队最好能够把硬件相关的寄存器信息提供给硬件团队,以帮助硬件团队判断。
- 硬件团队会在实验室复现
- 有时候,硬件团队根据寄存器的信息,判断硬件寄存器的故障不会影响客户的业务,这时候,就需要软件团队屏蔽这些硬件故障,以避免给公司给来不必要的损失。
- 还有一种情况,硬件的寄存器状态故障,是由于软件不合理的配置造成的,不一定是硬件问题,因此,软件人员不能根据寄存器的状态,就可以100%肯定是硬件的问题。
- 硬件故障问题:需要特别关注电压电压是否正常、时钟信号是否准确,复位时间是否足够等等。
总之,软硬件的交合处,是容易扯皮的地方,这需要软件人员也同时了解硬件的工作原理,在出故障时,能够更好的判断是软件使用的问题,还是硬件真的有故障?
第3章 内存与指针相关问题
3.1 隐性的内存泄露问题
@ 问题描述
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
它们会在应用程序运行时耗尽可用的
系统
内存,所以内存泄漏通常是
软件
老化的原因或促成因素。
因此,内存泄露是一个严重的慢性病,
不会立即展现出现,但不知道未来的哪一天,所有的设备,会在相近的时间爆发问题,导致大面积业务的瘫痪,因此,需要提前解决,否则对于公司而言,就像一个定时炸弹,随时会爆炸,给公司带来预想不到的风险。
另外,内存泄露,还会导致系统意外的重启,重启的原因可能千奇百怪。
因此,
检测
和
解决
内存泄露,就显得非常重要。
@ 问题分析:泄露的原因
- 内存泄漏主要是发生在堆内存分配方式中,即malloc方式中。
- 内存泄露的根本原因:指针指向的内存空间没有得到释放。
- malloc的内存指针被覆盖,内存直接泄露。
- malloc的内存指针是局部变量,函数返回时,没有显式释放内存,导致内存泄露。
- 数据结构中有指针成员,释放内数据结构的内存空间之前,没有释放指针成员的内存空间,导致内存泄露。
因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以主要依靠在程序运行过程中来判别和诊断。
@ 问题分析:泄露的类型
-
常发性内存泄漏:
发生内存泄漏的代码会被
多次
执行到,
每次
被执行时都会
导致一块内存
泄漏。 -
偶发性内存泄漏:
发生内存泄漏的代码只有在
某些特定环境或操作过程下才会发生
。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 -
一次性内存泄漏:
发生内存泄漏的代码
只会被执行一次
,或者由于
算法上的缺陷
,导致总会有一块且仅有
一块内存发生泄漏
。不易觉察。 -
隐式内存泄漏:
程序在运行过程中不停的分配内存,但是直到
结束的时候才释放内存
。理想的情况是,使用完就立即释放内存。
@解决之道 –
静态
检测或监控是否内存泄露
- 试图释放某内存块,该内存块有两个或两个以上的有效指针指向它。
- 试图释放某内存块,该内存块没有任何有效指针指向它。
LCLink是一种通过对源代码及添加到源代码中特定格式的注释说明进行静态分析的程序理解和检错工具的
检查对象是源程序
,能检查出的内存错误有:
内存分配释放故障、空指针的错误使用、使用未定义或已被释放的内存等程序错误。
@解决之道 –
动态
检测或监控是否内存泄露
-
监控系统内存:
周期性监控堆中可用内存的大小,是检测系统是否有
内存泄露的最有效的手段。虽然,系统的内存,短期会随着业务数据的变化而变化,但长期来看,可用的剩余可用内存会围绕一个中轴线上下波动,然后,存在内存泄露的系统,其剩余可用内存随随着时间的推移,逐渐减少,这个趋势线可以展现出来的 。 - 尽量使用C++的智能指针,智能指针本质上一个包含指针成员变量的“对象”,当对象函数的作用域时,自动执行指针的析构函数,在析构函数中,执行free,释放指针所向的malloc的内存空间,完成了内存的自动释放,避免内存泄露。
@解决之道 – 如何找到在哪儿内存泄露
- 动态代码检测工具,检测代码中有没有静态的内存泄露,如Valgrind。
- 网上有大量的动态检测内存泄露的工具,这里不再列举。
3.2 指针跑飞的问题引发crash
@问题描述
指针跑飞就是指针指向
不正确的位置
,导致系统跑飞,即crash。
@问题分析
常见的就是:
-
指针未初始化或赋值就使用
-
数组/指针越界访问
-
指针操作前未判断(比如 malloc 后)
@解决之道
指针跑飞是编程中常见的问题,虽然指针跑飞引发的问题很严重,
但解决起来其实并不难。
因此,指针跑飞,通常会出现crash,而系统crash时,平台软件会打印出函数调用栈、segment fault错误、代码出错的地方、coredump文件等信息。
有了这些信息,再分析源代码,其实是很容发现或找出当前代码中指针跑飞的原因的。
另外,由于指针跑飞在现场会导致严重的crash,因此,最好能够在编码时就能够提前预防,避免指针跑飞的情形的发生,而不是等待程序跑飞之后再定位、解决他们 。常见的手段有:
- 熟悉和遵守代码编写规范。
- 加强代码的评审,专家评审能够把大量问题消灭在编码阶段。
- 静态检测工具对代码进行检测。
-
增加边界性测试用例:
现场很多指针走飞的情形,是在边界或异常情形下发生的,常规情况早在单元测试和集成测试、系统测试解决就已经解决了。通过增加边界性测试,可以增大发现指针走飞的情形。 -
增加异常场景的测试:
同上,异常场景是违反常规的测试场景,这些异常业务场景,能够帮忙工程师尽可能、尽早的发现客户现场中的各种随机操作组合下的异常场景下可能引发的产品行为的异常。
3.3 空指针的问题引发crash
@故障描述
空指针是“指针跑飞”的一种
特殊情况,即指针为NULL
,空指针也是系统中引起系统软件crash的常见情形之一。
@问题分析
空指针通常出现在如下情形:
- 指针用NULL值初始化后,在某些case下,没有给指针赋值,就直接使用指针范围内存。
- 用函数调用给指针变量赋值时,忽略了函数的返回值有可能是空指针的情形,而直接使用该指针。
@解决之道
- 在使用指针前,检查指针是否为空,如果为空,在代码中执行异常处理流程,如打印出错信息,这样就可以避免引起更严重的crash问题。
- 通过静态代码检测工具,检测是否有空指针直接使用情形。
3.4 程序crash,但没有core dump文件的问题
@ 情形描述
对于目标系统而言,系统crash是一个非常严重的问题。因为它直接导致整个系统暂时无法提供服务。因此,尽可能要避免现场出现crash的情形,当出现crash的情形时,要尽早的解决这类问题。crash后,系统或重启,或死机。通常情况下,我们可以通过coredump文件或log文件,查看软件到底在哪个函数,哪行代码出现crash。然后,有时候会出现这样的一种情形,系统crash了,但没有coredump文件,这就比较尴尬了。没有coredump信息,是很难判断,软件到底在哪里出了指针问题。怎么办呢?
@解决之道
-
检查Linxu的配置,
要让Linux生成coredump文件,需要对Linux系统进行配置,默认是不产生coredump文件的。使用ulimit配置 coredump的生成。 -
检测log文件,
在coredump之前,平台软件会打印出错时的log,如segment fault异常等信息。这些信息其实可是帮助程序员定位出错的异常点的。系统中的任何程序收到SIGSEGV都会记录在内核日志中,根据内核日子可以查找代码中crash的位置,特别是内存地址信息。 -
把内存地址信息转换成源代码信息:
objdump -d ./xxx | more -
带symbol符号的目标代码,coredump文件
依赖编译的目标代码是否包含symbol,如果没有symbo,是很难指示在哪个函数,哪一行发生crash的,因此在编译代码时,通常会同时编译出两个版本的目标代码:一个是没有symbol信息的目标代码,这样的软件用于部署在现场,另一个是包含符号信息的目标文件,crash之后进行问题的定位。 -
检查是否是flash存储空间不够
,因为coredump文件size比较大,需要有足够的空间保存coredump文件,多次coredmp,就会消耗大量的Flash存储空间。
3.5 堆栈溢出导致的Crash
@问题描述
堆栈溢出时会访问不存在的RAM空间,造成代码跑飞,这时无法得到溢出时的上下文数据,也无法对后续的程序修改提供有用信息。
@问题分析
-
函数调用层次太深。函数递归调用时,系统要在栈中不断保存函数调用时的现场和产生的变量,如果
递归调用太深
,就会造成栈溢出,这时
递归无法返回
。再有,当函数调用层次过深时也可能导致栈无法容纳这些调用的返回地址而造成栈溢出。 - 局部数组变量的内存空间过大
- 局部数组变量的下标范围溢出,破坏了栈空间中的内容,导致函数调用返回是出错。
@解决之道
- 不静态分配,用new动态创建,从堆中分配的,堆的空间足够大。
- 修改栈空间的大小,确保栈空间足够大(每个进程有独立的栈空间)
- 用栈把递归转换成非递归
- 使用static对象替代nonstatic局部对象
3.6 案发现场与Crash抛尸现场不在同一处的问题
@问题描述
在实际工程实践中,经常会遇到这么一种情形,程序crash处打印的函数调用栈和打印的提示信息,看不出任何逻辑上的错误,比如在libc中报错。
@问题分析
解决此类问题比较麻烦,系统中有大量并行执行的线程和函数,出错的地方只是发现了错误,检测到了错误并报出了错误,而真正制造问题、制造错误的地方却隐藏在茫茫的代码之中。
特别是破坏者代码和检测到错误的代码的执行,并不是同时,而是相差较远的时间,在log信息上,并不相邻,而我们查找错误时,通常是从报错的地方入手的。这就给追查最终真正的凶手增加了更大的难度。
因此,Linux有这样一个原则:在最早发现异常的地方让程序Crash,而不是为让系统运行,隐藏或忽略异常。
程序员查找bug,与侦探破案有很多相似的地方。
大致分为几种情况:
- 函数执行错误,是因为提供给函数的输入参数或数据不对,比如指针,字符串长度等。而原始的数据,可能来自于遥远的其他代码出。
- 调用malloc的代码申请不到内存空间,而真正的原因是系统的其他地方存在内存泄露,至于什么地方,不太清楚。
- 程序执行出错,是由于堆栈不够了,而真正大量消耗堆栈的代码却隐藏在系统的其他地方。
- 程序执行出错,是由于堆栈遭到了破坏,而真正破坏堆栈的代码却隐藏在系统的其他地方。
- 读取消息队列出错,是由于消息队列在这之前已经遭到了破坏。
@解决之道
- 复现该问题是解决该问题的关键:这种问题,往往根据log很难直接发现根源,只有通过在实验室复现该问题,反复测试,反复分析log,逐渐排查,逐渐缩小根源的范围。
- 增加测试代码,比如检测是否有内存泄露等。
- 其他……………….
第4章 软件时序设计相关的问题
4.1 概述
在通信系统中,软件时序的数量和消息的数量,最能够反应了通信系统的复杂性
时序图与状态图的结合,使得系统的逻辑分支更加的复杂
时序图:反映的是线程间或对象间的消息或事件的交互
状态图:反映的是线程内或对象内的状态转换
通信系统中,定义了大量的进程、线程。进程和线程之间通过发送消息进行相互交互,不同的消息,按照一定的时间顺序进行发送,从而构成了时序,而时序有反应了不同场景下的业务需求。
无论是
OAM网管
,还是
信令协议
,还是
网络通信协议
,都存在大量的基于消息交互的时序图。
各种消息、各种时序搅合在一起,形成了复杂的、动态变化的通信系统。
系统越是复杂的地方,越是容易出问题的地方,越是bug越多的地方,越是难搞的地方。
在工程上,时序问题是最容易出问题的地方,时序图中的关键是:
“时”:
-
“时”代表时间顺序,
一旦时间
顺序错乱
,就会导致错误; -
代表时效性,
一旦过了失去时效,也会导致错误。
4.2 消息的串行化处理
@问题描述
每个线程,只能串行的处理一个个串行的消息,然而,其他线程发送消息并不是串行发送的,不同线程都是并行、异步发送消息的,这会导致线程在没有处理完一个消息,另一个消息又回来了。
如何把外部的并发消息转换成线程的串行处理呢?
@解决之道
每个线程都应该有一个消息队列,外部线程向消息队列中发送数据,目标线程从消息队列中读取消息,这样所有的消息被串行在消息队列中,线程就会串行的处理每个消息,只有当一个消息处理完(函数调用返回)时,才会处理另一个消息。如下图所示:
]
4.3 超时或消息丢失引发的问题
@问题描述
一个线性给另一个线程发送消息,等待对方的应答,有时候,对方由于忙,一直没有给发送进程/线程回复,等空闲时,再处理该消息,并应答时,发送方已经超时。发送方超时,就可以继续重复或进入异常处理。这里容易出问题的地方就是:
- 超时后的异常处理容易出错,它可能会引发一连串的异常处理反应,也有可能影响后续的正常消息的处理。
- 为啥会出现超时?为什么没有及时处理消息?内部处理效率太低?
@解决之道
-
消息丢失是必须考虑情况,我们不能假设接收方一定能够收到消息,也不能假设接收方一定能够及时的回应,必须充分考虑到消息因为传输的问题丢失或对方忙,没有及时回应的情形。
第5章 异常处理不充分问题
@问题概述
在软件设计中,我们通常首先考虑的是正常流程。
然而,在实际系统中,并发总数理想状态,系统总会遇到各种异常,异常情况考虑是否重复,是一个系统鲁棒性的一个重要标注。
健壮的系统,能够充分考虑到各种异常情况,一旦异常发生,程序也不会因此紊乱或crash
不健壮的系统,在外围环境正常时,系统工作正常;一旦遇到某个意想不到的异常,系统就陷入紊乱,甚至crash重启。
@解决之道
常见的异常与应对措施:
-
超时:
增加超时定时器事件以及事件处理,不能假设对方一定应答消息。 -
malloc:
不能假设一定能够申请到内存,要重复考虑到malloc返回为NULL的情形。 -
并发访问:
在并发执行的系统中,如果要访问全局变量,不能假设只有一个线程范围全局变量,需要通过锁对全局共享资源进行加锁,特别是要范围全局的数据结构。 -
消息队列:
不能假设消息队列始终有效,要重复考虑消息队列一时满或空的情形。 -
空指针:
通过指针范围内存对象时,要重复考虑指针可能为空的情形,需要及时的检查指针是否为空。 -
设计:
在软件设计时,就考虑软件的异常处理,而不是把这个工作留给编程人员。
第6章 性能本身问题
6.1 数据面性能问题
@ 问题描述
数据面的性能包括:
- throughput吞吐率:现场检测吞吐率下降,误码率提升是常见的数据面的问题。
- 用户体验速率
- 用户峰值速率
- 基站的最大覆盖范围
- 端到端的延时
@ 问题分析与解决之道
影响数据面性能的因素很多
-
基站的时钟同步精度不够,导致有来自邻站的干扰
- 基站小区频点和带宽配置与邻站有重叠
-
BBU与RRU之间的Delay计算不准确
- BBU和RRU之间的光纤的长度超过最大长度
- 小区的信号覆盖不够
- 周围有电磁波干扰
6.2 控制面性能问题
@ 问题描述
控制码的性能包括:
- 支持的最大终端的数量
- 单位面积支持的最大终端数量(连接密度)
- 终端的接入时间
- 终端的小区切换时间
- 终端的掉话时间
- 终端的掉话率
6.3 管理面性能问题
@ 问题描述
根据经验,管理面更多的是功能性问题,但也会遇到一些性能性问题。
常见的管理面的性能问题包括:
- 系统的启动时间过长,比如5分钟,10分钟。
- 当一个BBU通过串行的方式携带大量的RRU时,软件升级的时间太长,甚至超过了不可接受的程度。
- 当一个BBU通过串行的方式携带大量的RRU时,FPGA Image升级时间过长。
- 当一个BBU通过串行的方式携带大量的RRU时,系统的启动时间过程,导致超时重启。
- 当一个BBU通过串行的方式携带大量的RRU时,导致系统启动时,BBU测收到大量的来自RRU设备的状态信息,交互信息,而BBU在处理一个设备信息,需要查找数据库,比较耗时,导致某些RRU的信息无法得到及时的响应。
- 当管理终端,以频繁的方式polling 设备的状态时,导致设备忙于回应状态,无法进行配置管理。
@ 解决之道
- 由于每个RRU终端设备需要消耗较长的时间,比如下载几百字文件或升级FPGA image,此时串行下载或升级的方式就不太合适了,就需要为每个RRU建立一个线程,每个线程并发下载与升级。虽然增加了线程之间的同步或互斥机制,但提升了升级的效率
- 默认的BBU与RRU之间的CPRI OAM带宽小,只有几十兆,对于单个RRU,这个传输带宽是足够了,但对于上百个RRU,这个带宽就显得不足,因此需要增加CPRI OAM的带宽。
- 为了防止一个RRU抢占过多的带宽,其他RRU抢占不到数据带宽,导致消息超时、复位,因此,需要根据IP地址,通过IP table,为每个RRU OAM保留一定的带宽。
- 提升BBU侧的OAM访问数据库的效率。
- 不同的消息创建不同的消息队列,确保配置消息具有更高的优先级。
6.4 同步面性能问题
(1)晶体振荡器OCXO异常老化问题
@ 问题描述
晶体震动器是整个同步系统的核心,晶体震动器的频率的精确性和稳定性直接影响整个基站同步系统的稳定性和精确性。
通过控制DAC控制晶体震动器的输入电压,从而控制静态振荡器的频率。当晶体震动器老化后的频率范围超出DAC的控制范围时,OXCO就无法为基站提供精准的时钟同步了。
在工程实践中,我们就遇到过这种情况:
在某一段时间,在全国各地部署的基站,均出现大面积的无法同步的基站。怎么办?
@ 问题分析
- 观察log:GPS信号良好、GPS卫星的数目也是良好,都有7-8颗卫星。
- 实时监控同步时钟源的时钟信号:时钟源良好
- 实时监控同步算法的状态:观察到算法确实会失步
- 实时监控算法控制DAC值的变化,发现DAC的数值设置已经超过了最大值或最小值。
- 重启之后,亦然虽然短时间恢复,但亦然无效。
- 表明该机制的晶振前提老化
- 检查其他基站的失步,基本判断是同样的问题
- 回收硬件,硬件团队判断确实导致晶体振荡器没有达到预期的工作寿命,提取老化
- 进一步最终,是某一批OCXO硬件有质量问题。
@ 解决之道
- 更换OCXO模块
(2)GPS同步时钟源的问题
@ 问题描述:
GPS同步时钟源自身失步, 导致基站无法时钟同步
@ 分析
- GPS天线的部署不行,导致卫星信号收到遮蔽
- GPS电缆连接不好
- 可观察到的卫星数目不够
-
GPS 1PPS存在跳变,跳变后的1PPS无法恢复到标准状态,必须GPS接收机(这是一个非常难易观察到的现象,需要在现场安装仪表观察)
@ 解决之道
- 确保GPS电缆的连接良好
- 确保GPS天线的视野空间不受大楼的遮挡
- 通过驱动软件和同步算法监控GPS 1PPS的跳变,跳变后复位GPS接收机。
- 某个版本的GPS接收机软件(固件)有bug,在线升级该软件(固件)。
(3)1588同步的精度问题
@ 问题描述
使用1588作为时钟源,基站的同步误差较大。
@ 问题描述
(原因)与
解决之道
- 传输网络的中间设备不支持1588,存储转发的延时较大,导致PDV较大 =》 更换成支持1588的设备,中间设备也会记录1588包在设备内部的处理时间。
- 传输网络的中间环境太多 =》支持boundardy clock
- 基站的1588 client对中间设备的传输延时进行补偿
- 确保1588采用的是硬件时间戳
- 采用适当的滤波算法,过滤网络延时的抖动PDV
- syncE + 1588进行互补
- GPS + 1588进行互补