Load Testing gRPC services with Gatling
Mon Jul 03 2023
gRPC is a modern open source high performance Remote Procedure Call (RPC) that can efficiently connect services and can run better than REST services. We had an application built using gRPC services and wanted to perform load testing to get an idea about its performance under load.
For Load Testing REST API based applications we have an array of options but when it comes to testing gRPC we have limited options and here, we picked Gatling. Even with Gatling, testing gRPC service is not straight-forward, with the help of the gatling-grpc plugin we were able to perform load tests for the planned scenarios. Along with this plugin, we have a quickstart guide from the plugin developer that gives an overview of building our first gRPC test with Gatling Scala using sbt build tool.
With this blog, we will share some general and Scala specific gotchas that you may get into while building the tests.
Have a look into below points, before getting started with gRPC - Gatling tests.
At first, look into gRPC, Protocol Buffers and understand your gRPC service function, get familiar with your service .proto
file.
Try to hit the gRPC service manually with BloomRPC, Insomnia or any other tools and understand the parameters required, to get the desired output.
Suggest you to pick a familiar programming language (Scala, Java, Kotlin) supported by Gatling, so that it helps with building your gRPC tests.
Going to use Scala (as we did), check sbt scala build tool and ScalaPB compiler plugin to generate target classes for the .proto
file.
In general, it’s recommended to explore the methods within the generated target classes (Request, Response,ServiceGrpc
,..) for the .proto
file and understand the types used in method params.
From this article, you will get a basic gRPC test with gatling as below by passing an empty payload defaultInstance
and it works fine. Test gets passed!
grpc("request_1") .rpc(HealthGrpc.METHOD_CHECK) .payload(HealthCheckRequest.defaultInstance)
Ideally, this cannot be your scenario, we need to pass a payload to the request and at times we may need to handle a complex payload too.
Let’s see how to build your payload step by step:
compile
cmd executed, we get the target classes generated for the .proto
file.target/scala-2.13/src_managed
, check for *Request
class file (that represents the request that we’re intended to test) and import that method to your test within the payload().For passing the parameters we need to check the type used, and with Scala we would like to share a few, as below: (For more info. on Scala types, check here)
Option
- Represents optional values. Instances of Option are either an instance of Some or the object None.Seq
- is an interface for sequences, representing an ordered collection of elements.Examples for the aforementioned types are below, first for the Option
type we need to pass it within Some("value"/anotherRefMthd)
or None
and here we have used Some()
to pass the value.
.payload(RelationsRequest( query = Some(Query()), relationQuery = Some(RelationQuery( namespace = Some("TechConative"), `object` = Some("FormHouse.Pro") )), expandMask = Some(com.google.protobuf.field_mask.FieldMask()), pageSize=0 ))
Breaking down the above request method, it is a simple method of params
RelationsRequest(query, relationQuery, expandMask, pageSize)
and for relationQuery
- we are referring to another method with paramsRelationQuery(namespace,object)
Moving on to the next type Seq
with an example below, here we are passing multiple values in a sequence to process this request as a whole within this method and other things are the same as above referring to another method
/using Some
.
.payload(TransactRelationsRequest( Seq( RelationDelta( action = RelationDelta.Action.ACTION_INSERT, relationTuple = Some( RelationTuple( namespace = "TechConative", `object` = "FormHouse.Pro" subject = Some( Subject(Subject.Ref.Set(SubjectSet( namespace = "QE", `object` = "Performance" ))))) ))))
We are good with building/passing payload to a request regardless of its type, it works fine - Test Passed! But we are into Load Testing, need to pass xxx multiple requests and it can’t be of static data/payload.
For a Dynamic payload, one of the most popular ways is having csv files with all required data and referring it within tests. From Gatling we have CSV Feeders with different strategies built-in and we can have that in gRPC tests as well.
Usually ${csvHeader}
or #{csvHeader}
is used to pass reference but for gRPC payload, it is slightly different. We need to get the values from session
and an example to follow:
val csvFeeder = csv("data/tech.csv").circular val resourceRequest = grpc("${techType}"+":"+"#{techName}") .rpc(ReadServiceGrpc.METHOD_LIST_RELATIONS) .payload( session => RelationsRequest( query = Some(Query()), relationQuery = Some(RelationQuery( namespace = Some(session("techType").as[String]), `object` = Some(session("techName").as[String]) )), expandMask = Some(com.google.protobuf.field_mask.FieldMask()), pageSize = 0 )) val scn = scenario("For the resource, list all users") .feed(csvFeeder) .exec(resourceRequest)
In the payload we need to use session("csvHeader").as[String]
to get the values from the CSV file and we got our Dynamic Payload for Load Tests.
To verify/validate the response, tests should have a check (or multiple checks).
For that, adding .check()
to our gRPC request as below, does the verification/validation. And .extract()
will extract the response and that can be saved to pass/refer to another subsequent request from the session
.
val request = grpc("ListNames") .rpc(NamesServiceGrpc.METHOD_LIST_NAMES) .payload(NamesRequest.defaultInstance) .extract(_.names.some)(_ saveAs "names") .check(statusCode is Status.Code.OK) val scn = scenario("ListNames_Once_Trial_Run") .exec(request) .exec(session => { val names = session("names").as[String] println(s"***Response***: $names") session })
For setup, refer to plugin developer quickstart guide, to have the base setup or you can also refer below steps for Gatling - Scala - sbt project setup.
build.sbt
file with required dependencies
.proto
filestarget/scala-2.13/src_managed/*
In case of any issues with setup, try below things that helped us:
sbt
/ scala
/ scalapb
/ gatling
Additional things we did as a best practice along with the setup is having an application.conf
file to isolate the ipAddress
/Port
details and also added an utils
package to keep the common code for loading the conf file and getting the values.
We would love to hear from you! Reach us @
info@techconative.com