Since Quarkus 1.11, it introduced Resteasy reactive. It’s another jaxrs implementation, but it doesn’t have full jaxrs features. It is created to tightly integrate with Quarkus and better leverage Qurkus buildtime to do more annotation and metadata model processing. It is much faster than Resteasy extension. From Georgios Andrianakis presentation of this new feature, it’s 2x more faster than the old jaxrs service backed with Resteasy Quarkus Extension. What does improve the performance ? In this post, I’ll try to explain the different parts that Quarkus Reactive has been improved. First, We create a simple jaxrs resource method to respond the GET request and returns a simple JSON object for Person object:
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@Path("/reactive")
public class SimpleResource {
@GET
@Path("/person")
@Produces(MediaType.APPLICATION_JSON)
public Person getPerson() {
Person person = new Person();
person.setFirst("Bob");
person.setLast("Builder");
return person;
}
@POST
@Path("/person")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Person getPerson(Person person) {
if (BlockingOperationControl.isBlockingAllowed()) {
throw new RuntimeException("should not have dispatched");
}
return person;
}
}
The Person is a simple pojo object too and it only contains the first name and last name fields:
public class Person {
private String first;
private String last;
public Person() {
}
public String getFirst() {
return this.first;
}
public void setFirst(String first) {
this.first = first;
}
public String getLast() {
return this.last;
}
public void setLast(String last) {
this.last = last;
}
}
When this jaxrs resource is started, it replies the json response for GET request:
{"first":"Bob","last":"Builder"}
Then we use the wrk benchmark tool to compare the result for resteasy-reactive and resteasy extension. For resteasy-reactive extension, it get the requests/sec number 34250.
2021-02-23 15:07:16,398 INFO [io.quarkus] (main) Installed features: [cdi, resteasy-reactive, resteasy-reactive-jackson]
Running 1m test @ http://localhost:8081/reactive/person
30 threads and 30 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.93ms 20.97ms 479.69ms 99.06%
Req/Sec 1.16k 618.09 6.19k 73.44%
2058345 requests in 1.00m, 202.19MB read
Requests/sec: 34250.65
Transfer/sec: 3.36MB
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 69.26 s - in org.acme.resteasy.ReactiveResourceTest
Switch to resteasy extension, it only gets 13144 requests/sec.
2021-02-23 15:05:17,409 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jackson]
Running 1m test @ http://localhost:8081/resteasy/person
30 threads and 30 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.41ms 18.06ms 433.04ms 98.42%
Req/Sec 443.56 240.21 1.77k 64.75%
789747 requests in 1.00m, 77.58MB read
Requests/sec: 13144.62
Transfer/sec: 1.29MB
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 68.784 s - in org.acme.resteasy.ResteasyResourceTest
2021-02-23 15:06:18,251 INFO [io.quarkus] (main) Quarkus stopped in 0.116s
What does it lead to this result and resteasy-reactive gets this big performance result? These things could be the key success factors.
For a jaxrs implementation, it always needs to dispatch the request URI to resource method. In JaxRS spec, there is detailed algorithm to talk about how to match request with resource method(If you are interested, please look at JSR 399 section 3.7) resteasy-reactive extension moves this job to build time. This means the resource method invoker will be generated in byte code through analyzing the resource method annotation and signature. From the decompiled byte code by resteasy-reactive,
public class ResteasyReactiveProcessor$setupEndpoints-1591811826 implements StartupTask {
public void deploy(StartupContext var1) {
var1.setCurrentBuildStepName("ResteasyReactiveProcessor.setupEndpoints");
Object[] var2 = new Object[147];
this.deploy_0(var1, var2);
this.deploy_1(var1, var2);
}
public void deploy_0(StartupContext var1, Object[] var2) {
...
Supplier var18 = ((ResteasyReactiveRecorder)var6).invoker("org.acme.resteasy.SimpleResource$quarkusrestinvoker$getPerson_55543b09225dc5c83bf2e894ae037f590fe976a7");
var1.putValue("proxykey58", var18);
...
Object var309 = var1.getValue("proxykey58");
((ServerResourceMethod)var304).setInvoker((Supplier)var309);
((ServerResourceMethod)var304).setHttpMethod("GET");
((ServerResourceMethod)var304).setPath("/person");
...
}
With this path, http method info from ServerResourceMethod, it is easy to create a mapper to dispatch each request to resouce invoker when starts server.
resteasy extension relies on Resteasy core function to match the resouce method with the procedure defined in jaxrs spec. Although the matched result will be added to cache and the same request doesn’t have to run this match method overtime, it still consumes the double cpu time compare with resteasy-reactive.
This is resteasy-reactive resouce method cpu time. It shows the total resource method invocation time is 30975ms
We use the same parameter to start wrk and run against resteasy extension. The total resource method cpu time is :53236ms. The resteasy-reactive resource method invocation time is nearly half of resteasy’s.
In addition to resource method invocation time, there is another 43410ms takes to match the resource method. This cpu consumption is listed below the resource method invocation. There is still another improvement to make resteasy-reactive faster. Let’s continue with the second factor.
To make method invocation faster, resteasy-reactive generates byte code to call the object directly:
public class SimpleResource$quarkusrestinvoker$getPerson_55543b09225dc5c83bf2e894ae037f590fe976a7 implements EndpointInvoker {
public Object invoke(Object var1, Object[] var2) {
return ((SimpleResource)var1).getPerson();
}
}
instead of using the reflection method call like :
reflectMethod.invoke(targetObject, args);
The another job finished in build and start time by resteasy-reactive is get the correct reader/writer to read/write message. It is scanning the resource method annotation , analyzing the @Consume @Produce annotation value to find the correct component to read/write the message. From the generate byte code, the Jackson reader and writer alone with other builtin reader and writer will be added to runtime code:
BeanFactory var35 = ((ResteasyReactiveCommonRecorder)var6).factory("io.quarkus.resteasy.reactive.jackson.runtime.serialisers.JacksonMessageBodyWriter", (BeanContainer)var34);
var1.putValue("proxykey62", var35);
ResourceWriter var36 = new ResourceWriter();
var2[5] = var36;
Object var37 = var2[5];
List var38 = Collections.singletonList("application/json");
((ResourceWriter)var37).setMediaTypeStrings(var38);
Object var39 = var1.getValue("proxykey62");
((ResourceWriter)var37).setFactory((BeanFactory)var39);
Boolean var40 = Boolean.valueOf((boolean)0);
((ResourceWriter)var37).setBuiltin((Boolean)var40);
There is a FixedProduceHandler selects this jackson writer during bootstrap and set there to write each response:
resteasy extension selects the writer as JAXRS spec defined in section 3.8 in runtime. This still consumes a lot of cpu time as the following profiler stack shows:
Now both resteasy and resteasy-reactive extension all relied on vertx underneath to handle the http request. Vertx is trying to make all things reactive/noblocking and finish the event/process in 2000ms by default. If it doesn’t, there is thread blocking checker will warn something like :
2021-02-24 11:38:08,673 WARNING [io.ver.cor.imp.BlockedThreadChecker] (vertx-blocked-thread-checker) Thread Thread[vert.x-eventloop-thread-6,5,main]=Thread[vert.x-eventloop-thread-6,5,main] has been blocked for 508280 ms, time limit is 2000 ms: io.vertx.core.VertxException: Thread blocked
Due to reasteasy-reactive get all things speed up and it dramatically decreases the time to find the resource method, get the correct jackson writer , invoke the resource object without using reflection.It can running all things in an eventloop thread instead of using a worker thread like what resteasy extension does :
public class VertxRequestHandler implements Handler<RoutingContext> {
@Override
public void handle(RoutingContext request) {
// have to create input stream here. Cannot execute in another thread
// otherwise request handlers may not get set up before request ends
InputStream is;
if (request.getBody() != null) {
is = new ByteArrayInputStream(request.getBody().getBytes());
} else {
is = new VertxInputStream(request, readTimeout);
}
if (BlockingOperationControl.isBlockingAllowed()) {
try {
dispatch(request, is, new VertxBlockingOutput(request.request()));
} catch (Throwable e) {
request.fail(e);
}
} else {
executor.execute(new Runnable() {
@Override
public void run() {
try {
dispatch(request, is, new VertxBlockingOutput(request.request()));
} catch (Throwable e) {
request.fail(e);
}
}
});
}
}
These could be the things get a jaxrs service much faster with resteasy-reactive in quarkus. It might miss something or something I don’t know. If you find something, please comment to let me know or correct me.