In order to quantify image sizes in Unikraft, we generate a number of images for all combinations of DCE (Dead Code Elimnation) and LTO (Link Time Optimization), and for a helloworld VM and three other applications: nginx, Redis and SQLite. The results in Figure 1 show that Unikraft images are all under 2MBs for all of these applications. We further compare these results with other unikernels and Linux in Figure 2. As shown, Unikraft images are smaller than all other unikernel projects and comparable to Linux userspace binaries (note that the Linux sizes are just for the application; they do not include the size of glibc nor the kernel). This is a consequence of Unikraft's modular approach, drastically reducing the amount of code to be compiled and linked (e.g., for helloworld, no scheduler and no memory allocator are needed).
In our evaluation, we use standard virtualization toolstacks instead, and wish
to understand how quickly Unikraft VMs can boot. When running experiments, we
measure both the time taken by the VMM (e.g. Firecracker, QEMU, Solo5) and the
boot time of the actual unikernel/VM, measured from when the first guest
instruction is run until
main() is invoked.
The results are shown in Figure 3, showing how long a helloworld unikernel needs to boot with different VMMs. Unikraft's boot time on QEMU and Solo5 (guest only, without VMM overheads) ranges from tens (no NIC) to hundreds of microseconds (one NIC). On Firecracker, boot times are slightly longer but do not exceed 1ms. These results compare positively to previous work: MirageOS (1-2ms on Solo5), OSv (4-5ms on Firecracker with a read-only filesystem), Rump (14-15ms on Solo5), Hermitux (30-32ms on uHyve), Lupine (70ms on Firecracker, 18ms without KML), and Alpine Linux (around 330ms on Firecracker). This illustrates Unikraft's ability to only keep and initialize what is needed.
Overall, the total VM boot time is dominated by the VMM, with Solo5 and Firecracker being the fastest (3ms), QEMU microVM at around 10ms and QEMU the slowest at around 40ms. These results show that Unikraft can be readily used in scenarios where just-in-time instantiation of VMs is needed.
How much memory does Unikraft actually need when running real-world applications? To answer this question we ran experiments to measure the minimum amount of memory required to boot various applications as unikernels, finding that 2-6MBs of memory suffice for Unikraft guests (Figure 4).
We conduct all measurements with the same application config and where possible the same application version (this is limited by application support, e.g., Lupine, HermiTux, and Rump only support a specific version of Redis), on a single core. We did not optimize application or kernel configs for performance, however we took care of removing obvious performance bottlenecks for each system, e.g., switching on memory pools in Unikraft's networking stack (based on lwIP), or porting Lupine to QEMU/KVM in order to avoid Firecracker performance bottlenecks. Unikraft measurements use Mimalloc as the memory allocator.
The results are shown in Figures 5 and 6. For both apps, Unikraft is around 30%-80% faster than running the same app in a container, and 70%-170% faster than the same app running in a Linux VM. Surprisingly, Unikraft is also 10%-60% faster than Native Linux in both cases. We attribute these results to the cost of system calls (aggravated by the presence of KPTI — the gap between native Linux and Unikraft narrows to 0-50% without KPTI), and possibly the presence of Mimalloc as system-wide allocator in Unikraft; unfortunately it is not possible to use Mimalloc as kernel allocator in Linux without heavy code changes. Note that we did try to LD_PRELOAD Mimalloc into Redis, but the performance improvement was not significant. We expect that the improvement would be more notable if Mimalloc were present at compile time instead of relying on the preloading mechanism (making the compiler aware of the allocator allows it to perform compile/link time optimizations) but we could not perform this experiment since Mimalloc is not natively supported by the current Redis code base.
Compared to Lupine on QEMU/KVM, Unikraft is around 50% faster on both Redis and NGINX. These results may be due to overcutting in Lupine's official configuration, scheduling differences (we select Unikraft's cooperative scheduler since it fits well with Redis's single threaded approach), or remaining bloat in Lupine that could not be removed via configuration options. Compared to OSv, Unikraft is about 35% faster on Redis and 25% faster for nginx. Rump exhibits poorer performance: it has not been maintained for a while, effectively limiting the number of configurations we could apply. For instance, we could not set the file limits because the program that used to do it (rumpctrl) does not compile anymore. HermiTux does not support nginx; for Redis, its performance is rather unstable. This is likely due to the absence of virtio support, as well as performance bottlenecks at the VMM level (HermiTux relies on uHyve, a custom VMM that, like Firecracker, does not match the performance of QEMU/KVM). Unlike Lupine, porting it to QEMU/KVM requires significant code changes.
Feel free to ask questions, report issues, and meet new people.