BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations Quarkus

Quarkus

Bookmarks
51:26

Summary

Sanne Grinovero shows Quarkus in action, and explains how it works. Grinovero demonstrates what Quarkus can do by leveraging high density deployments via GraalVM native images or by targeting the traditional JVM.

Bio

Sanne Grinovero has been a member of the Hibernate team for 10 years; today he leads this project in his role of Sr. Principal Software Engineer at Red Hat, while also working on Quarkus.

About the conference

Software is changing the world. QCon empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.

Transcript

Grinovero: I work on many of the open source projects in the JVM world. I'm very interested in performance. I love contributing to other projects or receiving contributions to projects. During these meetings, one of these friends, his name is Alex, we were contributing to each other's projects a lot, exchanging ideas to have very high performance solutions of the JVM, working on caching technologies. At some point, he changed jobs. He was not contributing to Hibernate anymore or to Ehcache anymore. He went to work for Shopify. We met again at some event. He has this super interesting feedback about Shopify. He's happy because he has a super high performance infrastructure to manage. He's a technical lead there. It's very high load. I'm asking him, you will be learning a lot about how to tune Java, how to do even better database access or caching. He's like, "Sadly, I know all this stuff for JVM. We can't use Java here. This is all Ruby on Rails." I'm completely shocked, because he's telling me that they are willing to invest millions to get Ruby faster, because they have a very huge business case to make their infrastructure very efficient. There is a very serious amount of money to be made. They're spending a lot of money in cloud technologies. I'm like, "Why are you using Ruby? Everybody knows that if your performance is really important for you, you should move to the JVM." He's like, "The JVM has a warm-up problem." I was like, "We all know about warm-up. What is so special about your situation?"

Let's talk a little bit about warm-up. You're all experienced developers here, I think. You all have heard probably, that if you start an application in Java right away and you measure its performance, it's not going to be super great right away because you need to warm it up. While other technologies like Ruby on Rails running on its basic interpreted mode, it just starts performing right away. It will never achieve the same performance as the JVM. At a very high level, the performance is going to look something like this. You have your application. In Java, it starts slow and you need to explicitly warm it up. You need to have it get a little bit of load so that the runtime starts to learn, what am I supposed to do? Which are the hotspots? Which area is the code more likely to be invoked? While the other applications will just perform ok. They don't have the overhead of needing to watch and monitor everything that's going on. They have the performance advantages over there.

Of course, after some minutes, the JVM takes over. That's why we like Java, because it can get you very good performance per dollar. If we're talking about cloud, it's becoming more important to think, what's your total energy consumption of all these machines and containers you're running? Another aspect is, how much memory are you paying for all of these things? Another problem there is the JVM really is quite large. Even if you run a simple service, it starts by having half a gigabyte, at least, that's the baseline starting point for a small service. The problem here is really, it takes about 10 minutes after an average load for an application to take over then to be a good choice for your system. What if you do continuous delivery properly, and you have a large team who's pushing all the time like you have at Shopify? It turns out they're deploying in production directly from any commit. There is no intermediate QA or staging platform. They have a proper continuous delivery thing, they have gates and checks and canneries. Then it's pushed in production, on average, every 5 to 7 minutes. You never reach the pool performance. Or, in their use case, it's going to look something like this. If you revert this graph and think that these are your costs, it's clear that you really cannot use Java in this system, even though you want the best performing application, because it doesn't work at the performance you need. There are other runtimes that will perform better in your specific conditions. This was mind blowing to me. As a Java person, I was like, I understand continuous delivery is pretty good but this is a problem.

Black Friday: Our Worst Nightmare

We work in IT. You probably had nightmares of Black Friday is coming. You will need to be able to scale up your infrastructure a lot. Nobody's going to be able to tell you how much exactly, what's the load going to be? That depends on marketing. Is your marketing campaign going to be effective? Your marketing and business people are going to tell you, we are going to be crazy. It's going to be super awesome. The problem is, if you pre-run a lot of JVMs ready to get these billions of people that want to go to your website, you're going to pay a huge bill for all these servers that are running there, but not really processing much if the expectation isn't reached. The opposite is also possible. You're trying to scale dynamically depending on the load coming in but the JVM takes time to start. Then it takes time to warm up. You cannot predict the load as effectively as you would with another system which just starts right away. That's why Lambdas or simpler services are just way easier to run on a cloud environment if your load isn't predictable.

