Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JRuby on Rails: From Zero to Scale

headius
April 30, 2019

JRuby on Rails: From Zero to Scale

JRuby is deployed by hundreds of companies around the world, running Rails and other services at higher speeds and with better scalability than any other runtime. With JRuby you get better utilization of system resources, the performance and tooling of the JVM, and a massive collection of libraries to add to your toolbox, all without leaving Ruby behind.

In this talk, we'll walk you through the early stages of using JRuby, whether for a new app or a migration from CRuby. We will show how to deploy your JRuby app using various services. We'll cover the basics of troubleshooting performance and configuring your system for concurrency. By the end of this talk, you’ll have the knowledge you need to save money and time by building on JRuby.

(This talk was delivered at RailsConf 2019)

headius

April 30, 2019
Tweet

More Decks by headius

Other Decks in Programming

Transcript

  1. #FreeOlaBini • Ola Bini, former JRuby contributor, is being held

    without cause by Ecuadorian authorities in connection with Julian Assange • His detention is illegal and should worry all of us • Please tell your friends about Ola and support efforts to help force the Ecuadorian authorities to release him
  2. Ruby Implementation First! • As compatible as we can be

    • All your Ruby code should just work* • * - except a few things we cannot support like fork() • No native c extensions (but we have java extensions) • Please report any problems
  3. Roadmap • 9.2.7.0 last week, 9.2.8.0 in May • 9.1.x

    is EOL • How to handle 2.6? 9.1.17.0 ... 9.2.0.0 2.5.3+ 2.3.x 2.6? 9.2.7.0 master jruby-9_1 9.1.1.18.0 EOL ruby-2.6
  4. Skip 2.6 Support? • JRuby 9.3 -> Ruby 2.7? •

    How important is 2.6? • We skipped 2.4 to no ill effects • Less maintenance for us • 2.6 Checklist: https://github.com/jruby/jruby/issues/5576
  5. JRuby Install • Install a JDK • Java 8 recommended,

    there's many distributions out there • Java 9+ work well but may print warnings • Install JRuby • Recommended: system package, Ruby installer, Docker image • Download tarball/zip or Windows installer
  6. Test it out! [] ~ $ rvm use jruby Using

    /Users/headius/.rvm/gems/jruby-9.2.6.0 [] ~ $ irb jruby-9.2.6.0 :001 > runtime = java.lang.Runtime.runtime => #<Java::JavaLang::Runtime:0x64a896b0> jruby-9.2.6.0 :002 > runtime.available_processors => 8 jruby-9.2.6.0 :003 > runtime.free_memory => 91420584 jruby-9.2.6.0 :004 >
  7. Why Is This Hard? • CRuby: Mostly native code at

    startup • Parser, iseq compiler, iseq interpreter, core classes, extensions • JRuby: Mostly interpreted at startup • Everything in JRuby starts "cold" • JVM eventually optimizes...but it's too late for startup time
  8. total execution time (lower is better) 0s 1.25s 2.5s 3.75s

    5s gem --version gem list (~350 gems) 4.6s 3.6s 0.7s 0.4s CRuby JRuby (JDK8)
  9. Running in Same JVM total execution time (lower is better)

    0s 1.25s 2.5s 3.75s 5s gem list (~350 gems) 1.3s 1.6s 1.6s 1.6s 2.2s 1.7s 1.7s 2.0s 2.2s 3.5s 4.6s
  10. JRuby Flag: --dev • --dev flag • export JRUBY_OPTS="--dev" •

    Disables JRuby's JIT • Reduces JVM JIT • 30-40% reduction • Don't forget when benchmarking! total execution time (lower is better) 0s 1.15s 2.3s 3.45s 4.6s gem list (~350 gems) 3.0s 4.6s JRuby JRuby --dev
  11. OpenJDK Class Data Sharing • Class Data Sharing • Shared

    JVM class data • Reduced memory • Improved startup time • Install jruby-startup gem • Run "generate-appcds" • JRuby will use automatically total execution time (lower is better) 0s 1.75s 3.5s 5.25s 7s gem list (~350 gems) 3.2s 3.7s 6.3s JRuby --dev --dev + CDS
  12. OpenJ9 "quickstart" • "quickstart" and "shareclasses" • Shares class data

    across runs • Shares JIT results across runs • -Xquickstart
 -Xshareclasses:name=whatevs • JAVA_OPTS="..." total execution time (lower is better) 0s 2s 4s 6s 8s gem list (~350 gems) 3.1s 6.5s 7.1s JRuby --dev --dev + quick
  13. Best Times total execution time (lower is better) 0s 1s

    2s 3s 4s gem list (~350 gems) 3.1s 3.2s 3.0s 0.8s CRuby JRuby --dev JDK8 JRuby --dev JDK11 CDS JRuby --dev J9 quick
  14. Future: Ahead-of-time Compile • New JVM tools for precompiling code

    to native • GraalVM's Substrate VM • Reduces base VM startup to CRuby speeds • Does not help Ruby code loaded at runtime • Soon: precompiled JRuby *and* Ruby code • App startup as fast as CRuby?
  15. total execution time (lower is better) 0s 0.45s 0.9s 1.35s

    1.8s -e 1 0.5s 1.7s JRuby --dev TruffleRuby native total execution time (lower is better) 0s 4s 8s 12s 16s gem list (~350 gems) 14.9s 4.6s JRuby --dev TruffleRuby native
  16. Gems, Paths, etc • Don't share gem path with other

    Rubies • C extension gems have JRuby versions (e.g. Nokogiri) • Ruby installers will handle this for you • Be mindful if system packages do not • Watch out for .ruby-version silently switching
  17. Getting Help • JRuby on GitHub: https://github.com/jruby/jruby • Chat with

    JRuby devs, users • #jruby on Freenode IRC • jruby/jruby on Gitter • Experimental: jruby on Matrix • Mailing list: https://lists.ruby-lang.org
  18. Why JRuby on Rails? • JRuby has run Rails since

    2006! • "Are there JRuby users running Rails applications?" • Oh yes! And at large scale! • Benefit from the JVM, libraries, languages • The best way to scale large Rails apps today
  19. --- testapp_mri_pg/Gemfile +++ testapp_jruby_pg/Gemfile -# Use postgresql as the database

    for Active Record -gem 'pg', '>= 0.18', '< 2.0' +# Use jdbcpostgresql as the database for Active Record +gem 'activerecord-jdbcpostgresql-adapter' # See https://github.com/rails/execjs#readme for more supported runtimes -# gem 'mini_racer', platforms: :ruby - +gem 'therubyrhino' group :development do - # Spring speeds up development... - gem 'spring' - gem 'spring-watcher-listen', '~> 2.0.0' end Minimal Config Diffs --- testapp_mri_pg/config/database.yml +++ testapp_jruby_pg/config/database.yml @@ -65,5 +81,7 @@ production: <<: *default database: rails_prod + host: localhost + port: 5432 username: rails_prod password: rails_prod --- testapp_mri_pg/config/puma.rb 2019-04-19 04:48:51.425474315 +0000 +++ testapp_jruby_pg/config/puma.rb 2019-04-17 08:56:53.529154189 +0000 @@ -4,7 +4,7 @@ # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 2 } +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 20 } threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. @@ -21,7 +21,7 @@ # Workers do not work on JRuby or Windows (both of which do not support # processes). # -workers ENV.fetch("WEB_CONCURRENCY") { 2 } +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code
  20. Rails 5.2.3 actioncable: some results…hard pg dep for other fails

    actionmailer: 204 runs, 457 assertions, 0 failures, 0 errors actionpack: 3174 runs, 15884 assertions, 1 failures, 0 errors actionview: 1993 runs, 4398 assertions, 2 failures, 4 errors activejob: 180 runs, 415 assertions, 0 failures, 0 errors activemodel: 810 runs, 2265 assertions, 0 failures, 0 errors activestorage: 144 runs, 372 assertions, 0 failures, 0 errors activesupport: 4212 runs, 763229 assertions, 10 failures, 1 errors railties: uses fork() [issue #35900] 99.998% passing!
  21. ActiveRecordJDBC 52.2 • Historic dates • PG-specific tests in AR

    test suite (PRs needed yet) • Miscellaneous… sqlite3: 19 excludes, 5327 runs, 14961 assertions postgresql: 53 excludes, 5915 runs, 16626 assertions mysql2: 33 excludes, 5491 runs, 15527 assertions
  22. Rails 6.0.0.rc1 actioncable: 203 runs, 921 assertions, 0 failures, 10

    errors actionmailbox: 79 runs, 205 assertions, 0 failures, 2 errors actionmailer: 220 runs, 509 assertions, 0 failures, 0 errors actionpack: 3255 runs, 16027 assertions, 1 failures, 0 errors actiontext: 53 runs, 94 assertions, 5 failures, 0 errors actionview: 2068 runs, 4667 assertions, 2 failures, 3 errors activejob: 301 runs, 659 assertions, 0 failures, 0 errors activemodel: 844 runs, 2350 assertions, 0 failures, 0 errors activestorage:225 runs, 696 assertions, 0 failures, 0 errors activesupport: 4362 runs, 13909 assertions, 15 failures, 1 errors 99.902% passing!
  23. ARJDBC (master - 60.x) • 60.0.rc1 is out! • Same

    excludes as 52.x • New excludes as we work towards 6.0.0 final • Rails 6 apps basically work fine! sqlite3: 20 excludes, 6415 runs, 16749 assertions postgresql: 89 excludes, 6962 runs, 18289 assertions mysql2: 66 excludes, 6535 runs, 17210 assertions Daniel Ritz
  24. Use-Case: Discourse • Big well-known Rails application • “A platform

    for community discussion” • >500 gems • 250,000 lines of Ruby • JRuby is not currently supported
  25. JRuby-lint See how ready your Ruby code is to run

    on JRuby % gem install jruby-lint % cd my-app % jruby-lint STEP 1
  26. Strategy: Replace with Pure Ruby def ruby_xor!(x, y) i =

    0 max = (x.length < y.length ? x.length : y.length) while i < max x.setbyte(i, x.getbyte(i) ^ y.getbyte(i)) i += 1 end end ruby 3.702M (± 5.8%) i/s - 18.431M in 4.995973s xorcist 11.015M (± 8.1%) i/s - 54.710M in 5.007484s Unless the performance matters…
  27. Strategy: FFI - Foreign Function Interface • Bind to DLL/shared

    library • Call functions & interact with structs/pointers • Pure-Ruby syntax! • Portable across all Ruby implementations
  28. https://github.com/chuckremes/ffi-rzmq-core/blob/master/lib/ffi-rzmq-core/libzmq.rb module LibZMQ extend FFI::Library # ... ffi_lib(ZMQ_LIB_PATHS + %w{libzmq})

    attach_function :zmq_strerror, [:int], :pointer, :blocking => true # ... end Load FFI methods Load specific DLL Attach function Nam e of function Input param eters Return value puts "Error: #{LibZMQ.zmq_strerror(errno)}" Call it just like Ruby!
  29. Strategy: Script Java Library • JRuby allows call Java classes

    with Ruby syntax • Zillions of existing Java libraries • Usually minimal glue code
  30. mini_racer port • mini_racer is minimal bindings to V8 •

    JRuby will script Java library J2V8 • Native bindings to V8 from Java • Someone wrote the C so we don’t have to!
  31. mini_racer porting snippets require 'mini_racer/jruby/j2v8_linux_x86_64-4.8.0.jar' java_import com.eclipsesource.v8.V8 @v8 ||= V8::createV8Runtime

    JSToRuby(@v8.execute_script(src, file, 0)) Load Java Library Make Java V8 class accessible Call method on that class Tough to tell we are calling Java here
  32. Strategy: Native Java Extension • Write the extension in Java

    • Using our extension APIs • Generally fastest
  33. Porting oj • Popular JSON gem • 19810 lines of

    C • 7 parsers/dumpers (object, strict, compat, null, custom, rails, wab) https://github.com/ohler55/oj
  34. JRuby APIs @JRubyModule(name = "Oj") public class RubyOj extends RubyModule

    { // ... @JRubyMethod(module = true, required = 1, rest = true) public static IRubyObject strict_load(ThreadContext context, IRubyObject self, IRubyObject[] args, Block block) { OjLibrary oj = resolveOj(self); Options options = oj.default_options.dup(context); ParserSource source = processArgs(context, args, options); return new StrictParse(source, context, options).parse(oj, true, block); } } Define Oj module Define Oj.strict_load
  35. Porting Gems Conclusion • Pain when replacement does not already

    exist • Many strategies to move past the pain • Once someone does it we all get it • Yay for Open Source
  36. Running JRuby on Rails • Puma recommended as server •

    Deployment tools work the same on JRuby • Another option: single-file deployment with Warbler • JVM will try to use all your memory • Tweak JVM heap max with JAVA_OPTS=-Xmx500M • JRUBY_OPTS, JAVA_OPTS so child processes pick up flags
  37. Scaling Rails • Classic problem on MRI • No concurrent

    threads, so we need processes • Processes duplicate runtime state and waste resources • JRuby is the answer! • Multi-threaded single process runs your entire site • Single process with solid GC uses resources better
  38. One User's Story • Large Rails application using 40 c1.xlarge

    on EC2 • 40 Unicorn workers per server • 100k-150k requests per minute, 50-75ms response times • Migrated app to JRuby, made more use of threading • Down to 10 c1.xlarge, 75% cost reduction • Consistently over 150k requests per minute, 30ms response times
  39. Optimizing for Rails • Most important metric for Ruby performance

    • Very difficult framework to optimize • See also k0kubun's JIT talk • JRuby typically ran a bit slower than CRuby • Until recently!
  40. Baseline Rails App • Simple scaffolded "blog" application • EC2

    t2.xlarge, Ubuntu 18.04 • CRuby 2.6.2, JRuby 9.2.7, TruffleRuby RC16 • what the hell drivers different servers, numbers change • for MRI too • Workers versus threads briefly
  41. requests per second (higher is better) 0rps 300rps 600rps 900rps

    1200rps Scaffolded blog post view 1,160rps 1,014rps CRuby JRuby
  42. Caveats • Benchmark driver changes results • Keep-alive versus new

    connections • JVM takes time to fully warm up • Working on ways to mitigate this • Many users drive the system a bit after deploy • What about a larger app?
  43. rubygems.org benchmark • Laptop (i7 with 16Gb of memory) (localhost)

    • Production mode force_https = false • Puma (JRuby does not support unicorn) • All on same machine: • rails, postgresql, elasticsearch, toxiproxy, redis, memcached • client software: Apache Bench (ab)
  44. rubygems.org benchmark • http://localhost:9292/profiles/enebo • Gem push is not working

    (toxiproxy?) • Benchmark mystery! • wrk never would show improvements with multiple threads • ab keep alive problems (due to bug #1565 in Puma)
  45. Requests/second 0 20 40 60 80 Iteration (30s) 1 2

    3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 JRuby puma single 2.6.2 puma clustered 2.6.2 puma single ab -c 1 -t 30s http://localhost:9292/profiles/enebo The Warmup Tail
  46. Peak Requests/second 0 100 200 300 400 Concurrent threads 1

    2 4 6 8 10 12 14 16 18 20 JRuby puma single 2.6.2 puma clustered 2.6.2 puma single ab -c {n} -t 30 http://localhost:9292/profiles/enebo 3 16
  47. Memory Usage Memory (RES) 0 1000 2000 3000 4000 #

    of test runs 1 Run (35 mins) 6 Runs 11 Runs 16 Runs 21 Runs 1,536 1,536 1,536 1,434 1,228 3,620 3,540 3,460 3,340 3,060 CRuby (20 workers) JRuby (20 threads, 384m heap) +8.4% +14.4% +4.5% +6.7% +0% 2.5x smaller +0% +3.3% +3.2%
  48. Scaling Takeaways + Uses less memory (threads vs processes) •

    Whole duplicated stack + app uses more memory + More memory stable over time • CoW? Fragmenting in GC? Memory leak? + More CPU efficient • More error tolerant on same hardware – More warmup time
  49. JRuby Futures • Ruby 2.6 or 2.7? • Stop by

    chats and let us know • Native-compiling JRuby for startup time • Upcoming JVM features: true fibers, built-in FFI, new JITs and GCs
  50. JRuby on Rails Futures • Rails 6 is working today

    • Continue getting more tests green, JRuby in CI • We're here when you need us • As your app grows, JRuby can help you scale • Reduce resources, save money • Start with JRuby or keep it in mind as you go
  51. Help Wanted! • New features, Rails tests are great opportunities!

    • Learn more about how Ruby and JRuby work • Help us keep up with Ruby development • We are always standing by to help you!
  52. Thank You! • Charles Oliver Nutter • [email protected] • @headius

    • Tom Enebo • [email protected] • @tom_enebo • http://jruby.org • Ruby 2.6 checklist:
 https://github.com/jruby/jruby/issues/5576