1 线上问题
服务程序从发布开始,就面向线上用户提供服务了。
此后遇到的问题,可以统称为线上问题。
线上问题可以分为运行问题和业务问题。
运行问题包括内存泄露,宕机,卡死等;业务问题主要是业务逻辑出错,表现就是各种各样了。
1 运行问题
1.1 内存泄露
内存泄露是指随着服务的运行,程序占用的内存不断增长。
内存泄露发生的原因,主要是在堆上分配的内存空间没有在合适的时机回收,即new和delete不匹配,活着malloc和free没有对应。
内存泄漏的点有时候很容易确认,更多的时候却非常隐蔽,不容易排查。
如果能够确认A版本没有内存泄漏问题,而B版本出现了,则仔细排查AB版本之间的差异是最有效的方式。仔细梳理内存分配和回收,如果实在无法通过逻辑确认,在怀疑的地方增加日志,并进行压测或线上运行,通过日志信息进一步确认。
内存泄漏一般短时间不会造成太大影响,业务运行都表现正常。但服务一般都是长时间运行的,持续的内存增长到达上限后,可能引发其他异常或宕机。
因此,内存泄漏是必须要重视的问题,发生后需尽快查明原因,避免潜在的更严重的风险。
1.2 宕机
宕机是指服务异常中止运行。
宕机通常是由于内存的非法访问引起,可能是数组越界访问、野指针调用等。
服务宕机时,有可能生成dump文件,如果能够通过dump文件查找到宕机时的调用堆栈,确认是哪行代码引发的宕机,对于排查问题是极有帮助的。
有时候生成的dump文件无法定位,或者没有生成dump文件,就只能通过日志文件分析,在宕机前执行到哪个业务流程,大概在什么地方有问题,再进行细致排查。
宕机会造成用户内存状态的丢失,其影响程度跟整体后端服务体系有关,有可能影响严重,也可能无关痛痒。
但宕机是严重的开发问题,发现后需尽快解决。
1.3 卡死
卡死是指服务由于某些原因,程序在某个流程中间卡住,无法按照正常逻辑继续执行。
引发卡死的情形通常有死锁、进入死循环,活着sleep超长的时间等。
服务卡死后,无法继续执行,由于很多情况下前端需要等待服务的反馈才能继续执行,这可能导致用户前端也处于卡死或无响应状态。而服务卡死后,开发和运维又很难像宕机一样即时感知到,有时候卡死造成的影响比宕机还大。
2 业务逻辑出错
业务逻辑出错的情形是多种多样的,很多时候是边界测试不到位,在一些很极端的情况下发生逻辑错误。
这些情况在测试中不容易遇到,在灰度发布时也没有触发,直到线上全网发布,用户基数变大,偶然变成必然,运行后才会出现。
下面列举一些在项目中真实发生的过的案例。
2.1 特殊的用户昵称
用户昵称中出现了特殊字符,在使用用户昵称拼装json数据时,导致json解析出现问题。
这类错误很容易复现,因此找到问题原因,并解决也不是很复杂。
这类问题麻烦在于,在出现之前,很难想到昵称中的特殊字符会影响json字符串的解析。
这需要有相应的经验,针对这类问题完善自动化检测机制,对于不在系统控制的可变字符做适配性验证。
2.2 有极限的计时器
问题表现是在独立线程中的日志模块,在服务运行一段时间后,不再输出到文件,服务内存占用在一直升高。
经过代码分析发现,在毫秒级时间比较的时候,使用了clock()函数。
clock()函数可以获取程序启动后,到函数调用时经历的毫秒数,返回值类型clock_t是长整形,最大表示数据为2147483647,windows系统下最大表示24.8天。
当程序运行24天后,clock取值超过上限,时间比较出错,导致预期的逻辑无法执行,日志一直缓存在内容中,无法落盘。
服务端尽量不要使用clock()和getTickCount函数,取而代之应可以使用64位的函数getTickCount64,或使用C++11 chrono提供的高精度时间获取函数。
这类问题初始运行正常,只有运行很长时间(24天)后,才会触发异常逻辑,问题发现后很好解决,只能通过代码规范,禁止某些函数使用,或在这类函数出现后保持高度警惕。
2.3 碰撞的请求处理
问题表现A、B两个用户的订单串了:A支付的订单,商品给了B。
审查代码后发现,在支付相关消息处理时,复用了某条TCP连接,但缺少了必要的数据校验。当用户A和B同时发送请求的时候,A的请求先被处理,B的请求到达后,A的请求正好处理完,结果用于回复了B用户。
这类问题,在没有请求碰撞的情况下,逻辑正常;只有在AB两天请求在同一毫秒内到达时会触发。
这种问题在QA测试时极难发现,在单台服务灰度发布时也不容易遇到,而在全平台大量线上用户并发请求的场景下,偶然的事件就变成了必然。
这里也为我们带来一些启示:能在线上运行很多年,且稳定不出异常的程序,即使代码再难看,也有其价值。而再优秀的设计与实现,也只有经历线上业务的考验,才能证明自身。
2.4 被覆盖的协议
服务发布后,发现上个版本迭代的某些功能失效了,代码分析发现,是pb协议文件被覆盖导致。
pb协议文件,是proto文件通过google的转换工具,生成的C++代码文件。
在代码合并的时候,这部分的修改通常不会被特别重视。代码review更多关注业务逻辑代码的改变。
QA测试的时候,通常会针对当前版本的修改进行校验,以及可能影响到的模块和业务,通常情况下很难每次迭代,全面遍历验证。
而灰度测试的时候,用户量较少,且影响功能不是核心逻辑,也为收到用户反馈。
但是正式上线的时候,用户基数变大,很快就得到反馈,回退版本后,修正再次发布。
线上问题多种多样,有些很好解决,有些却不容易定位;有些影响很小,有些造成的后果很严重。
我们不仅要解决线上问题,还需要在线上问题发生时,做好稳妥应对,尽量缩小影响范围,减少经济损失。
问题解决后,还需要及时复盘,确认问题发生的原因,以及如何做才能避免类似的问题再次发生,或完善测试工具,或优化业务流程,或改进编程规范。最终目标是减少问题发生。
Last updated