极客时间已完结课程限时免费阅读

08丨案例: 手把手教你编写最简单的性能脚本

08丨案例: 手把手教你编写最简单的性能脚本-极客时间

08丨案例: 手把手教你编写最简单的性能脚本

讲述:高楼

时长29:10大小26.70M

通常我们会遇到要手写脚本的时候,就要针对一些接口编写脚本。这时候,我们需要知道接口规范和后台的数据是什么。而有些性能测试工程师写脚本时,并不知道后端的逻辑,只知道实现脚本,事实上,只知道实现脚本是远远不够的。
在这一篇文章中,我不打算讲复杂的内容,只想针对新手写一步步的操作,描述最简单的脚本编写。如果你已经具有丰富的脚本编写经验,会觉得本文很简单。
我没有打算把 JMeter 的功能点一一罗列出来,作为一个性能测试的专栏,不写一下脚本的实现似乎不像个样子。在脚本实现中,我们最常用的协议就是 HTTP 和 TCP 了吧,所以在今天的内容里,我简单地说一下如何编写 HTTP 和 TCP 脚本,以应测试主题。
我先画个图说明一下。
这样的图做性能的人一定要知道,相信很多人也画的出来。
我们知道 HTTP 是应用层的协议之一,现在很多场景都在用它,并且是用的 HTTP1.1 的版本,对应的是 RFC2616,当然还有补充协议 RFC7231、6265。
HTTP 中只规定了传输的规则,规定了请求、响应、连接、方法、状态定义等。我们写脚本的时候,必须符合这些规则。比如为什么要在脚本中定义个 Header?Header 里为什么要那样写?这些在 RFC 中都说得明明白白了。
还有一点也需要注意,HTTP 是通过 Socket 来使用 TCP 的,Socket 做为套接层 API,它本身不是协议,只规定了 API。
而我们通常在 JMeter 中写 TCP 脚本,就是直接调用 Socket 层的 API。TCP 脚本和 HTTP 脚本最大的区别就是,TCP 脚本中发送和接收的内容完全取决于 Socket server 是怎么处理的,并没有通用的规则。所以脚本中也就只有根据具体的项目来发挥了。

手工编写 HTTP 脚本

服务端代码逻辑说明

我们先自己编写一小段服务端代码的逻辑。现在用 Spring Boot 写一个示例,其实就是分分钟的事情。我们做性能测试的人至少要知道访问的是什么东西。
Controller 关键代码如下:
@RestController
@RequestMapping(value = "pa")
public class PAController {
@Autowired
private PAService paService;
//查询
@GetMapping("/query/{id}")
public ResultVO<User> getById(@PathVariable("id") String id) {
User user = paService.getById(id);
return ResultVO.<User>builder().success(user).build();
}
}
Service 关键代码如下:
public User getById(String id) {
return mapper.selectByPrimaryKey(id);
}
用 MyBatis 组件实现对 Mapper 的操作。由于不是基础开发教程,这里只是为了说明逻辑,如果你感兴趣的话,可以自己编写一个接口示例。
逻辑调用关系如下:
数据库中表的信息如下:
我们先看这个接口的访问逻辑:JMeter——SprintBoot 的应用——MySQL。

1. 编写 JMeter 脚本

1.1 创建线程组

