JRuby + Ratpack =❤️

多くのRuby開発者は、既存のサーバーでの非同期コード実行での動作を知っています。 EventMachineで何かを使用するか、Ruby :: Concurrent、Celluloidを思い浮かべてください。







いずれにせよ、これはGILのために非常に効率的に機能しません(Ruby 3を待ち望み、信じています)。

しかし、この問題のない実装があります。JVMの上にある実装の1つにJRubyがあり、ライブラリ自体がはるかに快適に感じられます。







私はあまりペイントしません、少なくとも誰もが彼について聞いたことがあると思います。 この実装の主な機能は、JVM上のライブラリとの簡単な統合です。 これにより、ライブラリとツールの選択の範囲が広がります。







そのため、Javaの世界には、Executorで標準の競合Javaモデルを使用して、アクターに実装することから私たちを救うライブラリがあります。 Nettyライブラリを呼び出します。 後に、Ratpackなど、他のものがそのベースで開発されました。







Ratpackは非同期Webサーバーであり、Nettyは内部にあるため、接続で非常に効率的に動作し、一般にIOで生産的なサーバーを構築するために必要なすべてが含まれています。







したがって、Ratpackの機能、Ruby(JRuby)の柔軟性とシンプルさを使用して、短いリンクを展開する最も単純なサービスを作成します。 インターネットには数多くの例がありますが、それらはすべてを実行して簡単な答えを得る方法になります。







メトリックの接続に関する別の例(最後にリンク)があります。Groovyにのみ提供されているため、ドキュメントのメソッドはJRubyにはまったく適していません。







この例では、次を考慮します。









ライブラリを接続する







すべてのRubyプログラマーはバンドラーを使用しますが、彼のいない生活は悲しく、ダンス、熊手、その他の冒険に満ちていました。







Javaの世界には、これらの依存関係を引き出してアプリケーションをビルドするさまざまなビルダーがありますが、これはRubyの方法ではありません。







そこでjbundlerが登場しました。 bundlerと同じ機能を実行しますが、Javaライブラリの場合、ロードされるとJRubyから利用可能になります。 美人!







したがって、Ratpackをアプリケーションに接続する必要があります。 コアのみで十分で、残りは使用していません。







Gemfile:







source 'https://rubygems.org' ruby '2.3.0', :engine => 'jruby', :engine_version => '9.1.2.0' gem 'rake' gem 'activesupport', '4.2.5' gem 'jruby-openssl' gem 'jbundler', '0.9.2' gem 'jrjackson' group :test, :development do gem 'pry' end group :test do gem 'rspec' gem 'simplecov', require: false end
      
      





jarfile:







 jar 'io.ratpack:ratpack-core', '1.4.2' jar 'org.slf4j:slf4j-simple', '1.7.10'
      
      





コンソールで実行します







 bundle install bundle exec jbundle install
      
      





将来的にはさらに2、3のライブラリを追加しますが、今のところこれについて詳しく説明します。







サーバー作成



すべての依存関係をダウンロードした後、ベースサーバーを作成し、すべてが機能することを確認します。 Rackがないため、通常のツールを使用してルーティングを行います。







まず、必要なJavaクラスをインポートします。







 require 'java' java_import 'ratpack.server.RatpackServer' java_import 'ratpack.server.ServerConfig' java_import 'java.net.InetAddress'
      
      





そして、サーバークラスを宣言します。







 module UrlExpander class Server attr_reader :port, :host def self.run new('0.0.0.0', ENV['PORT'] || 3000).tap(&:run) end def initializer(host, port) @host = host @port = port end def run @server = RatpackServer.of do |s| s.serverConfig(config) s.handlers do |chain| chain.get 'status', Handler::Status chain.all Handler::Default end end @server.start end def shutdown @server.stop end private def config ServerConfig.embedded .port(port.to_i) .address(InetAddress.getByName(host)) .development(ENV['RACK_ENV'] == 'development') .base_dir(BaseDir.find) .props("application.properties") end end end
      
      





サービスのエンドポイントステータスを作成しました。これにより、原則としてサーバーが稼働しているかどうかを確認できます。

handlersメソッドは、ルーティングを定義するChainインターフェースが渡されるブロックを受け入れます。 ステータスを宣言するには、HTTPメソッドと同等のgetメソッドを使用します。







2番目の引数は、 Handlerインターフェースを実装するオブジェクトです。 私たちの場合、これはhandleメソッドが宣言されているモジュールで、現在のコンテキストを取ります。 ご覧のとおり、すべてが非常にシンプルで明確です。 3階建ての工場などはありません。