This starts to be a big problem, because we're seeing that Java isn't then a good fit for continuous delivery, or it's not fit if you want to scale elastically, scale on the cloud. It doesn't fit with trends people. It doesn't fit with reality. We can go back and have our fun with performance and benchmarks. If we are not fitting with reality, then maybe, we shouldn't use this technology. That's a big one.

I'm Sanne Grinovero. I work in the middleware engineering for Red Hat. We do research and development in the area there. I'm very focused on the Java aspect. This was mind blowing to me, like, "The JVM is over. It's not a fit for the technology needs of today anymore, unless we can solve these issues." I also work on Hibernate, primarily. I've been working on that for 10 years. I'm leading the team now. These thoughts that we're having today got me to work with a small core team within Red Hat, which was the protean research team. Protean was the codename for what Quarkus became.

What Is Quarkus?

What is this Quarkus? It has this weird tagline. It might sound a bit embarrassing if you're looking for facts and numbers, but supersonic. What we mean by that is it's designed to be really fast, orders of magnitude faster than other technologies that we have seen before on the JVM. We're going to see how that's made possible. What are the drawbacks for you? What do we need to take into account? Or, is this a good platform for you to use? Why is it subatomic? We're referring really to the low memory overhead. A typical framework, for a small service, you will need at least half a gigabyte of memory, which used to be fine. Now if you're doing microservices and lots of containers, lots of small microservices, half a gigabyte each for a small thing you're starting to consider, we could write this in something else. It's going to take a tenth of the size? We want to achieve the same size without having to abandon all the nice tools that we have in the JVM. We don't want to have any performance drawbacks. The JVM is great. We want to keep having these advantages.

What is Quarkus really? It is a bit in the finer space. It's a toolkit. It's also a bit of a framework. Clearly, it's designed to create Java applications. At this point, it's a bit limited to web development, which we are going to expand that. The focus has been web applications, REST endpoints, components for microservices. It's very light. We have been designing it for GraalVM from upfront. When we saw what the GraalVM team was doing, and we were thinking about this general problem, we were thinking, "There are so many opportunities here to do something great together." GraalVM has some limitations in terms of, it's not really easy to port your existing application to a GraalVM native image. We are overcoming all those limitations. We actually love those limitations, because depending on how you look at those, the fact that you cannot do certain things but because it needs to optimize for certain things, they fit perfectly what our goals are with these other projects.

At a high level, what we do is we take your application, which is defined by classes and configuration files as you're used to. Rather than making a runnable Spring Boot application, or a WAR deployment, or an EAR deployment at your dead stack in another application server, we process this at build time and we create an optimized runnable JAR. This optimized runnable JAR, you can do two different things with it. You can either run it on the JVM, as-is. Java JAR, start this JAR thing. Or you can process it again with the GraalVM native compiler, and we're going to produce an ELF binary, so architectural specific little binary, which is pushing the boundaries even further.

The architecture of Quarkus is built around extensions because we actually need to do some specific magic for different aspects of what we're used to in the Java ecosystem. For example, Hibernate will have an extension. Quarkus has a core thing and there is a Quarkus extension for Hibernate. Then there is a Quarkus extension for Vert.x, and a Quarkus extension for RESTEasy, Camel, Netty, Infinispan. There is a ton more. This slide was one of the very early launch times. It's been a year since we announced the project. We have hundreds of extensions today contributed by many people.

It also has an aim to embrace reactive both in external API, so you can use reactive APIs or you can use imperative APIs if that's more your cup of tea. Internally, we are making sure that everything is moving to a reactive as much as possible so that you have zero overhead and extremely good performance. If you are more into the reactive camp, and you need to push the boundaries. If you want to just port applications you're used to writing, they will just work as-is out of the box. They will benefit from improved performance internally.