首先创建一个线程组,配置如下:
在这个线程组中,有几个关键配置,我来一一说明一下。
Number of Threads(users):我们都知道这是 JMeter 中的线程数,也可以称之为用户数。但是在第 2 篇文章中,我已经说得非常明确了,这个线程数是产生 TPS 的,而一个线程产生多少 TPS,取决于系统的响应时间有多快。所以我们用 TPS 这个概念来承载系统的负载能力,而不是用这里的线程数。
Ramp-up Period(in seconds):递增时间,以秒为单位。指的就是上面配置的线程数将在多长时间内会全部递增完。如果我们配置了 100 线程,这里配置为 10 秒,那么就是 100/(10s*1000ms)=1 线程 /100ms;如果我们配置了 10 线程,这里配置为 1 秒,则是 10/1000=1 线程 /100ms。这时我们要注意了哦,在 10 线程启动的这个阶段中,对服务器的压力是一样的。示意图如下:
Loop Count 这个值指的是一个线程中脚本迭代的次数。这里你需要注意,这个值和后面的 Scheduler 有一个判断关系,下面我们会提到。
Delay Thread creation until needed:这个含义从字面看不是特别清楚。这里有一个默认的知识点,那就是 JMeter 所有的线程是一开始就创建完成的,只是递增的时候会按照上面的规则递增。如果选择了这个选项,则不会在一开始创建所有线程,只有在需要时才会创建。这一点和 LoadRunner 中的初始化选项类似。只是不知道你有没有注意过,基本上,我们做性能测试的工程师,很少有选择这个选项的。选与不选之间,区别到底是什么呢?
如果不选择,在启动场景时,JMeter 会用更多的 CPU 来创建线程,它会影响前面的一些请求的响应时间,因为压力机的 CPU 在做其他事情嘛。
如果选择了的话,就会在使用时再创建,CPU 消耗会平均一些,但是这时会有另一个隐患,就是会稍微影响正在跑的线程。这个选项,选择与否,取决于压力机在执行过程中,它能产生多大的影响。如果你的线程数很多,一旦启动,压力机的 CPU 都被消耗在创建线程上了,那就可以考虑选择它,否则,可以不选择。
Scheduler Configuration:这里有一句重要的话,If Loop Count is not -1 or Forever, duration will be min(Duration, Loop Count * iteration duration)。举例来说,如果设置了 Loop Count 为 100,而响应时间是 0.1 秒,那么 Loop Count * iteration duration(这个就是响应时间) = 100 * 0.1 = 10秒
即便设置了 Scheduler 的 Duration 为 100 秒,线程仍然会以 10 秒为结束点。
如果没有设置 Scheduler 的 Duration,那么你会看到,在 JMeter 运行到 10 秒时,控制台中会出现如下信息:
2019-11-26 10:39:20,521 INFO o.a.j.t.JMeterThread: Thread finished: Thread Group 1-10
有些人不太理解这一点,经常会设置迭代次数,同时又设置 Scheduler 中的 Duration。而对 TPS 来说,就会产生这样的图:
场景没执行完,结果 TPS 全掉下去了,于是开始查后端系统,其实和后端没有任何关系。

1.2 创建 HTTP Sampler

1.2.1 GET 接口

看上图,我将 Method 选择为 GET。为什么要选择它?往上看我们的接口注解,这是一个 GetMapping,所以这里要选择 GET。
再看 path 中,这里是/pa/query/0808050c-0ae0-11ea-af5f-00163e124cff,对应着“/query/{id}”
然后执行:
User user = paService.getById(id);
返回执行结果:
return ResultVO.<User>builder().success(user).build();
为什么要解释这一段呢?
做开发的人可能会觉得,你这个解释毫无意义呀,代码里已经写得很清楚了。事实上,在我的工作经历中,会发现很多做性能测试脚本的,实际上并不知道后端采用了什么样的技术,实现的是什么样的逻辑。
所以还是希望你可以自己写一些 demo,去了解一些逻辑,然后在排除问题的时候,就非常清楚了。
接着我们执行脚本,就得到了如下结果:
这样一个最简单的 GET 脚本就做好了。
前面我们提到过,URL 中的 ID 是 0808050c-0ae0-11ea-af5f-00163e124cff,这个数据来自于数据库中的第一条。
如果我们随便写一个数据,会得到什么结果呢?
你会看到,结果一样得到了 200 的 code,但是这个结果明显就不对了,明明没有查到,还是返回了成功。
所以说,业务的成功,只能靠业务来判断。这里只是查询成功了,没返回数据也是查询成功了。我将在后面给你说明如何加断言。

1.2.2 POST 接口

