Conf42 Golang 2022 - Online

Load testing with F1

Video size:

Abstract

Due to a lack of functionality in existing solutions, we wrote our own open source load testing tool at Form3 to test our asynchronous system in. This talk focuses and on the importance of load testing and showcases our solution, named F1. It allows us to write our tests in Go - leveraging the powerful mechanisms of goroutines and channels in our tests! The key points of discussion in our talk are:

  • Load testing fundamentals: What is load testing and why should we care about it? What existing tools are there out there already and what are their limitations?
  • Introduction F1: What is F1 and how is it different from existing solutions?
  • Live demo: let’s see F1 in action on a Go demo app!
  • Join us to learn how we load test our services in Go using our open source tool, F1, and let’s make testing cool again!

Summary

  • Andela has matched thousands of technologists across the globe to their next career adventure. Now the future of work is yours to create. Anytime, anywhere. Today we will be looking into the world of load testing.
  • Performance testing examines stability, scalability and reliability of your software and infrastructure. Load testing tells us how many concurrent users or transactions your system can handle. Stress tests look for memory leaks, slowdowns, security issues, and even data corruption. Before you run your tests, it's important to have monitoring in place.
  • Performance testing is a very important test that should be run on your platform all the time. Our ideal tool should allow us to easily write asynchronous tests which integrate with our queues and services. We decided to write our own solution and then open source it for the community to use.
  • f one is our own internal load testing tool. It natively supports writing test scenarios in go. It does also support a variety of other modes of injecting load. Here's a 15 minute demo to show you how to use f one.
  • Use go to write load test scenarios from the command line. Use an SQS client to receive messages in the background of your test scenarios. Stitch together these messages to see what happens when a test fails.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
