Performance Tuning-how to do performance test?

之前也零散的做过性能测试,但是也是糊里糊涂(当时将性能测试简单理解为启动一批线程发请求给服务器,然后记录CPU和内存等关键指标,看看是否存在问题)。最近接手两种nio类型服务器的测试(jetty+resteasy构建的http服务,和apache mina构建的tcp服务)的性能测试,所以系统研究了下性能测试到底要关注什么?

 

性能测试的二个前提

 

(1)有没有做性能测试的必要?

基于最初的架构原型没有变化?如果没有变化且已知修改的代码不会影响性能,是否需要做重复的性能测试?

如果已预测性能瓶颈所在,何必先测后改,不如先改后测?

假设性能可能存在问题,短期内是否有充足时间去完善?

如果基于以上三种分析,我们仍认为有必要做性能测试,再去开展。

(2)测试对象的硬件配置和环境配置是什么样的?

这是进行性能测试的前提之一,假设服务部署在4核CPU,而性能测试是基于2核CPU做的,那测试得出的benchmart数据说服力不高,因为硬件配置不同。

所以在进行性能测试之前一定要和产线环境的机器配置完全或尽可能相同。

备注:实际操作时,也可以选用低配置的做一个预测试更容易暴露瓶颈,但是在最终测试时,还是要贴近实际。

 

性能测试的四个关注点

(1)吞吐量Throughput :单位时间内处理的请求总数,一般秒为单位。

(2)延时Latency: 系统对单个请求的响应,一般毫秒为单位。

(3)资源使用率:包含CPU,内存,IO, 网络等关键资源。

(4)请求失败率:请求失败的比例是多少。

在收集整理完在不同TPS的情况下,后三者的数据情况,才能为系统定一个合适的TPS提供标准,比如在100TPS的时候,延时超标了,或者成功率低或者资源使用率过高,那么TPS就不能选择100TPS。系统的处理能力是综合这四点的一个结论。

 

性能测试的七个要点

(1)延时不能仅仅看平均延时:性能测试中,关注延时是必然的,但是不能仅仅在意平均延时,而忽略各种延时值的分布,假想一个系统的平均延时是100ms, 确实为我们所接收,但是通过数据的分析,我们获知50%的请求是10ms以下,而50%的请求在190ms,这个时候,单纯认为延时就是100ms,就是为客户所接收就不合理。

(2)请求失败率是必须要考量的:如果单纯看延时,在某个TPS时,延时或许已经满足了需求,但是在这个TPS时,请求已经有少许失败,那么这种失败率一定要记录下来作为参考,否则单纯的记录TPS和延时就说系统可以胜任工作是不合理的,同时大多系统都允许一定的失败率,所以失败率一定要记录在案以提供参考。

(3)性能测试要有一定的持续时间:如果测试仅仅做了几秒或者几分钟,就得出相关的数据然后推断系统性能没问题是不科学的。假设一个系统是多线程接受+单线程处理,那么在初期性能可能会很好,而随着多线程接受的请求数一旦累加超过单线程的处理能力,会导致响应时间约来越长。所以说性能测试一定要注意是否持续了充足的时间。当然很多性能问题,最好先预估下性能瓶颈,否则持续的时间不是特别长都无法复现问题,而持续时间特别长对测试来说已变得不切实际。

(4)TPS的控制是否准确:在实际测试中,我们模拟一定的TPS效果,但是一定要核算下是否真的做到了某个TPS的效果。比如1台机器启动300个线程,每个线程模拟1秒内发5个请求,据此推断TPS是1500TPS,这样是否科学,还需要实际复查,因为对于单个机器CPU处理能力有限,对于这个业务请求并发度真的有那么高么?

(5)计算的时延是包含发出+接受,还是仅仅是处理时间:这点很重要,如果仅仅是记录处理时间,那么不能完全说一个请求的时延是多少,因为可能接受到请求会排队,之前也有建立连接等过程,所以一定要区分自己所记录的时延是从什么开始到什么时候结束。

 (6)性能测试方式对系统本身的影响:例如对于或许延时,我们可能使用profiler系统的方式进行,或者直接加一些log来计算,这些都会对系统本身产生一定的影响,一定要意识到这种影响,并且在发现影响过大时规避这些影响带来的错误结果。

 (7)是否真实反映了应用场景:大多时候我们会从不同角度简单化测试点,例如单纯并发多少写,单纯并发多少读,但实际中,我们还需要考虑实际的真实场景:例如读写都会有且比例不同。另外对于一个系统可能提供多种类型丰富的应用:例如不仅提供http也提供tcp服务,如果不考虑彼此,单纯的测试某一项服务只能反映只提供这项服务时的系统性能,系统的真实性能应该是综合同时提供服务的综合性能。

 

 性能测试工具的设计:

包括3个阶段:

prepare, stress, analyst.

 

性能测试的四个结论

(1)系统的TPS是多少? 

(2)系统的并发度是多少?