One of these goals is really that it's designed to be container friendly, which is a new thing for Java. First, we had Java and we didn't have containers. Containers came along and existing frameworks have to work with it, but they were not really designed for that upfront. Our aim here is that the application that is built is also actually small on disk. All the stuff that we can recognize this is not going to be useful at runtime, it's stripped out. It's not going to be necessary. We're also making sure that the way the dependencies are layered, we're not uber-JARing everything in one thing, because that makes layering of container images not very effective at all. We keep the libraries separate and change it, and your application separate, so you can change your application very quickly and ship just some kilobytes of difference as a Delta on top of your existing images, and boot it. Of course, we want to boot very fast, and we want to consume a lot less memory. When it comes to memory, we're not talking about heap sizes. That's not the point at all. If your application is running in a container, the Linux kernel is watching it from outside. It doesn't really care how much heap you're using, what it cares is how much resident sets memory you're using. This needs to include all the metadata and overhead that the JVM has on top of the heap and stacks. The more processes you have, you will consume some more memory. The garbage collection process will need some memory on its own. All the definitions of classes, the metadata of classes, and Char tables, all of the nuts and bolts that actually make the JVM. They consume memory that we normally don't see when we're profiling applications. We're trying to take into account all this black matter, as well, and minimize for that.

Measuring Memory

When we're measuring memory, this is how we suggest doing it. This is while on Linux, or Mac, it works as well. You just need to put the process ID here and it will dump you something like this. This is in kilobytes. This is the total memory consumption of the process. It's not the heap. This is typically far larger. You can constrain the total memory. There are some new flags here. I have put a link here to a blog from Christine Flood who is one of our colleagues at Red Hat. She's super deep into how garbage collectors work. She's the designer of the Shenandoah garbage collector. They added some flags in JVM to allow you to better control Java running in a container and explicitly bound not only the heap but also the full process memory. What happens when you are not thinking about it? The kernel will see, if you're running Kubernetes or OpenShift, if you're going above the threshold that you allocated, your process is killed. Then it will restart it, but you're back in the warm-up problem and your service wasn't available for a while.

How do we compare? If I build a small REST service, and by REST we are going to use RESTEasy and Undertow, and we expose some toy operations. It's not a very complex application. Then we compile this to GraalVM native. We can run this in a process of 13 MBs total. The heap size, I believe, is 2 MBs. Then there is 11 MBs of overhead. That's thanks to the GraalVM native image compiler. You can run the same thing on OpenJDK hotspot, on the manager of the JVM, it will consume 74 MBs. This is compared to any other best-of-breed stack that you can find. If you can test most of them, the best of those will consume about twice Quarkus.

If you start making a more complex application, let's introduce Hibernate transaction manager. Infinispan for caching, and the JDBC driver, the JDBC connection pool. We're not talking about toy connection pools, but Agroal is a very high performance connection pool. These are production grade components, but running in Quarkus. The total memory consumption of the process in native image is still extremely low compared to what we're used to. Same for JVM. To give you a ballpark idea, this 130 MBs here, this is probably consuming 20 MBs of heap. It gives you an idea, you're wasting 110 MBs in other stuff in the JVM that you normally are not looking at. Watch for it. It's not a small amount in the big picture.

How are you faring with startup time? First off, how we measure this as well. It's not fair to measure what the application itself is logging because the first timestamp it's taking is after the application actually was able to do anything but throws at least the JVM starting before that, which is a very large amount of stuff. The other problem is many frameworks actually initialize some stuff lazily. They will report, now I'm done starting. They measure way after they started. Then they report I'm done way before they actually finish because there is a lot of lazy stuff happening in the background. They're telling you, it was just 2 seconds. That's not how we want to measure things. We want to measure from before the process was created to actually successfully returning the first request because that's what matters for our business.

Time to First Request

I have a little script for that. It turns out on REST, in GraalVM native, we can respond from process doesn't exist to actually respond in about 10 milliseconds, as a full running, no lazy loaded services. On OpenJDK, it's way longer. We are talking orders of magnitude difference, but we're still below the single second mark. It's much more suitable to scale this dynamically or run it in containers at high speed. There is no other framework at this point that can get closer to 4 seconds, if you do these external measurements. You start introducing more frameworks. Let's do JPA as well. Competition skyrockets to 9-and-something seconds, while Quarkus in native can still boot in around 50 milliseconds. We can actually boot this in 10 milliseconds as well, even on this very low power laptop. What happens in 40 of those milliseconds is we're creating this schema on the database. We're establishing all those connections and doing stuff on the database. The actual boot of the framework is really still in the same ballpark as the rest. On JVM, it's a bit more complex because there actually are a lot of classes to be loaded. It's still going to be about 2 seconds. We have 2 seconds, but on a very underpowered machine. We're measuring here on a single core CPU. Why are we doing that? To be able to compare that to different things. The amount of cores you have actually influences a lot on how much memory you're consuming. To make sure we can compare things right, we are doing all metrics on single core, small, limited containers. In those conditions, you can boot in 2 seconds the full stack.

