Benchmarking jBCrypt in EC2 With JMH
A few weeks ago my friend Rob and I relaunched a small beer website we took over. We had rewritten the entire site in ratpack over the previous two months and were excited to show it off to the world. Alomst immediately upon launching our login and password reset pages were slowing to a crawl and timing out for most requests. After several hours of debugging we determined two main causes:
- I had picked a woefully inadequate instance type for our application. (Because I’m cheap.)
- We had compounded the problem by picking a log rounds value for password hashing that took a very long time in ec2. Particularly on the instance type I selected.
After moving to a sane ec2 instance type and decreasing the log rounds our performance issues have gone away.
This experience made me wonder if anyone had benchmarked this in ec2 before and without finding anything in a quick google search I decided to do it myself.
JMH (Java Micro-benchmarking Harness) is a framework for writing micro-benchmarks in java and I’ve been wanting to try it for a while. JMH handles the tricky parts of benchmarking code on the JVM by handling things like JVM warmup and garbage collection. It provides annotations to generate the benchmarking code as well as the benchmark runner. The JMH documentation isn’t fantastic. I preferred this blog post, and this set of samples.
For this case I wanted to benchmark the amount of time it would take to hash a string using a salt generated by bcrypt over various number of log rounds. The higher the number of log rounds used, the more time it takes to hash the password and is more secure. However, the time to hash the password is going to be the lower bound on the amount of time it takes to authenticate a user. Pick a number that is too high and users could be waiting a very long time. (Particularly if your server has 1 cpu and a slow clock speed.) The method being benchmarked in this case is
BCrypt.hashpw using various sizes of salts. The salt is generated by
BCrypt.gensalt(logRounds) and log rounds varies from 6 to 12, increasing by 2 each time.
After I wrote the JMH benchmark, I needed an easy way to create ec2 instances, copy the jar file to it, execute the benchmark and capture the results. And I wanted to be able run this on several different ec2 instance types and compare the results. I didn’t find anything that could do this already so I wrote a small groovy script that leverages the Gramazon library to help out.
MacBook Pro – i7 2.5gHz
Although we didn’t have these numbers before we launched, it would have been really useful. We sarted on an m3.medium with a log rounds value of 16. I didn’t even include it in this test but it took multiple seconds. And there was only a single vCPU so our server was quickly overwhelmed when we sent out the email saying a big new release had been launched.
Having these numbers allows us to make an informed choice about the tradeoffs between security and performance.
All of the code can be found on github of course. First the JMH test itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
This is all the code you have to write for a simple JMH test. Additionally I used gradle to build it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
The other piece required was a script to execute the benchmark on an ec 2instance. A lot could be done to make this script much more generic but it fit the bill for what I needed. To try this on your own you would need an amazon access and secret key, as well as the pem file generated for the security group used to create the ec2 instance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
Lets take the script in pieces. First is the script that uses Gramazon and Jsch for the heavy lifting. All we’re left with is starting the instance, then sshing to the instance and running several commands. Finally we stop the instance which is important if you don’t want to continue to pay for it once you’re finished. Each time I wanted to try a new instance type I would just change the
m3.large string to the next value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
This SSHes to the instance and then executes the provided closure, passing the ssh session to it.
1 2 3 4 5 6 7 8 9
This copies the Jar file built with gradle. This should probably be parameterized so the jar file isn’t hard coded.
1 2 3 4 5 6
The AMI I selected doesn’t have java installed so this takes care of that. I could select a different AMI or build my own if that was important enough.
1 2 3 4 5
Executes the JMH benchmark harness. This takes about 20-30 minutes as it runs multiple iterations to get accurate numbers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
This is just a bit of jsch to execute a command via the shell and stream the output back to standard out. I think I cut and pasted this from an example somewhere.