実際にはハンドラー自体、すべてがOKであると答えてください:







 module UrlExpander module Handler class Status def self.handle(ctx) ctx.render 'OK' end end end end
      
      





Ratpackには独自のヘルスチェックの実装もありますが、この例では冗長です。







接続メトリック



これでサービスのステータスを監視できるようになりましたが、サービスの内容、応答時間、リクエストの数、その他の指標を把握しておくと便利です。







このためには、メトリックが必要です。 RatpackはDropwizardと統合されているため、Jarfileにいくつかのパッケージを追加してインストールする必要があります







 jar 'io.ratpack:ratpack-guice', '1.4.2' jar 'io.ratpack:ratpack-dropwizard-metrics', '1.4.2'
      
      





次に、サーバーに接続します。 これは非常に簡単で、いくつかのセクションを変更するだけです。







 java_import 'ratpack.guice.Guice' java_import 'ratpack.dropwizard.metrics.DropwizardMetricsConfig' java_import 'ratpack.dropwizard.metrics.DropwizardMetricsModule' java_import 'ratpack.dropwizard.metrics.MetricsWebsocketBroadcastHandler'
      
      





レジストリにモジュールを登録します。







  s.serverConfig(config) s.registry(Guice.registry { |g| g.module(DropwizardMetricsModule.new) })
      
      





そしてその設定をロードします:







  def config ServerConfig.embedded .port(port.to_i) .address(InetAddress.getByName(host)) .development(ENV['RACK_ENV'] == 'development') .base_dir(BaseDir.find) .props("application.properties") .require("/metrics", DropwizardMetricsConfig.java_class) end
      
      





また、WebSocketを使用してメトリックを受け取り、このハンドラーを追加します。







  s.handlers do |chain| chain.get 'status', Handler::Status chain.get 'metrics-report', MetricsWebsocketBroadcastHandler.new chain.all Handler::Default end
      
      





完了したら、メトリックのアップロードをコンソールまたはStatsDに接続することもできます。 出力用のWebSocketができたので、表示用のページを追加します。







スキームは標準であり、パブリックフォルダーにはすべての統計が含まれます。 それを返すために、フォルダーとインデックスファイルの名前を指定して、追加のルートを作成します。







  s.handlers do |chain| chain.files do |f| f.dir('public').indexFiles('index.html') end chain.get 'status', Handler::Status chain.get 'metrics-report', MetricsWebsocketBroadcastHandler.new chain.all Handler::Default end
      
      





外部リソースへのリクエストの非同期実行



サーバーは当社で起動し、指定されたポートをリッスンし、リクエストに応答します。 次に、エンドポイントを追加します。これにより、ショートリンクが通過するすべてのURLが返されます。 アルゴリズムは単純で、リダイレクトごとに新しいLocationを配列に保存してから返します。







 s.handlers do |chain| chain.get 'status', Handler::Status chain.path 'expand', Handler::Expander chain.all Handler::Default end
      
      





追加されたエンドポイントは、POST要求とGET要求の両方を受け入れます。







ブロッキングAPIのみがある場合、各リクエストは独自のスレッドで処理されます。処理されると、サーバーからの応答を待機する時間の90%になります。 最小限の便利な計算があります。 しかし幸いなことに、Ratpackは非同期サーバーであり、非同期httpクライアントやPromiseなどの完全なコンポーネントセットを提供します。

それで、各ソースリンクにPromiseを作成してみましょう。Promiseは正常に完了するとLocation配列を返します。







内部では、URLでGETを実行し、コールバックを切ってサーバーから新しい場所を取得します。







したがって、宛先URLとすべての中間URLを配列に入れます。







 module UrlExpander module Handler class Expander < Base java_import 'ratpack.exec.util.ParallelBatch' java_import 'ratpack.http.client.HttpClient' def execute data = request.present? ? JrJackson::Json.load(request) : {}
      
      





リンクを収集するHttpClientを作成します







  httpClient = ctx.get HttpClient.java_class
      
      





渡されたすべてのURLを収集し、何もない場合は、すぐに空のマップでPromiseを返します。







  urls = [*data['urls'], *data['url'], *params['url']].compact unless urls.present? return Promise.value({}) end
      
      





