低配终端环境下如何模拟大规模负载

本文探讨了在低配置终端环境下如何模拟大规模负载,指出测试工具的限制,如线程消耗内存、高额系统开销等问题。文章介绍了优化方法,包括线程池、IO多路复用模型(Reactor和Java纤程库Quasar),并对比了不同策略在调度性能和资源利用率上的优劣。实验表明,异步方法和纤程库在保持编程模式的同时提供了更好的性能和资源管理。
摘要由CSDN通过智能技术生成

什么束缚了你的手脚

很多时候我会听到这样的抱怨声“我的终端设备配置太差了,跑测试工具只能模拟很少量的虚拟用户”。是的,我只想说有时候就算你使用高配置的服务器在某些场景下也只能模拟出十位数字的用户。

这不是危言耸听,比如LoadRunner一些特殊的协议,像Microsoft .Net协议这样可以录制一个.Net的Client程序的通信交互逻辑,通过引用Client程序的dll动态链接库,利用代理到的通信交互过程,可以分析并反向工程出一套客户端程序代码的C#.Net脚本。利用这样的脚本运行并发测试,就好像你的一台设备上同时启动了几十个真实的Client(因为受限于反向工程时的一些技术限制,甚至比真实的Client还要笨重),可想其消耗资源之大,会跑到操作系统不能自理。

因此,并不完全是硬件配置原因限制了你的目标,还有其他很多原因,这里面可能最直接的几点原因都是来源于测试工具本身:

(1)有些优秀的工具在安装时会帮助你完成对操作系统内核参数的优化,以及工具本身可以根据所搭载设备的硬件配置进行自适应式的优化调整,而你所使用的大部分工具不具备这样的能力;

(2)要知道目前你所获得的绝大数性能测试工具历史都相当悠久,工具的核心框架仍然保持着它最初的模样,比如这些工具对线程的控制与应用方面,虽然随着升级进行了一定的优化,但仍沿袭了“同步阻塞IO模型”;

(3)许多版本的迭代并没有有效地提升其在模拟执行测试时的性能,反而使得工具变得“厚重”。比如JMeter这样的工具,10几年来不断增加额外的新特性,这些特性并没有显著地优化其自身性能,有时反而适得其反,比如BeanShell甚至在使用不当的情况下在测试时会搅乱工具的整体性能表现。

很多时候工具会限制你的思维方式、束缚住你的手脚,你无法怪罪于工具,比如,“同步阻塞IO模型”的应用是最易驾驭且在线程级别上最能体现“并发”概念的,对于一个通用型多协议支持的测试工具这样的模型最为稳妥而且具备普适性,这当然值得去牺牲一些性能。

当享受使用这些工具带来便利的同时,你可能根本不会进一步去思考如何来优化,从而改良对于一些特定场景下的测试手段,最终来达到一些惊人的效果。

线程

需要达到惊人的效果,我们需要了解是什么限制住了这些工具的发挥,前面提到了“同步阻塞IO模型”的概念,是的,很大程度上你可以将性能问题的产生归咎于这类线程应用模型的使用上。

消耗内存

我们知道比如在JVM这样的运行环境下,每创建一个线程是需要为其分配一定大小额度的线程栈内存的,在一般条件下默认值为1M字节。这些线程一旦数量太多就会占用大量内存,给GC带来回收压力,当把内存耗尽时,你将得到以下异常:

java.lang.OutOfMemoryError: unable to create new native thread

我们可以通过配置的方式尽力挽回:

当你创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统级线程的内存用的并不是JVM分配内存,而是系统中剩下的内存,一个普遍认可的最大创建线程数的计算公式如下:

Max number of threads(最大创建线程数) = (进程最大可用内存 - JVM分配内存 - OS Reserved Memory(JNI,本地方法栈)) / thread stack size(线程栈大小)