What if you could work with some of the world's most innovative companies, all from the comfort of a remote workplace? Andela has matched thousands of technologists across the globe to their next career adventure. We're empowering new talent worldwide, from Sao Paulo to Egypt and Lagos to Warsaw. Now the future of work is yours to create. Anytime, anywhere. The world is at your fingertips. This is Andela. Hello everyone. Thanks for having us at this year's Conf 42 Golang. You know who Andy and I are from our introductions, but let us introduce form three as well. Form three are a payments technology provider who work with some great customers and partners. As you can see on this slide, we have a fully go code base and work with some great cloud native technologies. We currently have around 260 employees, of which about 130 care engineers. We are a fully remote company and we're hiring. Today we will be looking into the world of load testing. I will very quickly cover performance testing fundamentals and some common tools that we can use to write our tests. Then Andy will take over and tell you about our open source testing tool f one and give you a live demo of how to use it. Everyone loves a bit of live coding in a good demo, right? Okay, without any further ado, let's dive into the world of performance testing. Performance testing is the general name for tests that check how the system behaves and performs. Hence the named performance testing. Performance testing examines stability, scalability and reliability of your software and infrastructure. Before performance testing, it's important to determine your system's business needs, so you can tell if your system behaves satisfactorily or not according to your customer needs. Often, performance testing can be done on anticipated future load to see a system's growth Runway, this is important for a high volume payments platform like ours. Under the umbrella of performance existing, we can look at three test subtypes, load spike and soak tests. Load testing tells us how many concurrent users or transactions your system can actually handle. Different load scenarios require different resources, so it's important to write multiple tests. Load tests should be performed all the time in order to ensure that your system is always on point, which is why it should be integrated into your continuous integration cycles. Now, on the other hand, a stress test is a type of performance test that checks the upper limits of your system by testing it under extreme loads. Stress tests also look for memory leaks, slowdowns, security issues, and even data corruption. There are two types of stress tests, spike and soak. If your stress test includes a sudden high ramp up in the number of virtual users. It's called a spike test. If your stress test is over a long period of time to check the system's sustainability over time, but with a slow ramp up that is called a soak test. You should run stress tests before major events like, for example, Black Friday. If you're a commercial retailer. Before you run your tests, it's important to have monitoring in place and agree what your failure threshold should be. You can see some common things to monitor on this slide, such as average response time, error rate, or cpu usage, which are important indicators that can show you whether your system is healthy. These important metrics should therefore be monitored and alerted on before you write your tests at form three a lot of our systems use asynchronous processing and queues. Today we'll be looking at this simple example application. We create a service which exposes a single endpoint payments. This service receives requests, does background processing on them, and then outputs a message to an SQS queue once processing is complete. Now we need a way of connecting requests to their corresponding result from the SQS queue. If we rely on the response, the 202 accepted response, which you can see on this slide, will make it seem like the request completes immediately when actually background processing is still happening. F one is the open source solution we will be talking about today, which can help you do just that. So now that we have established the basics of performance testing, let's have a look at two common tools that we can use. The first tool we'll be talking about is jmeter, and it is an open source Java testing tool that is widely used. It allows us to configure tests using a recording GUI and some predefined templates. For asynchronous testing support, we can use long polling or another request to check whether an operation has completed. We can then configure a number of threads and a ramp up period in seconds for load specification. Jmeter also offers a plugin for step and spike ramp up, even though it is not supported natively in jmeter. Next up, another common tool used for performance testing is k six. It is an open source go project run by Grafana tests care configured using a scripting language similar to JavaScript. K six does not provide support for promises or asynchronous execution, but we can achieve asynchronous testing support using virtual users. We can then configure the load for the test using an options object which states how many requests to run for each stage of the test and how long each stage of the test is. This allows us to configure linear and step ramp up. Now at form three, we invest a lot of engineering time into performance testing our platform. As we have already seen, it is a very important test that should be run on your platform all the time. We initially used K Six to develop and run these tests, but it did not fully fit our ideal load existing tool, which you can see described on this slide. Our ideal tool should allow us to easily write asynchronous tests which integrate with our queues and services. This was not always easy to do in JavaScript, especially because our platform is fully written in Go. It should also allow our engineers to write tests in Go, which is what they're most comfortable in anyways. And it should also integrate well with our CI pipelines as we want to perform and test our platform. Often, writing in our tests in Go would be a huge game changer for our engineers, as it would allow us to make use of go routines and channels for test configuration, and these are really important components in the care language that we would like to leverage. Finally, as our platform operates under huge amounts of load, the tool should allow us to run different modes of load as well, not just linear or step ramp up. Andy will tell you more about the different modes of load that we need later. And as you can see, the existing solutions did not provide us any of these features. So this is why we decided to write our own solution and then open source it for the community to use. I'll now hand over to Andy who will tell you all about f one. Take it away, Andy. Okay, so I'm going to take you through what f one is and why we decided to write it. So what is f one? Well, f one is our own internal load testing tool. We wrote it initially to support our own use cases, but then we realized actually it was a pretty general purpose tool, so we decided to open source it. And it's written in Go. So it natively supports writing test scenarios in go. And that means that you can use all of the sort of concurrent and asynchronous primitives that go offers when writing your test scenarios. So existing these kind of asynchronous systems is pretty straightforward in Go, much more straightforward than it was, for example, using JavaScript based tests in k six. One of the other things f one supports is a variety of different modes for injecting load. One of the problems we had with k six was that it basically only supports a single mode of operation, which is using a cool of virtual users to apply load that wasn't aggressive enough for some of our use cases. So for some of our use cases, we really needed to be able to apply load more aggressively. And so when we wrote f one, we built that in from the beginning. So f one supports this idea of using a cool of virtual users, but it does also support a variety of other modes of injecting load, which makes it much more suitable to our use cases. So what I'm going to do now is basically take you through a demo for 15 minutes or so. We're going to set up a system to test that looks sort of similar to this asynchronous system that Adelina mentioned earlier. And then we're going to write a simple load test that's going to sort of exercise that system. So I'm just starting with sort of a blank folder here, and we're going to start from the beginning. So first of all, I'm going to set up an environment that we're going to use to run load against using docker compose. So what I've done here is created a Docker compose file with two containers. In the first container go AWS is a local SQs mock. So it's a mock of the AWS SQS service, and we're going to use that sort of to mock out an AWS based message queue. And this docker compose file also contains a dummy service which we're going to write in a minute. And you might notice here that go AWS requires some configuration. So let's create one of those config files. So this config file basically just contains a single queue that we're going to use called test queue. Let's also create a docker file for our service. So our docker compose file is using a local docker file. So here's a Docker file. So what we're going to do is just build an app that's in this command service main go file. So let's dub out that service. What I'm going to do here is I'm going to make a new directory command service, put in a file, a minimal file, and initialize a go module. So now I've got a go mod file, and if I have a look in this directory, I've got an empty main function. So what we're going to do now is we're going to implement a sort of simple application in that file that we're going to use when we're writing our load tests. We're going to inject load against that. So let's edit that file. Okay, so first of all, let's delete this empty function, and what we'll do is we'll put in a main function which listens for HTTP requests on this relative URL payments and an HTTP handler for that. That's just going to return an accepted status code. So there's a sort of simple application. Now what we want to do is we want to publish SQS messages when our web requests are made. And that's going to simulate this sort of asynchronous feedback where we're injecting load synchronously via HTTP and then asynchronously consuming feedback via SQS. So let's set up some global variables to store an SQS client, and then let's initialize that SQS client at the start of our main function. Okay, so what are we doing here? We are setting up an AWS client to use go AWS. So our local sqs mock using some dummy credentials, creating a new SQS client and getting the key URL of our test queue. Okay, I just need to replace one of these imports. Let's pick up the wrong imports. Okay, got some. Got there. Okay. Right, so there we go. We've got an application set up there with an SQS client. Okay, so what do we want to do next? Right, well let's go to the HTTP handler. Rather than just returning an accepted status code, let's add some functionality to that handler. Let's just delete this entirely. Okay, so what are we doing here? So we're saying, okay, we only want to handle post requests. If we don't get a post request, we're going to return a 405. Then we're going to construct an SQS message and send it to that queue. So that's our sort of asynchronous feedback. And this demonstrates our system doing some work asynchronously. Oh yeah, and let's, I shouldn't have deleted that. Let's leave the status accepted on the end it. Okay, so with any luck that will run. So let's just download the dependencies for that app, and then we should be able to run it using docker compose app. Just wait for these dependencies to download. Now, when we run it locally, we should be able to make web requests to that endpoint and get a 202 back. And we should also be able to see SQS messages being published as a result. So let's see if we can run that app. Okay, so if I have a look at what's running in Docker. Sorry, I'll just kill all my running containers and then start again. Okay, so what have I got running here? Okay, so I've got go AWS running, that's my sqs mock and I've got my test service. So if I try making a web request, great, I get a 202 so I can make some web requests. And then if I just configure the AWS cli locally, oh yeah, that's all set up. So I should be able to list my queues. There's my test queue and if I get the queue attributes I got three messages there. So if I make another web request, I've got four messages there. So sending my web request is publishing sqs messages that demonstrates our app. Okay, so now let's write a load test. So what I'm going to do is write another command line entry point. So I've now got this command f one main go. So let's edit that. First thing we're going to do is add an import for f one. So we'll just import this, oops, this package go imports getting carried away. And in order to use f one, all I need to do in an application entry point is new up this f one type and call execute. And this will give me a fully fledged f one command line interface. So if I download that dependency and then run that application, I will get a command line interface pre configured with all of the bells and whistles that f one comes with. So this is how you use f one. It's not like a separate binary that you download and add load tests to or something. You just use this package directly and build your own binary. So if I run this entry point, just put help on the end, we'll compile the app and then we should get some help out. Here we go. So this is the f one command line interface that I've got just from importing that package basically. Okay, so let's go back to our file. What we need to do now is we need to start registering new test scenarios. So I didn't add that correctly. Here we go. Let's, so this allows me to register a new, a new test scenario and that test scenario will be available from our command line interface. So let's just add a sort of dummy implementation here. So what does this dummy implementation do? So basically this function runs some code and then returns a run function. And this code that it runs at the beginning is where you would put any setup code that you need to run one time at the beginning of your test scenario. And then this function that you return is executed every time you run a test iteration in your load test. So if you're running 100 iterations a second, this run function gets executed 100 times per second. And this testing t has a sort of similar API to the go testing t, but you'll notice it's actually an f one type. Okay, and now if I go run, add another command on the end, hopefully we should see that our test scenario has been registered. Great. So this is now a test scenario that we can execute from the command line. Okay, so let's do something similar to our HTTP handler. Let's configure an SQS client here. Oops. Okay, so what we're doing here is we're configuring an SQS client to again use go AWS locally with some dummy credentials, getting the QRL for our test queue. And so I've got an SQS client here available within this function, which I can use in all of my test iterations to receive messages. And this is where we start to stumble upon the power of using go to write these load test scenarios. Because what I'm going to want to do is consume messages from my SQs queue in the background and then check what messages are arriving from my test iterations. So let's do that here. Let's run a go routine in the background to do that. So what have I got here? Okay, so having initialized my SQS client, I've now created a channel that I'm going to use in memory for receiving messages. And I've started goroutines, which is basically polling the SQs queue and sending messages into that channel. So in the background of my load test, I've got this channel which is sort of buffering inbound messages from SQS. And that means that the actual implementation of my run function is pretty straightforward. So let's do that. Let's put an implementation in, and what we want to do here is basically what we were doing from the terminal earlier. So we're going to make a post request. So we're going to make a web request to our local web service. We're going to check that we got a 202. Then we're going to wait for up to 10 seconds for a message to be received from that channel. Now, in real life, you'd probably want to do some logic, execute some logic here to sort of stitch together the SQs message that was received with the web request that you sent, maybe by id or something like that. And in reality, that's what we do is we'll send some kind of HTTP request. There'll be some attributes about that HTTP request that identify that unit of work. Then there's a whole load of background asynchronous processing, the output of which is an SQS message that contains some ids that let us stitch it back together. So our test iteration function here will be sending that web request and then waiting for inbound SQs messages that we can stitch back together. And if you don't receive one within 10 seconds, that test iteration fails. So that's it. We've written our load test scenario, so we should be able to run it. So we've still got our app running in the background. Let's compile our application into a handy binary called f one. So I should be able to f one help f one scenarios list. Okay, so let's see now. I should be able to f one run constant. So this is one of the modes, constant mode. I'm going to run test scenario array of 1 /second form, let's say 10 seconds. Okay, so we've got one tests iteration per second using executed. They're taking about 1.5 milliseconds and they all passed. I can also do this in a similar mode to k six, so I can use a pool of virtual users with a concurrent cool, let's say of, I don't know, one user, and you'll see now I get something quite different. So now I'm running, let's say close to 1000 requests per second, taking up to three or four milliseconds. And that's because my pool of one virtual user is making requests one after another as quickly as it can. So these two modes allow you to sort of inject load however you like. That constant mode allows you to really control the rate which load is being injected. And that becomes particularly useful when your cool of virtual users would become saturated. So if you've got ten users and each request or each sort of asynchronous process takes 2 seconds, your pool of ten virtual users can only make five requests per second because they're each making their requests consecutively, one after the other, and they have to wait 2 seconds between requests. And that's problematic if you really, really want to apply ten requests per second load to your system. The constant mode doesn't care about virtual users, and it will aggressively apply ten requests per second to your application. And that's one of the reasons we developed f one separately. So that's it. I hope you found that demo useful. So I guess just to sum up, f one is a sort of battle tests load testing tool that we use now. We use it every day. We use it to apply synthetic load in a number of our pre production environments. It sends hundreds or thousands of sort of payments per second into our environments and it's really a first class citizen in our software development lifecycle. So I guess this statement at the top is important is that I think for large systems, which will be processing volume at scale in production, load testing needs to be a first class citizen. It shouldn't be an afterthought and it should be part of the way you're developing your applications. And certainly for us, we found it really useful at spotting performance bottlenecks or scalability problems well before we would have encountered those problems in production, which gives us plenty of time to fix those problems. So this has been us, Adelina and I, thanks for listening. And if you're interested in learning more, check out f one on our open source organization or look us up online at the addresses shown here. So, yeah, thanks a lot.
...

Adelina Simion

Tech Evangelist @ Form3

Adelina Simion's LinkedIn account Adelina Simion's twitter account

Andy Kuszyk

Head of International Engineering @ Form3

Andy Kuszyk's LinkedIn account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways