RailsConf 2022

Scaling Rails with JRuby in 2022

Scaling Rails with JRuby in 2022

by Charles Oliver Nutter

The video titled "Scaling Rails with JRuby in 2022" presented by Charles Oliver Nutter at RailsConf 2022 discusses the advancements of JRuby, particularly its compatibility with Ruby 3.1 and enhancements related to performance and scalability. The main goal of JRuby is to provide a robust Ruby implementation that leverages the Java Virtual Machine (JVM), enabling developers to handle concurrent users efficiently and reduce latency and resource costs. Key points include:

  • Overview of JRuby: JRuby is a Ruby implementation founded on the JVM, designed to be as Ruby-like as possible while also capitalizing on JVM benefits.
  • Performance Improvements: JRuby 9.4, which supports Ruby 3.1, aims for better performance and is generally faster than standard C Ruby. This is due to the JVM's superior just-in-time (JIT) compilation, efficient garbage collection, and true parallel threading capabilities.
  • Use Cases and Deployment: Companies globally use JRuby in large-scale applications. The speaker illustrated this with examples such as the Oslo International Airport, showcasing JRuby's reliability in mission-critical applications.
  • Migration to JRuby: Developers looking to transition their Rails applications to JRuby can often do so with minimal effort, as many Ruby libraries function the same. Tools like jruby lint assist in identifying potential issues during migration, especially concerning C extensions.
  • Scalability: Nutter highlighted how JRuby allows for handling high concurrent requests with fewer resource requirements. A case study was presented where a company significantly reduced costs by migrating from C Ruby to JRuby while handling more requests per minute.
  • Challenges and Future Outlook: Some challenges include slower startup and warm-up times due to the JVM interpreting code. However, ongoing optimizations in future versions aim to address these issues.

In conclusion, JRuby offers a scalable solution for Rails applications, allowing developers to optimize resource usage while maintaining performance, making it a viable alternative for large-scale deployments. The video encourages developers interested in scaling their Rails apps to consider JRuby as a pathway to achieving their performance goals.

