Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hello everyone. One welcome to my talk. The good, the bad,
the native. I am Gregorio Palama and
I will shortly introduce myself. I work
as a DevOps and cloud engineer in Finwave.
I am a Google Cloud innovator champion and I also am
a community manager in GDG Pescara.
You can find me on Twitter
or on LinkedIn and here
you can find the QR codes to my profiles.
Okay, let's get into details and let's start with
a question. What is cloud native?
I used the definition that
Priyanka Sharma, the CNCF general manager loves
to use when she talks to people that is attached
people or to people that is not touch at
all. And she says that cloud native technologies,
when engineers and software people utilize cloud computing
to build touch, that's faster and more resilient,
and they do that to meet customer demand really
quickly. Well, what we can understand
from these words is that cloud native technology
is something that is related, strictly related
to innovation, and that's important.
Moreover, if we search about
the definition of cloud native, we can find that
the three major cloud platforms,
public cloud platforms,
offer a whole page to
give a definition of what cloud
native is. And even the links in
this slide can show us how
much Google, Amazon and Microsoft
believe that it's important to give a strong definition
to what this technology is.
I use the definition that Google gives to
cloud native to going further
into details and to understand what
we are addressing in things talk. First of all,
we are talking about using microservices.
Also, when we use microservices, we are using
containers because containers,
as CNCF says,
are the building blocks of the cloud. As for now,
of course, when we use containers, we have to orchestrate
them. So again,
containers are the building blocks of the cloud.
But Kubernetes is, again, as CNCF
says, Kubernetes is the
operating system for the cloud. So we have to orchestrate
containers in order to have a microservice solution
in the cloud native world. Well,
what does it mean in terms
of cloud native efficiency?
We have scalability. Cloud native architectures
employ infrastructure automation, helping to eliminate
downtime due to human error.
We can balance load based on demand,
allowing you us to optimize cost and performance
better. What's important here is that we
want to optimize cost and we want better performance.
Lower cost? Well, we want better
performance. But detailed
optimized cost is referring to
an objective that we want to reach to lower costs.
And a streamlined software delivery process reduces
the cost of delivering new updates and features.
Cloud native application also allow
us for sharing resources and on demand consumption.
Well, that's something that is to reach
when we are using microservices and when we
are designing them. With sharing
resources and on demand consumption in mind.
We have something that is very important
when we talk about cloud native iger availability.
Cloud native architectures provide IG availability and
reliability as they reduce operational complexity,
simplify configuration changes, and offer auto
scaling and self filling. Well, self filling
is something that we design. Auto scaling we have
to design it to inside our microservices.
So what we can say is that cloud native applications
make the most of modern infrastructure's dynamic
distributed nature to achieve greater speed,
agility, scalability, reliability and
cost efficiency. Well,
since things talk is about using an
innovative technology to better
design and build cloud native microservices,
we want to concentrate on those
words that in this slide are involved. So greater speed,
scalability and cost efficiency because
those words help us to concentrate
in building better microservices
that are available
and are scalable and reduce
costs and so on. What we want to achieve is to
have a smaller memory footprint because that
allow us to lower the cost and also
because that allow us to share resources
between our microservices. Also,
we want to use less CPU and
we also want a lower startup time.
Let's get to the JVM microservices frameworks ecosystem
and let's start with the three most used ones,
Quarkus, Springboot and Micronaut.
They are Java frameworks because
they usually use Java
to allow the development
of our microservices. Well, we can use other languages
too, but Java is the most common
choice for those three frameworks.
They are not the only one. For example, we have microprofile
for example. We also have other languages because
Java is not the only one that run on JVM.
We have also scala for example with ACA,
Lagom. Or we also have languages that
are designed with cloud native in mind,
such as ballerina, considering what we want to achieve.
So a smaller memory footprint,
less CPU consumption or faster
startup. Those frameworks
and every other one are equally
we can consider them in an
equally way. We will concentrate just
on one of them just for simplicity
and to demonstrate powerful
innovation that are brought to the JVM
ecosystem by gradm. Native image we will
concentrate on Springboot and let's
get to our demo project.
I will switch to intellij.
Ive created a very simple project.
As you can see, we all just have a demo
application and a demo controller.
No services, no repositories nothing
at all, just one endpoint with a greeting
get mapping that will answer us nullodemo
string and I wanted keep
it simple because even if
it is this simple, we will see how much
gralvm native image will help us to
optimize memory CPUs consumption.
So let's start this application.
It will take just a few seconds,
compile it and to start it,
I expect something like eight or
9 seconds to start it and oh even
less six.
It's really fast to
start, but not fast enough as
we can see. We want to make sure that
it works. So let's perform azure to our endpoint
and we can see that the answer is hello
demo. But we also want
to see for example how much
memory it is using. So we will
the system to print us the
resident set side that
gives us measure of
how much memory this process is using
in this moment. And that is the
real memory that this process is using. So you
can see 49 KB
more or less. It's easy, it's working.
We have something that is not very big
in memory and it starts in just
a few seconds, 6 seconds.
Let's get back to our
presentation and let's see what we
can do from here on.
First of all, the talk is called the
good the bad de native, referring to the
movie the good the ugly from
Ser Giuliana. And I imagined
what one of the three characters
could say after seeing
this demo. And the character is angelize
the bed. And he would say,
well, those bastards out there want more
memory and I have just a few resources.
How do you think I can scale and lower the
costs? Let's remember that we want to lower
the costs and we want to scale and we want to share resources.
So the more we say okay,
it's good, the less we can scale and
share resources. We want to make
sure that there is a way to use
less memory, less CPU, and to
make our application stuff faster.
So let's get into the details of gralvm.
The JVM is an abstraction of an underlying actual
machine that interprets the bytecode generated
by the compilation of a code supported by the JVM itself.
So what we can see
here from this statement
is that what makes Java portable
is the JVM technology. It is
where compile once runs, everywhere comes from.
We compile once into a bytecode
and the bytecode gets interpreted by the JVM.
And a standard way
that we can see of
how a JVM works is this one.
It is the hotspot JVM so the standard JVM.
So we have our bytecode, that is something
that is generated from our start code and the
bytecode will be interpreted
or compiled by adjusting some
compiler in what? Well,
in binary code. So the interpreter
or the JIT compiler will transform our
bytecode into something that our machine,
so the real machine can execute well
inside the JVM. We also have other
components, they are very important.
We have a garbage collector. We have thread
management, we have memory management.
We also have class loader and native method
libraries. All of these allows
us to create a JVM and allows
us to use our bytecode,
a single bytecode everywhere where
the same JVM is present.
So if we have a deep hotspot JVM
on Linux or on macOS or on windows
with the same bytecode, we can run.
Well, let's get to the details. Because we
said that the bytecode gets interpreted
or compiled,
that's not just about compiling
it or interpreting it. The first
way to get the
bytecode and execute it in the underlying
machine is to interpret it with
the interpreter. It is very slow because
it has to interpret line by line our bytecode,
and while it interprets and
execute our bytecode, it collects profiling
information. It also has
faster startup because well, we don't
have to load into our memory anything at all.
We are interpreting line by line,
but that kind of operation line by
line is very slow. The second option is
the C one jit compiler. The C
one compiles code when it gets frequently
executed. So we have the first way
to switch from the interpreter to the JIT
compiler. When the code gets frequently executed,
it stops being interpreted and it starts
being compiled by the C one JIT compiled compiler.
It also continue collecting profiling information
and it has a facet warm up.
We also have another JIT compiler, the C
two. It starts compiling our
bytecode and optimizing it when
it is executed often enough and reaches
certain thresholds. It uses
the profile information that are collected
by the interpreter and the C one compiler,
and it has the high peak performance.
So when we say we have to warm
up our JVM, we are referring to this
kind of compilation. We want to
execute our code enough times
and we want it to reach those
certain thresholds because when it
does, the C two JIT compiler
optimize our code and compiles
it after optimizing so we can have
the high peak performance.
Well, let's go GralvM
and let's understand what
kind of optimization it brings.
It is a polyglot VM. What does
it mean? It means that we can execute many
languages, not just the JVM classic
languages, but also languages that usually
doesn't run on a Java virtual machine.
So we can have Python, we can have Ruby JavaScript
and so on. Gralvm also
has a new compiler, a JIt compiler.
So it is the gral compiler. It is
a C two implementation. It has various
optimization, a lot of them actually,
and it removes unnecessary object allocation
on the heap memory. Also we have native image
it is not a JIT compiler, just in time
compiler. It is ahead of
time compiler. So it compiles everything
before a hit, before it is executed.
And the compilation must generate
something because it is
the kind of compiler that
will use everything that it knows
to gives us an executable file.
And of course it compiles into native
platform executable. Okay, so let's get
into the differences between the oddspot
VM and the gralvM. In the Oddspot VM
we have the oddspot VM as a
Java virtual machine. We have the compiler
interface, and we have the C
one and C two just in time compiler.
Well, in this scenario we can have
something that is called tired compilation,
and it is used when
we start by interpreting
our code, our bytecode. When our bytecode
is said often
enough, DC one compiler starts
to compile it just in time, and when
it is executed even more often
enough, C two compiler starts compilation,
optimizing it and compiling it. This is called
tired compilation. The tired compilation
is something we also have on GralVM,
but on grav we have something that
is slightly different.
We have a gral compiler instead of
a C two compiler. And instead of
compiler interface for the C two,
we have the JVM Ci for
the gral compiler. So the JVM compiler interface
is a new interface written in Java,
the same for the gral compiler. So these
two new components are totally written in Java.
And this is something that should
tell us something because, well, if it is
written in Java it will also get
compiled, and then in native
code it means that the gral
compiler itself and the JVM Ci will get
compiled by the C one compiler and then by
graph compiler itself. And if
we start thinking about tired compilation,
we can imagine that the more often our
gral compiler gets used, the more
it gets optimized by the tired compilation.
And it also has the gral compiler
optimization. If we
compare it with the C two compiler,
it has a lot of optimization.
So the graph compiler will start
producing very optimized native
binary code. And this is something that
already gives us a lot of optimization
in terms of memory consumption.
But this is not the only compiler
that GralvM offer us. We also have a native
image. Let's get into the details of
how the ahead of time compilation works,
and let's start from the inputs.
Our application code. We also have the
libraries that it uses and the JDK.
Of course, in our demo application we add
the demo application itself, the libraries.
So for example spring and spring boot,
and the JDK. Our demo application
itself uses string, and string is
inside the JDK.
Okay, these are the input for our build
phase and the build phase with the
native image compiler, a loop of
three different phases, a point to analysis,
a run of initializations,
and a heap snapshotting. While of
all of this, the ahead of time compilation
can't execute our application,
so it has to perform a static analysis of
our code to understand everything
that it needs to be initialization before
it gets executed, and everything
that needs to be initialized will
be created into the
heap and get snapshotted. And after
that, maybe if we go
on and keep analyzing our code,
we see that after
our initialization we can go
further on some executions.
And so we start again performing the point to
analysis because we can make sure we
have analyzed everything that can be
executed and will be executed.
Also, we can see from this scheme that
no oddspot VM at
all. And that's something that is not okay,
because the Oddspot VM and the gral
VM performs other operations such as
garbage collecting or class loading and so on. And this
kind of operation needs to be executed when
we perform an
ahead of time compilation too. That's why when we
use the native image compilation, we also
have a substrate VM. The substrate
VM is written in Java, and it
is something that will be compiled together with our application
and libraries and JDK, because it will
be compiled against
the target machine, against the target
architecture, and it will be
optimized by our native image compilation.
We have the output after our
build phase, and our output will
be the native executable. The native executable
will have a code in the text section that
will come from the out compilation.
Also, we have an image heap in that section,
and it will come from our image heap writing
well, let's get back to our demo project and
let's start talking about native this time.
What I will perform here is
something that is slightly different.
Let's stop our application, the JVM one,
and let's perform a native compilation.
I will use the native profile and
I will ask my
maven installation to perform a
package command. So it will create
everything that I will need to execute
my application in native wave.
Okay, let's get into the execution. It will
takes a while because of all
the build phase. So it will perform
those three steps. The point to
analysis. It will
run in its alley sessions and it will perform snapshotting.
And these three steps will
get executed again and again and again
until it will reach the end
of the process when every other step or
every other loop of our
phase step will add nothing more.
So it will stop and start generating the
native executable. Our compilation has finished,
so let's get into its details.
We can see that it has performed analysis
and it has identified all the reachable
types and fields and methods.
It has building and inlining
compilation and in the end
it created an image.
You can see that it will give
us information on the top ten origins
of the code and of the top
object types in image. It will
create an executable and the executable
is this one target demo.
Okay, we can see that target is
an executable. What we want to try
to do is to execute it. So let's get to
execute it and we will see that it will get
executed just as our application,
that user, the JVM user to work.
The first thing that we can observe is the
starting time. We had 6.6
seconds for the application that was running
in the JVM. We have 0.8
seconds for this kind of application.
It is the same application, but what
has changed is that this time
we have a native compiled application.
So node JVM,
everything has been compiled into native
code. Well, let's get sure that it
works. So we had a greeting
endpoint, so we will check
if it will give us the same result. And it
just says hello demo. And let's
get into the detail of process
ID. So let's see how much memory
using this time. You can see
that this time the memory is less
of when we use the JVM.
Well, the demo application
is very simple, so we
will not see a lot less of
resident memory that will be used.
But what we can see is that
it is just less memory that when
we use the JVM,
also combined with a very
small startup time and a
CPU consumption that is very optimized,
we can say that this time
our application is definitely more
scalable, offers us more
opportunities to scale application because it
will use less resources and those
resources are shared between our
microservices and instance of microservices.
Okay, let's get back to our presentation.
Let's get back to our objectives. We wanted to
achieve a smaller memory footprint and
we also wanted to have less CPU consumption
and a lower startup time.
With our demo application, it's difficult to
see the less CPU consumption just because the application is very
simple, but we have seen the smaller memory footprint
and the lower startup time that is very
lower. We are talking about
6 seconds against 0.8
seconds. Well, all of things
achievement helps us to have
a better scalability, to lower the costs
and to have higher availability. That's because we
are using less resources.
So if we are using less resources, we can scale
more using the same nodes
in our cluster. And if we can scale more,
of course we have higher availability. And that's
not just about scaling, it's about
using our resources in a better way because,
well, we are starting our service in 0.8
seconds, not in 6 seconds.
So we don't have to wait 6
seconds before our application can
serve our users.
It's just at least almost
immediate. We can start after zero
pains, 8 seconds, and it will
allow us to have just a few
replicas of our same microservice
to serve our users without having
them to wait until the single
replica is ready to serve them. And of
course all of these will lower the cost.
Getting back to the movie of
Sergio Leone, we could imagine Blondie the good
saying, I will sleep peacefully
because I know that the native is watching over
me. Okay, we've seen the good
things of the native image process,
but we also have a native building
drawbacks. First of all,
lot of time and more resources to build
our application. We've seen that
when I started the application using
the JVM, I didn't have to stop the recording because,
well, it takes just a few seconds to
build the application using
the JVM, so generating the bytecode,
but it will require a lot of time and a lot of
resources to perform the static analysis and to
create the native executable.
So this is the first drawback.
Also, native image generates metadata
performing static analysis.
And that static analysis is under
a closed world assumption because we are not executing
our application and everything that it
collects, those reachable
methods and fields and so on,
those are information that are gatorade with
a static analysis. So some dynamic
features require additional configuration.
Of course they can somehow
be investigated and find out by
the static analysis. But the dynamic
features such as reflection and dynamic proxying
are not so easy to be found by
the static analysis. So we will reach a
point to analysis where no more
data can be collected. And to
be sure that everything is collected we have to manually
create those metadata and
give them to the native image generation.
For example, go back to our
demo application and see slightly
different example.
We have a DTO, it's a record with a name.
And our demo controller will
have a get mapping just like before
but also post mapping. And in the
post mapping we will have a
request body and the hello demo that we
had before. This time we'll say hello
and we'll concert the name that we
give in input. Let's compile this
and let's see what kind of metadata and
what metadata it generates.
Okay, again, we have our native application,
we can start it. We have 0.9
seconds. And what we will do is
just test that the get mapping is working
well and that well
let's test the post mapping too.
And okay, it is saying hello Gregorio because
the name that we gave
the endpoint is Gregorio.
Okay, what we want to see here is not
the memory footprint part.
And let's see that all
of things application has been processed
and analyzed with some
steps that produce metadata.
And for example one of them
is this one. The GralvM
reachability metadata is a folder that contains
information that are provided by the libraries
that we are using. As we can see there
is no reference to our package
application example.
So these are metadata that are provided
by the libraries that we are using.
What is referring
to our application is inside this folder because
spring provided a plugin to
the Gralvm native image compiler that helps
the compiler understand the reachability of
the application and everything inside the application
together with the information about the reachability
of spring framework two.
And the first thing that we will
see is these folder resources.
We can see that we have a native image properties
file, but we also have a resource and
a reflect compilation. The resource
will tell to native image that
everything inside, for example meta
has to be included inside
the bundle, inside the native image that will be generated
together with the application properties and so on.
But we also have this file reflect config
and we can also for example
have other configuration
files based on what we put inside
of our application. We can see that the
demo application has been processed and the
information, the metadata that are
generated tells to native image that
it has to query all public methods because
for certain they will be used.
And for example, we can see that the demo
controller will use the greeting
DTO and the greeting DTO will
be exposing the declared fields and
the declared constructor.
Well, also it will have a method
that is the accessor to the name property.
All of this is
inside the resources folder, but we also have
a sources folder with a example
demo and inside of things.
Things spring AOT plugin generated
other information such as for example bin
definitions or pin factory registration.
These are the information that spring framework
generates when it starts. So when we
started it in the JVM mode,
it generated all of this
information and things. Information required
those 6 seconds in
the startup time together with all the initialization
of the framework. Well, in this scenario,
the plugin generated everything that
will be needed to the ahead
of time compilation to create snapshot
that will include everything that spring
would create in those 6.6
seconds. So every initialization,
everything that is related to
bin proxying and so on is inside of
these and will be used to generate the
hip snapshotting and the native image.
Okay, let's get back to the presentation.
So we have dynamic features
that will require additional compilation.
In some cases,
the native image will not be
able to collect the metadata. And this
happened when for example, we use a lot
of reflection and dynamic proxying.
Well, in this situation, in this kind of situation we
have an agent, a tracing agent that can help
us a lot because it will gather metadata
for us and prepare compilation files such as reflect
config JSON that we can use
and provide manually to
the compiler. It is very
useful because it will collect everything
based on an execution of our application. So the
tracing agent can be used. When we start
the application in the JVM, we can
use the application. So for example, we could run some
end to end tests because those
are something that will simulate
a real scenario. And this
real scenario will generate everything,
every metadata that will be used by the native image.
If we don't provide this reachability metadata,
what we can obtain is that the compilation
will go smoothly, it will generate our
executable, but when we execute it,
when it reaches the point that is
not generated using the right reachability
metadata, it will give us an
error telling us that the method
or the class that is required is not
found things is something similar
to no
method exception or something
similar to it, not really an
exception. It is an error that tells us that we
didn't provide enough metadata for
the native mage to
create the right executable.
So to include all of the methods
and classes and so on that the native
executable will need. We have to remember
that things kind of compilation will generate a
small executable and everything
that is not rigid by the static
analysis will not be included in the native
executable because we don't need it,
including it inside of the executable.
We have to obtain
a small footprint and a
lower setup time, so we will use
just what we will really need. Another drawback
is that some libraries does not provide good enough
reachability metadatas. This is something that
GralvM team is working on a lot,
together with everyone that builds frameworks
and libraries. So every time a library doesn't
provide reachability metadata,
the granitem team will work together
with the people that
creates libraries to provide
those metadatas. And this is something that is getting better
and better. Also, some include
the dependencies that we may need to manually
exclude. For example two different
dependencies that initialize two
different login libraries. Well,
things is something that we don't want
to be included inside our native
executable and this is something that maybe will
generate an error because the login facade
will not be able to choose which one implementation to
use. So we will need to manually
exclude one implementation. But it's just an
example and there
might be different cases of this
kind. Also, there are some libraries that will need
some changes, either in the library itself or
in gralvium native mage compiler
one example is aspect J. Aspect J as for
now uses agents to perform,
for example load time weaving. And this
is something that can't be done, as for now
inside the gradm native mage compiler.
So either the native mage compiler will
accept the agents or aspect j
will be changed to, well perform load
time weaving in a different way.
Last, it is better to avoid shaded libraries.
Shaded libraries are some libraries that uses
dependencies or classes that has
a name, but they change the
name or the package of those
classes. So this is the shading of
the library performs on our classes.
Well, this is something that doesn't work really
good inside native
executable. So it is better to avoid shaded libraries
and to use the unshaded one.
Well, this is my
journey. It's a denative way of
using a JVM framework
with a GralvM native image compiling.
And we are at the end
of this talk, so let's get back to the
movie and imagine what the
well, ugly, but in this case
the native will say. So Tuko
in this case will say when you go native,
you go native. So everything
that Priyanka Sharma said about
the cloud native technology should tell us
that we have to innovate.
If we innovate, we can think about
using cloud computing to build touch that's faster and
more resilient. So we said
that we want to have
less CPU consumption, lower the
costs and have smaller memory footprint.
Well, we can achieve it starting
using ralbm native image we
have to remember to use the tracing agent because
it helps us a lot and save us
a lot of time of manually
providing those reachability metadata and trying and
trying and trying over and over again. Well, just use
the tracing agent because it does all the
job for us.
And test the native executable or
perform native tests.
The native executable is something that may
have something that is not configured correctly.
For example, the reachability metadata could need
some improvement. So it's better after
creating the native executable to test
to perform tests. Maybe it's end
to end tests are a great starting
point and we have to test it because
it's something that is definitely innovative.
So we have to be sure that that
kind of compilation added everything that is
needed by our use cases. And since the
end to end tests maps the
real world scenario use case well,
they are something that is really good
to use to test the native executable
pool and be sure that we included
everything with our native image compilation.
In this slide you can find some of
the link that I found
very useful starting from the native image
documentation going to the metadata
collection and the native image plugin
of spring. And I also included
the native image guide for Quarkus.
Remember that everything that we've seen with
the demo application using spring is exactly
the same and is valid for every
microservices framework built with
a language on the JVM.
And this is all so thank you for watching me.
Please tell me if you have
found all of things inspiring and useful and.