Managing Ruby's Memory Bloating In Sidekiq: Strategies and Solutions
Tags: ruby, sidekiq, memory, aws
Published: 2023-10-07 07:27:30

## Introduction

Ruby, a versatile programming language, is known for its ease of use and flexibility. However, it also comes with its own set of challenges, particularly related to memory management. In this article, we'll talk about Ruby's memory handling, the issues it can pose, and strategies to mitigate them.

---

## Ruby's Memory Architecture

Ruby manages objects in two main places: **Object Space** and **Memory Arena**. Objects smaller than 24 bytes reside in Object Space, while larger objects are allocated to Memory Arena, with only their addresses stored in Object Space.

Ruby, implemented in C, allocates memory to Memory Arena using `malloc` in `glib.c`. To prevent concurrency issues, each Memory Arena has a lock. However, this can lead to performance bottlenecks in multi-threaded programs. To address this, Ruby creates multiple Memory Arenas and assign one Memory Arena to certain thread, but this can result in memory bloat.

## Why we get the bloating?
But why? Think about Sidekiq with multiple threads. Maximum 1 Memory Arena will be assigned to each thread instead of each thread waiting for it's turn to access a Memory Arena. So, more than needed Memory have to be created, leading to more Memory usage.

Plus, there is another problem inside the allocator. It does not want to free the empty memory to the kernel. With these conditions altogether, We finally get the long lasting Memory Bloating!

## Strategies to Address Ruby's Memory Issues

So, how can we deal with them? There are several strategies to tackle these memory problems:

1. **Changing Memory Allocator**: Consider switching from the default `malloc` allocator to an alternative like `jemalloc`. Note that compatibility [issues](https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/#:~:text=compatibility%20issues%20with%20Alpine%20Linux) may arise on Alpine-based systems.

2. **Setting Memory Arena Limits**: You can set a tighter limit on the number of Memory Arenas using `MALLOC_ARENA_MAX = 2`. This can help mitigate concurrency-related performance issues.

3. **Freeing Memory Not In Use**: Another potential solution is to address Ruby's reluctance to free memory after use. Check out this insightful [article](https://www.joyfulbikeshedding.com/blog/2019-03-14-what-causes-ruby-memory-bloat.html) for more information on this approach.

4. **Rerunning the Program**: There might be some companies hesitating to modify the magical environment variable using Apline-based docker image just like my case. Then, periodically restarting the program can be a solution too. I will concentrate on this solution below.

---

# Rerunning the Program as a Temporary Fix

In some cases, a quick workaround involves periodically restarting your Ruby application. This can temporarily alleviate memory issues for especially those who are hesitating to manipulate the magical environment variable. However, it's not a fundamental solution.

## Memory Challenges and Job Retries

When dealing with Sidekiq containers, shutting them down can lead to unexpected job retries. Whenever a Sidekiq process shuts down, the jobs it was working on return to the Redis queue. When the process restarts, it reprocesses all the jobs, regardless of their previous progress.

To address this side effect, consider implementing a generous shutdown function in your logic. This function ensures that jobs in progress are not retried unnecessarily. It's a crucial addition to managing memory issues in Sidekiq.
<br/>

## Implementing the Memory Management Solution

Here's a high-level overview of the approach:

- Utilize AWS EventBridge to run a function every 5-10 minutes.
- The function uses the AWS SDK to monitor the running-time of processes.
- The function pauses processes that have been running for an extended period using the TSTP [signal](https://github.com/sidekiq/sidekiq/wiki/Signals).
- After the process completes its last job, the function terminates it using the TERM signal.

Additional considerations include maintaining at least one running process, limiting the maximum number of processes, and implementing scaling logic.


---

## Conclusion

Ruby's memory management challenges are a common issue for developers, but they are not insurmountable. By understanding the fundamentals of Ruby's memory handling and implementing appropriate strategies, you can optimize the performance of your Ruby applications. If you've encountered similar challenges or have questions, feel free to share your experiences and seek assistance within the community.



## References
- [Malloc Can Double Multi-threaded Ruby Program Memory Usage](https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html)

- [What causes Ruby memory bloat?](https://www.joyfulbikeshedding.com/blog/2019-03-14-what-causes-ruby-memory-bloat.html)

Thank you for reading!