CRUD Demo

I have this small CRUD demo. It's off because it's not running. Let me show you the code first. This is IntelliJ. This is how a configuration file in Quarkus looks like. It's a simple properties file. What we need is some JDBC configuration, of course, we cannot really guess those. Pretty much everything else is guessed or inferred. You can have additional properties but that's more to override what Quarkus is automatically understanding from your application. Because during build, we're actually analyzing everything. It's an X-ray of your application and your dependencies. From there, there is some logic that's being triggered. I also wanted to drop and recreate the schema every time we start, and import my new example dataset.

My example dataset is here. We have three fruits to be inserted in a database. Then we have a web page, which I'm not going to show you. It's showing you what's going on. We are doing the backend here. We have a JPA entity. This is full Hibernate. Full, unmodified Hibernate latest version. Unmodified, of course we patched it, but now all the patches are in Hibernate. This is using a name it query. It's a cacheable object. You can use anything you normally do in Hibernate. It has a name and it has an ID. We also have a JAX-RS REST endpoint mapped with RESTEasy. We're going to have a method to list all the fruits, a method to retrieve one fruit from the database to create a new fruit and persist it, update it, and delete it, CRUD. I also have a helper thing to print timestamps when I measure for booting, and some more helper.

This is all the application. There is no need to add additional ceremony because you wanted a REST endpoint. Fine. What do you do? You inject an entity manager. There is CDI here of place. CDI isn't going to be running at runtime. Everything is being processed at build time and when generating the code that's making this magic work at runtime, as you would expect. Of course, this is a Maven project. You could use Gradle as well. We are importing some dependencies from Quarkus. Then, we are importing the support. If you remember, this approach via extensions. We are importing the extension which takes care of Hibernate. It's transitively pulling in a dependency in Hibernate. It's not having a different version of Hibernate. It's just making it easier by having transitive dependencies. Then we want the connection pool, RESTEasy, and the JSONB encoders. The JDBC driver for MariaDB, and smaller open APIs, I like to use those. We have testing support.

Let's run this. First, I need to build it. Clean package. Not much magic here going on. It's just Quarkus. There is a Quarkus plugin here, Quarkus Maven plugin. That's what's doing all the magic. You add that thing, and now what we have here is the JAR which contains those two classes and the resource file that we have. We also created a runner JAR, which you can run like this. Quarkus demo MariaDB. I want to run their version. Start this up. There it is, up. It started. Let me go back to our app service. I have those three fruits that we created, a cherry, a banana, and still just an apple there. Let me store a new fruit. Everything is working fine. This is pretty much Quarkus. The difference really is how it processed all of these things. It booted in a second and a half, but we're not doing the proper measurement I explained.

There are some scripts here, show the RSS. This is consuming now 290 MBs of memory. Of course, because we didn't really constrain the memory at all. The JVM does use ergonomics. Let's stick this in a 30 MB heap and see, does it still start? Does it still work? Yes. It didn't even consume far more. Of course, this is getting quite lower because heap is there, but there still is some other overhead like this. Of course, if you compare this with other frameworks, you will see a very higher cost. This is complete cost of the processing memory.

How a Traditional Stack Works

How does this work? Before explaining how this works, let me explain how Hibernate, or other frameworks, or the typical libraries normally would. What we have is your application, which is a Java JAR or maybe a deployment. At boot time when you're starting this thing, let's say you're booting Hibernate. You have many frameworks in your stack but you're going to boot one by one all of these components. First thing is, where are the configuration files? I don't know if you're using a persistence XML file, or maybe a Hibernate properties, or Hibernate HBM file. There are multiple ways that you can configure this thing. I need to search for all of these things. I need to search them on the file system in various places, and I also need to search them across the resources. We live in a modular world, modular application servers. Searching for resources means I need to actually check different class loaders in different orders, there are their rules to expect. I'm going to check for the first resource here, I fail over there, I fail again, exceptions being swallowed. There is a lot of stuff going on there.

