首页 > 编程知识 正文

开源压测工具,源码检测工具

时间:2023-05-06 07:21:30 阅读:232829 作者:4781

序言

github地址:https://github.com/tsenart/vegeta

第一次写源码解析的博客,就拿自己最熟悉的压测工具vegeta(bldfn)来介绍。本篇文章只介绍vegeta的lib库,也就是vegeta核心的发压功能。

实现思路

首先看下lib库里面的文件目录。

.├── attack.go // 起压力├── attack_test.go├── histogram.go // 柱状图,用于结果统计├── histogram_test.go├── lttb├── metrics.go // 统计指标,进行结果处理├── metrics_test.go├── pacer.go // 定速器,用于控制发压速率├── pacer_test.go├── plot├── reporters.go // 产生报告├── results.go // 一次http请求后的结果├── results_easyjson.go├── results_test.go├── target.schema.json├── targets.go // 压测目标, 代表http请求├── targets_easyjson.go└── targets_test.go

http压测工具发压的过程就像是一次进攻一样。 这里用attack来表示发压的动作, 一次打击(hit)代表一次http请求。 打击的目标(target)代表http接口。 发压的qps叫做打击的速率(rate),用专门的定速器(pacer)来控制发压的qps。

部署环境

分析源码,首先要部署一个能看到源码的环境。平时开发使用vim, 这里就只演示在终端下的操作。

go get -u github.com/tsenart/vegeta

如果遇到timeout的情况,更新go版本到1.12及以上, 设置环境变量

export GOPROXY=https://goproxy.ioexport GO111MODULE=on

平常go的项目被我放在~/Workspace/golang/mod目录下,这里我就新建一个文件夹test. 并执行go mod init test初始化一个GO项目。

创建一个main.go的文件,并从github上复制粘贴示例代码,加上注释后:

