The heart of the Sunshower.io platform is a distributed virtual machine that we call Gyre. Developers can extend Gyre through Sunshower’s plugin system, and one of the nicer features that Gyre plugins provide is a dependency-injection context populated with your task’s dependencies, as well as extension-point fulfillments exported by your plugin. For example, take (part) of our DigitalOcean plugin:
The details are beyond the scope of this post, but basically Gyre will accept an intermediate representation (IR) from one of its front-ends and rewrite it into a set of typed tuples whose values are derived from fields with certain annotations. The task above will be rewritten into
(ComputeTask, Vertex, TaskResolver, TaskName, Credential). This tuple-format is convenient for expressing type-judgements, especially on the composite and recursive types that constitute Gyre programs.
As an aside, there are Gyre rewrite rules that can infer recursive and co-recursive types (think singly-linked lists) up to a configurable depth. Sunshower.io uses Spring extensively, and it was quick and convenient to, for a given program, subsequently rewrite a Gyre program into a set of typed tuples, union those suckers together, and if the resulting tuple was satisfiable, create a Spring ApplicationContext with all of the dependencies, start it up, and then execute the program in that context.
For quite a few tasks that we’ve encountered in the wild, these tuples can grow quite large, and so I was curious as to how memory utilization would grow relative to Gyre program size.
We have used Sunshower.io’s optimization capabilities to analyze our own workloads, and for current uses, 4 GB heaps work quite well for us from both a cost and performance perspective. However, we don’t have much experience with thousands of concurrently-executing Gyre programs outside of simulations, so I wanted to figure out how badly using a Spring ApplicationContext per Gyre program would burn us, if at all.
I wrote up a quick utility to inspect the size of an object using Java’s Instrumentation mechanism. Basically, we just perform a breadth-first search of the object graph and sum the individual sizes of each field as reported by
java.lang.instrument.Instrumentation (feel free to ask me for the code — I haven’t had time to open-source it, but I do intend to).
Now, I used the Gyre to generate a configuration that looks like the following:
Now, testing it out yields the following memory utilizations for different numbers of beans
(All sizes in bytes)
Now, Spring has a lot of features, and is correspondingly large. The base size of the object graph of a AnnotationConfigApplicationContext is ~3.2 MB. The size of an empty, loading AnnotationConfigApplicationContext is ~4.7 MB. Now, there isn’t a whole lot of information to be gleaned from this, other than that Spring has a (relatively) large initial memory footprint. The good news is that it doesn’t grow very quickly. As an example, with all of our existing plugins loaded into their own application contexts, as well as Sunshower:Stratosphere, Sunshower:Core, and Sunshower:Kernel (about 10 individual application contexts and about 1400 services, including a JPA context), the total application size is ~78 MB. Is that a lot? Not at all — remember our 4GB heap. As a percentage of available memory, that amounts to a paltry 1.95%.
However, the problem is that we have to serve thousands or tens of thousands of these application contexts per minute. An empty Gyre graph will set us back 4.7 MB, which will allow us to have ~850 concurrent Gyre operations/node at best. And that doesn’t allow us to have any user data at all. That’s a bit of a problem for us. So, in an effort to reduce Gyre memory footprint, I decided to look at Google’s Guice.
The setup is basically the same. I generated a Guice Module, once again using Gyre, that looks like:
This used a pretty paltry 0.11 MB on average, which remained largely unchanged even as we defined several of the required services from our Spring application context into the Gyre Guice context. That allows us to easily meet our target of 500 concurrent Gyres per node without eating into our user data space.
I want to point out that, relatively speaking, very little application time and application memory are typically consumed by application data. We are certainly not moving the entirety of Sunshower.io from Spring to Guice. Guice and Spring are both quite modular, and Sunshower.io uses virtually all of Spring’s modules. This is to say nothing of the heavy dependency we have on Spring Test. We only required dependency injection in the Gyre, and Guice turned out to be a memory-efficient way of providing that.