使用erlang实现简单的http性能测试工具,替代loadrunner、loadspace

使用loadrunner、loadspace等,首先是付费的,其次对机器的要求较高。loadspace的并非能力本身不怎样。

如果你需要在现网测试,为了避免网络影响,想找一个离业务服务器较的服务器运行测试,安装loadrunner、loadspace也很麻烦,特别是在linux、unix操作系统中。

所以使用erlang来实现,基于gen_server。

基本常见的所有操作系统都可以运行erlang,编译安装也很简单,如果使用ssl,就需要你的机器上有openssl的lib

从www.erlang.org下载一个最新版本,在linux、unix上编译,编译很简单,windows下一般不用编译。


./configure --prefix=你的路径 --with-ssl

make & make install


下面代码不多总共不到300行,我实测时,进程数使用过5000都没有问题,不过服务端受不了了:)。测试时服务器能达到8000多并发,所以这个客户端的并发、性能还是比较强的,不过要看运行的服务器、网络情况、服务端的能力几个方面,性能测试是暴露问题,并不能解决问题。


将下面的代码拷贝保存到hpt.erl中,然后在erlang的bin目录下运行erl,进入erlang命令行

在命令行中输入

cd("hpt.erl所在目录"). %% 切换目录

c(hpt, [load, {outdir, "./"}, native, {hipe, [o3]}]). %% 编译

hpt:start("测试url","参数...\"test~$count28.10.0B\"...\"~$time32.10.0B\"...", "成功结果中需包含的字符串\"resultcode\":0").

%%设置测试url及参数,支持两个可变的参数,~$count是当前发送的个数,~$time是当前的时间戳,其他的格式化请参考erlang的io:format

hpt:post(300, 10000). %% 启动一次测试,并指定模拟客户端数量、每个客户端发送次数,也可以使用get方法

hpt:stop(). %% 终止测试

测试结果会5秒中输出一次,同时会记录到当前路径的log文件中,使用简单的查找替换,将这些数字用tab键分隔,就可以用excel打开,利用excel的功能输出图形化的测试报告。


start、post、get还有更多可选参数,请参照代码,

不懂也没关系,找到这几个函数,看参数列表也能看出来:)

======================================================================================================

-module(hpt).
-author('[email protected]').
-vsn('1.0').
-behaviour(gen_server).

-export([
    init/1,
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    code_change/3,
    terminate/2
]).

-export([
    start/5,
    start/4,
    start/3,
    start/2,
    get/3,
    get/2,
    post/3,
    post/2,
    stop/0
]).

-record(state, {
    url,                     %% request url
    para,                    %% in post is request body, and in get is parameters after url
    logFile,                 %% log file name, default is "./log"
    log,                     %% log file handler
    search,                  %% confirm whether the response is ok or not
    testers = array:new(),   %% all sub processes that send request
    report = 5000,           %% interval of report, default is 5 seconds
    printTime = 0,           %% time when print fore-time
    start,                   %% time when start to send request
    min = 1000000000000,     %% minimal interval of request
    max = 0,                 %% maximal interval of request
    interval = 0,            %% sum(interval of each request)
    curNum = 0,              %% number of request which in report interval
    total = 0,               %% number of all request
    curFailed = 0,           %% number of request which failed in report interval
    failed = 0               %% number of all failed request
}).    

start(Url, Para) -> start(Url, Para, null).
start(Url, Para, Search) ->    start(Url, Para, Search, 5000).
start(Url, Para, Search, ReportInterval) ->    start(Url, Para, Search, ReportInterval, "./log").
start(Url, Para, Search, ReportInterval, LogFile) ->
    State = #state{url=Url, para=Para, search=Search, logFile=LogFile, report=ReportInterval},
    gen_server:start_link({local, ?MODULE}, ?MODULE, State, [])
.

stop() ->
    gen_server:cast(?MODULE, stop)
.

post(ClientNum, TryTimes) -> post(ClientNum, TryTimes, 10000).
post(ClientNum, TryTimes, Timeout) ->
    gen_server:cast(?MODULE, {post, ClientNum, TryTimes, Timeout})
.

get(ClientNum, TryTimes) -> get(ClientNum, TryTimes, 10000).
get(ClientNum, TryTimes, Timeout) ->
    gen_server:cast(?MODULE, {get, ClientNum, TryTimes, Timeout})