When I find one, let's say I find the persistence XML. It's an XML file. Let me boot the XML parser that the JVM has. That's 300 classes that need to be loaded. They need to be initialized. They have their static constants in there that need to be initialized as well, that's 19 MBs of memory just in constants, if you measure that. That's not to parse the file, that's just to boot the XML parser. We can finally parse this thing and create a model, which is our configuration file. From there, there is more work to do. At that point, it's like, this is the configuration, where are the entities? Off we go, again. Let's find anything that might be annotated with JPA annotations or Hibernate annotations everywhere in the classpath, because I have no idea where you might be hiding those, and you're going to complain if you're not finding them. This scanning, in the old times, we would do it with reflection and it would be very slow. The cost of this is depending on how many JARs and how large are these files that you're having on cluster. We have many of those normally.

Then we need to find extension points. You have this additional plugin, or service, or callback factory that you want to be registered with the integration thing that we automatically register and bind to it. That's more plus scanning and searching. Finally, we build this in-memory metamodel, and most of the time an exception, you forgot a critical annotation there. Let's start over, kill it. When you're lucky, at this point we need to create an optimized version of this model so that then when we are actually working, we have most of the prepared statements that we will need. At runtime, they will need to be ready so that we're not generating too much garbage at runtime. All the injection points need to be created. We will need to create class definitions for proxies. We might need to enhance your objects, which means bytecode manipulation of your classes, which also implies if I'm changing the bytecode of your class, then the classpath. We need to get rid of the old definition, we need to drop the classpath, create a new class loader and start over. Then finally, you validate world. This was just Hibernate.

The next framework starts and says, let me find everything which has the JAXB annotation or the CDI annotation. This is why it takes a long time to boot and why there is a ton of classes being initialized in the JDK because there is a lot of work happening when your application is starting. Finally, eventually, we can open [inaudible 00:31:57], we can start responding.

That's quite some work. It's intensive on the JVM side. It's loading a lot of classes like this XML parser. It is 200 XML parser files, which you only need once to parse the configuration file beginning there, all being loaded, and maybe optimized, maybe evaluated for compilation by Git and all that. That's a lot of fat. When you put this thing in a container, and this fat starts to matter, that's one thing. You're paying some cost there. What's more worrying is that you're paying this cost many times when you go to microservices, or continuous delivery, and you're redeploying over and over, every time you're redeploying or scaling up your service. You're paying that cost. You can put some number in dollars on that cost, $1, $2, it depends on your system, but it's one each.

While in Quarkus: Build Time Boot

In Quarkus, we try to do everything, or as much as possible during the build of the application. It makes a lot of sense, if you think of it in terms of containers and cloud environments. We have bytecode processing, or bytecode generation. We capture the state of what's a working state for your application and dump that to this, so that then this is the state you can go directly into runtime at. This is how it looks like. The configuration, the analyses, the scanning thing, we do it via this Maven plugin, or doing the Gradle plugin. The drawback is you're not allowed to make changes after the application is built. Forget about the idea of, I'm packaging my application, then I deploy it on a server and configure. I change the data source from Postgres to Oracle. Who will do that anyway? I hope you know which database you're going to target. We all know that it's not a good idea to change a container image after you shipped it, so you're building a container image here and that's meant to be immutable. The state you need there is all you have. You're not going to make changes, or you want to maybe build another one. That's entirely possible. This state at the end, that goes directly to load the classes we need for actual running, and only those, and that's up.

