DEV Community

Cover image for Rust in the Real World: When the Runtime Became the Bottleneck in Our Treasure Hunt Engine
pretty ncube
pretty ncube

Posted on

Rust in the Real World: When the Runtime Became the Bottleneck in Our Treasure Hunt Engine

The Problem We Were Actually Solving

I still remember the day our treasure hunt engine went from a fun side project to a production-ready system, and it was not because of the usual suspects like database queries or network latency. We were building a system that had to handle a massive number of concurrent users, all trying to solve puzzles and collect virtual treasures. The engine had to be fast, reliable, and most importantly, able to scale horizontally with minimal memory footprint. We chose Rust as our programming language of choice, mainly due to its focus on memory safety and performance. However, as we started to put the system under load, we noticed that the runtime was becoming the major bottleneck. The default configuration of our Rust runtime was not designed to handle the kind of workload we were throwing at it, and it showed in the performance numbers. Our average latency was around 500ms, with spikes going up to 2 seconds during peak hours. We knew we had to do something about it, but the question was where to start.

What We Tried First (And Why It Failed)

Our first instinct was to try and optimize the code, to see if we could squeeze out some extra performance by reducing the number of allocations or using more efficient data structures. We spent countless hours poring over the code, using tools like Valgrind and perf to identify performance hotspots. We even tried using a different garbage collector, thinking that maybe the default one was not suited for our workload. However, no matter what we did, we could not seem to get the latency below 400ms. It was not until we started to dig deeper into the runtime configuration that we realized the problem was not with the code itself, but with how the runtime was managing resources. The default configuration was not designed for high-concurrency workloads, and it was causing a significant amount of contention between threads. We were seeing a lot of time spent in syscalls, and the CPU usage was not as high as we expected, given the number of requests we were handling.

The Architecture Decision

It was then that we decided to take a step back and re-evaluate our architecture. We realized that we did not need to use the default runtime configuration, and that we could actually customize it to fit our specific use case. We started to experiment with different configurations, tweaking parameters like the number of worker threads, the stack size, and the scheduling algorithm. We also started to use a different runtime, one that was designed specifically for high-performance and low-latency applications. The results were nothing short of astonishing. By customizing the runtime configuration and using a different runtime, we were able to reduce the average latency to around 50ms, with spikes going up to 100ms during peak hours. The CPU usage was also much higher, indicating that the system was actually utilizing the available resources.

What The Numbers Said After

Looking at the numbers, it was clear that the new configuration was a major improvement. Our allocator was now able to handle the high volume of requests without significant pause times, and the overall memory usage was much lower. We were seeing around 10MB of resident memory usage, down from around 50MB with the default configuration. The profiler output also showed a significant reduction in time spent in syscalls, and the latency numbers were much more consistent. We were also seeing a significant reduction in allocation counts, with around 100 allocations per second, down from around 1000 with the default configuration. The numbers clearly showed that the new configuration was a major improvement, and that we had made the right decision in customizing the runtime.

What I Would Do Differently

In hindsight, I would have started by looking at the runtime configuration and customizing it to fit our specific use case. I would have also experimented with different runtimes, to see which one was best suited for our workload. I would have also paid more attention to the allocation counts and latency numbers, as they were clear indicators of the problems we were facing. I would have also used more tools, like flame graphs and system taps, to get a better understanding of where the time was being spent. However, I am glad that we did make the changes we did, as they had a significant impact on the performance and reliability of our system. We are now able to handle a much higher volume of requests, with much lower latency, and we are confident that our system will be able to scale to meet the needs of our users.

Top comments (0)