.
%%-----------------------------------------------------------------------------
init(State) ->
    inets:start(),
    case file:open(State#state.logFile, [write, binary, raw, append, delayed_write]) of
        {ok, FP} ->
            Now = calcTime(),
            {ok, State#state{log=FP, start=Now, printTime=Now}};
            
        {error, Reason} ->
            io:format("Fail to open log file,~p ~n", [Reason]),
            {stop, {error, Reason}}
    end
.

handle_cast({report, -1, _IsOK, Result}, State=#state{printTime=PrintTime}) ->
    Now = calcTime(),
    printState(State, Now - PrintTime, Now, Result),
    {noreply, State}
;
handle_cast({report, Interval, IsOK, Result}, State=#state{printTime=PrintTime}) ->
    Max = if Interval > State#state.max -> Interval; true -> State#state.max end,
    Min = if Interval < State#state.min -> Interval; true -> State#state.min end,
    TotalInterval = State#state.interval + Interval,
    Total = State#state.total + 1,
    CurNum = State#state.curNum + 1,

    Failed = State#state.failed + (1 - IsOK),
    CurFailed = State#state.curFailed + (1 - IsOK),
    
    Now = calcTime(),
    Time = Now - PrintTime,
    
    {PrintTime1, CurNum1, CurFailed1} =
    if
        Time > State#state.report ->
            printState(State, Time, Now, Result),
            {Now, 0, 0};
            
        true ->
            {PrintTime, CurNum, CurFailed}
    end,
    
    State1 = State#state{
                            max = Max,
                            min = Min,
                            total = Total,
                            curNum = CurNum1,
                            printTime = PrintTime1,
                            interval = TotalInterval,
                            failed = Failed,
                            curFailed = CurFailed1
                        },
    
    {noreply, State1}
;
handle_cast({post, ClientNum, TryTimes, Timeout}, State) ->
    io:format("post:start ~p clients and each try ~p times~n", [ClientNum, TryTimes]),
    State1 = resetState(State),
    Testers1 = postClient(ClientNum, TryTimes, State1, Timeout, array:new()),
    {noreply, State1#state{testers=Testers1}}
;
handle_cast({get, ClientNum, TryTimes, Timeout}, State) ->
    io:format("get:start ~p clients and each try ~p times~n", [ClientNum, TryTimes]),
    State1 = resetState(State),
    Testers1 = getClient(ClientNum, TryTimes, State, Timeout, array:new()),
    {noreply, State1#state{testers=Testers1}}
;
handle_cast(stop, State=#state{testers=Testers}) ->
    io:format("stop all clients~n"),
    array:map(fun(_I, Pid) -> exit(Pid, normal) end, Testers),
    {stop, normal, State}
;
handle_cast(_Request, State) ->
    {noreply, State}
.

handle_call(_Request, _From, State) ->
    {noreply, State}
.

handle_info(_Info, State) ->
    {noreply, State}
.
code_change(_OldVsn, State, _Extra) ->
    {ok, State}
.
terminate(_Reason, #state{log=FP}) ->
    file:close(FP),
    ok
.
%%-----------------------------------------------------------------------------
postClient(0, _TryTimes, _State, _Timeout, Testers) ->
    io:format("post started~n"),
    Testers
;
postClient(ClientNum, TryTimes, State, Timeout, Testers) ->
    Pid = spawn_link(fun()->
                   #state{url=Url, para=Para, search=Search} = State,
                   put(start, ClientNum * TryTimes),
                   sendPost(TryTimes, Url, Para, Timeout, Search)
               end),
    postClient(ClientNum - 1, TryTimes, State, Timeout, array:set(ClientNum - 1, Pid, Testers))
.

sendPost(0, _Url, _Para, _Timeout, _Search) ->
    gen_server:cast(?MODULE, {report, -1, 1, ""})
;
sendPost(TryTimes, Url, Para, Timeout, Search) ->
    Now = calcTime(),
    Para1 = formatPara(Para, Now, get(start) + TryTimes, [], []),
    Result = httpc:request(post,
                           {Url, [], "text/plain", Para1},
                           [{timeout, Timeout}],
                           []),
    reportResult(Result, Search, Now),
    sendPost(TryTimes - 1, Url, Para, Timeout, Search)
.

getClient(0, _TryTimes, _State, _Timeout, Testers) ->
    io:format("get started~n"),
    Testers
;
getClient(ClientNum, TryTimes, State, Timeout, Testers) ->
    Pid = spawn_link(fun()->
                   #state{url=Url, para=Para, search=Search} = State,
                   put(start, ClientNum * TryTimes),
                   sendGet(TryTimes, Url, Para, Timeout, Search)
               end),
    getClient(ClientNum - 1, TryTimes, State, Timeout, array:set(ClientNum - 1, Pid, Testers))
.

sendGet(0, _Url, _Para, _Timeout, _Search) ->
    gen_server:cast(?MODULE, {report, -1, 1, ""})
;
sendGet(TryTimes, Url, Para, Timeout, Search) ->
    Now = calcTime(),
    Para1 = formatPara(Para, Now, get(start) + TryTimes, [], []),
    Result = httpc:request(get,
                           {Url++Para1, [{"content-type","application/json;charset=utf-8"}]},
                           [{timeout, Timeout}],
                           []),
    reportResult(Result, Search, Now),
    sendGet(TryTimes - 1, Url, Para, Timeout, Search)
.

reportResult(Result, Search, Start) ->
    Interval = calcTime() - Start,
    case Result of
        {ok, {{_, 200, _},_, Body}}  ->
            Pos = if Search /= null -> string:str(Body, Search); true -> 1 end,
            if
                Pos =:= 0 ->
                    gen_server:cast(?MODULE, {report, Interval, 0, Body});
                    
                true ->
                    gen_server:cast(?MODULE, {report, Interval, 1, Body})
            end;
            
        _Error ->
            gen_server:cast(?MODULE, {report, Interval, 0, Result})
    end
.

formatPara([], _Now, _Count, Para, List) ->
    S = lists:flatten(io_lib:format(lists:flatten(lists:reverse(Para)), lists:reverse(List))),
    lists:flatten(S)
;
formatPara("$time"++Para, Now, Count, NewPara, List) ->
    formatPara(Para, Now, Count, NewPara, [Now|List])
;
formatPara("$count"++Para, Now, Count, NewPara, List) ->
    formatPara(Para, Now, Count, NewPara, [Count|List])
;
formatPara([C|Para], Now, Count, NewPara, List) ->
    formatPara(Para, Now, Count, [C|NewPara], List)
.

printState(State, 0, Now, Result) -> printState(State, 1, Now, Result);
printState(State, Interval, Now, Result) ->
    TotalInterval = if Now - State#state.start > 0 -> Now - State#state.start; true -> 1 end,
    Total = if State#state.total > 0 -> State#state.total; true -> 1 end,
    Str = io_lib:format("Speed[Cur:~p,Avg:~p],Total:~p,Fail[Cur:~p,All:~p],Interval[Max:~p,Min:~p,Avg:~p],Time:~p~n",
                         [
                            (State#state.curNum * 1000) div Interval,
                            (State#state.total * 1000) div TotalInterval,
                            State#state.total,
                            State#state.curFailed,
                            State#state.failed,
                            State#state.max,
                            State#state.min,
                            State#state.interval div Total,
                            Interval
                        ]
                       ),
    file:write(State#state.log, list_to_binary(Str)),
    io:format(Str),
    Str1 = io_lib:format("Result:~p~n", [Result]),
    file:write(State#state.log, list_to_binary(Str1))
.

resetState(State=#state{testers=Testers}) ->
    array:map(fun(_I, Pid) -> exit(Pid, normal) end, Testers),
    
    Now = calcTime(),
    State#state{
                start = Now,
                min = 1000000000000,
                max = 0,
                interval = 0,
                curNum = 0,
                total = 0,
                curFailed = 0,
                failed = 0
    }
.

calcTime() ->
    {Ms, S, MMs} = now(),
    (Ms * 1000000 + S) * 1000 + (MMs div 1000)
.

%% c(hpt, [load, {outdir, "./"}, native, {hipe, [o3]}]).
%% hpt:start("http://192.168.9.145:8080/xxxx/SingleMsgServlet","{\"nsp_ProviderID\":\"1\",\"deviceToken\":\"test~$count12.10.0B0000000001000001\",\"message\":\"~$time32.10.0B\",\"priority\":1,\"cacheMode\":1,\"msgType\":1}", "\"resultcode\":0").
%% hpt:start("http://192.168.9.149:8080/xxx/test","?n=100", "\"resultcode\":0").
%% hpt:start("http://192.168.9.149:8080/xxx/SingleMsgServlet","{\"nsp_ProviderID\":\"1\",\"requestID\":\"1001_1349770334054\",\"deviceToken\":\"test~$count12.10.0B0000001001000001\",\"message\":\"caimessi\",\"priority\":1,\"cacheMode\":1,\"msgType\":1}", "\"resultcode\":\"0\"").
%% hpt:start("http://localhost:5224/PushCRS/AcceptDataServlet","{\"nsp_ProviderID\":\"1\",\"nsp_key\":\"DAECF160C8BEC4AFC3EBFE9E2FE0724F88AD9CA47829F54A2951718C14E1C89D\",\"deviceToken\":\"1a1a1a1a1a1a1a1a0000000000000000\",\"message\":\"caimessi\",\"priority\":1,\"cacheMode\":1}", "\"resultcode\":0").
%% hpt:post(30, 1000).
%% hpt:stop().


你可能感兴趣的:(erlang,erlang,tools,工具,性能,测试)