locust4j 1.0.0 发布

不知不觉,维护 boomer 已经两年了。说来也奇怪,接到的性能测试需求,一般都是私有协议的,很难直接用 jmeter 这类通用的工具,一般只能基于 boomer 自己写一个。前不久需要压测 Flume 的 Avro Source,这货偏偏没有一个 Go 的客户端!

由于时间紧,只好用 Java 写一个简单版的命令行工具。事后研究一下 Avro 的 ipc 协议,写了一个玩票性质的 avroipc。最近,索性又写了一个 Java 版的 boomer,即 locust4j。灵感来自一位印度哥们写的 nomadacris,得到他的同意,直接就用了他写的 msgpack 相关序列化和反序列化的代码。实现上,基本上是从 boomer 翻译过来的,除了用线程替代 Goroutine,用 ConcurrentLinkedQueue 替代 channel。

So,现在也可以用 Java 来写 locust 的 slave 部分了,即用 Java 来写压测逻辑。无论是 Go 还是 Java,性能方面都能碾压原生的 Python 实现。boomer 和 locust4j,都是从队列中取出用户侧上报的测试结果,统计汇总后再定时上报的。从测试情况来看,locust4j 统计的性能比 boomer 还要厉害。空跑一个死循环的情况下,在我的 MBP 上可以跑到 200w 的 RPS,boomer 只能到 50w。但很少单实例能压到这么高的 RPS,大部分情况下,CPU 和内存都是实际的压测代码在使用。一般压测服务端,Go 标准库用起来更爽一点,如果是 Java,还要去找一个能利用非阻塞 IO 的客户端,不然线程都阻塞在 IO 操作上了,也快不起来。

目前 locust 的 1.0.0 版本已经发布到 maven 的中心库,用 maven 的项目,声明一下即可使用。

<dependency>
    <groupId>com.github.myzhan</groupId>
    <artifactId>locust4j</artifactId>
    <version>1.0.0</version>
</dependency>

如果不用 maven,可以在 release 页面 下一个预先编译好的 jar 包。

在代码库中,有一个用于测试的小程序,里面包含了所有 locust4j 提供的 API。

由于 JVM 中 JIT 和 GC 的存在,locust4j 收集到的响应时间,有可能是错的,用的时候需要注意。JIT 会导致响应时间逐渐变短,GC 会导致响应时间偶尔变长。实际的结果,需要参考被测程序的反应。

Enjoy!

用 golang 来编写压测工具

之前在项目中做服务端性能测试,一般用的都是 LoadRunner,或者 jmeter。随着接触的项目越来越多,各种各样的代码都有,光传输协议,就有 HTTP,TCP,UDP。有时数据传的不是明文,可能是 protobuf,也可能是 RSA 加密后的密文。继续用 LR 或 jmeter,有点难以招架。

于是,我开始关注 Locust 这个压测工具。与 LR 使用的 C 和 jmeter 使用的 Java 相比,Locust 使用 Python 来编写压测代码,代码量会少很多。且 Locust 本身的概念少,学习成本较低,上手很快。

在实现并发方面,Locust 并没有使用进程、线程来实现,事实上,CPython 的实现也对多线程不友好。于是,Locust 使用 gevent 提供的非阻塞 IO,和 coroutine来实现网络层的并发请求。实际使用中,使用 Locust 编写的压测脚本,单个实例,可以提供 1000 rps 左右的压力,如果还需要更多的压力,挂多几个 slave,也能满足需求。不过我生性爱折腾,一直想着摆脱 CPython 的 GIL,和 gevent 的 monkey_patch(),过年在家无聊,就一直在构思着怎样实现一个性能更好的施压端,用golang 提供的 goroutine,应该是个不错的选择。

Locust 已经实现了 master & slave 的架构,一个 Locust 进程如果运行在 master 模式,那它要做的事情,就只是收集各个 slave 汇报上来的信息,并展示在 web界面上而已。我的想法很简单,用 golang 来实现 slave 这一部分。实现为一个框架,在运行时,按需创建 goroutine 来跑调用方提供的 function,然后定时将信息汇报给 Locust 的 master。

过年回来撸了一周,最后弄了一个 boomer ,已开源。参考 Locust 的 slave 部分代码来实现的,连文件的命名都一致,应该很容易理解。与 Locust 最大的区别,就是用 goroutine,取代了 gevent。golang 本身的性能,也是一个优势。

用 boomer 来编写一个简单的 http 压测例子。

package main

import boomer "github.com/myzhan/boomer"

import (
	"time"
	"net/http"
	"log"
)


func now() int64 {
	return time.Now().UnixNano() / int64(time.Millisecond)
}