对于如JMeter这样由Java语言所编写的性能测试工具来说,简单的通过减小JVM分配内存数,来增加最大创建线程数是不可靠的,每一个创建线程都是要完成特定的任务,而在任务生命周期中将会创建大量的Java对象,这些对象将会很快耗尽JVM分配内存。因此,你需要倍加小心地配合着调节以下参数来扩大理论上的最大创建线程数:

-Xmx:设置JVM最大堆大小
-Xss或-XX:ThreadStackSize=<value>:设置线程的栈大小(字节数)(0表示默认) 

当然,操作系统不会任由单个进程创建无数的线程,大部分情况下会有内核级参数的限制,比如Linux操作系统可以通过调节以下内核参数从理论上来消除这些限制:

/proc/sys/kernel/pid_max 
/proc/sys/kernel/thread-max
max_user_process(ulimit -u)
/proc/sys/vm/max_map_count

高额的系统开销

创建和使用一个线程的代价是十分昂贵的,如果针对每个任务都启动一条线程进行处理,当可运行的线程(Runnable状态)数量大于可用处理器数量(核心数),那么有些线程会得不到调度,CPU将会耗费大把的精力来协调管理这群线程,在频繁的线程上下文切换中度过余生。

“同步阻塞IO模型”将这种场景发挥得淋漓尽致,它特别不适合处理IO密集型的业务场景,在大规模并行环境下,经常性的等待和切换耗费着大量的系统资源,却换来的是低效的性能,上面说了由于历史原因和易于控制,大部分测试工具都采用了这种模型完成并发的模拟。
##是时候改变些什么了
以下我们将以Java语言及其运行环境为例,讨论如何利用优化线程模型的方法来改变在低配终端环境下以往使用测试工具(如JMeter)时所面临的无法模拟大规模负载的窘境。

首先需要明确的是,在模拟大规模负载的场景前,你需要逆向思考,将压力生成终端如同服务器一样对待,因为它即将成为一台反向生成大规模TCP/IP请求的设备(安装Linux操作系统)。

操作系统TCP优化

(1)我们知道“Linux操作系统一切皆文件”,一切系统IO操作都会最终被抽象为对应的文件,并可以通过“文件描述符”建立关联。作为IO操作的Socket,每创建一个连接都会创建一个对应的文件并通过文件操作完成具体工作,因此,首先需要调整用户进程可打开文件数限制的系统参数,来增加创建Socket的数量,主要方法如下:

使用ulimit设置系统允许当前用户进程打开的文件数限制:

ulimit -n 65535

(2)当模拟大量TCP/IP请求时,需要修改网络内核对TCP连接的有关限制,并优化相关参数,主要方法如下:

修改/etc/sysctl.conf文件,在文件中添加如下行(一些经验参数):

net.ipv4.ip_local_port_range = 1024 65535
fs.file-max = 65535
kernel.pid_max = 65536   
net.ipv4.tcp_syncookies = 1  
net.ipv4.tcp_synack_retries = 2  
net.ipv4.tcp_syn_retries = 2  
net.ipv4.tcp_timestsmps = 0  
net.ipv4.tcp_tw_reuse = 1  
net.ipv4.tcp_tw_recycle = 1  
net.ipv4.tcp_fin_timeout = 30  
net.ipv4.tcp_keepalive_time = 1200  
net.ipv4.ip_local_port_range = 10000 65535  
net.ipv4.tcp_max_syn_backlog = 8192  
net.ipv4.tcp_max_tw_buckets = 5000  
net.ipv4.tcp_wmem = 8192 436600 873200  
net.ipv4.tcp_rmem  = 32768 436600 873200  
net.ipv4.tcp_mem = 94500000 91500000 92700000  
net.ipv4.tcp_max_orphans = 3276800  
net.core.netdev_max_backlog = 32768  
net.core.somaxconn = 32768  
net.core.wmem_default = 8388608  
net.core.rmem_default = 8388608  
net.core.rmem_max = 16777216  
net.core.wmem_max = 16777216  

之后,执行sysctl -p使其生效。

探索优化方法