Let's get the second tool, so the extension model. These extensions that we have, they take care of helping each of these individual frameworks to accomplish these optimizations. We can also physically strip out all the stuff that you don't need. For example, I had this rant about XML parsers. If you need XML parsers, because you're parsing configuration here, but you don't need them at runtime, in a Quarkus build application, the XML parsers will not be in the final shipped with code. They are not there because they're not really needed. The same goes for a lot of other dependencies. Hibernate does bytecode enhancements of entities. We use Byte Buddy for that, which uses ASM. That's many very cool libraries, but we only need them during the build. They will not be included in your image at the end, which is great for many reasons. You're not going to support those libraries at runtime. They're not a security liability or anything. An extension looks like this in terms of Maven models. You have a deployment Maven model, and the runtime Maven model. Every single extension in Quarkus is modeled with the same pattern. We can actually separate the code physically, like this is going to be run in this phase, and this is going to be run there. Also, we're only going to ship the dependencies and the model of runtime itself.

Jandex is a library we use to find all the annotations. It has many great features. The main one is it doesn't initialize the classes at all. It's going to search or give you a way to identify classes which have specific annotations or specific interfaces directly from an index. This index can be pre-built. It can be stored in your JARs. If you want you can do many interesting things. The most important aspect is we do this once on behalf of all the frameworks that are booting. There is no exponential complexity there. Arc is a new project that we use. It implements most of CDI. It's not a full implementation of CDI at this point yet. It does all the dependency injection. It does it all at build time. You have no overhead at runtime. Gizmo is a bytecode generation library that's helpful to create this state or create these representations of state at your application.

Design Consequences

This means you're loading far less classes, and it's smaller. The final point is really interesting. Most of the things that we do at build time are exactly the red flags to compile something in GraalVM native images. There are some rules, you cannot do bytecode enhancement, you cannot do proxies, you cannot do reflections unless you specifically make the full list of all those things. All of these things were done at build time. They are not run later. They are not even part of the set of code that's being compiled by GraalVM. They're not a problem at all. We can do this in a far easier way than convert a traditional application to GraalVM.

Just general architecture. We have these basic components, the Graal SDK, which we use to give hints to Graal compiler about what each of these components are needing. Every extension and lots of people have contributed extensions, you can add your own there. Every extension is driving these basic core components, so that for example it can let the Graal compiler know about I'm going to need these specific flags or this specific behavior. Or, I'm going to need annotation scanning and so on.

There is one more consequence here that I have not mentioned yet. It's really nice. We call it developer's joy aspect of Quarkus. This is a consequence of being so extremely light in booting and also not loading all those classes at all. Let me show you, I'm going to kill the application I started before. We're going to run it in a different mode, development mode. Develop mode, is up and running. It even opened the debug port. What does it imply? First off, it resets my database. We are setting this to import the schema every time. In development mode, you have the JVM running there, and it's now watching your project for changes. If I switch to my IDE, now I can make any change I like. Let me add a fruit here. If I add a fruit, now easy, I change a resource. Let me refresh this. What's happened? You see in the bottom left, hot replace total time, 360 milliseconds. This was a full reboot of the stack. Why was it that when I start using JVM mode it takes 2 seconds, and now it took only 300 milliseconds? Because we are not restarting the JDK. The biggest overhead is all the JDK classes that need to be reinitialized. Here we have a complete drop of the code of your application. It's completely being rebuilt, plus enhancements, scanning, everything that we need to do to build your application is being done in those 300 milliseconds and then is restarted again.

Do I have a mango here? Yes, I have. Let me delete some. This was easy, I changed the resource. We're not using agents like JRebel, or anything. Those things, they have been amazing. There was a difference between restarting your application from scratch and clean, and seeing if the agent was able to do some magic for you. Let's do something which is a bit crazier. I could change the entity. There is this find all here, they are sorted by ID. Let me change the query. Sort by name. I save. I go here. I refresh. There I have a, b, c. I still have the mango in my input, of course. Don't you believe you changed the code? I can go back and set the ID because it was maybe not too clear. It's no longer a, b, c. I made the change to the entity. Everything has been rebuilt so even the metamodel of Hibernate has been restarted and the whole application has been restarted. That's already because it is very lighter, there isn't much that needs to be reinitialized or rebooted at all. That's developer joy. You just change your code while it's running and you keep going.

I can even make errors. Let me show you one. I forgot the ID here. You get there. The Hibernate saying you have no identifier here. We can go back and fix my thing. There we are. Application working again. It feels a bit like PHP, it feels a bit dirty. That really is the point. You might have seen people that you need something very light, you do it in PHP. Where a change is needed, you just SSH to the server, make the change to the page, and you're done. It's very dirty, but sometimes it's very effective. These extensions you are needing them to bypass the GraalVM limitations, but they also enable you to do all of these optimizations.

