I used Python for a load test, and look what happened next…

Hiring

We are Binx. We make every organization cloud-native.

After I added the Google Cloud Storage backend to PrivateBin, I was
interested in its performance characteristics. To fake a real PrivateBin client, I needed to generate an encrypted message.
I searched for a client library and found PBinCLI. As it is written in Python,
I could write a simple load test in Locust. Nice and easy. How wrong was I.

create the test

Within a few minutes I had a test script to create, retrieve and delete a paste.
To validate the functional correctness, I ran it against https://privatebin.net:

$ locust -f locustfile.py \
   --users 1 \
   --spawn-rate 1 \
   --run-time 1m \
   --headless \
   --host https://privatebin.net

The following table shows measured response times:

NameAvg (ms)Min (ms)Max (ms)Median (ms)
create-paste237140884200
get-paste28273029
delete-paste27272927

Now, I had a working load test script and a baseline to compare the performance with.

baseline on Google Cloud Storage

After deploying the service to Google Cloud Run, I ran the single user test. And it was promising.

$ locust -f locustfile.py \
  --users 1 \
  --spawn-rate 1 \
  --run-time 1m \
  --headless \
  --host https://privatebin-deadbeef-ez.a.run.app

Sure, it is not lightning fast, but I did not expect that. The response times looked
acceptable to me. After all, privatebin is not used often nor heavily.

NameAvg (ms)Min (ms)Max (ms)Median (ms)
create-paste506410664500
get-paste394335514380
delete-paste587443974550

multi-user run on Google Cloud Storage

next I wanted to know the performance of PrivateBin with my Google Cloud Storage onder moderate load. So, I scaled the load test to 5 concurrent users.

$ locust -f locustfile.py \
  --users 5 \
  --spawn-rate 1 \
  --run-time 1m \
  --headless

The results were shocking!

NameAvg (ms)Min (ms)Max (ms)Median (ms)
create-paste4130662106664500
delete-paste344976862833800
get-paste290952055692400

How? Why? Was there a bottleneck at the storage level? I checked the logs and saw steady response times reported by Cloud Run:

POST 200 1.46 KB 142 ms python-requests/2.25.1  https://privatebin-37ckwey3cq-ez.a.run.app/
POST 200 1.25 KB 382 ms python-requests/2.25.1  https://privatebin-37ckwey3cq-ez.a.run.app/
GET  200 1.46 KB 348 ms python-requests/2.25.1  https://privatebin-37ckwey3cq-ez.a.run.app/?d7e4c494ce4f613f

It took me a while to discover that locust was trashing my little M1. It was running at 100% CPU without
even blowing a fan to create the encrypted messages! So I needed something more efficient.

k6 to the rescue!

When my brain thinks fast, it thinks golang. So I downloaded k6. The user scripts
are written in JavaScript, but the engine is pure go. Unfortunately, the interpreter
is custom built and has limited compatibility with nodejs and browser JavaScript engines.
This meant that I could not use any existing JavaScript libraries to create an encrypted message.

Fortunately with xk6 you can call a golang
function from your JavaScript code. This is what I needed! So I created the k6 privatebin extension and wrote
an equivalent load test script.

build a customized k6

To use this new extension, I build k6 locally with the following commands:

go install github.com/k6io/xk6/cmd/xk6@latest
xk6 build --with github.com/binxio/xk6-privatebin@v0.1.2

k6 baseline run on Google Cloud Storage

Now I was ready to run the baseline for the single user, using k6:

$ ./k6 --log-output stderr run -u 1 -i 100  test.js

The baseline result looked pretty much like the Locust run:

NameAvg (ms)Min (ms)Max (ms)Median (ms)
create-paste440396590429
get-paste355320407353
delete-paste382322599357

k6 multi-user run on Google Cloud Storage

What would happen, I i scaled up to 5 concurrent users?

$ ./k6 --log-output stderr run -u 5 -i 100  test.js

Woohoo! The response times stayed pretty flat.

NameAvg (ms)Min (ms)Max (ms)Median (ms)
create-paste5843502612555
get-paste484316808490
delete-paste460295843436

The chart below shows the sum of the medians for the single- and multi-user load test on Locust and k6:

It is clear that Locust was skewing the results way too much. With k6, I could even simulate 20 concurrent users, and still only use 25% of my M1.

NameAvg (ms)Min (ms)Max (ms)Median (ms)
create-paste7134141204671
get-paste562352894540
delete-paste515351818495

These are way more users than i expect on my privatebin installation and these response times are very acceptable for me. Mission accomplished!

conclusion

In the future, my goto tool for load and performance tests will be k6. It is a single binary which you can run anywhere. It is fast, and the golang extensions makes it easy to include any compute intensive tasks in a very user-friendly manner.

Mark van Holsteijn is a senior software systems architect, and CTO of binx.io. He is passionate about removing waste in the software delivery process and keeping things clear and simple.
Share this article: Tweet this post / Post on LinkedIn