我们按照往常的惯用策略,在讨论优化方法前,首先构建一个Mock环境来测试所研究方法的效果,之后通过分析得到我们的结论。

一个Mock环境

我们这里使用resteasy构建一个简单的RESTful API 服务,程序清单参考以下内容:
(1)maven的pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.xreztento.mock</groupId>
	<artifactId>restful</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>restful Maven Webapp</name>
	<url>http://maven.apache.org</url>
	<repositories>
		<repository>
			<id>JBoss repository</id>
			<url>https://repository.jboss.org/nexus/content/groups/public-jboss/</url>
		</repository>
	</repositories>
	<dependencies>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jaxrs</artifactId>
			<version>2.2.1.GA</version>
		</dependency>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jackson-provider</artifactId>
			<version>3.0.19.Final</version>
		</dependency>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jaxb-provider</artifactId>
			<version>3.0.19.Final</version>
		</dependency>
	</dependencies>
	<build>
		<finalName>restful</finalName>
	</build>
</project>

(2)web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
	<display-name>Mock Restful Web Application</display-name>

	<context-param>
		<param-name>resteasy.resources</param-name>
		<param-value>
			org.xreztento.mock.restful.MockService
		</param-value>
	</context-param>

	<context-param>
		<param-name>resteasy.servlet.mapping.prefix</param-name>
		<param-value>/</param-value>
	</context-param>

	<listener>
		<listener-class>
			org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap
		</listener-class>
	</listener>

	<servlet>
		<servlet-name>resteasy-servlet</servlet-name>
		<servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
	</servlet>

	<servlet-mapping>
		<servlet-name>resteasy-servlet</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>

</web-app>

(3)一个用于接口方法返回的数据对象类MockResult

public class MockResult {
   
	public String getResult() {
   
		return result;
	}

	public void setResult(String result) {
   
		this.result = result;
	}

	private String result = null;
}

(4)一个RESTful API服务实现类

@Path("/api") 
public class MockService {
   
	@GET  
    @Path("/{mock}")
	@NoCache
    @Produces(MediaType.APPLICATION_JSON)
    public MockResult getByUsername(@PathParam("mock") String mock, @Context HttpServletResponse response) throws InterruptedException {
   
		MockResult result = new MockResult();
		result.setResult(mock);
        return result;
    }  
}

启动服务后,我们可以通过浏览器访问测试接口服务,结果如下:

之后,我们就以该接口作为被测试接口对象进行各种优化方法的比较。

基准方法(Thread)

我们按照“同步阻塞IO模型”来实现一个性能最糟糕的基准方法(甚至每一个Thread下都会新创建一个HttpClient对象),用于验证优化方法到底能够达到什么效果,对于HttpClient我们保持与JMeter一致,使用Apache-HttpClient,可以参考以下代码:
(1)一个用于记录结果的类TestResult

public class TestResult {
   
    private long min;
    private long max;
    private long time90;
    private long avg;
    private long last;

    public long getMin() {
   
        return min;
    }
    public void setMin(long min) {
   
        this.min = min;
    }
    public long getMax() {
   
        return max;
    }
    public void setMax(long max) {
   
        this.max = max;
    }
    public long getTime90() {
   
        return time90;
    }
    public void setTime90(long time90) {
   
        this.time90 = time90;
    }
    public long getAvg() {
   
        return avg;
    }
    public void setAvg(long avg) {
   
        this.avg = avg;
    }
    public long getLast() {
   
        return last;
    }
    public void setLast(long last) {
   
        this.last = last;
    }

    @Override
    public String toString(){
   
        return "avg : " + getAvg() + "\n"
                + "min : " + getMin() + "\n"
                + "max : " + getMax() + "\n"
                + "90% : " + getTime90() + "\n"
                + "last : " + getLast() + "\n";
    }
}

(2)一个用于结果统计和计算的类TestResultComputer

import java.util.Arrays;
import java.util.Vector;
import java
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值