浅谈异常测试

2,721 阅读19分钟
原文链接: zhuanlan.zhihu.com

异常测试是有别于功能测试和性能测试又一种测试类型,通过异常测试,可以发现由于系统异常、依赖服务异常、应用本身异常等原因引起的性能瓶颈,提高系统的稳定性。

一、

本文首先通过近期业界出现的影响面较广的由异常触发的故障案例,以及笔者亲身经历过的线上故障为出发点,讲述为什么要做异常测试,以及异常测试的重要性;其次介绍了异常测试具体包含哪些方面,即我们在做异常测试时,需要从哪些角度进行异常测试的考虑;然后基于经验,以加入购物车为案例,讲述异常测试的实施过程;随后对异常测试工具,以及异常测试框架进行了简单的介绍;最后从保障系统稳定性角度,介绍了常用的保障措施。

二、 为什么做异常测试

相信大家前不久都听说过以下事件:

事件一:2018年6月27日下午,由于运维的操作失误,导致一些客户访问阿里云官网控制台和使用部分产品功能出现问题,引发了大量吐槽。事故原因是工程师团队在上线一个自动化运维新功能中,执行了一项变更验证操作。这一功能在测试环境验证中并未发生问题,上线到自动化运维系统后,触发了一个未知代码bug,错误代码禁用了部分内部IP,导致部分产品访问链路不通。

事件二:2018年8月5日,创业公司前沿数控技术发布公开申明称,因腾讯云操作故障导致其数据完全丢失,且无法恢复。该故障源于磁盘静默错误导致的单副本数据错误,外加运维人员为了加速完成搬迁任务,违规关闭了数据校验,以及违规对源仓库进行了数据回收,导致最终的数据完全不可丢失。

此外,在我之前就职的部门,也曾因为线上节点假死,没有对回调信息做正确的处理,导致相同的请求不断重试,短时间内触发大量回调,回调逻辑中需要查询DDB,短时间内DDB积累了大量的查询,节点出现了较高的负载,从而导致其他的依赖数据库的接口请求响应变慢及甚至超时,引发连锁反应。

常规的功能测试和性能测试过程中,主要关注的是业务的正常逻辑是否能够走通,以及在正常逻辑多并发运行过程中,是否有潜在的性能问题,往往忽视了各种异常对服务造成的影响。随着产品规模的扩大以及用户数的递增,线上机器规模越来越大,我们无法预料每台机器,每个应用可能出现的异常情况。从前面的案例我们可以知道,一些异常表现对线上可能会造成致命的影响,虽然有较完备的报警机制,一方面进程假死等异常无法监控,另一方面报警时往往已经对线上造成影响了,问题排查过程往往也需要耗费一定的时间,异常测试受到了越来越多的关注。当前普遍的做法,是在线下预先进行故障演练,发现可能潜在的性能问题,并制定相对完备的预案。异常测试主要有以下几个方面的作用:

1、 满足可靠性要求。一旦系统的某个部分出现异常后,保障产品的绝大多数功能能够正常运行,尽量减小对用户的影响;

2、 避免错误累积,导致系统雪崩。系统的某一服务多次出现异常后,保障异常不会形成灾难,导致整个功能,甚至整个产品不可用;

3、 保障系统稳定性。系统在压力情况下,出现这样或那样的故障时,在故障预案的支撑下,依然能够稳定运行;

4、 加深对产品的理解。做一轮异常测试可以加深测试人员对产品业务和架构上的了解。

三、 异常测试是什么

异常测试指通过人为制造错误情况测试系统对错误操作、错误报文的反应,检查系统是否给出了清晰且充分的提示或约束;一旦出现错误情况,系统是否能正常报告,并检查系统的错误提示是否清晰且充分;测试系统是否处理了用户的异常操作,还是造成死机或处理错误。

从异常测试的定义来看,想要开展异常测试,首先需要知道怎么去人为的制造异常,应该从哪些方面来模拟异常呢?接下来我会从服务器系统异常、依赖服务出现异常、应用本身异常三个维度进行具体的介绍。

