Ruby
Resilient by Design
Summarized using AI

Resilient by Design

by Smit Shah

The video titled "Resilient by Design" presented by Smit Shah at the Garden City Ruby 2015 event focuses on the importance of building resilient modern distributed systems to handle issues such as downtime and high traffic. The speaker emphasizes that developers, especially those who dread late-night on-call pages, need to prioritize resilience in their systems to prevent cascading failures and minimize business ramifications.

Key Points Discussed:
- Importance of Resilience: Resilience is crucial for software systems to function effectively under failure conditions. Developers often lose sleep over system downtimes and the customers affected by them.
- Real-World Analogies: Shah compares resilient software systems to cars and nuclear reactors, highlighting how both are designed with built-in failure mechanisms from the outset.
- Common Pitfalls: Many developers realize the importance of resilience too late, often after production failures. The need to anticipate potential issues, such as service dependencies going down or database failures, is emphasized.
- Resilient Design Patterns: The talk introduces several design patterns intended to bolster system resilience:
- Bounding: This involves setting limits for timeouts, memory usage, and queue sizes to handle failures gracefully and maintain service continuity, stressing the need for proper timeout configurations and memory management strategies.
- Circuit Breakers: This pattern prevents system overload by stopping repeated calls to unreliable services and allows for fallbacks to maintain user experience during failures.
- Bulkhead Design: This concept involves compartmentalizing services to ensure that failures in one service do not lead to the collapse of others, enhancing overall system reliability.
- The Role of Specifications: Detailed specifications can uncover potential pitfalls in the coding process, allowing developers to address edge cases and enhance the reliability of their services.

In conclusion, the video stresses that planning for resilience throughout the software development lifecycle, even for simpler services, is essential in today's complex distributed systems. By adopting resilient design patterns, developers can significantly reduce the chances of unexpected production issues and ensure more stable systems.

00:00:15.040 First of all, as they just mentioned, I work at Nilenzo, and I'm also part of the Bunder core team. Today, I want to talk about how to build resilient systems.
00:00:24.400 Why do we care about resilient systems in the first place? Anyone who loses sleep over system downtime, customer issues, or business ramifications would care about the resilience of their systems. Downtime can mean disaster for them, and developers may have to wake up at night to answer on-call pages. Thus, we want to build systems that continue to work and perform under conditions of failure or high traffic.
00:00:43.680 Let's move away from software for a bit and think about cars. Cars are designed to be resilient; they have airbag systems that activate during an accident to save lives. Similarly, nuclear power reactors don't leak radiation if there's a power outage; they have built-in mechanisms to handle such failures. These resilience mechanisms are considered from the start of the design process.
00:01:02.239 Unfortunately, in software resilience, the pattern tends to be that folks build the entire feature and only then think about what happens if a key component, like a cache server, goes down. This is often a realization too late, usually after something has gone wrong in production. One fine day, when you need your cache to be operational and your server is under heavy load, it might fail. At that point, developers are left to manage the aftermath.
00:01:40.560 To design a resilient system, it must be front-loaded with considerations about resiliency. This means anticipating things like, 'What if the service I depend on goes down?' or 'What if my database doesn’t respond?' Many developers often overlook these aspects.
00:02:01.120 Instead, I want to discuss patterns that can help you plan for these situations. It's crucial to think ahead about the limitations of your code. The main crux of my talk today will revolve around resilient design patterns. While I can't guarantee that using these patterns will prevent failures, they can significantly improve system uptime.
00:02:29.200 Let me give an example of why design patterns are preferable to ad-hoc solutions. We had a production system where over time the memory usage of our Unicorn web servers increased significantly. After about a week or two, the memory usage would escalate to a point that it would start swapping. The only option then was to restart the web server, which was quite painful.
00:03:12.000 We couldn't determine the cause at first. Initially, we suspected memory leaks from native Ruby extensions. Eventually, we discovered the issue related to non-unique keys in JSON parsing, specifically timestamps. Every time we parsed the JSON, we tapped into Ruby's symbol table, which doesn't allow for garbage collection of symbols before Ruby 2.2. This mistake compounded over time, leading to an increase in memory usage and swapping, which we needed to control.
00:05:57.480 To mitigate the problem, we had to employ memory limits for our worker processes. We put a limit on the memory a worker could use, and if it increased beyond a certain threshold, we would restart it. This approach allowed us to maintain service continuity while we took our time identifying the root cause of the memory issue.
00:07:37.440 This brings me to the patterns I would be covering today. The first pattern is bounding, particularly timeouts. It's essential to know the default timeout settings in your applications. For instance, the HTTP default timeout in Ruby is a staggering 60 seconds, which may not be suitable for production. A timeout this high can create significant issues in high-load situations.
00:08:00.879 Another critical point of bounding is memory management. For any worker processes you have, ensure that memory usage is monitored, and establish behavior that should occur when thresholds are breached. The next aspect of bounding is to be cautious about your queues and buffers, especially in high-load situations. Having controlled and limited buffer sizes allows you to apply back pressure and better manage the flow of processes.
00:09:05.120 Next, let's talk about circuit breakers—a popular design pattern that can handle failures gracefully. Circuit breakers prevent operations from trying to call services that have already proven to be unreliable. This functionality ensures better resource management and protects your system from cascading failures; if you’re trying to access service C and it keeps failing, the circuit breaker will prevent further attempts until the service is confirmed to be back up.
00:10:43.600 Circuit breakers also allow you to implement fallbacks during failures, directing clients to alternative content or cached responses. This helps ensure a seamless user experience even in the event of failures. Over time, you can transition from an open state to a half-open state to see if the service is back online without overwhelming it.
00:12:03.120 Resilience patterns are not just applicable to service communication but can extend to other elements, like databases. Implementing these practices in MySQL can prevent queries from monopolizing resources, allowing queries to be handled more efficiently.
00:13:38.120 The final resilience pattern I want to impart to you today is the bulkhead design. Borrowed from shipbuilding, the bulkhead pattern suggests compartmentalization to localize failures. In the context of microservices, this means isolating service instances so that, if one service fails, it won’t bring down others relying on it. This approach adds an extra layer of reliability.
00:15:36.239 Lastly, I want to stress the importance of specifications. Writing detailed specifications before you begin coding helps illuminate potential pitfalls and ensures that you're considering edge cases. Effective specifications can lead to better code, preventing oversights from manifesting in production. When you plan patterns into your development cycle, even if you’re working on a smaller service, the complexity and unreliability of distributed systems emphasize the necessity of these patterns. By implementing resilience design patterns, you will lessen the chances of sleepless nights spent on firefighting production issues.
Explore all talks recorded at Garden City Ruby 2015
+8