こんにちは。CTOの馬場です。
HTTP負荷テストツール Tsung の話の第2回です。
今回は実際にガツガツ使ったうえで遭遇した課題と解決策を紹介します。 特にフォームまわりは大変で、 Tsungは "受け取ったフォームをSubmitする" ことが できない ので、 擬似的に実現するためにいろいろと工夫する必要があります。
なおこのエントリはTsung 1.6.0ベースで書いています。
N回繰り返す
for
か foreach
か repeat
で実現できます。
シナリオ作成時に回数が決まっているものは for
、
取得したHTML内の全imgタグを取得するような回数が動的に決定するものは foreach
、
ジョブをキューイングし結果が画面に反映されるまでポーリングするような特定条件を満たすまで繰り返すものは repeat
を使うのがオススメです。
リダイレクトをフォローする
会員制サイトのログイン・ログアウトなど、 リダイレクトを利用するシーンは多々あります。
リダイレクトをフォロー(追跡)するためには、以下の段取りが必要です。
- リクエストをサーバに送信する
- サーバからリダイレクトレスポンスを受け取る
- リダイレクトレスポンスのLocationヘッダを読み取る
- Locationヘッダで指示されたURLにリクエストを送信する
1.はふつうの処理なのでよいのですが、 2と3は少し工夫が必要です。
この話、実はFAQに書いてあります。 せっかくなのでFAQのコードをもとに説明します。
10.5. Can I dynamically follow redirect with HTTP?
<request>
<dyn_variable name="redirect" re="Location: (http://.*)\r"/>
<http url="index.html" method="GET" ></http>
</request>
<request subst="true">
<http url="%%_redirect%%" method="GET"></http>
</request>
- 1つ目の
request
dyn_variable
でredirect
という名前の変数に値を格納しますre
属性を使い正規表現で値を抽出しますdyn_variable
を除いたぶん (=http
) が1つ目のリクエスト自体の設定です- HTTPで
GET index.html
というリクエストを出す設定になっています
- HTTPで
- 2つ目の
request
request
タグのsubst
属性をtrue
にすることで、リクエスト内での変数利用を有効にしますhttp
タグのurl
属性で、1つ目のrequest
で取得したredirect
という名前の変数を展開します- ※Tsungでは
%%_変数名%%
で変数展開します %%
も_
も必須です- ややこしい
- ※Tsungでは
実際に使うときは1つ目の request
できちんと
redirect
に値が入らないことがあるので、
if や match
でエラー処理しましょう。
6.7.4. Checking the server's response
この例では re
属性を使って正規表現で文字列を特定しています。
正規表現の他にXPathやJSONPathを使うこともできます。
csrf_token
や authenticity_token
を読み込んで次のPOSTで使う場合は大抵XPathを使います。
最近はAPIバックエンドなどJSONがインターフェイスになるシステムが多いので、 JSONPathも意外とよく使います(使いました)。
ID・パスワードを別ファイルから読む
これはドキュメントにありますが、 CSVファイルを読み込んで変数として利用することができます。
こちらもドキュメントのコードをもとに解説します。
<setdynvars sourcetype="file" fileid="userlist.csv" delimiter=";" order="iter">
<var name="username" />
<var name="user_password" />
</setdynvars>
<request subst="true">
<http url='/login.cgi' version='1.0'
contents='username=%%_username%%&password=%%_user_password%%&op=login'
content_type='application/x-www-form-urlencoded' method='POST'>
</http>
</request>
setdynvars
userlist.csv
というファイルで、デリミタは;
と設定しています- ファイルの1列目の値が
username
、2列目の値がuser_password
に設定されます - 順番ではなくランダムに利用する場合は
order
属性をrandom
にします
request
request
タグのsubst
属性をtrue
にすることで、リクエスト内での変数利用を有効にしますhttp
タグのmethod
属性にPOST
を設定します- POSTするときは
content_type
を忘れずに指定しましょう contents
属性で実際に送信するデータを設定します- 変数指定のときに、変数名の先頭の
_
を忘れずに contents
は&
区切りで複数キー・値を設定できるものですが、XML内に記載するため&
は&
と書く必要があります- 控え目に言ってめんどくさいです
- というわけで
contents
に設定する値はHTML encodeしたうえでXML用にエスケープしておく必要があります - 変数展開(置換)が実行されるのはXMLを読み取った後なので、1つの変数で複数キーをむりくり設定したい場合は
&
はXMLエスケープしません
- POSTするときは
XMLがSyntax Error になる
いろいろやった結果XMLが壊れてしまうことが稀によくあります。
そんなときは xmllint
を使うとエラー箇所をわかりやすく教えてくれます。
$ xmllint example.xml
example.xml:5: parser error : Opening and ending tag mismatch: tsung line 3 and clients
^
example.xml:6: parser error : Extra content at the end of the document
^
なお xmllint
ではXPathを試すこともできます。
試行錯誤の段階で重宝するのでぜひご活用ください。
ちなみに Google Chrome のDeveloper Tool
で特定したいElementsを選択して右クリックすると
「Copy XPath」
というメニューがあるので、
特定の要素を取得するためのXPathを書く参考になります。
cat example.xml | xmllint --xpath "//form//input" --html - 2>/dev/null
ファイルアップロード
画像などのバイナリファイルアップロードするのはちょっと手間です。
Tsung自体には機能がないようなので、 独自モジュールを作成します。
今回は hb
という名前のモジュールを作成し、
シナリオ中のhttpのcontentsで
%%hb:image_file
と書くことでデータを送信できるようにします。
まず以下のようなファイルを hb.erl
という名前で作成します。
なんのことはない、ブラウザが送るデータと同じものを合成して送信するだけです。
-module(hb).
-export([image_file/1]).
image_file({_,DynVars})->
Filename = "/home/centos/scenario/img/kuma.jpg",
{_,ImageData,_Size}=ts_utils:read_file_raw(Filename),
lists:concat([
"------WebKitFormBoundaryF6QBOK9ESy7Gy7O8\r\n",
"Content-Disposition: form-data; name=\"files[]\"; filename=\"", Filename, "\"\r\n",
"Content-Type: image/jpeg\r\n",
"\r\n",
binary_to_list(ImageData),
"\r\n",
"\r\n",
"------WebKitFormBoundaryF6QBOK9ESy7Gy7O8--"
]).
次に上記ファイルをコンパイルしTsungのディレクトリに配置します。
erlc -o /usr/lib64/erlang/lib/tsung-1.6.0/ebin/ hb.erl
コンパイルが成功すると /usr/lib64/erlang/lib/tsung-1.6.0/ebin/hb.beam
ができるはずです。
この状態で以下のようなシナリオを書くことでファイルをアップロードできます。
<request subst="true">
<http url='http://localhost/file_upload' version='1.1' contents='%%hb:image_file%%' content_type="multipart/form-data; boundary=----WebKitFormBoundaryF6QBOK9ESy7Gy7O8" method='POST'>
</http>
</request>
request
- 今回のようにモジュール呼び出しの場合は
%%モジュール名:関数名%%
と書きます。_
は不要です - 呼び出す関数はエクスポートされている必要があります
- 今回のようにモジュール呼び出しの場合は
XML用に取得した値をエスケープする
特に csrf_token
や authenticity_token
など記号を含む場合がありまして、
そのような場合はGET/POSTする際にURL encodeしておく必要があります。
Tsung標準ではそのような処理を見つけられず、
やむなく自分で書きました。ということがありました。
( http_uri
モジュールが意図通りに動かずやむなく... )
シナリオ中で authenticity_token
を raw_authenticity_token
という名前で取得し、
それを %%hb:authenticity_token
という形で呼びたすためのモジュール例は以下の通りです。
対象文字が足りないなど不完全なのであくまで参考として見てください。
-module(hb).
-export([authenticity_token/1, escape/1]).
escape([H|T], Val) when H =:= equal ->
Replaced = re:replace(Val, "=", "%3D", [global, {return, list}]),
escape(T, Replaced);
escape([H|T], Val) when H =:= slash ->
Replaced = re:replace(Val, "/", "%2F", [global, {return, list}]),
escape(T, Replaced);
escape([H|T], Val) when H =:= plus ->
Replaced = re:replace(Val, "\\+", "%2B", [global, {return, list}]),
escape(T, Replaced);
escape([], Val) ->
Val.
escape(Val) ->
escape([equal, slash, plus], Val).
authenticity_token({_, DynVars}) ->
{_,Val}=ts_dynvars:lookup(raw_authenticity_token,DynVars),
escape(Val).
フォームをSubmitする
冒頭にも書きましたが、 Tsungは "受け取ったフォームをSubmitする" ことが できない です。
なので、自分で必要な要素を取り出してがんばってなんとかしましょう。
具体的にはページ内の input
タグと textarea
タグを取り出して、
値をうまいこと取り出したり設定したりしてなんとかします。
受け取ったHTMLからタグを取り出すためのXPathの例は以下の通りです。
<dyn_variable name="all_input_tags" xpath='//input[@type="text" or @type="password" or @type="hidden" or @type="file"]|//input[@type="checkbox" and @checked="checked"]|//input[@type="radio" and @checked="checked"]|//textarea|//select/option/parent::*'/>
このXPathから以下のようなデータが取得できるはずです。
Val =
[
{<<"input">>,
[ {<<"type">>, <<"radio">>}, {<<"value">>, <<"false">>}, ... ],
[]},
...,
{<<"select">>,
[ {<<"class">>, <<"select_sel_item">>}, {<<"name">>, <<"select_name">>}, ... ],
[
{<<"option">>,[{<<"selected">>,<<"selected">>},{<<"value">>,<<"0">>}],[<<"x">>] },
{<<"option">>,[{<<"value">>,<<"1">>}],[<<"a">>]},
{<<"option">>,[{<<"value">>,<<"2">>}],[<<"b">>]},
{<<"option">>,[{<<"value">>,<<"7">>}],[<<"c">>]}
]
},
{<<"textarea">>,
[{<<"class">>, <<"xxx_text_field">>}, {<<"maxlength">>, <<"400">>}, {<<"name">>, <<"foo_name">>}, ... ],
[<<"\n">>]
},
...
]
あとはこのデータを処理するモジュールを作成します。
-module(hb).
-export([all_input_tags_to_contents/1, tag_to_contents/1]).
find_selected_option_value([{<<"option">>, Attrs, _}|T]) ->
case lists:keymember(<<"selected">>, 1, Attrs) of
false -> find_selected_option_value(T);
_ ->
{_,Value}=lists:keyfind(<<"value">>, 1, Attrs),
Value
end;
find_selected_option_value([_|T]) ->
find_selected_option_value(T);
find_selected_option_value([{<<"option">>, Attrs, _}]) ->
{_,Value}=lists:keyfind(<<"value">>, 1, Attrs), Value;
find_selected_option_value([]) ->
false.
select_to_kv(Attrs, Subs) ->
{_,Name}=lists:keyfind(<<"name">>, 1, Attrs),
Value = find_selected_option_value(lists:reverse(Subs)),
{Name, Value}.
input_to_kv(Attrs) ->
{_,Name}=lists:keyfind(<<"name">>, 1, Attrs),
case lists:keyfind(<<"value">>, 1, Attrs) of
{K,V} ->
{Name,V};
false ->
{Name,<<"">>}
end.
tag_to_contents({<<"select">>, Attrs, Subs}) ->
case Result = select_to_kv(Attrs, Subs) of
{Name,Value} = Result when Value =:= false -> "";
_ ->
{Name,Value} = Result,
lists:concat([http_uri:encode(binary_to_list(Name)),
"=",
encode_uri_rfc3986:encode(re:replace(Value,"\n","",[global,{return,list}]))])
end;
tag_to_contents({<<"textarea">>, Attrs, [Text]}) ->
{_,Name}=lists:keyfind(<<"name">>, 1, Attrs),
lists:concat([http_uri:encode(binary_to_list(Name)),
"=",
encode_uri_rfc3986:encode(re:replace(binary_to_list(Text),"\n","",[global,{return,list}]))]);
tag_to_contents({<<"input">>, Attrs, _}) ->
{Name,Value} = input_to_kv(Attrs),
lists:concat([http_uri:encode(binary_to_list(Name)),
"=",
encode_uri_rfc3986:encode(binary_to_list(Value))]).
tag_to_contents("", [Tag|T]) ->
tag_to_contents(tag_to_contents(Tag), T);
tag_to_contents(Contents, [Tag|T]) ->
NewContents = lists:concat([Contents, "&", tag_to_contents(Tag)]),
tag_to_contents(NewContents, T);
tag_to_contents(Contents, []) ->
Contents.
all_input_tags_to_contents({_,DynVars}) ->
{_,Val} = ts_dynvars:lookup(all_input_tags, DynVars),
re:replace(tag_to_contents("", Val), "&+", "\\&", [global,{return,list}]).
実際にはパースして値を再利用するだけではなくて、 入力するところは入力するなどの操作が必要だと思うので、 なんというかがんばってください。
値の再利用をしなくていいよう、 シナリオとデータを工夫することで全体の工数が下がるのでそちらをオススメします。
なんかすごい謎のコードが登場しましたが、 Erlangは書いていてとても楽しいのでぜひチャレンジしてみてください!!!!!
→もひとつ書きましたTsungで負荷テストしよう(3) - リアルな負荷のためのTips