package mainimport ( "fmt" "time" vegeta "github.com/tsenart/vegeta/lib")func main() { // 1. 压测时长&速率 rate := vegeta.Rate{Freq: 100, Per: time.Second} duration := 4 * time.Second // 2. 压测接口 targeter := vegeta.NewStaticTargeter(vegeta.Target{ Method: "GET", URL: "http://localhost:9100/", }) // 3. 启动压测并收集结果 var metrics vegeta.Metrics attacker := vegeta.NewAttacker() for res := range attacker.Attack(targeter, rate, duration, "Big Bang!") { metrics.Add(res) } // 4. 处理结果 metrics.Close() // 5. 打印感兴趣的指标 fmt.Printf("99th percentile: %sn", metrics.Latencies.P99)} 开始

程序执行的过程很简单, 看上面的代码就一目了然了。先定义压测时长和速率,说明压测哪个或哪些接口。 发起压力,并收集结果。发完压力后统计结果。然后输出感兴趣的指标。

一、定义压测时长和速率 // 1. 压测时长&速率 rate := vegeta.Rate{Freq: 100, Per: time.Second} duration := 4 * time.Second

这里定义了rate, 跳转到源码:

type Rate = ConstantPacer// 定义了一个恒定的定速装置type ConstantPacer struct { Freq int // Frequency (number of occurrences) per ... Per time.Duration // Time unit, usually 1s}

使用vim的Tagbar插件看到ConstantPacer这个结构体有哪些方法。
github.com/tsenart/vegeta/lib/pacer.go

Pace方法, 传入消逝的时间,和打击次数, 计算出要sleep多久后,开始下一次打击。
hit 和 sleep操作交替进行。(hit指发起一次进攻,这里是一次http请求)。从而控制发压的速率。

二、定义压测接口 // 2. 压测接口 targeter := vegeta.NewStaticTargeter(vegeta.Target{ Method: "GET", URL: "http://localhost:9100/", })

直接看NewStaticTargeter有点费劲,因为你不是道Target是用来干嘛的。所以看一下vegeta.Target的定义。

注释写的清清楚楚,target代表了一个http请求的样式。

Method: 请求方式, GET POST等URL:请求地址Body :请求体Header :请求头部, cookie放在这里

同样这里也看下Target有哪些方法:

Request方法,看方法签名就知道是用来生成http.Request的。
ps:http包抽象了http协议,这里的http.Request就是http请求报文的抽象。

Target其实是代表了一个http请求的样式,可以通过Request方法生成一个http.Request用于发请求。

接下来看vegeta.NewStaticTargeter方法,

// A Targeter decodes a Target or returns an error in case of failure.// Implementations must be safe for concurrent use.type Targeter func(*Target) error// NewStaticTargeter returns a Targeter which round-robins over the passed// Targets.func NewStaticTargeter(tgts ...Target) Targeter { i := int64(-1) return func(tgt *Target) error { if tgt == nil { return ErrNilTarget } *tgt = tgts[atomic.AddInt64(&i, 1)%int64(len(tgts))] return nil }}

NewStaticTargeter方法接受不定数量的Target对象。返回一个Targeter类型的参数。Targeter是形如 func(*Target) error的函数的代表。

NewStaticTargeter方法的实现使用了闭包。 让返回的Targeter仍然能够使用NewStaticTargeter方法传入的target的slice。 并且以Targeter方法定义的顺序返回。 这里闭包,包含了两个父函数的变量, 一个是tgts, 一个是i这个int64值。 返回的Targeter方法,每次轮询的从tgts中取一个target对象,赋予传进来的tgt参数。

这里不理解也没有关系。等后面用到的时候,就知道为什么会这么写了。

到这里,main.go定义了一个targeter变量, 通过这个变量,每次调用一次targeter就能获得一个targe对象。

ps 闭包:闭包是一种特殊的对象,由两部分组成,函数,以及创建该函数的环境。 闭包能够让我们仍然能够使用之前函数内部的变量。

三、启动压测 // 3. 启动压测并收集结果 var metrics vegeta.Metrics attacker := vegeta.NewAttacker() for res := range attacker.Attack(targeter, rate, duration, "Big Bang!") { metrics.Add(res) } // 4. 处理结果 metrics.Close()

启动压测前,定义了一个metrics变量,用于度量压测过程的相关信息。
查看vegeta.Metrics结构体的定义:

可以看到,压测过程中。会收集响应时延,响应数据总大小,请求数据总大小,结束时间,耗时,请求数量,请求速率,成功数量,状态码和错误信息。

新建收集指标的metrics后,就可以开始发压力了。

调用attacker的Attack方法,传入压测目标,定速装置,请求时长就可以得到一个请求结果的管道,这个请求结果,是每次hit的结果。 每次hit把相关指标收集后,放到管道里。然后由metrics变量收集(metrics.Add(res)). 并处理(metrics.Close())。
Attack方法的签名如下:

attacker.Attack方法是vegeta的核心发压逻辑。在介绍这个方法之前,先简单的梳理下思路。
为了能在短时间内发出很高的压力,单线程的方式肯定不行, 发压算法需要有以下几个功能:

多线程,动态调整goroutine数量
假定发送100qps的压力,1s内就要同时有100个goroutine去发送请求,并且是同时的。每个请求都由一个goroutine发出。如果100qps起100个goroutine的话,第一秒的请求肯定能达到100qps, 但是下一秒呢? 万一有个请求因为网络拥堵,或者接口本身耗时就超过1s, 第二秒可能就达不到100qps了。所以不能简单根据qps来定起多少goroutine, 发压算法本身需要支持动态调整goroutine的数量来保证恒定的发压速率。异步的结果处理
假设在1s内,一个gouroutine发送完了请求,并记录下了这个hit过程中的相关指标。不能马上去统计,否则发压过程中有多余的计算,而且,也需要同步各个goroutine的结果计算。这里可以利用golang的channel,将结果送到管道里,由另外的一个goroutine去做专门的统计。压测结果串行计算就可以了。随时可以停止
发压的过程有很多不可控的因素,需要能马上停止发压逻辑。这里用select语法结合channel就可以做到。

知道要实现的功能了,接下来就可以看看Attack方法的源码了。 然后根据功能来一一对应源码。
相关代码是:

var wg sync.WaitGroup workers := a.workers if workers > a.maxWorkers { workers = a.maxWorkers }

代码一开头,就先定义了WaitGroup来同步发压的goroutine。 10s的发压时间,并不是说10s内一定能发完压力并获得到响应结果。10s之后不会产生新的hit, 但是要保证10s内已经发起hit的goroutine能顺顺利利的完成自己的任务。不能10s一到就强行结束了。这里还限制了maxWorkers,防止goroutine过多一直在争抢资源。

for i := uint64(0); i < workers; i++ { wg.Add(1) go a.attack(tr, name, &wg, ticks, results) }

上面这个for循环就没有什么好讲的了。WaitGroup的用法。
下面来看这个select的用法:

select { case ticks <- struct{}{}: count++ continue case <-a.stopch: return default: // all workers are blocked. start one more and try again workers++ wg.Add(1) go a.attack(tr, name, &wg, ticks, results) }

select监听了2个管道,看名字就知道它的作用了。一个是ticks, 这个是用来同步发压的goroutine, 稍后讲解。另一个是-a.stopch, 这个是停止发压的管道。
select 的特点是,监听所有管道,如果有IO操作则执行相应case逻辑,如果没有执行default逻辑。 这里的意思就是如果没有发压信号,也没有停止的信号,则说明所有worker都在忙碌,这个时候需要新增worker,否则下一次tick信号来了,就没有worker去hit了。这里就实现了动态增加goroutine数量。

很多个goroutine如何保证恒定速率发压力呢?time包里有个Tick方法,它会返回一个管道,并每过一段时间往管道塞入一个值。这里vegeta的作者模仿了这个逻辑:

首先定义一个ticks的管道,注意长度为0. 在for循环外获取当前时间, 当进入循环后,获得从开始发压到现在过了多久,将这个时间和当前是第几次发压传给定速装置,定速装置计算出下次要等待的时间, 和一个是否要关闭的标志stop。 这个stop和a.stopch并不一样,这个stop表示已经发完最后一个hit了,接下来程序要结束了。 stopch是用户用来停止程序的。 获得到等待时间后,就 time.Sleep(wait)。在select中,如果发现ticks没有阻塞,就往里面塞值,发压的所有的goroutine都在消费ticks里的信号,ticks一旦有数据,就会有一个goroutine来消费这个信号,去发请求。当所有goroutine都忙的时候,就根据goroutine的数量来决定是否新增goroutine。

这个模型和公司运营模式一样,老板来分配任务,每个员工争相来处理。当员工数量不够了,老板再去聘请新的员工。老板的预算有限,所以不能无休止的聘请新员工。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。