00:00:12.420 First of all, I'm just curious how many of you have heard of JRuby before? All right, good! That's changed a bit over the years. Not many hands were raised at the first RailsConf when I did a talk like this. How many of you are running something on JRuby, or have run something on JRuby? Okay, probably about a third of the room. That's cool! This will be new for some of you and an update for the rest of you. Let's get right to it; I've got a lot of ground to cover.
00:00:31.920 One thing you may know about me, if you follow me at all, is that I am a big craft beer fan. I brought three very nice craft beers from Minnesota that I'll be giving away to anyone who wants to ask a question or just likes beer. If you don't like IPAs, I have a very nice Imperial Honey Lager that’s also quite good. So, thank you all first for Red Hat for sponsoring our work on JRuby for the past almost ten years.
00:00:58.500 Prior to that, we worked at Sun Microsystems and Engine Yard. We're very happy for all the folks that have sponsored us over the years. One thing that's missing for me this year is my other half, Tom Enebo, who has worked with me since we got employed by Sun Microsystems in 2006. He is still trying to stay safe from COVID and remains somewhat sheltered at home. But please wish him a happy birthday on Twitter; he truly wishes he could be here for this conference.
00:01:22.860 So, a little bit about JRuby. Most of you know what it is and the basics, but we’ll get a little bit of background here. JRuby is a Ruby implementation running on top of the Java Virtual Machine (JVM). We focus on being a Ruby implementation first, ensuring that it behaves as much like running regular Ruby as possible.
00:01:39.240 The things that you’re used to—commands, the command line world, the tooling—should generally work the same. But we are also a JVM language, which means we get the benefit of being on top of the JVM. We'll delve into that a bit later. We really expect your Ruby code to generally just work. Sometimes there are extensions that need to be replaced, such as needing to use a non-forking server or something along those lines, but usually, most code should just function.
00:02:04.920 There are lots of folks deploying JRuby now and over the years. This is a collection of logos I gathered a few years ago of companies running JRuby in various capacities. Some have moved on from JRuby and some have even transitioned from Ruby altogether. Regardless, there are some exciting examples here, like a telescope array in California orchestrated by a JRuby-based application. Meanwhile, the Oslo International Airport ran all of its airplane refueling terminals off a JRuby application. It’s a bit terrifying to think about, but exciting nonetheless!
00:02:39.660 Ruby compatibility is important. As I said, we try to be a Ruby implementation first, but our status has lagged a little bit due to the pandemic. The current version of JRuby (9.3.4) is compatible with Ruby 2.6, which, if you’re keeping track, is about four years old now.
00:02:50.400 However, we are making a significant leap forward with JRuby 9.4 to be released any day now. This version will support Ruby 3.1, putting us back on track with compatibility. After that, we can comfortably focus on performance again, taking advantage of some new JVM features and generally improving the JRuby experience.
00:03:11.880 Indeed, we are generally faster than C Ruby in most cases, and this optimization will only make it better. Now, what are these JVM benefits we gain from this setup? Well, first, it's widely deployed and supported. It seems every major software company has their own flavor of JVM they maintain and contribute to these days.
00:03:25.560 There are many different JVMs and platforms supported. We get JIT compilation for free, along with multiple garbage collectors that can scale on varying loads and different sizes of data. JRuby can run on almost any platform, all thanks to the many individuals maintaining JVMs. We also benefit from real parallel threads, real concurrency, and excellent tools that come with JVM. Additionally, there are tens of thousands or even hundreds of thousands of JVM libraries available that we can leverage.”
00:03:56.220 You can install JRuby on any platform that supports JVM. An example of one valuable tool is VisualVM, which utilizes the Visual GC plugin. This tool provides a live view of various JVM heaps in the garbage collector—showing how they fill up, empty, and dump old objects into the next generation.
00:04:07.680 We have real parallel threads. Unlike regular CRuby, when you spin up multiple threads, you can use multiple cores. You can take one JRuby process and max it out on any system you want, able to handle the load and distribute it across all processors. There is fun stuff we can create. For instance, Progen is a JRuby framework on Minecraft Java Edition that allows you to write plugins in Ruby.
00:04:36.840 This particular example adjusts how many chickens come out of an egg when you throw it. Tom developed this as a fun project when we were both playing Minecraft together, but he ended up ruining a world because he spawned too many chickens and released wolves on them, causing the server to collapse.
00:04:54.720 To get started with JRuby, you can visit jruby.org, which contains all the information, including downloads and nightly builds. Generally, the only requirement for your system is a JDK—a version of OpenJDK from anyone, whether it's Amazon, Microsoft, or Oracle, or even Azul. There are many ways to obtain a JVM for your platform.
00:05:07.560 We still support Java 8, and this will be continued through the 9.4 release. Afterward, we will likely drop support for it as it has been around for about ten years now. You can install JRuby similarly to how you would install regular Ruby. If you’re using RVM or Rbenv along with Ruby Build, they all have support for JRuby—most even support JRuby head, so you can pull down the 9.4 versions before the official release.
00:05:33.300 Of course, we also have a tarball available for those that want to unpack it manually, as well as a Windows installer, should you need one. It’s quite easy to get the JDK installed, and from there, you should be good to go.
00:05:51.840 Here's a little session with IRB just calling into one of the standard Java APIs, specifically the Java Lang Runtime, checking for available processors and the free memory. This just shows how easy it is to mix Ruby code with Java libraries and APIs.
00:06:10.680 Now, let's focus on JRuby on Rails, which is why we're really here. Why would we want to run JRuby on Rails? We've actually worked on this for a long time; I think we're closing in on the 15th anniversary of running Rails for the first time! I don’t think it was even version 1.0 at that time.
00:06:30.420 Many folks are running JRuby for Rails applications as well as smaller frameworks like Sinatra or Rhoda, or for various service APIs, and at enormous scales—sometimes handling tens of thousands to hundreds of thousands of requests per minute.
00:06:58.920 We gain all the benefits of the JVM, including access to numerous libraries and the ability to mix and match with other languages like Java, Scala, Kotlin, or Clojure in your Rails application. We believe this remains one of the best ways to scale large Rails apps. Anyone who has tried to scale their application to handle thousands of requests per minute knows how challenging it can be.
00:07:24.939 It takes a lot of work to scale a standard CRuby Rails application. JRuby simplifies this process—just one JRuby process can handle multiple threads and requests; configuration is usually minimal.
00:07:43.620 You may need to swap out some native extension C libraries that we don’t support, and do some modifications to the database configuration since many JDBC drivers don’t support Unix sockets. You would simply specify a TCP port to connect to. JRuby doesn't require separate processes; you specify however many threads you want to handle requests, and it runs smoothly.
00:08:06.240 As I mentioned, we are a little bit behind. Ruby on Rails 6 works well with JRuby 9.3.4. How many people here have already moved to Rails 7? Not too many! If you have existing Rails 6 applications that you would like to scale, you can certainly do that with JRuby, as we currently have this stable version running in production worldwide.
00:08:25.320 That's something we know because people come to us commenting about their applications, so we are aware of the global usage of Rails on JRuby. We are working on getting JRuby caught up; we are releasing Ruby 3.1 support very soon, which will allow us to run Rails 7. We’re already testing it, and everything is looking great.
00:09:00.360 The main work remaining is to update our version of ActiveRecord to ensure it integrates with the Java database APIs, offering support for all the various databases. This involves everything except Action Record, while approximately 99% of our tests are passing. We haven’t expected these to change significantly.
00:09:15.420 Generally, the remaining failures are minor and not very critical. Our ActiveRecord runs on top of JDBC, and we aim to keep our version matching so you know that Rails 6 will use version 6.0 of our driver, and Rails 7 will use version 7.0. This process is already in progress.
00:09:35.400 For this talk, I tested SQLite and MySQL quite a bit, and the compatibility & performance is looking good. PostgreSQL requires the most work and adjustment in the Rails codebase, but we will be addressing that soon with a release in the next month or so.
00:09:53.460 Migrating to JRuby is usually fairly straightforward; the simplest way is to try to bundle install your app. You’ll find that almost all standard Ruby libraries work well. Popular extensions like Nokogiri and JSON also have JRuby versions, and they generally function smoothly. The failures usually come from smaller or more obscure C extensions specific to your project.
00:10:20.520 We also have a tool called JRuby Lint that you can gem install, go into your app, and run. This tool provides tips about C extensions that we know have replacements, looking through your code while offering insights on concurrency or parallel execution. It makes migration to JRuby a bit easier.
00:10:43.620 Regarding C extensions, there are a few ways we recommend handling them. Sometimes, you don’t even need the extension—it might have been used strictly for development, or as a profiling tool that you are not currently utilizing. In many cases, we find that users can simply remove the code, opting for a pure Ruby version instead.
00:11:06.180 Often, whatever the library is, there’ll be a standard JVM library, probably a dozen or more. You can pull one of those down, integrate it into your application, and start calling it directly from Ruby code, which is often the simplest and quickest way to substitute an unsupported extension.
00:11:31.560 In many cases, if you opt for a pure Ruby implementation of the same library, it will still work, albeit perhaps not as fast as the C code, but often a sufficient speed for most applications, especially if it's not used frequently.
00:11:49.680 Finally, if you are dealing with a library many people use and we want to support it, we absolutely welcome your help in porting it into a JRuby extension. Our API is similar to C Ruby’s, though written in Java. A good example is the Psych YAML engine, which wraps a YAML library in Java. Porting code often goes quite quickly.
00:12:08.460 We also maintain a wiki page that lists known extensions lacking a direct or automatic replacement, attempting to keep this updated. The JRuby Lint tool can parse this page to suggest alternatives. Whenever we find a suitable replacement, we make an effort to include it.
00:12:24.480 Once we’ve successfully migrated and got our application running, the next step is deployment, which should follow the same process as deploying a regular C Ruby app. We recommend Puma since it manages multiple threads and high concurrency very well, and it is now the default server for Rails.
00:12:36.900 Deployment will proceed much like it would for typical Ruby applications. I’m not sure what the latest trends are in deploying Ruby apps, but it will operate the same way on JRuby. Alternatively, you can bundle JRuby, its dependencies, and your app as a Java web application, allowing you to deploy it on any server that supports Java web applications.
00:12:56.340 It’s essentially one file that you can throw into the cloud and make live. Plus, there’s no requirement for a C compiler since we don’t support C extensions; JRuby extensions are generally pre-compiled and shipped as a JAR file. This follows the ‘write once, run anywhere’ philosophy.
00:13:20.520 Once it’s released as a gem, you can pull it in without needing to rebuild anything. There will be some tweaks—our wiki has recommendations on tuning the JVM. The most important move is to tell the JVM not to use all the memory in your system.
00:13:35.160 If you don’t provide these parameters, the JVM will utilize more memory than necessary. You can set it to, say, a 500MB heap or a 250MB heap, and that would suffice for the application. We also suggest using environment variables like JRuby_OPTS and JAVA_OPTS, along with a few others, to ensure that any subprocesses launched pick up those flags as well.
00:14:00.600 Now, after migrating to JRuby and deploying it, we want to ensure it's scaling effectively, which is the main focus of this talk. Scaling Rails has been a classic issue with C Ruby. It has improved significantly, especially with Puma’s ability to manage workers and distribute load, but it still isn’t as efficient as it could be with concurrent threads.
00:14:24.120 Even with shared memory and application preloading, multiple processes waste resources. You might end up with two copies of an application in memory and concurrent garbage collectors fighting for memory allocation. It’s just not as efficient as having everything running in the same process space.
00:14:42.420 On the other hand, JRuby's structure truly allows a single process outfitted with 10, 20, or even 100 threads to handle numerous requests while using a single garbage collector and memory space, making for much more efficient resource utilization.
00:15:01.260 I did some benchmarking; it has been a long time since we evaluated performance, but I conducted simple benchmarks on a scaffolded blog post application running on MySQL. I ran this on my local laptop with a recent generation i7 chip, which is pretty similar to production—it has its caveats.
00:15:18.179 Comparing JRuby 9.4 running Rails 7 with C Ruby 3.1, I found that with JRuby, I configured it for 20 threads, whereas with C Ruby, I ran 10 workers with two threads. My machine only has about eight cores, so ideally, ten processes should fully utilize them, but sometimes threads can assist when blocked on I/O.
00:15:40.080 Caveats exist, of course, as these benchmarks were conducted locally with the database on the same machine. It's not the best way to benchmark, but good enough for demonstration purposes. I used Siege to drive the testing, though you might have your preferred HTTP driver that could behave differently with respect to keep-alives and so on. Warm-up time is certainly a factor.
00:16:00.480 For this simple application and just retrieving a view of one of the blog posts, C Ruby averages around 2,500 requests per second. Meanwhile, JRuby achieves another 1,000 requests per second just without any tuning or modifications. We’re already seeing significant performance improvements in end-to-end efficiency.
00:16:18.120 As most Rails applications are bottlenecked by Active Record performance, it is steadily improving. Historically, it used to be much slower, but that trend has reversed with better use of prepared statements and a more efficient API for building queries. Nonetheless, it still requires a notable amount of Ruby code and effort to run quickly.
00:16:35.400 JRuby has seen continued improvements paralleling work done on Active Record, and we also benefit from the JVM's enhancements in garbage collection, just-in-time compilation, and optimization. Overall, we feel very satisfied with the current performance while still recognizing there is room to grow.
00:16:52.320 This testing utilized a local MySQL instance, and while results are promising, there’s still more work to do. The best process is for you to take your application, deploy it, test it, and assess its performance on JRuby.
00:17:06.480 For common operations, I conducted basic selects retrieving varied field types, pulling data from specific rows in the database. C Ruby shows consistent performance across the board, while JRuby performs almost twice as fast for these operations regarding lighter-weight Active Record interactions.
00:17:20.700 Update performance on JRuby is notably faster as well, showcasing its efficiency for applications focusing on viewing and updating records. The performance comparisons we've observed verify this, and while we’re satisfied with our progress, we know more improvements are possible.
00:17:35.520 As a concluding note, we had a user with a large Rails application utilizing 40 extra-large EC2 instances, leading to significant costs—tens of hundreds of thousands of dollars in hosting. They implemented 40 worker processes per server using C Ruby, managing about 100,000 to 150,000 requests per minute at a respectable response time of 50 to 75 milliseconds.
00:17:52.740 They then decided to prototype a migration to JRuby, making better use of threading and reducing instances to just 10 extra-large servers, cutting hosting costs down to 25% of what they previously paid. They noted a consistent 150,000 requests per minute, with their initial attempt, while subsequent optimizations further enhanced performance.
00:18:14.880 The response times were primarily below 50 milliseconds, demonstrating that JRuby effectively supports large-scale applications while also potentially allowing for more economic resource usage. That said, there are some downsides to using JRuby.
00:18:32.040 Being on the JVM, we need to address startup and warm-up times more than CRuby, where nearly everything starts as native compiled C code. In JRuby, the process involves interpreting Ruby code via the JVM, which leads to slower startup times and a longer warm-up period.
00:18:53.160 We are implementing strategies to tackle these issues, and tips are available if these hurdles affect your application significantly. It is essential to consider that warm-up time is a known factor.
00:19:11.340 For example, conducting a gem listing of 350 gems may take significantly longer on JRuby compared to CRuby due to JVM optimizations. Although we still strive to improve these figures, running the same operation repeatedly in the same process does help speed it up by saving optimized data.
00:19:26.520 We also offer a --dev flag that can disable some optimizations, which is useful for local development since it reduces the time needed to start commands, but it’s important to be attentive to how this impacts performance in production.
00:19:42.180 The summary of our performance findings, as discussed, indicate the need for warm-up time. We performed benchmarks with results showing a transition period of about two to three minutes before full performance was attained. Some enterprises opt to pre-warm their applications by running a deployment script that executes common activities to warm the server configuration.
00:20:00.399 In the future, we’re exploring open JDK improvements for startup and warm-up, as well as JVMs that can maintain a running state to ensure they remain hot for the next deployment. Additionally, we’ll assess native and ahead-of-time compilation solutions; these are recognized challenges, and we’re actively working on them.
00:20:16.080 In conclusion, JRuby 9.4 will be released shortly. You can try out JRuby head or download one of our nightly builds now. For this talk, I ran all the demos, benchmarks, and tests on JRuby Master, JRuby 9.4, and Rails 7, so we’re quite close to a stable release.
00:20:39.240 We also have a solid amount of optimization work scheduled for the rest of the year, and we look forward to taking advantage of some new and exciting JVM features—like native fibers, built-in foreign function interfaces for calling C libraries, as well as ensuring our JIT and garbage collection performance continues to improve.
00:21:03.720 Regarding JRuby on Rails, Rails 6 support is stable, and Rails 7 support is just around the corner. Our goal remains unchanged: to help developers save money while minimizing resource usage. Importantly, we don’t profit from marketing JRuby; Red Hat primarily sponsors our efforts because it is genuinely beneficial for the community.
00:21:18.840 Please feel free to come talk to me so we can see how we can help you get your app running on JRuby, ultimately saving you dollars and resources. That’s all I have for now.
00:21:36.360 It looks like we have about four minutes for questions. Please feel free to ask!
00:21:46.080 When you're writing with JRuby in mind, do you need to pay more attention to concurrency and parallelism to ensure your code is safe? Rails generally does a great job here. If you build a Rails application as intended, typically the state isn't shared, helping avoid most threading issues. If you work close to the metal and opt for multiple threads or thread pools, then yes, more attention is needed.
00:22:06.480 What aspects of JRuby have positively impacted the ecosystem? One significant aspect is Rack, which resembles servlets from the Java community, allowing for server swapping beneath our applications. The push for Rails to be thread-safe was largely influenced by the desire to run on JRuby, where we used JRuby for testing.
00:22:22.080 We remain active contributors to the Ruby spec and engage with various projects, ensuring that we help enhance performance and concurrency across the ecosystem. Projects like Mutant and TorqueBox were tools aimed at integrating the entire Java Enterprise stack into Ruby app servers. Unfortunately, there's currently limited interest in reviving TorqueBox.
00:22:38.340 How does hot reload function with JRuby? It operates exactly like rolling restarts or deployments do; be aware of the warm-up time required before servers are fully operational. Regarding hot reloading at the framework or library level, it should seamlessly work with JRuby.
00:22:56.340 Does JRuby run into memory issues based on your benchmarks? While I didn’t include detailed measurements, I observed that JRuby tends to launch larger memory instances, like 2.2 GB for 20 threads. However, by limiting to a 250 MB heap, it could run efficiently around 700 to 800 MB. For C Ruby, each process remained around 120 MB.
00:23:12.180 This becomes advantageous when handling around eight to ten concurrent requests, which is when JRuby generally starts to outperform others. Ultimately, if you're scaling to handle large requests, JRuby significantly excels.
00:23:28.620 If you want a beer, please come on up and let me show you JRuby in action!