服务器系统异常包括系统资源不足和服务器异常。系统资源包括cpu、内存、磁盘、网络、inode等,应用服务在正常运行的过程中需要消耗一定的系统资源,系统资源不足验证系统资源繁忙或资源耗尽的过程中,应用服务是否会由于资源无法分配受到影响,导致进程hang住或服务不可用;服务器异常包括服务器的关机、重启等,验证系统恢复后是否能够对请求正常响应,是否会出现数据不一致等问题。

依赖服务异常表示系统在运行过程中,数据库、缓存和第三方依赖服务出现无响应、响应时间变慢、丢包等情况时,验证应用服务是否设置了合适的超时时间,或采取一定的降级策略,让服务能够正常处理这些异常,不会因此占用线程资源导致线程池满,其它正常请求无法处理的问题;以及依赖服务重启后,系统是否能自动恢复服务;依赖服务宕机时,系统是否能够继续提供服务,且依赖服务恢复后,不需要对服务进行重启。

应用本身出现异常,包括应用进程重启、挂掉、假死、响应时间变慢、抛异常、资源超限等,其中应用进程重启、挂掉和假死,若服务为单点部署,主要验证故障对正常请求的影响,若服务为多点故障,主要验证服务的高可用性;服务响应时间慢主要验证应用的超时和重试机制是否合理;应用抛异常,主要为了验证是否能够对异常信息进行正确的处理;应用资源超限包括线程池、连接池和队列等,主要模拟这些资源不足时,系统能否合理的拒绝服务,资源恢复是能否合理的恢复服务。

想必大家对异常测试的模拟内容已经有了一定的了解,应该如何去模拟上诉异常呢?下图为异常测试过程中主要用到的工具。

服务器系统异常

Cpu:BurnInTest让cpu满负载,CpuLimit可知cpu占用量

内存:memtester,模拟内存的频繁申请和释放

磁盘:dd生成大文件,模拟磁盘空间被占用;fio可以模拟磁盘读写

网络:netperf客户端和服务端模拟出入口带宽被占用

文件系统系统:创建大量小文件占用inode资源

机器异常:人为进行关机、重启和掉电

依赖异常

响应变慢/丢包/带宽上限:tc工具模拟

无响应:iptables模拟网络断链

返回错误:mock服务模拟不同错误返回

应用本身异常

故障恢复:kill命令模拟进程故障、假死

资源超限:改小线程池/连接池/队列大小,或增大响应时间

故障注入:修改代码或使用动态注入工具jvm-sandbox实现代码运行异常、代码运行超时和具体函数返回指定错误

四、 如何开展异常测试

通过上诉异常测试概念的了解,如果现在有一个异常测试需求,你会如何开展?接下来我会以加购物车场景为例进行具体的讲解。

开展异常测试之前,我们首先需要了解待测系统架构,以及针对我们测试的业务,掌握数据流走向。以下为加购物车的数据流走向图(简易版),从图中我们知道加购物车的场景需要涉及到前端node服务、应用服务节点、qs节点和dbn节点,其中qs和dbn是多节点部署,数据流走向为node服务->nginx->应用服务->qs节点->dbn节点。明确该系统架构和数据流走向之后,我们可以从服务器系统异常、依赖异常、应用服务本身异常三个角度进行异常用例的设计。



制定异常测试用例的过程中,在我们对被测系统了解的还比较粗浅时,我们可以根据前面的列表逐一设计用例。其中在制定依赖服务响应变慢的异常时,我们需要首先了解服务和被依赖服务之间是否有超时时间,以及超时时间是多少,当超时时间为5s时,我们设计的用例中,模拟超时时间为6s和10s的效果一样,没有必要对此设计两个用例,能够一定程度上减轻异常测试人员的负担。此外针对系统异常,对于调用量较少的应用,cpu资源往往较低,而且线上监控系统对cpu资源的监控较完备,cpu满负载时,能够快速报警,相关负责人收到报警后,可以对服务集群进行迅速的扩容以及后续的优化,在用例较多是,我们可以将该用例的优先级设计的低一点,在排期有限的情况下,能够专注于高优先级、高风险的异常用例执行。

设计异常用例后,根据上诉的列表可以很快确定测试工具,随后我们需要根据自己的经验和对系统的了解,明确异常触发后的预期表现,以及异常清除后的预期表现。预期表现这块最好能够和开发check一遍,异常执行完成之后,只需要将不符合预期的部分和开发进一步讨论,确认是否需要进行优化即可。

