序言

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

第一次写源码解析的博客,就拿自己最熟悉的压测工具vegeta(贝吉塔)来介绍。本篇文章只介绍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.io
export 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.Metricsattacker := vegeta.NewAttacker()for res := range attacker.Attack(targeter, rate, duration, "Big Bang!") {metrics.Add(res)}// 4. 处理结果metrics.Close()// 5. 打印感兴趣的指标fmt.Printf("99th percentile: %s\n", 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.Metricsattacker := 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的核心发压逻辑。在介绍这个方法之前,先简单的梳理下思路。
为了能在短时间内发出很高的压力,单线程的方式肯定不行, 发压算法需要有以下几个功能:

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

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

 var wg sync.WaitGroupworkers := a.workersif 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++continuecase <-a.stopch:returndefault:// all workers are blocked. start one more and try againworkers++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。

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

【源码解析】压测工具vegeta相关推荐

  1. golang 压测工具vegeta改造-支持自定义压测任务

    背景 之前在公司做压测工作的时候,使用了web压测工具vegeta.后续又接到过dns的性能压测.redis的性能压测等任务.http的压测工具vegeta并不能满足需求了.于是模仿vegeta的li ...

  2. Go语言使用之JSO使用、源码解析和JSON工具类

    在go语言网络编程中,经常会有这样的需求:保存结构体和读取结构体数据.如果你使用redis数据库存储数据,你怎么做?Redis仅支持五种数据类型( String(字符串) .Hash (哈希).Lis ...

  3. golang tollbooth 中间件 压测工具 vegeta

    参考: 1.[译] Go 中基于 IP 地址的 HTTP 限流 2.Tollbooth - Fasthttp integration layer 3.didip/tollbooth Simple mi ...

  4. 网站压测工具 Webbench 源码分析

    介绍 Webbench是一个在Linux下使用的非常简单的网站压测工具.它的源代码只有500多行,挺值得一看的开源项目. 实现原理 只是简单的fork()出多个子进程模拟客户端去访问设定的URL,测试 ...

  5. 多线程与高并发(九):单机压测工具JMH,单机最快MQ - Disruptor原理解析

    单机压测工具JMH JMH Java准测试工具套件 什么是JMH 官网 http://openjdk.java.net/projects/code-tools/jmh/ 创建JMH测试 1.创建Mav ...

  6. Python|excel表格数据一键转json格式小工具|支持xlsx、xls格式转json|【源码+解析】

    背景    最近在使用JavaScript编写一些浏览器RPA脚本,脚本使用过程中遇到一些问题,脚本使用的数据往往存放在excel表,但运行时只能读取json数据,导致频繁人工excel转json,效 ...

  7. 免Root 实现App加载Xposed插件的工具Xpatch源码解析(一)

    前言 Xpatch是一款免Root实现App加载Xposed插件的工具,可以非常方便地实现App的逆向破解(再也不用改smali代码了),源码也已经上传到Github上,欢迎各位Fork and St ...

  8. Nginx HLS压测工具之vegeta

    HLS压测工具之vegeta 1. MAC安装 brew update && brew install vegeta 2. 构造target.txt 创建target.txt文件,内容 ...

  9. shiro反序列化工具_Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)源码解析

    Apache Shiro Apache Shiro是一个功能强大且灵活的开源安全框架,主要功能包括用户认证.授权.会话管理以及加密.在了解该漏洞之前,建议学习下Apache Shiro是怎么使用. d ...

最新文章

  1. 【Python小游戏】扫雷游戏竟有世界排行榜,中国90后00后霸占半壁江山?
  2. 老娘不就是没化妆吗?你几个意思?
  3. codeforces438 D. The Child and Sequence
  4. 最实用的Git命令总结:新建本地分支、远程分支、关联和取消关联分支、清除本地和远程分支、合并分支、版本还原、tag命令、中文乱码解决方案、如何fork一个分支和修改后发起合并请求
  5. 【网络信息安全】密码学入门笔记
  6. 160505、oracle 修改字符集 修改为ZHS16GBK
  7. Java I/O(输入输出流)
  8. 修改箱线图的横坐标顺序
  9. 数学建模与数学实验3.4习题1
  10. 使用Tor绕过防火墙进行远程匿名访问
  11. 深入浅出node.js第9章玩转进程摘录
  12. 华三路由交换配置命令_华三华为交换机路由器配置常用命令
  13. 开源机器学习平台tipdm
  14. 每天睡6小时和8小时的区别 看完再不敢熬夜了
  15. 移动硬盘突然在电脑上无法显示
  16. Aspose.3D使用教程:使用 Java 将 FBX 转换为 RVM 或 RVM 转换为 FBX 文件
  17. C/C++面试高频知识点八股文
  18. 项目管理-5大过程组-10大知识领域-47过程
  19. 递归学习_组合_全组合排列
  20. 超好看的导航页面(静态页面)

热门文章

  1. 【科大讯飞】全球首款,Mobius莫比斯同声翻译耳机 ,AI智能运动耳机 ,支持英日法韩俄西班牙6种语音...
  2. 菜鸟学院~2020年谁在挑战云上“霸权”?
  3. PostCSS及其常用插件介绍
  4. LAMP安装明细(apache,mysql,php)
  5. 好看的皮囊千篇一律,内涵的“可视化大屏”万里挑一
  6. java代码审计入门--01
  7. 小白联通300M流量领取工具
  8. 远程桌面连接报错(CredSSP加密数据库修正)解决方案
  9. iframe跨域的几种常用方法
  10. 【shell】shuf命令提取文件的随机行