Where is the catch? There isn't really. It's running, consuming less memory, which is just a good thing. You're also having far less code actually loaded in the JVM, which is great because the just in time compiler can focus on the code that matters for your runtime and not on all the other garbage which is there. A lot of optimizations the JVM can do are based on how many types of these kind are around? Can I optimize this polymorphic or megamorphic call or not? The performance is pretty good.

What do I mean by no compromises? You're really running here the best-of-breed of the JVM stacks here, they are unmodified or even better in some cases. For example, the HTTP layer is based on Undertow. It used to win all the benchmarks on older TechEmpower benchmarks. At some point they were being run and they would have been won by Undertow. That was Undertow 2. Then Vert.x took over. The Vert.x team and the Undertow team, they're working together on the Quarkus REST endpoint. It's going to be the best-of-breed of those two things. This is work in progress, but it's almost merged already. That's just an example. We are also very focused on, we want the best runtime performance. It's not just about the build and things.

Quarkus Wrap-up

It's the same good old Java. You have seen this code. How long is it going to take you to learn this? It's the same RESTEasy. It's the same Hibernate. It's the same other 200 frameworks that we are supporting there. It's just being built and packaged in a different way. There is this notion of application server, which we are getting rid of because of containers and how the classes are working.

Native Image Performance

A note about native image performance. When it's compiled all the way to native, the performance is slightly lower. Let's build this as a native application. Let me check if I'm running the right JVM here, no. I'm using the Graal version 19.3.1 for JDK 11 now. Let me do a clean package, but enabling the native profile. This is going to do all the same processing that we did before. This is going to take a couple minutes. That's why I let it run right away. It's also interesting to watch what it is doing. If you see here, that's the command line that's invoking the native image thing. You have some statistics here, but in between, you'll see, Hibernate core 5.4.10. What? This is the compiler. It's starting the frameworks in the JVM. The compiler is running in Java. That's how GraalVM works. It's actually starting our libraries. We can delegate the work of building this metadata, validating the model, creating the state that you need at runtime that's being created within the memory space of the compiler. We have booted up Hibernate, the only thing that's missing there is of course, we didn't do connections to the database. We stopped right before that point. When you have that state, now we take a snapshot of the heap and that's what the native image is then compiling to native code.

The actual booting of the frameworks is completely done. There is some very nice additional benefit from this. It means that all of the state that's being generated and captured during the boot is a compile time constant. The compiler can go nuts on optimizing additional code based on the assumptions that it's gathering from this output. By nuts I mean it can actually remove every single symbol and method or class, which is not going to be absolutely necessary at runtime. It's almost done. It is slightly lower than the JVM mode, at least in our tests. It will depend of course on your specific application. Generally speaking, with C, it is 30% slower with our typical Quarkus REST endpoints. Still, it is a winner. It's a very good economical choice of technology because it consumes far less memory. If your build is in amount of megabytes that you're consuming, and most of us are, maybe you're not aware but all machines on cloud they are scaling up mostly by memory cost more than by CPUs' costs. You don't need warm-up. My big problem is JVM is not fit for this line of business. Now it is. This isn't a source problem. This is making it fit to run your applications written in Java using Java libraries. You have no warm-up problems and you can instantly scale up and down. You can create lambdas with this code. There is a very nice additional thought, which is you don't really need to make a choice upfront about this. You can build everything in Java, use the debuggers. You have the JVM, have fun on the JVM. Then, at some point, if you see that the needs you have at runtime are a better fit for the native image, you just compile it in a different way.

Let's see if the compilation finished. It's there. This is a binary, so I don't use Java to start it. It's target. There we have Quarkus demo, MariaDB runner, no JAR. What is this file? It's an ELF Linux binary, and if I press enter, it's up in 17 milliseconds. That was including connecting to the database and creating the schema and everything. We can check for that. Where is my browser? There it is, again with the four things that we save. It's a fully functional application. There is absolutely no difference because the compiler just took the same code we had. It packaged it in a different way.

 

See more presentations with transcripts

 

Recorded at:

Jul 08, 2020

BT