$30 off During Our Annual Pro Sale. View Details »

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. JRuby: From Zero To Scale
    Charles Oliver Nutter (@headius)
    Thomas Enebo (@tom_enebo)

    View Slide

  2. • JRuby co-leads
    • Red Hat Inc.
    Charles Thomas Ruby
    Java
    Beer

    View Slide

  3. #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

    View Slide

  4. View Slide

  5. 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

    View Slide

  6. JVM Language
    Gets all the benefits the Java platform has to offer...

    View Slide

  7. JVM Tools and GC

    View Slide

  8. Parallel and Concurrent

    View Slide

  9. Access to JVM Libraries
    Everything which has ever been made also has a JVM library for it

    View Slide

  10. 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

    View Slide

  11. 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

    View Slide

  12. Getting Started

    View Slide

  13. View Slide

  14. 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

    View Slide

  15. That's it!

    View Slide

  16. 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
    => #
    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 >

    View Slide

  17. Startup Time

    View Slide

  18. 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

    View Slide

  19. 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)

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. 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

    View Slide

  24. 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

    View Slide

  25. 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?

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. JRuby on Rails

    View Slide

  30. 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

    View Slide

  31. --- 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

    View Slide

  32. Rails Support Update

    View Slide

  33. 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!

    View Slide

  34. Failure:
    TimeWithZoneTest#test_minus_with_time_precision [activesupport/
    test/core_ext/time_with_zone_test.rb:340]:
    Expected: 86399.999999998
    Actual: 86399.99999999799

    View Slide

  35. 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

    View Slide

  36. 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!

    View Slide

  37. 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

    View Slide

  38. Migrating an App

    View Slide

  39. 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

    View Slide

  40. JRuby-lint
    See how ready your Ruby code is to run on JRuby
    % gem install jruby-lint

    % cd my-app

    % jruby-lint
    STEP 1

    View Slide

  41. Questionable Gems

    View Slide

  42. https://github.com/jruby/jruby/wiki/C-Extension-Alternatives

    View Slide

  43. Questionable Assignments?

    View Slide

  44. Other Issues…

    View Slide

  45. Native C Extensions!
    % bundle install
    STEP 2

    View Slide

  46. Strategy: Ignore?
    gem ‘byebug’, platform: :ruby

    View Slide

  47. 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…

    View Slide

  48. Strategy: FFI - Foreign Function Interface
    • Bind to DLL/shared library
    • Call functions & interact with structs/pointers
    • Pure-Ruby syntax!
    • Portable across all Ruby implementations

    View Slide

  49. 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!

    View Slide

  50. Strategy: Script Java Library
    • JRuby allows call Java classes with Ruby syntax
    • Zillions of existing Java libraries
    • Usually minimal glue code

    View Slide

  51. 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!

    View Slide

  52. 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

    View Slide

  53. Strategy: Native Java Extension
    • Write the extension in Java
    • Using our extension APIs
    • Generally fastest

    View Slide

  54. Porting oj
    • Popular JSON gem
    • 19810 lines of C
    • 7 parsers/dumpers (object, strict, compat, null, custom, rails, wab)
    https://github.com/ohler55/oj

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. 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

    View Slide

  58. 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

    View Slide

  59. 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

    View Slide

  60. 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!

    View Slide

  61. 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

    View Slide

  62. requests per second (higher is better)
    0rps
    300rps
    600rps
    900rps
    1200rps
    Scaffolded blog post view
    1,160rps
    1,014rps
    CRuby JRuby

    View Slide

  63. 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?

    View Slide

  64. rubygems.org performance

    View Slide

  65. 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)

    View Slide

  66. 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)

    View Slide

  67. 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

    View Slide

  68. 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

    View Slide

  69. 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%

    View Slide

  70. 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

    View Slide

  71. Wrapping Up

    View Slide

  72. 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

    View Slide

  73. 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

    View Slide

  74. View Slide

  75. 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!

    View Slide

  76. Try JRuby!
    Let us know how it goes!

    View Slide

  77. 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

    View Slide