下面我将 Method 改为 POST,POST 接口与 GET 接口的区别有这么几处:
要把 Path 改为 /pa/add;
输入 JSON 格式的 Body Data。
执行起来,查看下结果。
你会发现上来就错了,提示如下:
"status":415,"error":"Unsupported Media Type","message":"Content type 'text/plain;charset=UTF-8' not supported"
这里你需要注意,无论遇到什么问题,都要针对问题来处理。当看不懂问题信息时,先查资料,想办法看懂。这是处理问题的关键,我发现很多做性能测试的新同学,一旦碰到问题就懵了,晕头转向地瞎尝试。
我经常对我的团队成员说,先看懂问题,再处理问题,别瞎蒙!
上面这个问题其实提示得很清楚:“不支持的媒体类型”。这里就两个信息,一个是 Content type,一个是 charset。它们是 JMeter 中 HTTP Header 里默认自带的。我们要发送的是 JSON 数据,而 JMeter 默认是把它当成 text 发出去的,这就出现了问题。所以我们要加一个 Header,将 Content type 指定为 JSON。
加一个 HTTP Header,如下所示:
如果你不知道加什么样的 Header,建议你用 HTTP 抓包工具抓一个看一看,比如说用 Charles,抓到如下信息:
这时你就会知道头里的 Content-Type 原来是application/json;charset=UTF-8。这里的 charset=UTF-8 可以不用写,因为它和默认的一样。
这时再回放,你就会看到如下结果:
到此,一个 POST 脚本就完成了。是不是很简单。
在这里,我需要跟你强调的是,手工编写 HTTP 脚本时,要注意以下几点:
要知道请求的类型,我们选择的类型和后端接口的实现类型要是一致的。
业务的成功要有明确的业务判断(在下面的 TCP 中,我们再加断言来判断)。
判断问题时,请求的逻辑路径要清晰。
编写完 HTTP 脚本时,我们再来看一下如何编写 TCP 脚本。

手工编写 TCP 脚本

服务端代码逻辑说明

