Simon Willison’s Weblog

Subscribe

Running a load testing Go utility using Docker for Mac

5th November 2017

I’m playing around with Zeit Now at the moment (see my previous entry) and decided to hit it with some traffic using Apache Bench. I got this SSL handshake error:

simonw$ ab -n 10 -c 2 'https://json-head.now.sh/'
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking json-head.now.sh (be patient)...SSL handshake failed (1).
140735278280784:error:14077438:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert internal error:/Library/Caches/com.apple.xbs/Sources/libressl/libressl-1.60.1.2.1/libressl/ssl/s23_clnt.c:541:
SSL handshake failed (1).

Some brief Googling turned up this thread on Stack Overflow, which suggested trying hey as an alternative. Hey is a load testing utility written in Go, and the installation instructions are as follows:

go get -u github.com/rakyll/hey

Unfortunately, I don’t have a current Go environment set up on this laptop—I have Go 1.6, but Hey calls for at least Go 1.7.

Rather than work through upgrading my Go environment, I decided to see if I could get this tool working using Docker for Mac.

We recently switched to Docker for Mac for running our development environments at work, and having worked through various iterations of Docker over the past few years Docker for Mac offers by far the most pleasant developer experience. You download the installer, run it, and now docker info in a terminal will reveal a fully functioning Docker environment. Couldn’t be simpler.

But how to use it to run a one-off tool written in Go? This article on the official Docker blog gave me everything I needed to know.

First step: run the go get command in a brand new Docker container, like so:

docker run golang go get -v github.com/rakyll/hey

This runs the go get command in a new instance of the official golang container. If you’ve never used the container before, Docker will download everything it needs before executing the rest of the command.

Once this command finishes, we have a container with the Go program compiled and installed in it. But how to run it?

We can “commit” the container to freeze it into a new image that bakes in the command. Here’s how to do that:

docker commit $(docker ps -lq) heyimage

The nested docker ps -lq command outputs the container ID. The outer docker commit command then creates a new image freezing those latest changes.

Having frozen the container, we can run the command like this:

docker run heyimage hey -n 10 -c 2 'https://json-head.now.sh/'

And the command runs, exactly as if I’d installed it without using Docker at all.

simonw$ docker run heyimage hey -n 10 -c 2 'https://json-head.now.sh/'
Summary:
  Total:    0.9778 secs
  Slowest:  0.6794 secs
  Fastest:  0.0564 secs
  Average:  0.1954 secs
  Requests/sec: 10.2266

Response time histogram:
  0.056 [1] |∎∎∎∎∎∎
  0.119 [7] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  0.181 [0] |
  0.243 [0] |
  0.306 [0] |
  0.368 [0] |
  0.430 [0] |
  0.493 [0] |
  0.555 [0] |
  0.617 [0] |
  0.679 [2] |∎∎∎∎∎∎∎∎∎∎∎

Latency distribution:
  10% in 0.0588 secs
  25% in 0.0653 secs
  50% in 0.0868 secs
  75% in 0.6792 secs
  90% in 0.6794 secs

Details (average, fastest, slowest):
  DNS+dialup:    0.1221 secs, 0.0000 secs, 0.6109 secs
  DNS-lookup:    0.0981 secs, 0.0000 secs, 0.4906 secs
  req write:     0.0001 secs, 0.0000 secs, 0.0001 secs
  resp wait:     0.0727 secs, 0.0561 secs, 0.0904 secs
  resp read:     0.0004 secs, 0.0001 secs, 0.0012 secs

Status code distribution:
  [200] 10 responses

One last puzzle: the above command worked for load testing externally hosted URLs, but I also wanted to try running it against a web server running on port 8000 on my Mac itself. Running hey against http://localhost:8000/ didn’t work inside the container. Instead, I ran ipconfig getifaddr en0 to find the local network IP address of my Mac and then ran hey against that IP address (thanks again, Stack Overflow):

simonw$ docker run heyimage hey -n 100 -c 10 'http://10.0.0.12:8000/'
Summary:
  Total:	0.2481 secs
  ...

For me, this use-case illustrates a huge part of the value of Docker: it lets you execute tools written in basically anything without having to pollute your laptop with environment junk.

Running commands against files

Update: 9th November 2017

I decided to use this technique to try out this Go minify tool by Taco de Wolff. Building the tool into a container used the same pattern:

docker run golang go get -v github.com/tdewolff/minify/cmd/minify
docker commit $(docker ps -lq) minify

Running the command this time is a bit harder, because it needs access to files on my filesystem. I can give it access by mounting my current directory as part of the docker run command, like so:

docker run -v `pwd`:/mnt minify minify /mnt/all.css

Running this minifies the contents of the all.css file in my current directory and outputs the result to standard out. If I want to save it I can redirect it to a file like so:

docker run -v `pwd`:/mnt minify minify /mnt/all.css > all.min.css