func test_http() {
	/*
	一个常规的 HTTP GET 操作,实际使用时,这里放业务自身的处理过程
	只要将处理结果,通过 boomer 暴露出来的函数汇报就行了
	请求成功,类似 Locust 的 events.request_success.fire
	boomer.Events.Publish("request_success", type, name, 处理时间, 响应耗时)
	请求失败,类似 Locust 的 events.request_failure.fire
	boomer.Events.Publish("request_failure", type, name, 处理时间, 错误信息)
	 */
	startTime := now()
	resp, err := http.Get("http://localhost:8080/")
	defer resp.Body.Close()
	endTime := now()
	log.Println(float64(endTime - startTime))
	if err != nil {
		boomer.Events.Publish("request_failure", "demo", "http", 0.0, err.Error())
	}else {
		boomer.Events.Publish("request_success", "demo", "http", float64(endTime - startTime), resp.ContentLength)
	}
}


func main() {

	task := &boomer.Task{
        // Weight 权重,和 Locust 的 task 权重类似,在有多个 task 的时候生效
				// FIXED: 之前误写为Weith
				Weight: 10,
        // Fn 类似于 Locust 的 task
				Fn: test_http,
	}

	/*
	通知 boomer 去执行自定义函数,支持多个
	boomer.Run(task1, task2, task3)
	*/

	boomer.Run(task)

}

运行

# dummy.py 可以在 boomer 代码库里找到,由于 master 不再负责实际的业务逻辑
# 所以 dummy.py 的格式,只要符合 Locust 脚本的规范就行了
locust -f dummy.py --master --master-bind-host=127.0.0.1 --master-bind-port=5557
go run main.go --master-host=127.0.0.1 --master-port=5557

boomer 目前比较完整地实现了 Locust slave 端的功能,在实际使用中,较原生的 Python 实现,有 5-10 倍以上的性能提升。

Locust 官方表示,master 和 slave 间通讯,用 zeromq 会有较大的性能提升,但是我目前并没有感觉到明显的性能差异,可能是我挂的 slave 数量还不够多吧。目前 boomer 还不支持zeromq,如果将来这里确实是个瓶颈,实现起来也很快。

Enjoy!

jenkins源码阅读:渲染主页

jenkins 的一切 URL 请求,都托管给了 Stapler,要了解主页是如何渲染的,依然要从 Stapler 开始。用现在 Web 开发的目光来看 Stapler,它是一个纯粹的路由组件,即根据用户访问的 URL ,找到对应的处理代码。不过对于 Stapler 来说,“约定”优于“配置”,除了在 web.xml 中将 /* 交给 Stapler 处理,再也没有任何配置。免“配置”,对于开发来说,省了很多麻烦,只要在文档中详细说明“约定”,大家通读一遍文档,也就知道了。

Stapler 的文档里面记录了很多细节,与主页(Index)有关的,有“Index View”和“Index Action Method”这两个。不过 $JENKINS_SRC/core/src/main/resources/jenkins/model/Jenkins 目录下面,并没有 index.jelly 文件,jenkins.model.Jenkins 类里面,也没有 doIndex 方法。

Google 了一下,找到了一份PPT一篇文章,原来 jenkins.model.Jenkins 实现了 StaplerFallback 接口。Stapler 实在找不到处理方法的时候,会调用 getStaplerFallback 方法,jenkins.model.Jenkins 就是靠这个方法,实现了主页的渲染。

getStaplerFallback 方法返回了一个 hudson.model.View 对象,Stapler 会根据这个对象,再去找对应的模板,这是最后的方法了,如果还是找不到,就会返回 404。终于,在 $JENKINS_SRC/core/src/main/resources/hudson/model/View 目录下,找到了 index.jelly 文件,主页的模板!

来试着“劫持”一下 jenkins 的主页。修改 Jenkins.java 文件,加上下面的方法。

$JENKINS_SRC/core/src/main/java/jenkins/model/Jenkins.java

public void doIndex( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
    rsp.getWriter().println("Index hijack");
}

用浏览器访问 http://localhost:8080/ ,成功看到了“Index hijack”!

继续翻看 Stapler 的源码,找到了比文档更清楚的答案。

/org/kohsuke/stapler/Stapler.java

boolean tryInvoke(RequestImpl req, ResponseImpl rsp, Object node ) throws IOException, ServletException {
    //省略很多代码
    if(node instanceof StaplerFallback) {
        if(traceable())
            traceEval(req,rsp,node,"((StaplerFallback)",").getStaplerFallback()");
        Object n;
        try {
            n = ((StaplerFallback)node).getStaplerFallback();
        } catch (RuntimeException e) {
            if (Function.renderResponse(req,rsp,node,e))
                return true; // let the exception serve the request and we are done
            else
                throw e;     // unprocessed exception
        }
        if(n!=node && n!=null) {
            // delegate to the fallback object
            invoke(req,rsp,n);
            return true;
        }
   }
}