在功能测试中,测试人员主要关注的是功能是否能符合预期的正常运行,比如测试一个下单的流程,关注下单、购买、发货流程是否能一气呵成。但仅仅关注正常流程是不够的,在实际的使用场景下,会出现各种非正常的情况:
一些具体的需求需要通过业务特性挖掘:
-
如果是秒杀这样的高并发业务场景,那么需要考虑降级、并发。
-
依赖消息通知的业务,需要关注NSQ推送和消费相关的异常。
-
使用缓存的业务,需要关注缓存访问失败的情况,以及异常情况下数据一致性。
-
如果业务流程很重要必须要保证成功,那么需要关注是否有降级、容错、重试的需求,然后在异常中验证。
-
异步流程需要关注时序的问题。
确定需要做哪些异常的场景的判断依据有:
-
根据核心链路,对重要的流程优先进行异常测试。
-
根据业务特性,对于大流量、高并发、资金相关的功能优先进行异常测试。
-
根据开发反馈线上业务的稳定情况、调用异常的情况,确认相关功能是否需要关注异常。
-
根据线上的错误统计和反馈的线上问题,确认是否有可以挖掘的异常场景。
异常测试的分类实施
1. 功能异常
功能测试是基于用户使用场景进行设计的异常,是功能测试重要的补充部分,一般建议在功能测试阶段中就进行考虑和执行,一般采用手动的方式执行。
其中操作异常属于比较典型的功能异常,需要根据对业务的了解设计场景,拿退款的场景来说,有
-
重复操作:如一笔订单退款完再退款。
-
乱序操作:如还未支付成功就进行退款。
-
违规操作:如退别人的款、退款超过支付金额限制。
-
操作中断: 如未提交刷新/回退页面。
执行过程中需要关注:
-
报错信息是否恰当、准确。
-
错误操作后功能是否可用。
2. 接口异常
接口异常需要关注接口逻辑的正确性:
-
代码内部逻辑处理是否正常。
-
接口对外表现是否符合预期。
-
异常信息返回是否符合要求。
最常见的接口异常的用例就是接口参数检查的用例了。
一般接口参数需要对非法的参数进行检查,同时对于参数的类型和长度也需要做一定限制。非法的参数处理不当容易造成脏数据的问题,导致后续业务处理异常,严重的情况如SQL注入还会导致安全方面的问题,需要引起重视。
常见的处理方式有:特殊字符的转义或是报错拦截等。
除了非法参数,调用接口时的常见异常情况还有重复访问、并发访问等,这往往要求接口逻辑里采用事务机制、幂等性、锁等方式进行处理,这些都可以作为验证的点。
3. 依赖服务的异常
依赖分析
如果异常场景中需要考虑被调用服务的异常,那么需要对于该功能依赖的服务和特性进行分析并进行梳理。
常用的手段:
- 根据系统时序图或者代码review梳理出功能相关的依赖。
- 可以查看调用链数据作为分析参考。
依赖服务的异常
依赖服务的异常指的是依赖的服务无法正常响应时,观察系统的表现。
主要类型:
- 服务故障:因为各种原因的无法响应请求。
- 服务报错:返回特定错误、异常。
- 状态不一致:如:异步处理的异常或者底层服务处理成功,上层服务处理响应失败或未收到请求。
说到依赖,会有一个基本概念叫做强依赖和弱依赖:不影响核心业务流程,不影响系统可用性的依赖都可以叫做弱依赖,反之就是强依赖。
系统之间调用是很复杂的,有应用之间的调用,也有对缓存、DB等中间件的调用,需要思考一下:锁、缓存、消息等是不是对业务来说必须的,应该是强依赖还是弱依赖。
几个处理依赖服务异常的原则:
- 除非走投无路,不会因为依赖的系统引起当前应用出现系统级可用性的故障。
- 对于弱依赖,一般都要配置限流或是自动降级策略。
- 理想情况下,应用启动应该做到0强依赖启动。
- 通过本级的报错信息能够知道是下级哪个依赖引起的问题。
4. 系统异常
前面的接口异常以及依赖服务的异常往往关注的是异常对局部功能的影响,而系统异常是在系统实际拓扑架构下,注入异常,观察系统层面的稳定性,需要关注整个系统功能的可用性和系统的故障处理机制。
这种方式需要精心设计,以及多方配合进行执行和观察,一般的实施方式是进行线上演练。
5.页面UI异常
https://blog.csdn.net/weixin_42343424/article/details/84557450?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-3&spm=1001.2101.3001.4242
没有具体长度限制的暴力测试。查看页面展示,兼容性、消息传递正确性等。
6. MQ异常
https://www.cnblogs.com/longxok/p/10907176.html
1. 什么是MQ:
MQ 是message queue ,消息队列,也叫消息中间件,遵守JMS(java message service)规范的一种软件。(同时还有另一个叫AMQP的应用层协议,语言无关性不受产品 语言等限制,rabbitMQ支持这个 )
是类似于数据库一样需要独立部署在服务器上的一种应用,提供接口给其他系统调用。
2. 作用是什么:
队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。
3. 使用场景是什么
https://www.cnblogs.com/taotaozhuanyong/p/11762995.html
在处理高并发,而且不需要立即获取结果的时候。
4. 常用MQ都有哪些,区别是什么:
目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ
- activeMQ 对java支持良好,缺点是对其他语言支持不够友好,适合中小企业系统
- rabbitMQ 对java支持良好,对其他语言也支持良好,跨平台,语言无关
- kaffka 日志消息中间件 支持大数据场景。
5. 原生MQ实际代码例子, 创建生产者、消费者、queue
生产者发送消息
------------------------
//1.创建connectionFactory 与mq服务器进行连接
ConnectionFactory connectionFactory=new
XXMQConnectionFactory(url) ; //XX 为该mq的实现 比如activeMQ rabbitMQ等
// url为服务器地址 tcp格式 比如 tcp://xxxx:xxx
//2.创建Connection
Connection connection=connectionFactory.createConnection();
//3.启动连接
connection,start();
//4.创建会话
Session session=connection.createSession(XXX); //xx为该session的创建时候的参数 比如设定事务,模式等等,具体依据不同的实现形式
//5.创建一个目标
Destination destination=session.createQueue(queueName);
//6.创建一个生产者
MessageProducer producer=session.createProducer(destination);
//7.创建消息
TextMessage textMessage=session.createTextMessage("text");
//8.发送消息
producer.send(textMessage);
//9.关闭连接
connection.close();
---------------
消费者获取消息
//1.创建connectionFactory 与mq服务器进行连接
ConnectionFactory connectionFactory=new
XXMQConnectionFactory(url) ; //XX 为该mq的实现 比如activeMQ rabbitMQ等
//2.创建Connection
Connection connection=connectionFactory.createConnection();
//3.启动连接
connection,start();
//4.创建会话
Session session=connection.createSession(XXX); //xx为该session的创建时候的参数 比如设定事务,模式等等,具体依据不同的实现形式
//5.创建一个目标
Destination destination=session.createQueue(queueName);
//6.创建一个消费者
MessageConsumer consumer=session.createConsumer(destination);
//7.创建一个监听器
consumer.setMessageListener(
new MessageListener(){
public void onMessage(Message message){
TextMessage textMessage=(TextMessage)message;
//8.获取消息
textMessage.getText();
}});
//9.关闭连接
connection.close();
.spring集成下的代码例子
---------------
ConnectionFactory用于管理连接的连接工厂 这个是
spring提供地连接池,spring提供了SingleConnectionFactory和CachingConnectionFactory
JmsTemplate用于发送和接受消息的模板类,spring提供的 只要注入这个bean,既可以
用jmsTempalate方便的操作jms 不需要像之前那样写一堆重复代码
这个是线程安全的
MessageListerner 消息监听器,实现了一个onMessage方法,该方法只接收一个Message参数
发送消息的方法
@Autowired
JmsTemplate jmsTemplate;
@Autowired
Destination destination;
jmsTemplate.send(destination,new MessgaeCreator(){
public Message createMessage(Session session) {
TextMessage textMessage=session.createTextMessage("message")
return textMessage;
}});
6. 测试点:
了解了上面介绍的MQ的基础使用,下面说下针对MQ的异常测试。
-
MQ消息体中某些必填参数为NULL,或者全部必填为NULL,字段类型、长度是否不符合约定
-
MQ消息体中参数位置错误
-
消息重复发送,只消费一条 —幂等性
一般根据消息内容中唯一标识来去重 -
消息到达顺序不一致,导致业务异常
比如业务是有先后顺序的
案例1:订单下单后再取消,如果先收到取消的消息,再收到下单消息,就会有问题
案例2:一条政策新增后马上删除,政策同步时,政策删除的消息先到达,新增的消息后到,就会导致最小价该条政策没删除,只能等全量同步的时候再删除 -
消息发送失败,重试次数
1)Producer端重试
比如网络抖动导致生产者发送消息到MQ失败,可以手动设置发送失败重试的次数
2)Consumer端重试
默认16次,重试时间间隔会越来越长,如果失败的多,容易堆积
重试次数可自定义设置
消费者端失败分2种
A、Exception
如反序列化失败
B、timeout
只有消息推送失败才需要重推,需要注意开发不要把其他失败的情况也进行重试
如收到消息,但解析消息时,序列化失败,这种算消息发送成功的
还比如政策同步时,消费者收到消息,但特殊情况消费异常,也去做了重试 -
机器重启时间段的消息,消费者能否消费到
-
push的类型,需要测试当有生产者生成消息时,消费者是否能及时得到信息并消费
-
Pull类型的消费者,需要测试拉取的时间间隔,间隔一段时间再有消息延迟性
-
消费时消费节点测试
接线上已有的生产者,需要注意,必须设置消费开始时间,不然上线时会大批量消息过来会造成堆积,造成故障 -
消息丢失,业务是否兼容,是否有补偿或者监控机制
比如政策同步消息丢失,还有全量航线同步进行补偿;
供应商退票先发消息给供应商,退订这边会监控,临近跨退规节点,会去调供应商接口检查是否已推送供应商退票,没有退票的会自动再推一遍 -
消费模式注意,消息争用
如果是集群模式,同一topic下新增新的消费组,没有申请新的group,导致一条消息投递过来,多个消费组争抢
案例:有时候开发为了省事,预发和线上同一个topic,消费组的group也一样,上线后,可能有效消息就被预发消费组消费了
案例:预发环境政策同步MQ的消费的grounp同一个,导致太湖和新海宜争消费 -
保存之后MQ比落库更快
案例:基础数据机场接口新增或者变更一条数据后,点击发布,会同步更新数据库和发送MQ消息,测试时发现,查询收到MQ消息后去查全量接口(数据来源数据库)却没有更新的数据
数据库更新速度没有MQ快,不应该同步,应该异步
7. redis异常
1. redis基础介绍:
Redis 是一个开源(BSD许可)的,基于内存亦可持久化的日志型、Key-Value高性能数据库
它支持存储的value类型:string(字符串)、list(链表)、set(集合)、zset(sorted set 有序集合)和hash(哈希类型)。
这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的,与memcached一样,数据都是缓存在内存中。。
redis会周期性的把更新的数据写入磁盘(rdb)或者把修改操作写入追加的记录文件(aof),并且在此基础上实现了master-slave(主从)同步。
通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性
2.为什么使用redis:
redis数据库就是一款缓存数据库,用于存储使用频繁的数据,这样减少访问数据库的次数,提高运行效率。
按key设置过期时间,过期后将会自动删除。
使用场景:https://blog.csdn.net/weixin_39850167/article/details/111953693
3.redis分片集群和哨兵集群:
https://www.cnblogs.com/xuwc/p/8900717.html
哨兵的作用:
Redis-Sentinel是用于管理Redis集群,该系统执行以下三个任务:
1.监控(Monitoring):Sentinel会不断地检查你的主服务器和从服务器是否运作正常
2.提醒(Notification):当被监控的某个Redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用程序发送通知
3.自动故障迁移(Automatic failover):当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,它会将失效主
服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器;当客户端试图连接失效的主
服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器
分片概念:
是分割数据到多个Redis实例的处理过程,因此每个实例只保存key的一个子集。分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。
4. 异常测试点:
1.更新KEY异常,先删再存还是直接覆盖
案例:
查询 退改XX起 原来线上是每8小时刷新一次缓存,查询 每8小时去查退改签库拉退改签数据,根据航司作为KEY存到Redis中,再放到各机器内存中使用
这次调整为退改签有改动就发mq消息给查询,查询收到消息,则重新去拉全量数据
开发设计时有变动就全量更新,且是先删除后存入,这样可能存在大量请求过来没有拿到数据
推动修改放案:从退改数据库拉数据之后,先和Redis原来的key(航司)做hash比对,如果原来的Redis多了,则清除,剩下的再做更新操作
2.KEY删除和丢失区分
案例:
政策先同步到Redis,再发消息给数据同步build站,build站收到消息后,会去拿Redis的数据更新到mongodb,如果没有查到key会做删除操作
如果Redis 数据丢失,key不会存在,则会造成误删
解决方案:
删除key时,生成key,数据为[],查到KEY的数据为[]则删除数据;数据丢了,不生成KEY,没查到KEY去实时调接口查
build收到消息后,去更新mongdb,如果查到的key是[],则删除该条航线
build收到消息后,去更新mongdb,如果没查到该key,则在去实时调官网DSF接口
3.KEY 过期策略不当造成内存泄漏
TTL KEY TTL过期时间为秒
当 key 不存在时,返回 -2 。
当 key 存在但没有设置剩余生存时间时,返回 -1 。
否则,以秒为单位,返回 key 的剩余生存时间。
大多数业务redis都会设置过期时间,key过期时如何清理的,也需要了解下
惰性清理(被动清理):
某个KEY过期后,不会立马被删除,下次使用时检查时候过期,过期就删除
缺点:浪费内存,长期不访问没法清理,垃圾数据过多,可能引起内存泄漏
定期清理(主动清理)
Redis会定期主动淘汰一批已过期的key(随机抽取一批key检查)
缺点:KEY已过期,仍未清理,需要等待JOB扫
内存淘汰机制
当前已用内存超过maxmemory限定时,触发主动清理策略:大多数会设置一个阀值,达到一定阀值自动扩容,除非自动扩容失败,则会出问题
noeviction(默认策略):当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
目前公司一般采取惰性和定期清理配合使用
4、查询Redis异常,是否实时调接口/数据库
很多情况redis只是做一个缓存机制,如果redis异常或者未取到数据,是否有实时获取数据的兜底方案,需要考虑
5、Redis击穿
某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方案:可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据;或者用Hash方法对比KEY进行增删改
6、缓存雪崩
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
解决方案:
事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据
7、缓存穿透
对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。
黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
解决方案:
每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
8、Redis锁,使用不当造成锁不能释放,陷入死锁
目前常用的2种锁:
1)SET Key UniqId Seconds
仅在单实例的场景下是安全的,不用setnx+expire+del 中间断了仍可能造成死锁;不用SET Key UnixTimestamp Seconds NX,高并发可能存在相同时间戳
2)分布式Redis锁:Redlock
此种方式比原先的单节点的方法更安全
安全性:在同一时间不允许多个Client同时持有锁。
活性死锁:锁最终应该能够被释放,即使Client端crash或者出现网络分区(通常基于超时机制)。
容错性:只要超过半数Redis节点可用,锁都能被正确获取和释放。
案例:
综合推荐获取航班动态的redis数据时,某个航线没有推荐出来,高并发生成相同时间戳redis的KEYLOCK,redis解锁失败导致
9、Redis持久化
当Redis数据需要长久有效时,需要考虑是否做RDB和AOF持久化,一般RDB和AOF配合使用,但做持久化,会影响性能,目前做持久化的很少见
比如如来Redis数据是长久有效的,但却为了响应快不影响性能,未做持久化;采用了其他的降级方案Hbase,以及业务的兜底
10、缓存与数据库双写时的数据一致性
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。
并行写数据库和缓存,可以加个事务都写成功才成功,有一个环节失败了就回滚事务,全失败
问题场景 | 描述 |
---|---|
先写缓存,再写数据库,缓存写成功,数据库写失败 | 缓存写成功,但写数据库失败或者响应延迟,则下次读取(并发读)缓存时,就出现脏读 |
先写数据库,再写缓存,数据库写成功,缓存写失败 | 写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据 |
需要缓存异步刷新 | 指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候 |