异常测试过程中,异常触发后系统的性能表现,在没有真正执行前无法预判,因此我们需要能够较全面的观察整个系统性能,以便于能够发现所有可能存在的性能瓶颈,让异常测试能够发挥它真正的作用。以下是性能测试过程中需要监控的一些内容。



五、 异常测试工具简介

异常测试工具很多,本章节主要选择了模拟网络异常的tc(Traffic Control)、netem和iptables,模拟代码注入故障的jvm-sandbox工具,以及在生产环境随机模拟各种故障的Chao Monkey框架进行介绍。

1、 tc和netem

netem是 Linux 2.6 及以上内核版本提供的一个网络模拟功能模块。该功能模块可以用来在性能良好的局域网中,模拟出复杂的互联网传输性能,诸如低带宽、传输延迟、丢包等等情况。使用 Linux 2.6 (或以上) 版本内核的很多发行版本Linux都开启了该内核功能,比如 Fedora、Ubuntu、Redhat、OpenSuse、CentOS、Debian等等。tc是Linux系统中的一个工具,全名为traffic control(流量控制),可以用来控制netem的工作模式。也就是说,如果想使用netem,需要至少两个条件,一个是内核中的netem功能被包含,另一个是要有tc 。



如上图所示,报文分组从输入接口(Input Interfaces)接收进来,经过流量限制(Ingress Policing)丢弃不符合规定的数据包,由输入多路分配器(Input deMultiplexing)进行判断选择,以确定是发给本机的,还是需要转发的。如果是发给本机的,就直接向上递交给上层的协议,比如TCP或UDP;如果是转发的,则将数据包发给转发块(Forwarding Block),转发块同时也接收本主机上层(TCP、UDP)产生的包,通过输出队列(Output Queue),从输出网卡发出。网络流量的控制通常发生在输出网卡处,虽然在路由器的入口处也可以进行流量控制,Linux也具有相关的功能,但一般说来,由于我们无法控制自己网络之外的设备,入口处的流量控制相对较难。因此我们这里处理的流量控制一般指出口处的流量控制。流量控制的一个基本概念是队列(Qdisc),每个网卡都与一个Qdisc相联系,每当内核需要将报文分组从网卡发送出去,都会首先将该报文分组添加到该网卡所配置的队列中,由该队列决定报文分组的发送顺序。因此可以说,所有的流量控制都发生在队列中。

以下为在故障演练过程中,使用tc和netem模拟网络延迟和丢包过程这种主要用到的命令。

1)网络延迟

sudo tc qdisc add dev eth0 root netem delay 100ms

2)网络丢包

sudo tc qdisc add dev eth0 root netem loss 1%

3)基于filter

(1)sudo tc qdisc add dev eth0 root handle 1: prio

(2)sudo tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 1s loss 2%

(3)sudo tc filter add dev eth0 protocol ip parent 1: prio 1 u32 match ip dst 192.168.33.1 flowid 1:1

4)删除规则

sudo tc qdisc del dev eth0 root

2、 iptables

iptables是隔离主机以及网络的工具,通过自己设定的规则以及处理动作对数据报文进行检测以及处理。在异常测试过程中,我们主要使用iptables命令模拟主机间的网络断链,主要的使用命令主要有以下几种:

1)模拟从源主机到目的地址为dstIp,端口为dport的数据包全部丢弃

iptables -I OUTPUT 1 -p tcp --dst <dstIp> --dport <dport> -j DROP

2)模拟从源主机到目的地址为dstIp,目的端口为dport的数据包全部丢弃

iptables -I INPUT 1 -p tcp --src <dstIp> --dport <dport> -j DROP

3)清除命令:

(1)INPUT链使用iptables -D INPUT 1

(2)OUTPUT链使用iptables -D OUTPUT 1

3、 jvm-sandbox

jvm-sandbox是阿里的开源工具,它的主要思想是将任何一个Java方法的调用都分解为BEFORE、RETURN和THROWS三个环节,并通过这三个环节,实现对入参、返回值和流程进行控制,实现更细粒度的代码层故障注入。

// BEFORE

try {

/*

* do something...

*/

// RETURN

return;

} catch (Throwable cause) {

// THROWS

}

基于BEFORE、RETURN和THROWS三个环节事件,可以完成很多类AOP的操作。其中针对入参、返回值和流程上,可以完成的操作主要有:

1)可以感知和改变方法调用的入参

2)可以感知和改变方法调用返回值和抛出的异常

3)可以改变方法执行的流程。包括在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行;在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常;在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回

以下代码为感知重载方法,并当方法参数为一个时,在方法执行之前抛出异常,在方法参数为2个时,在方法执行前通过进行5s的延迟。



4、 Chaos Monkey

Chaos Monkey框架是Netflix开发的一套异常测试框架。秉承“避免失败的最好办法是经常失败”的原则,Netflix公司设计了很多种捣蛋的Monkey,在云主机中进行了各种破坏行为或为了节省云主机资源的行为,包括模拟网络延迟的Latency Monkey,可以随机关闭生成环境中的实例的Chaos Monkey,以及查找不符合最佳实践的实例的Conformity Monkey,查找不再需要的资源并将其回收的Janitor Monkey等。

Chao Monkey框架主要包含了以下几个功能:

1)任务执行时间:可以定义在哪个时间段进行随机触发故障,在工作日的工作时间段进行故障的触发,有利于工程师们能够对故障进行更快速的响应,毕竟该故障模拟的目的是为了发现线上更多的性能问题,而不是真正搞挂线上服务;

2)故障类型选择:我们同样可以指定各种想要模拟的故障类型,对可以对各种类型的故障设置优先级。Chaos Monkey在故障触发时,会随机的从所有预先定义好的故障类型中选择,保证了故障类型的不确定性;

3)邮件发送:触发故障时,会对相关的负责人进行邮件通知。

4)异常执行记录:对随机触发的故障进行记录,便于后续问题的排查。



框架中的思想非常值得借鉴,我们可以基于上诉思想,设计出一套适用于考拉业务场景的猴子军团,在我们线上异常测试执行和线上故障预案足够成熟之后,将其作为一套常规化的故障演练实施方式,一定能够大大提高线上环境的稳定性和健壮性。

六、 系统稳定性保障

系统稳定性保障通常来讲表示保障系统在运行和运维的过程中,即使面对各种极端情况或突发事件,仍然能够提供持续的、可靠的服务能力。极端情况或突发事件包括系统服务器故障、依赖的第三方服务故障、依赖的数据库故障、由于突发流量导致的应用本身出现假死或响应慢、光纤断了、数据库被删等等。持续的、可靠的服务能力表示系统可以在可接受的时间范围内完成故障的恢复或者失败转移,能够提供正确的服务和足够的计算能力。

异常测试是保障系统稳定性的一种手段,通过故障演练的实施,可以达到以下目的:

1) 检验当前系统的稳定性

2) 发现系统可能存在的瓶颈点

3) 制定完备的应急预案措施

如果将故障演练实施过程中,各种异常用例的执行当成是对系统的一次次攻击,那么缓存、限流和降级等措施,则是应对这些攻击强有力的措施。

在高并发系统中,没有缓存,数据库很容易因为读写请求过多,导致数据库负载过高,响应变慢甚至服务不可用。使用缓存不仅能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的方式。缓存对于读服务来说可谓抗高并发流量的银弹,缓存技术多种多样,包括浏览器端缓存、APP客户端缓存、CDN缓存、应用层缓存、接入层缓存、分布式缓存等。

降级是指当访问量剧增、服务出问题(如响应时间长或不响应)或者非核心服务影响到核心流程的性能时,仍然需要保证服务的可用性,此时可以暂时屏蔽掉部分功能,待高峰过去或者问题解决后再打开。降级设置的阈值有很多标准,包括按照服务响应时间设定的超时降级,按照请求失败次数设置的统计失败次数降级,根据服务是否可用的故障降级,以及根据服务是否达到限流阈值降级等。常用的降级框架有Hystrix和Sentinel。

限流的目的是通过对并发请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队、降级等,限流可以通过拒绝处理过载的请求有效的防止系统过载、防止恶意请求流量攻击,保障系统的高可用。限流内容包括限制数据库连接池、线程池的总并发数,限制瞬时并发数和时间窗口内的平均速率,限制远程接口调用速率,限制MQ的消费速率等。此外,也可以根据网络连接数、网络流量、cpu和内存负载等系统资源指标进行限流。常用的限流算法有令牌桶算法和漏桶算法。