転送されたすべてのリンクに対して並列リクエストを作成します。







  tasks = urls.map do |url| Promise.async do |down| uri = Java.java.net.URI.new(url) locations = [url] httpClient.get(uri) do |spec| spec.onRedirect do |resposne, action| locations << resposne.getHeaders.get('Location') action end end .then do |_resp| down.success(locations); end end end
      
      





実行を待って、結果を収集して返します。







  ParallelBatch.of(tasks).yieldAll.flatMap do |results| response = results.each_with_object({}) do |result, locations| result.value.try do |list| locations[list.first] = list[1..-1] end end Promise.value response end end end end end
      
      





その結果、Promiseからチェーンを取得し、コードを非同期に実行します。







サービスをテストする



彼らが書いたものをテストする時です。 古き良きrspecをテストしますが、ニュアンスがあります。 なぜなら Ratpack + Promiseを使用する場合、ライブラリから隔離してテストすることはできません。何らかの理由でこれらのPromiseを実行する必要があります。 動作するイベントループが必要です。 これを行うには、キットから追加のJARライブラリーを接続します。







 jar 'io.ratpack:ratpack-test', '1.4.2'
      
      





このライブラリを使用すると、テストリクエスト(テストサーバーの作成)とPromiseの実行の両方を整理できます。 後者の場合、 ExecHarnessクラスが使用されます。ドキュメントでは詳細に説明されており、サンプルはJRubyに簡単に移植できます。







GET要求の実行方法をテストし、テストサーバーを実行できるEmbeddedAppを使用します。 作成を簡素化するためのさまざまな静的メソッドがあります。

特定の場合。 パスに関係なくハンドラーをテストするだけなので、次のように作成します。







 describe UrlExpander::Handler::Expander do let(:server) do EmbeddedApp.fromHandlers do |chain| chain.all(described_class) end end #... end
      
      





そして、すべてが正常に機能することを確認します。







  let(:url) { 'http://bit.ly/1bh0k2I' } context 'get request' do it do server.test do |client| response = client.params do |builder| builder.put('url', url) end .getText response = JrJackson::Json.load(response) expect(response).to be_present expect(response).to be_key url expect(response[url].last).to match /\/ya\.ru/ end end end
      
      





テストメソッドは実行を開始し、ブロックにリクエストが実行されるTestHTTPClientのインスタンスを渡します。 次に、受信した応答を確認します。 ご覧のとおり、すべてが非常に簡単です。

ExecHarnessとは異なり、EmbeddedAppはチェックごとにサーバーを再作成しますが、ExecHarnessはEventLoopを1回だけ実行します。







したがって、コードをできる限りRatpack Contextから分離して、独立してテストできるようにすることをお勧めします。







Herokuで起動



すべての準備が整ったら、herokuでプロジェクトを開始します。 この手順は、通常のルビーサービスを開始する場合とほとんど変わりません。







唯一の違いは、JARライブラリをインストールする必要があり、herokuはこの操作を自動的に実行しないことです。







このために小さなハックが行われます。 原則として、それはどこでも説明されていますが、整合性のためにここで繰り返します。 ビルドプロセス中に、スタティックがビルドされます。これを使用して、次のレーキタスクを追加します。







 task "assets:precompile" do require 'jbundler' config = JBundler::Config.new JBundler::LockDown.new( config ).lock_down JBundler::LockDown.new( config ).lock_down("--vendor") end
      
      





アセンブリ中に、Jarfileで指定されたライブラリもすべてインストールされます。







おわりに



ご覧のとおり、RatpackをJRubyと組み合わせて使用​​することはそれほど難しくはありませんが、同時にJVMとNettyのすべての機能にアクセスできます。 それに基づいて、高性能の非同期サーバーを構築できます。 これらはすべて本番環境に対応しており、Hello Worldのテストでは、ウォームアップ後にドッカーコンテナー内のEC2 c4.largeで最大25k rpsが表示されます。 ウォームアップのために約3万件のリクエストが行われ、最初は時間が変動していますが、最後にはすでに安定しています。 さらに、かなり複雑なロジックであっても、クエリの実行時間は数ミリ秒です。 確かにタスクに依存しますが、PumaをRatpack(時間を評価するためにテスト済み)に置き換えるだけでも、大幅に増加しました。 コードを完全にリファクタリングおよび再考し、JVMの厳密な最適化を行った後、時間は桁違いに短縮されました。 だから、Javaのパフォーマンスと柔軟性、Rubyの開発速度を探している人がいる一方で、このペアを見ることをお勧めする実証済みのコードもあります。







参照資料






All Articles