Cloud Native Blog - Container Solutions

Minimesos gets a REST API - finding the lightest Java web framework

Written by Ádám Sándor | May 31, 2016 1:01:23 PM

We started work with Frank on what is probably the most important new feature of minimesos in recent months - a REST API that gives easy access to minimesos from any language. Minimesos is a testing framework that lets you run and test a Mesos cluster on your machine. It is implemented in Java and up till now only had Java and CLI APIs. This changes now - a RESTful webservice will wrap the Java code and enable the implementation of different clients.

Our first step was to select a suitable web framework to build our API. As the cluster management logic is already written in Java we constrained ourselves to Javaland. We quickly realised that the framework should first and foremost be extremely lightweight. Our goal was want to minimise the overhead we add to the Mesos cluster. With this in mind we collected 3 frameworks for our shortlist:

  • Spring Boot - because Spring is the most mainstream Java application framework and Spring Boot is aimed specifically to be a microservice framework build on top of Spring.
  • Spark - because it looked super lightweight
  • Vert.x - lightweight and cool by having an async callback-based programming model

We are sure that this list could be extended but we felt picking three such diverse frameworks gives us good enough options. To measure them we created this test: https://github.com/ContainerSolutions/minimesos-webfw-evaluation. It's a collection of 3 Maven modules (one for each framework) each building a web app with a single /hello endpoint. Two Groovy scripts do the testing. At this point I would like to mention that we are building a very simple service so we felt we won't do a lengthy selection process. Neither the shortlist nor the measured metrics are super scientific - they are only good enough for our purposes. But I feel that the test framework shown below can be nicely extended with additional frameworks and metrics so it's a suitable base for anyone's more thorough investigation.

RestServerStartupTest.groovy Executes a single test run of a single framework. It starts the web server and polls it until a response is received. Afterwards it writes the results to the csv file of that framework.

 
def jarFile = args[0]
def fw = args[1]
 
def outFile = new File("${fw}.result.csv")
if (!outFile.exists()) {
   outFile << "Size (B),Memory (B),Time (ms)\n"
}
 
println "Starting server"
def proc = new ProcessBuilder("java", "-jar", jarFile).redirectErrorStream(true).start()
proc.consumeProcessOutputStream(System.out as OutputStream)
 
def pollingThread = new Thread({
   def t0 = System.currentTimeMillis()
   def poll = {
      try {
         def body = new URL("http://localhost:8080/hello").text
         println "Got response: ${body}"
         return new groovy.json.JsonSlurper().parseText(body)
      } catch (IOException ex) {
         return null
      }
   }
   while ((resp = poll()) == null) {
      sleep(1)
   }
   def time = System.currentTimeMillis() - t0
   def size = (new File(jarFile).size() / 1024) as int
   def mem = (resp.usedMemory / 1024) as int
   println "StartupTime[$fw]=${time}ms"
   println "ExecutableSize[$fw]=${size} KB"
   println "UsedMemory[$fw]=${mem} MB"
 
   outFile << "$size,$mem,$time\n"
})
 
try {
   pollingThread.start()
   pollingThread.join(10000)
 
} finally {
   proc.destroy()
}
  
  

BuildAndRun.groovy Builds all 3 Maven modules (uses mvnw wrapper script, so no need to install Maven), then runs the main script 10 times for each.

 
println "Starting..."
 
["springboot", "spark", "vertx"].each {
   println "building $it ..."
   "./mvnw -f $it/pom.xml clean package -DskipTests".execute().waitFor()
}
 
new File("").absoluteFile.listFiles().each {
   if (it.name.endsWith(".csv")) {
      it.delete()
   }
}
 
(1..10).each {
   run(new File("RestServerStartupTest.groovy"), "springboot/target/springboot-0.0.1-SNAPSHOT.jar", "Spring")
}
(1..10).each {
   run(new File("RestServerStartupTest.groovy"), "spark/target/spark-0.0.1-SNAPSHOT.jar", "Spark")
}
(1..10).each {
   run(new File("RestServerStartupTest.groovy"), "vertx/target/vertx-0.0.1-SNAPSHOT-fat.jar", "Vertx")
}
 
println "...Finished"
  
  

Running the scripts on my machine produced the following results:

Framework Executable Size Memory Usage Startup Time
Spark 2.3 MB 3.5 MB 0.5 s
Spring 12.6 MB 14.3 MB 4.5 s
Vert.x 4.3 MB 5.2 MB 0.7 s

 

The verdict

It's clear that Spring is the heavyweight champion here and that's not what we want. It's a bit tough to leave Spring behind as there is always the cozy feeling of knowing that any possible feature you will need will be on hand but we probably will need very few. Clearly Spring Boot is still bogged down by the long legacy of Spring MVC and the Java Servlet API underneath. It's huge compared to the competition by any measurement.

Vert.x is clearly fast and small. It lags behind Spark by a negligible amount. With Frank we were both excited to learn the reactive style Vert.x offers but in the end decided against it. The minimesos API will be a very straightforward and simple webapp so it's not the right time to dive into the complexities of a new programming model.

So we opted for Spark which is both the lightest and simplest of the three. It does lack some features I really liked in Spring MVC like mapping data from the request to method parameters but we can live with that. JSON serialisation is not provided out of the box either and there is no plugin mechanism to hide this from the main code which again is a step back from Spring. On the other hand Spring does such a great job at hiding all the boilerplate that my controllers usually end up as one-liners. So if we are going to have some web code we might as well put some logic in there. (Not the best argument I know but what the heck). Here is how the main method looks like excluding supporting code:

 
public static void main(String[] args) {
   port(8080);
   get("/hello", (request, response) -> {
      response.header("Content-Type", "application/json");
      return new HelloData("param1", "param2", usedMemory());
   }, JsonUtil.json());
}