Tsung is a multi-protocol distributed load testing tool released under the
GPLv2 license. In this article, we will see how we can create a scenario that
triggers the download of a file from a Swift container using the S3 API. In
fact, we are using Swift3, a compatibility layer that implements the S3 API
on top of OpenStack Swift. To do so, we will have to use some of the advanced
features of Tsung, but people not familiar with either Tsung or Erlang
should still be able to enjoy this article.
Understanding the issue
We want to benchmark the Swift3 component to decide whether it can be used in
production. It is an implementation of the Amazon API on top of OpenStack
Swift, which is very useful for people switching from the former to the latter.
Any cluster running OpenStack can be used to run this benchmark; in fact, since
this article focuses on writing an Erlang module to extend Tsung, it can be as
simple as a working devstack, as long as Swift3 is installed. The installation
process is clearly explained in the project’s README.
Tsung, which can be installed from your distribution repositories or from
source, must be run on a machine that can access Keystone and Swift. You may
want to check that the Swift3 middleware is working, for instance by trying to
upload files to a Swift container using s3curl.
We want to use the Amazon API to retrieve files from a bucket (here, a Swift
container, since we are using Swift3). Here is the relevant extract from the
documentation of the REST API for the “GET Object” operation:
GET /ObjectName HTTP/1.1 Host: BucketName.s3.amazonaws.com Date: date Authorization: authorization string
So, we can start by writing the following XML:
$ cat test.xml <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd" []> <tsung loglevel="info" dumptraffic="true"> <clients> <client host="localhost" cpu="1" maxusers="1" use_controller_vm="true"/> </clients>
This only tells Tsung to run tests from this machine on which it is called, and
not load more than one user. Note the “dumptraffic” attribute, that will allow
us to see a complete dump of the request and its response, but will slow the
benchmark: this should only be used when debugging.
<servers> <server host="${HOST}" port="${PORT}" type="tcp"/> </servers>
Here, $HOST and $PORT are specific to your architecture. Running “keystone
endpoint-get –service object-store” may help you find these values, otherwise
your system administrator should know these.
<load> <arrivalphase phase="1" duration="10" unit="second"> <users arrivalrate="1" unit="second" maxnumber="1"/> </arrivalphase> </load>
Tsung allows users to define a load progression. Since we just want to test
our module, and not run the whole benchmark, a single phase made of a single
user will be enough.
<sessions> <session name="test-session" probability="100" type="ts_http"> <request subst="true"> <http url="/s3-test/test-file" method="GET"> <http_header name="Date" value="FIXME"/> <http_header name="Authorization" value="FIXME"/> </http> </request> </session> </sessions> </tsung>
Readers not familiar with Tsung may safely ignore the session-related tags here
(but learn more if they wish), and focus on the definition of the request.
We will be sending a GET request to “/s3-test/test-file”, which is the way to
retrieve the file called “test-file” from the bucket (or container, depending
on the terminology) called “s3-test” using the Amazon API. We also need to
provide two HTTP headers:
– the first one with the date, that will change at every run;
– the second one with an authorization string, that depends on the date .
Since the values of these HTTP headers will change for every run, we cannot
hardcode them in the XML file. Instead, we need to compute them dynamically.
To do this, we will need to write some code and call it from within the XML. Let
us see how it is done.
Extending Tsung
We are going to write an Erlang module called “aws”, that will later be copied
to /usr/lib/eralnd/lib/tsung-${VERSION}/ebin by running the following commands:
$ erlc aws.erl # This creates aws.beam $ sudo cp aws.beam /usr/lib/erlang/lib/tsung-1.4.1/ebin
If you do not have root privileges, you may want to add this to ~/.erlang:
code:add_patha("/path/to/the/directory/where/aws.beam/can/be/found")
So, in order to try this at home, one must install an Erlang compiler (erlc can
be found in erlang-base on Debian, in erlang-erts on Fedora).
Finally, we will be able to call the functions defined in this module from
within the XML file above.
Getting the current date and time
This is actually quite easy: the hardest part is the formatting:
-import(calendar,[universal_time/0,day_of_the_week/1]). day_to_weekday (D) -> N = day_of_the_week(D), case N of 1 -> "Mon"; 2 -> "Tue"; 3 -> "Wed"; 4 -> "Thu"; 5 -> "Fri"; 6 -> "Sat"; 7 -> "Sun" end. month_int_to_str (M) -> case M of 1 -> "Jan"; 2 -> "Feb"; 3 -> "Mar"; 4 -> "Apr"; 5 -> "May"; 6 -> "Jun"; 7 -> "Jul"; 8 -> "Aug"; 9 -> "Sep"; 10 -> "Oct"; 11 -> "Nov"; 12 -> "Dec" end. get_date(_) -> {Date, {H, M, S}} = universal_time(), {Year, Month, Day} = Date, DateAsString = io_lib:format("~s, ~2.10.0B ~s ~4.10.0B ~2.10.0B:~2.10.0B:~2.10.0B +0000", [day_to_weekday(Date), Day, month_int_to_str(Month), Year, H, M, S]), lists:flatten(DateAsString).
The “day_to_weekday” and “month_int_to_str” functions are pretty
straightforward, though they might be replaced by more elegant code using the
standard library. The “get_date” function just computes the current date and
time and returns them as a properly formatted string. Note the harcoded
“+0000”: there is room for improvement! Also, this function takes one (ignored)
argument, because Tsung will pass a” {Pid, DynData}” tuple to it automatically:
“Pid” is the process ID (as an integer), and DynData (a list of key/value
tuples), dynamic data (the id of the user created by Tsung, for instance).
Computing the authorization string
The S3 REST API requires that the requests contain an authentication string
that we will need to generate. Here is the formula we are interested in:
Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature; Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ); StringToSign = HTTP-Verb + "\n" + Content-MD5 + "\n" + Content-Type + "\n" + Date + "\n" + CanonicalizedAmzHeaders + CanonicalizedResource; CanonicalizedResource = [ "/" + Bucket ] + <HTTP-Request-URI, from the protocol name up to the query string> + [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
Using a tool such as s3curl, we can see that “Content-MD5”, “Content-Type”
and “CanonicalizedAmzHeaders” can be empty in our case, which makes things a
bit easier.
We can start by writing a “sign” function that will sign the “StringToSign”:
-import(base64, [encode_to_string/1]). -import(crypto, [start/0, sha_mac/2]). sign(StringToSign) -> start(), encode_to_string(sha_mac("YourSecretAccessKeyID", StringToSign)).
We now just have to compute “StringToSign”, and sign it:
authorization_string_get_file(_) -> Method = "GET", ContentMD5 = "", ContentType = "", Date = get_date({}), Resource = "/s3-test/test-file", ToSign = lists:concat([Method, "\n", ContentMD5, "\n", ContentType, "\n", Date, "\n", Resource]), sign(ToSign).
Note that there are still some hardcoded values, so that the code can be short
enough to be integrated inside this blog article. In a real benchmark, we would
probably want every Tsung user to download a specific file, instead of having
them all download the same one; this could be done by naming files “file-0001”,
“file-0002”, and so on, and computing the “Resource” variable by using the
tsung user id passed to “authorization_string_get_file”.
The whole code
Here is the whole code for the aws module:
-module(aws). -import(base64, [encode_to_string/1]). -import(crypto, [start/0, sha_mac/2]). -import(calendar,[universal_time/0,day_of_the_week/1]). -export([authorization_string_get_file/1,get_date/1]). sign(StringToSign) -> start(), K = "YourSecretAccessKeyID", encode_to_string(sha_mac(K, StringToSign)). day_to_weekday (D) -> N = day_of_the_week(D), case N of 1 -> "Mon"; 2 -> "Tue"; 3 -> "Wed"; 4 -> "Thu"; 5 -> "Fri"; 6 -> "Sat"; 7 -> "Sun" end. month_int_to_str (M) -> case M of 1 -> "Jan"; 2 -> "Feb"; 3 -> "Mar"; 4 -> "Apr"; 5 -> "May"; 6 -> "Jun"; 7 -> "Jul"; 8 -> "Aug"; 9 -> "Sep"; 10 -> "Oct"; 11 -> "Nov"; 12 -> "Dec" end. get_date(_) -> {Date, {H, M, S}} = universal_time(), {Year, Month, Day} = Date, DateAsString = io_lib:format("~s, ~2.10.0B ~s ~4.10.0B ~2.10.0B:~2.10.0B:~2.10.0B +0000", [day_to_weekday(Date), Day, month_int_to_str(Month), Year, H, M, S]), lists:flatten(DateAsString). authorization_string_get_file(_) -> Method = "GET", ContentMD5 = "", ContentType = "", Date = get_date({}), Resource = "/s3-test/test-file", ToSign = lists:concat([Method, "\n", ContentMD5, "\n", ContentType, "\n", Date, "\n", Resource]), sign(ToSign).
Once we have compiled the module and copied it where it belongs, as explained
previously, we can call our new functions from the XML we wrote in the first
part of this article:
<http_header name="Date" value="%%aws:get_date%%"/> <http_header name="Authorization" value="AWS AWSAccessKeyId:%%aws:authorization_string_get_file%%"/>
Testing it
$ cat test-file This is a test file. $ swift upload s3-test test-file $ tsung -f test.xml start ... $ cat ~/.tsung/log/*/*.dump ... Recv:1429743835.179243:<0.90.0>:HTTP/1.1 200 OK Last-Modified: Wed, 22 Apr 2015 12:33:13 GMT Content-Length: 21 Etag: 6425054c2b859d483ff69a8a41d9f634 Content-Type: application/octet-stream X-Amz-Meta-Mtime: 1429705985.611017 X-Trans-Id: txcd17b949bd094880836a6-00553828db Date: Wed, 22 Apr 2015 23:03:55 GMT This is a test file. ...
We can see that we successfully retrieved the content of the file we were
interested in.
Conclusion
The code for the AWS module is quite easy to read, and was not too hard to
write, even though I’m not familiar with Erlang. There are many other
S3-related benchmarks that could be implemented using Tsung: pushing objects
to a bucket (this would require reading the file to upload to push its content),
deleting objects from a bucket… And all of this can be achieved with the help
of an Erlang module such as the one described in this article.
[…] Cyril Roelandt: Extending Tsung to benchmark […]