我在这里写一个非常简单的服务端接收线程(如果你是开发,不要笑话,我只是为了说明脚本怎么写)。
package demo.socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class SocketReceiver {
//定义初始
public static final int corePoolSize = 5;
//定义最大线程池
public static final int maximumPoolSize = 5;
//定义socket队列长度
public static final int blockingQueue = 50;
/**
* 初始化并启动服务
*/
public void init() {
//定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 0L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue(blockingQueue));
//定义serverSocket
ServerSocket serverSocket = null;
try {
//启动serverSocket
serverSocket = new ServerSocket(Constants.PORT);
//输出服务启动地址
System.out.println("服务已启动:" + serverSocket.getLocalSocketAddress().toString());
//接收信息并传递给线程池
while (true) {
Socket socket = serverSocket.accept();
executor.submit(new Handler(socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close(); //释放serverSocket
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//处理请求类
class Handler implements Runnable {
private Socket socket;
public Handler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
// 接收客户端的信息
InputStream in = socket.getInputStream();
int count = 0;
while (count == 0) {
count = in.available();
}
byte[] b = new byte[count];
in.read(b);
String message = new String(b);
System.out.println(" receive request: " + socket.getInetAddress() + " " + message);
// 睡2秒模拟思考时间,这里是为了模拟服务器端的业务处理时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 向客户端发送确认消息
//定义输出流outer
OutputStream outer = socket.getOutputStream();
//将客户端发送的信息加上确认信息ok
String response = message + " is OK";
//将输入信息保存到b_out中
byte[] b_out = response.getBytes();
//写入输入流
outer.write(b_out);
//推送输入流到客户端
outer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭socket
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//程序入口
public static void main(String[] args) {
//定义服务端
SocketReceiver receiver = new SocketReceiver();
//启动服务端
receiver.init();
}
}

编写 JMeter 脚本

首先创建 TCP Sampler。右键点击 Thread Group - Add - Sampler - TCP Sampler 即可创建。
输入配置和要发送的信息。
IP 地址和端口是必须要输入的。对于创建一个 TCP 协议的 JMeter 脚本来说,简单地说,过程就是这样的:创建连接 - 发数据 - 关闭连接。
就这样,这个手工的脚本就完成了。
你可能会问,就这么简单吗?是的,手工编写就是这么简单。
但是(对嘛,但是才是重点),通常我们在创建 TCP 协议的脚本时,都是根据业务接口规范来说的,复杂点其实不在脚本本身上,而是在接口的规则上

添加断言

我回放了一下脚本,发现如下情况:
都执行对了呀,为什么下面的没有返回信息呢?这种情况下只有第一个请求有返回信息,但是下面也没有报错。这里就需要注意了。
测试工具的成功,并不等于业务的成功
所以我们必须要做的就是响应断言,也就是返回值的判断。在 JMeter 中,断言有以下这些:
因为今天的文章不是工具的教程,所以我不打算全讲一遍。这里我只用最基础的响应断言。什么是断言呢?
断言指的就是服务器端有一个业务成功的标识,会传递给客户端,客户端判断是否正常接收到了这个标识的过程。
在这里我添加了一个断言,用以判断服务器是否返回了 OK。 你要注意这个“OK”是从哪来的哦,它是从服务端的这一行代码中来的。
String response = message + " is OK";
请注意,这个断言的信息,一是可以判断出业务的正确性。我在工作中发现有些人用页面中一些并不必要的文字来判断,这样就不对了,我们应该用有业务含义的判断标识。
如果我们再次回放脚本,你会发现除了第一个请求,后面 9 个请求都错了。
所以,在做脚本时,请你一定要注意,断言是必须要加的

长短连接的问题

既然有错,肯定是要处理。我们查看一下 JMeter 的控制台错误信息:
2019-11-26 09:51:51,587 ERROR o.a.j.p.t.s.TCPSampler:
java.net.SocketException: Broken pipe (Write failed)
at java.net.SocketOutputStream.socketWrite0(Native Method) ~[?:1.8.0_111]
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:109) ~[?:1.8.0_111]
at java.net.SocketOutputStream.write(SocketOutputStream.java:141) ~[?:1.8.0_111]
at org.apache.jmeter.protocol.tcp.sampler.TCPClientImpl.write(TCPClientImpl.java:78) ~[ApacheJMeter_tcp.jar:5.1.1 r1855137]
at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.sample(TCPSampler.java:401) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.doSampling(JMeterThread.java:622) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.executeSamplePackage(JMeterThread.java:546) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.processSampler(JMeterThread.java:486) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:253) [ApacheJMeter_core.jar:5.1.1 r1855137]
at java.lang.Thread.run(Thread.java:745) [?:1.8.0_111]
从字面上来看,就是通道瓦塔(被破坏)了,Broken pipe。这个提示表明客户端上没有这个连接了,而 JMeter 还以为有这个链接,于是接着用这个链接来发,显然是找不到这个通道,于是就报错了。
这是一个典型的压力工具这边的问题。
而服务端,只收到了一条请求。
为什么会报这个错呢?因为我们代码是短链接的,服务端处理完之后,就把这个链接给断掉了。
这里是压力机上的抓包信息:
//从这里开始,上面已经看到了有Fin(结束)包了,后面还在发Push(发送数据)包。显然是通不了,还被服务端啪啪抽了两次reset。
11:58:07.042915 IP localhost.57677 > 60.205.107.9.m-oap: Flags [P.], seq 34:67, ack 41, win 4119, options [nop,nop,TS val 163718903 ecr 2122793206], length 33
11:58:07.046075 IP localhost.57677 > 60.205.107.9.m-oap: Flags [FP.], seq 67:331, ack 41, win 4119, options [nop,nop,TS val 163718906 ecr 2122793206], length 264
11:58:07.076393 IP 60.205.107.9.m-oap > localhost.57677: Flags [R], seq 3986768192, win 0, length 0
11:58:07.079156 IP 60.205.107.9.m-oap > localhost.57677: Flags [R], seq 3986768192, win 0, length 0
服务端的抓包信息:
//服务端也是没有办法,只能在看到了Push包之后,给回了个Reset包。
11:58:07.047001 IP 124.64.16.240.bones > 7dgroup1.enc-eps-mc-sec: Flags [P.], seq 34:67, ack 41, win 4119, options [nop,nop,TS val 163718903 ecr 2122793206], length 33
11:58:07.047077 IP 7dgroup1.enc-eps-mc-sec > 124.64.16.240.bones: Flags [R], seq 3986768192, win 0, length 0
11:58:07.054757 IP 124.64.16.240.bones > 7dgroup1.enc-eps-mc-sec: Flags [FP.], seq 67:331, ack 41, win 4119, options [nop,nop,TS val 163718906 ecr 2122793206], length 264
11:58:07.054844 IP 7dgroup1.enc-eps-mc-sec > 124.64.16.240.bones: Flags [R], seq 3986768192, win 0, length 0
这是为什么呢?因为在 JMeter 中,默认是复用 TCP 连接的,但是在我们这个示例中,服务端并没有保存这个连接。所以,我们应该在脚本中,把下图中的 Re-use connection 给去掉。
这时再回放脚本,你就会发现 10 次迭代全都对了。如下图所示:
但是,这里还有一个知识点,希望你注意。短连接的时候,必然会产生更多的 TCP 连接的创建和销毁,对性能来说,这会让系统变得缓慢。
所以你可以看到上面 10 条迭代全都对了的同时,响应时间也增加了。
可能会有人问,那这怎么办呢?长短连接的选择取决于业务的需要,如果必须用短链接,那可能就需要更多的 CPU 来支撑;要是长连接,就需要更多的内存来支撑(用以保存 TCP 连接)。
根据业务需要,我们选择一个合适的就好。

TCP 连接超时

这个问题,应该说非常常见,我们这里只做问题的现象说明和解决,不做原理的探讨。原理的部分,我会在监控和分析部分加一说明。
下面这个错误,属于典型的主机连不上。
java.net.ConnectException: Operation timed out (Connection timed out)
at java.net.PlainSocketImpl.socketConnect(Native Method) ~[?:1.8.0_111]
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) ~[?:1.8.0_111]
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) ~[?:1.8.0_111]
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) ~[?:1.8.0_111]
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) ~[?:1.8.0_111]
at java.net.Socket.connect(Socket.java:589) ~[?:1.8.0_111]
at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.getSocket(TCPSampler.java:168) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
at org.apache.jmeter.protocol.tcp.sampler.TCPSampler.sample(TCPSampler.java:384) [ApacheJMeter_tcp.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.doSampling(JMeterThread.java:622) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.executeSamplePackage(JMeterThread.java:546) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.processSampler(JMeterThread.java:486) [ApacheJMeter_core.jar:5.1.1 r1855137]
at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:253) [ApacheJMeter_core.jar:5.1.1 r1855137]
at java.lang.Thread.run(Thread.java:745) [?:1.8.0_111]
time out 是个如果你理解了逻辑,就觉得很简单,如果没理解逻辑,就觉得非常复杂的问题。
要想解决这个问题,就要先确定服务端是可以正常连通的。
如果不能正常连通,那么通常都是 IP 不正确、端口不正确、防火墙阻止之类的问题。解决了网络连通性的问题,就可以解决 connection timed out 的问题。