例如假设我们算出1个请求的处理时间是100ms, 所以原则上我们知道TPS是10TPS,但是实际测算我们的TPS是100.那么可知系统肯定是多线程模型,并发度达到了10.如果系统的TPS就是10,那么明显是单线程模型

(3)系统瓶颈所在?

在拿到系统时延,资源使用率后,我们可以profiler系统找到系统的瓶颈所在。以有针对性的去解决。

(4)一个请求完成需要多长时间?

在不同的并发度前提下,单个请求的完成延时是多少?

 

性能测试是涉及知识领域比较多比较深入的话题,上面简单分享了下自己的体会,多有不足,还需要在日后的工作中慢慢体会。

Performance Tuning-how to create and keep TPS?

性能测试中,我们常使用TPS来衡量服务的性能,这就需要我们在S内均匀的做到N个业务,而这里所要求的均匀,即保持固定频率,不是指间隔相同的时间,去做一些请求,因为实际处理请求的时间可能稍有不同,如果用固定的间隔时间去发送请求,那么所达到效果不见得是均匀,这里所述的固定频率或者说均匀实际上包含了本身请求处理的时间。

所以这里提供2种简单的方式:

(1)频率>=MS的:

采用JDK自带的Timer去处理:

jdk的timer核心源码:

  private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // Wait for queue to become non-empty
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; // Queue is empty and will forever remain; die

                    // Queue nonempty; look at first evt and do the right thing
                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized(task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { // Non-repeating, remove
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // Repeating task, reschedule
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired) // Task hasn't yet fired; wait
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  // Task fired; run it, holding no locks
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }

timer用法:

			timer.scheduleAtFixedRate(task, 4, 50);
		        timer.cancel();  //达到自己需要发送的总时间可以去取消

(2)频率<MS:

由于JDK自带的Timer只能精确到1MS,所以如果发送间隔要求精确到微妙或者纳秒,可以使用JDK的另外一个类,例如控制到微妙:

ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(1);
newScheduledThreadPool.scheduleAtFixedRate(task, 0, 1000, TimeUnit.MICROSECONDS);

之前笔者不知道这个方式,所以提供一个简单的实现:

 	long sendIntervalInNano = 100 * 1000;

		long totalExecuteTimeInSecond = 10;  //控制最长发多久
		long lastSendTime = System.nanoTime();
		long endTime = lastSendTime + totalExecuteTimeInSecond*1000*1000*1000;
  		long offsite = 0;  //补偿一些请求占用太长时间

		while (lastSendTime < endTime) {

			doThing();
			long interval = sendIntervalInNano - (System.nanoTime() - lastSendTime)
					+ offsite;
			if (interval > 0) {
				TimeUnit.NANOSECONDS.sleep(interval);
				offsite = 0;
			} else {
				offsite = interval;
			}

			lastSendTime = System.nanoTime();
		}

总结: 比较和JDK timer内部实现,核心的区别在于timer使用的是Object.wait来实现等待,而后者使用了sleep的方式。

具体区别:

(1)除了都可以被线程中断打断外,在等待的过程中,如果需要停止,前者可以优美的使用配套的Object.notify(Timer的cancel实现),而使用sleep方式就没有中断之外的方法了。(Timer使用它的一个因素,更重要的是为了保持线程安全,用了同步块,顺其自然用了同步块锁的wait方法)
换句网友的说法比较贴切: 区别在于”(wait)同时又“积极”地等待条件发生改变”。
(2)sleep不会释放任何锁资源,而wait会释放对象锁资源,让其他线程获取这个锁,以便通知它可以继续执行,而
(3)sleep写在任何地方都可以,但是wait不行,wait必须在synchronized锁的锁范围内书写。
(4)sleep来源thread方法或者timeunit,wait来源于所有object,因为所有对象都有锁以提供支持。

题外话:

ScheduledExecutorService在task耗时大于调度间隔时间时,并不会使用更多的线程来并发,所以使用这种方式来达到预期的TPS不见得生效,所以最好直接使用thread pool来算好间隔时间,直接提交,同时不固定线程数,来弹性增加或者减少,虽然有线程大多挂掉的风险,但是还是比较准确的。需要结合具体运行情况来调整。

在保持一定的TPS的基础上,我们如果需要海量的TPS压力,就不能仅仅使用单台机器多线程去完成,且不说操作系统对线程数有限制,CPU本身提供的并发能力也有限。所以在单台机器创建的TPS已经无法满足需求时,我们要协同多台机器去做。

这里提供一种方法是使用持续集成平台jenkin的multiconfigure job来完成。构建多配置项目,选择N台结点,这样构建时,会同时并发N台结点做任务,当然这种方式创建的TPS和预想的会有细微差距:虽然是同时启动N台机器上的任务去做,但是难免因为机器不同等原因导致并发时间有差距。但是即使自己去实现一套多机器协同系统,也很难实现的比jenkin更好。

 

001

002