编写 LoadRunner 脚本

针对上面这个示例,如果你要想编写一个 LoadRunner 的示例脚本,也是简单到不行。
首先创建一个空的 winsock 脚本,复制下面代码到 action 里面。
//创建socket1
lrs_create_socket("socket1", "TCP", "RemoteHost=60.205.10.9:5567", LrsLastArg);
//走socket1, 发送buf1中定义的数据
lrs_send ("socket1", "buf1", LrsLastArg );
//走socket1,接收数据保存在buf2中
lrs_receive("socket1", "buf2", LrsLastArg);
//关掉socket1
lrs_close_socket("socket1");
从上面的信息就可以看到,socket1 这个标识是我们操作的基础。如果你在一个脚本中想处理两个 socket,也是可以的,只要控制好你的标识不会乱就行。
接着再将下面的内容复制到 data.ws 里面。
send buf1 5
"12345"
recv buf2 10
你可能会问,这个 recv 怎么不写返回的值是什么?
当你手写 socket 脚本的时候,都还没有运行,你怎么知道返回值是什么呢?所以这里,可以不用写。
而 recv 后面的 10 是指接收 10 个字节。如果多了怎么办?截掉?!不会的,LoadRunner 还是会把所有信息全部接收并保存下来,除非你提前定义了截取字符长度的函数。
最后看下我们回放的结果:
Action.c(6): lrs_create_socket(socket1, TCP, ...)
Action.c(7): lrs_send(socket1, buf1)
Action.c(8): lrs_receive(socket1, buf2)
Action.c(8): Mismatch in buffer's length (expected 10 bytes, 11 bytes actually received, difference in 1 bytes)
================================EXPECTED BUFFER================================
===============================================================================
================================RECEIVED BUFFER================================
"12345 is OK"
===============================================================================
Action.c(8): callRecv:11 bytes were received
Action.c(9): lrs_close_socket(socket1)
看,脚本正常执行了,只是报了一个 Mismatch,这是因为我们定义了 buf2 是 10 字节,而我们实际上接收了 11 字节,所以这里给出了 Mismatch。
到此,一个 LoadRunner 的手工 TCP 脚本就完成了。后面我们就可以根据需要,增强脚本了,加个参数化、关联、检查点等等。

总结

其实这篇文章只想告诉你一件事情,手工编写脚本,从基础上说,是非常简单的,只是有三点需要特别强调:
涉及到业务规则和逻辑判断之后,编写脚本就复杂了起来。但是了解业务规则是做脚本的前提条件,也是性能测试工程师的第一步。
编写脚本的时候,要知道后端的逻辑。这里的意思不是说,你一开始写脚本的时候,就要去读后端的代码,而是说你在遇到问题的时候,要分析整个链路上每个环节使用到了什么技术,以便快速地分析判断。
写脚本是以最简为最佳,用不着故意复杂。
脚本的细节功能有很多,而现在我们可以看到市场上的书籍也好,文档也好,基本上是在教人如何用工具,很少会从前到后地说明一个数据从哪发到哪,谁来处理这样的逻辑。
希望学习性能测试工具的你,不仅知其然,更知其所以然。

思考题

学习完今天的内容,你不妨思考一下,HTTP 的 GET 和 POST 请求,在后端处理中有什么不同?断言的作用是什么?如何使用断言呢?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 7

提建议

上一篇
07丨性能测试工具:如何录制脚本?
下一篇
09丨关联和断言:一动一静,核心都是在取数据
unpreview
 写留言

精选留言(38)

  • zuozewei
    2020-01-02
    前面几个同学回答的蛮好,我在此补充下幂等性相关的区别吧。 ## 什么是幂等性 一次和多次请求某一个资源应该具有同样的副作用(对资源变更带来连锁反应或影响):f(x) = f(f(x))。 ## 为什么要幂等性设计? 系统解耦后,系统间服务调用存在三种状态: * 成功 * 失败 * 超时(中间状态) 前面两种是明确的,超时是不知道什么状态,一般引起原因: * 请求没有到达服务方(网络延时或丢失) * 请求达到了服务方,服务方处理超时 * 请求到达了服务方并且处理完返回结果,但接收方没有收到 相关例子:订单创建、库存扣减、订单支付 ## 怎么做幂等性设计? * 下游(响应方)提供查询接口,上游(请求方)对于状态疑异订单进行查询 * 下游(响应方)系统坐幂等性设计:确保不会重复 * 全局ID:Twitter 的 Snowflake 算法/UUID * 存储冲突来解决(唯一约束) * 插入重复无效,`insert into … values … on DUPLICATE KEY UPDATE …` * 更新状态:`update table set status = “paid” where id = xxx and status = “unpaid”;` ## HTTP幂等性 - HTTP GET 方法用于获取资源,不应有副作用,所以是幂等的。 - HTTP POST 方法用于创建资源,所对应的 URI 并非创建的资源本身,而是去执行创建动作的操作者,有副作用,不满足幂等性。 - 只有 POST 需要特殊处理,其他都具有幂等性 - 前端生成 token,后端存(唯一约束) - PRG 模式
    展开
    共 2 条评论
    21
  • 善行通
    2020-01-01
    感谢高老师无私分享,在刚开始学习性能测试的时候,一直不理解做脚本为什么要这样做,也报名参加过培训机构,也许培训机构的老师都不会,或者自己没有写过后端代码,更不会讲解后端怎么实现,还有调用关系,或者根本不想让学员知道,担心教会徒弟饿死师父。 就像老师说的:【自己写一些 demo,去了解一些逻辑,然后在排除问题的时候,就非常清楚了】 要是早早听到老师这样的课程,估计自己的水平能快速提高谢谢老师分析Jmeter调用后端简单逻辑【Jmeter-->controller--->interface--->service[业务实现层]-->Mappper-->DB】 GET请求对于springboot框架来说是通@RequestMapping(method = RequestMethod.GET)中的@GetMapping来处理,这是框架定义好的接口,关键是get执行的业务操作是什么; POST请求也是springboot框架来说是通@RequestMapping(method = PostMapping.GET)中的@PostMapping处理数据; 一般来说get是获取数据数据会在url上显示,post是提交数据,提交数据不会显示到url上, 而且Get方法提交的数据大小长度并没有限制,HTTP协议规范没有对URL长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE对URL长度的限制是2083字节;理论上讲,POST是没有大小限制的。HTTP协议规范也没有进行大小限制,起限制作用的是服务器的处理程序的处理能力【Tomcat默认2M】;对数据请求频繁,数据不敏感且数据量在普通浏览器最小限定的2k范围内,这样的情况使用GET。其他地方使用POST 断言的作用是什么? 理解断言是为了校验请求是否正确,只要增加合理的断言,才可以做性能测试,如果不加断言就不知道业务请求是否正确,再加没有断言TPS会很平稳,对实际压测结果意义不大。 如何使用断言呢? 理解:在请求结束后的响应增加断言。
    展开

    作者回复: 理解的很深刻哦。

    共 3 条评论
    19
  • 律飛
    2020-01-01
    1.HTTP 的 GET 和 POST 请求,在后端处理中有什么不同? 由于spring的RequestParam注解接收的参数是来自于requestHeader中,即请求头,也就是在url中,格式为xxx?username=123&password=456,而RequestBody注解接收的参数则是来自于requestBody中,即请求体中。 因此如果为get请求时,后台接收参数的注解应该为RequestParam,如果为post请求时,则后台接收参数的注解就是为RequestBody。 2.断言的作用是什么?如何使用断言呢? 断言指的就是服务器端有一个业务成功的标识,会传递给客户端,客户端判断是否正常接收到了这个标识的过程。 应该用有业务含义的判断标识。需要对业务进行分析,选取合适的判断标识。 善行通已经说得很好了,画蛇添足一下。
    展开

    作者回复: 你也说的很好。

    11
  • 羊羊
    2020-12-23
    关于递增时间,文中提到“如果我们配置了 10 线程,这里配置为 1 秒,则是 10/1000=1 线程 /100ms”,是指压力机会每100ms发送一个请求么?是匀速发送请求?前面的文章中老师讲到“线性递增”,根据接口不同的响应时间,配置不同的递增幅度,对应到Jmeter是如何配置?

    作者回复: 是100ms增加一个线程。增加的线程会持续运行。 jmeter配置递增很容易呀。只要线程数、ramp-up period、duration就行了。

    5
  • 大QA
    2020-03-21
    高楼老师,请问压测环境要准备什么样的?测试环境和生产环境的服务器配置差异往往很大,在测试环境压测不一定能反应生产环境的问题。

    作者回复: 这个话题太大。通常我只能建议接近生产,不然就得做大量的数据采集分析,然后建模。

    2
  • 莫西 👫 小妞儿 ...
    2020-03-10
    高老师,添加断言的话,会影响性能测试结果么?比如会影响到响应时间么?

    作者回复: 不会,都是压力工具里判断的。

    共 2 条评论
    2
  • Geek_5860ac
    2020-01-10
    “如果我们配置了 100 线程,这里配置为 10 秒,那么就是 100/(10s*1000ms)=1 线程 /10ms”这个地方不明白,怎么算都是1线程/ 100ms啊,请老师指正

    作者回复: 少了一个零。哈,马上让编辑小美女加上。

    2
  • [root@localhost ~]...
    2021-04-02
    高楼老师,你好,请问你用jmeter展示tps图标用的是什么插件?而我用网上下载的插件使用会大量报错最终导致jmeter卡死。错误内容是:Error o.a.j.t ListenerNotifier: Deteced problem in Listener Java.lang.NullPointer Exception: Null。请问这个该如何解决呢?多谢

    作者回复: 不需要插件。那个是html结果报告。仔细看看jmeter的report命令吧。

    共 2 条评论
    1
  • Geek_WHY
    2021-01-15
    高老师您好,麻烦打扰,我想问下您看我做的对不对: 对一种场景进行性能验证,验证系统在不同的硬件配置下,tps能达到多少。 我这边不确定是用阶梯性一点一点慢慢的加压,加压到一定的线程数,还是在一分钟内快速的加压到一定的线程数。 盼望回复,感谢~

    作者回复: 慢慢加压,不用快。

    2
  • 2020-05-24
    有两点很认同: 第一解决问题的第一步必须是先理解问题,把问题搞清楚,这就像走路方向如果错误,越努力错的将会越离谱,看电影电视剧里都是如此,误会不解开,人生的悲剧将不可避免 第二原理不清楚,对于问题的理解就像男追女隔窗如隔山,超时的问题我最近也在解决一个,比较尴尬基本原理知道不过证据链还未抓到,应该是TCP/IP这块原理理解不深耽误的

    作者回复: 基础知识体系是非常重要的部分。就像地基。 而上层建筑不管怎么好看,没有地基也不稳定。

    1
  • 自渡。
    2020-03-23
    老师,请问看TPS推荐使用什么工具看,我看的是的jmeter的reportgenerator生成的报告

    作者回复: 这个报告就可以。也可以用influxdb+grafana输出。

    1
  • songyy
    2020-01-13
    思考题 - HTTP 的 GET 和 POST 请求,在后端处理中有什么不同? 其实后端处理,没有本质的不同。不同的是人们对于它的约定:GET的预期,是不进行数据修改;而POST,通常意味着创建新的数据。但实际上,后端也可以在GET的时候,进行数据修改 - 断言的作用是什么:断言可以用来判断,一个测试用例是否成功 如何使用断言呢:在收到返回结果后,对比结果是否和预期相符。就像写测试时候的断言(assertion)一样
    展开

    作者回复: 理解的很对。

    1
  • 悦霖
    2020-01-03
    抓包信息看不懂怎么办😂

    作者回复: 说明需要补充网络知识。

    1
  • 月亮和六便士
    2020-01-02
    老师,我还有问题想问:前面的文章中提到过,1. 什么叫压力补偿,压力补偿的作用是什么? 2. 还有为什么要动态扩展? 比如内存不够了,我们不应该找到谁占用了内存吗? 3.每次测试前需要清理缓存吗?比如我跑一轮脚本 就需要把redis 缓存清一下吗 ?

    作者回复: 1. 就是在场景执行的过程中,发现场景产生的压力和生产上比例不一致,或者有中间需要的增加的第三方压力。就需要在场景执行过程中再增加新的线程或者压力机。 2. 动态扩展是验证线上的能力。如果业务流量突增了。就需要动态扩展哇。 3. 看机制。如果不是预热类型的。 可以在每次跑之前清一下。

    2
  • 新思维
    2020-01-02
    get请求,一般后端服务只是通过传过来的参数查询数据库,返回结果;post请求,一般后端服务会将请求所包含的内容更新到数据库,返回更新结果。 断言判断后端服务返回的请求是否为所期望的请求结果。涉及到业务逻辑的断言需要对响应内容进行检查,包括关键字检查、或者数据处理逻辑结果检查等。

    作者回复: 理解的非常对。

    1
  • 小芽儿🍃
    2022-06-15
    老师,断言和提取器会影响压测结果,那我们应该怎样合理使用这两个呢

    作者回复: 这个会影响很大吗?我没遇到过哦,你有数据来说明使用了这样的功能之后压测结果会有很大差异吗?

  • 2021-11-30
    压测一个socks5的转发程序,发起请求过程 压测客户端--转发程序---目标服务器 响应返回过程相反,目标服务器是不是要写一个接收程序,咋写呢

    作者回复: 我理解你是需要一个支持socks5的Mock Server。如果对响应内容没有特别的要求,那倒是比较简单,直接返回固定的字符就可以了。 如果要对响应内容有要求,确实是需要自己写个Mock Server,来实现逻辑的判断了。具体怎么实现,你可以参考一下Moco这样的开源系统。

  • 0909
    2021-06-07
    提问:如果我们配置了 100 线程,这里配置为 10 秒,那么就是 100/(10s*1000ms)=1 线程 /100ms 这个是100ms增加一个线程的意思吗

    作者回复: 对的。就是这个意思。

  • 何鹿娇
    2021-05-11
    Number of Threads(users):我们都知道这是 JMeter 中的线程数,也可以称之为用户数。但是在第 2 篇文章中,我已经说得非常明确了,这个线程数是产生 TPS 的,而一个线程产生多少 TPS,取决于系统的响应时间有多快。所以我们用 TPS 这个概念来承载系统的负载能力,而不是用这里的线程数。 如果是websocket协议,一个线程能产生多少TPS怎么来决定呢?还是取决于响应时间嘛?还是取决于什么时候断开长链接
    展开

    作者回复: 取决于响应时间。

  • ^_^
    2021-04-26
    如果配置了100个线程,时间是10s,要算每个线程的响应时间,不是应该用时间/线程数吗?应该是10*1000/100啊。如果用线程数/时间,得到的结果是0.01线程/ms

    作者回复: 咦,你这个用线程算?出来的结果也是线程?不是要算TPS吗?