JSONデータ圧縮方式をSnappyからzstdに切り替えた事例紹介

JSONデータ圧縮方式をzstdに切り替えデータ量を38.3%削減した事例、及びマイクロサービスの無停止アップデート事例について紹介したいと思います。

はじめに

JPRゲーム事業本部開発基盤部の池田周平です。先日Rails5対応についてDeNA techブログに投稿した@namusyakaと同じチームで働いています。

JSON文字列をRDBに格納する際の圧縮フォーマットをSnappyからzstdに切り替え、データ量を削減した事例を紹介したいと思います。本対応を実施した目的はDB負荷対策です。DBで扱うデータをより小さくすることで、DBサーバのDiskI/O負荷とMaster-Slave間のレプリケーション遅延対策を目的としています。

「Sakasho」は、DeNAが持つモバイルゲームのためのプラットフォームです。複数タイトルのゲームを取り扱っており、一部データはゲーム毎の仕様差を吸収し柔軟に取り扱うため、あえてスキーマレスなJSONを採用しています。JSON文字列に圧縮を掛けRDBに保存しているのです。RDBにJSONを格納している狙いはデータ不整合を避けるためで、トランザクションを張りデータ操作しています。

Snappy vs zstd

サンプル数はそれぞれ500万件くらいで、平均6Kbyte 最大2MbyteのJSONデータに対する圧縮時のログを集計して計算しました。結果データ圧縮率はzstdが12.4062% Snappyが20.1272%でした。Snappyからzstdに切り替えた結果データ量が38.3%削減されました。またAPPサーバのCPU使用率は反映前後で変化がなくサーバ追加等の対応は発生しませんでした。

あくまでDeNAで運用しているサービスの1事例です。正確な情報はZstandard公式ページをご覧ください。

Snappyとzstdについて

zstd正式名称Zstandardは、Facebookが2016年に公開したBSDライセンスのリアルタイム圧縮アルゴリズムです。リアルタイム圧縮とはデータを高速に圧縮と解凍することに主眼を置いたアルゴリズムであることを意味しています。公式ドキュメントによるとzlibと比較して圧縮率は変わらず、圧縮速度3.9倍、解凍速度2.8倍の性能です。またトレーニング機能を有し、これはデータ毎に固有辞書を生成する機能で、より効率的なデータ圧縮を実現します。

※ 圧縮解凍速度は、Zstandard公式ページに公開されているベンチマークデータから計算しました。またSnappyはGoogleが2011年に公開した圧縮アルゴリズムです。

Sakashoとは

Sakashoは、DeNAが持つモバイルゲームのためのプラットフォームです。 モバイルゲームを開発するために必要な機能を一通り提供し、ゲームの開発の効率化を図るための共通基盤として開発・運用されています。マイクロサービス構成となっており、役割ごとに10の独立したサービスと、管理ツールによって構成されています。

sakasho.jpg

マイクロサービスの無停止アップデート

データ圧縮方式をSnappyからzstdに切り替えるにあたり、無停止でアップデートを実施するためにdeploy方法を工夫しました。

仮にマイクロサービス群に対して順にdeployを行っていくと、後半にdeployするサービスで障害が発生してしまいます。 zstd_deploy_1.png

無停止でアップデートするために、下図のようにdeployを2段階に分け、1段目でSnappyとzstdどちらでもデータをreadできる対応をdeployし、2段目でデータ圧縮方式を切り替える対応をdeployすることで複数のマイクロサービスにまたがる修正を本番反映しました。

zstd_deploy_2.png zstd_deploy_3.png

zstdライブラリの選定

各サービスは主にRubyで開発しています。zstdもRubyから扱う場合がほとんどです。gemに登録されている複数のzstdライブラリのうち、どれを選ぶべきか悩んでいたらruby expertな先輩からnative extentionでビルドしているため、メモリリークの観点で調査するべきだとアドバイスもらいました。

検証結果とコードはこちらです。


# memory_usage_zstd.rb
require 'zstd'

def current_process_memory_usage
  `ps ax -o pid,rss | grep -E "^[[:space:]]*#{Process.pid}"`.strip.split.map(&:to_i)[1]
end

JSONFILE_PATH = 'data_139k.json'
json_data = open(JSONFILE_PATH){ |io| io.to_s }
compressed_data = Zstd.new.compress(json_data)

1_000_000_000.times do |n|
  Zstd.new.decompress(compressed_data)
  puts "loop:#{n} memory usage: #{current_process_memory_usage} Kbytes" if n % 1_000_000 == 0
end

# memory_usage_zstd_ruby.rb
require 'zstd-ruby'
.....
compressed_data = Zstd.compress(json_data)

1_000_000_000.times do |n|
  Zstd.decompress(compressed_data)
  puts "loop:#{n} memory usage: #{current_process_memory_usage} Kbytes" if n % 1_000_000 == 0
end

# gem: zstd (残念ながらメモリリークした)
# https://rubygems.org/gems/zstd/

$ ruby memory_usage_zstd.rb
loop:0 memory usage: 10884 Kbytes
loop:1000000 memory usage: 43200 Kbytes
loop:2000000 memory usage: 74776 Kbytes
loop:3000000 memory usage: 106692 Kbytes
loop:4000000 memory usage: 138616 Kbytes
loop:5000000 memory usage: 169960 Kbytes
loop:6000000 memory usage: 201592 Kbytes
loop:7000000 memory usage: 233584 Kbytes
loop:8000000 memory usage: 265236 Kbytes
loop:9000000 memory usage: 297132 Kbytes
loop:10000000 memory usage: 329136 Kbytes

# gem: zstd-ruby 
# https://rubygems.org/gems/zstd-ruby

$ ruby memory_usage_zstd_ruby.rb
loop:0 memory usage: 10836 bytes
loop:1000000 memory usage: 13060 Kbytes
loop:2000000 memory usage: 13808 Kbytes
loop:3000000 memory usage: 13816 Kbytes
loop:4000000 memory usage: 13944 Kbytes
loop:5000000 memory usage: 13944 Kbytes
loop:6000000 memory usage: 13944 Kbytes
loop:7000000 memory usage: 14176 Kbytes
loop:8000000 memory usage: 14176 Kbytes
loop:9000000 memory usage: 14184 Kbytes
loop:10000000 memory usage: 14184 Kbytes

まとめ

JSONをzstd圧縮してDBに格納したら、Snappyと比較してデータ量が38.3%削減。JSON文字列と比較して87.6%データ削減できました。 Rubyでzstdフォーマットを扱う場合はzstd-rubyがオススメです。

続きを読む
ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

「Sakasho」のRubyを2.4に、Railsを5にアップグレードしました

はじめに

JPRゲーム事業本部開発基盤部の@namusyakaです。

業務ではDeNAのゲームプラットフォームであるSakashoのバックエンドやインフラ周りの開発・運用をしています。

そして最近アイコンを8~9年ぶりくらいに変えました。よろしくお願いいたします。

さて本題ですが、Sakashoでは今年の2月に管理アプリケーションのRuby・Railsのバージョンの大幅なアップグレードを実施しました。この記事ではそのアップグレード対応について、一つの事例として紹介させていただければと思います。

概略

冒頭でも触れましたが、アップグレードしたのはDeNAのモバイルゲームプラットフォームであるSakashoの機能を制御するための管理アプリケーションになります。

Sakashoとは

Sakashoは、DeNAが持つモバイルゲームのためのプラットフォームです。 モバイルゲームを開発するために必要な機能を一通り提供し、ゲームの開発の効率化を図るための共通基盤として開発・運用されています。

Sakasho全体のアーキテクチャは次のようなイメージです。

sakasho architecture

今回のアップグレードは、この図の右端に位置する管理アプリケーションが対象となります。 ところで、余談ではありますが、SakashoのAPIは原則としてRailsではなくSinatraをベースに実装されています。 機会があれば、そのアプリケーションアーキテクチャについても、今後紹介できればと思います。

管理アプリケーションの特性

主に次のような特性を持ちます。

  • ruby-2.1.4/rails-4.1.1で動作している
  • ゲームプラットフォームが持つ全機能に対する制御系統を持つ。
    • ゆえに、コードベースがかなり大きい。
  • 複数DBを前提とした作りになっている。
    • 一部シャーディングもしている。
  • テストはある程度揃っている

実際の規模感をお伝えするために、rake statsの実行結果を貼っておきます。

+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |  19075 |  15518 |     247 |    2224 |   9 |     4 |
| Helpers              |   1879 |   1657 |       0 |     225 |   0 |     5 |
| Models               |  21974 |  16928 |     581 |    1442 |   2 |     9 |
| Mailers              |      0 |      0 |       0 |       0 |   0 |     0 |
| Javascripts          |   3000 |   2763 |       0 |     620 |   0 |     2 |
| Libraries            |  10018 |   7358 |      96 |     395 |   4 |    16 |
| Tasks                |   4416 |   3132 |       2 |      15 |   7 |   206 |
| Config specs         |     23 |     20 |       0 |       0 |   0 |     0 |
| Controller specs     |  45014 |  40239 |      28 |       4 |   0 | 10057 |
| Helper specs         |    850 |    727 |       0 |       1 |   0 |   725 |
| Lib specs            |   9852 |   8384 |       1 |      10 |  10 |   836 |
| Model specs          |  17982 |  15974 |       0 |       3 |   0 |  5322 |
| Support specs        |   1038 |    943 |       2 |      13 |   6 |    70 |
| Validator specs      |    396 |    328 |       2 |       7 |   3 |    44 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                | 135517 | 113971 |     959 |    4959 |   5 |    20 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 47356     Test LOC: 66615     Code to Test Ratio: 1:1.4

簡単に見方を説明すると、LOCというのはLines Of Codeの略で、コードの行数を指します。 その他にも、例えばモデルと分類されるクラスが何個あるか、といった情報を読み取ることができるため、参考にしていただければ幸いです。

改修期間

対応のために掛けた期間は着手から本番リリースまで凡そ一ヶ月弱です。 大きく「開発」・「テスト」の2つのフェーズに分けることができ、それぞれにかかった期間としては次のようになります。

  • ruby-2.4.0・rails-5.0.1にアップグレードし、全てのテストケースがpassすること (一週間)
  • QAによるテスト (二週間)

開発期間中のアップグレード作業は私一人で進め、変更点のレビューなどはチームメンバー全体に依頼する形で進めました。 今思うと大分駆け足だった感はありますが、現時点で大きな障害もなく稼働しています。

なお、一応補足しますが、この記事は「テスト」ではなく「開発」のフェーズについての解説となります。

アップグレード戦略

前提となりますが、この管理アプリケーションはruby-2.1.4/rails-4.1.1で動作していました。

それらを目的のruby-2.4.0/rails-5.0.1にアップグレードするために参考にしたのが、Railsが公式に提供しているA Guide for Upgrading Ruby on Railsです。

このアップグレードガイドによると、rails-5にアップグレードするには、対象のアプリケーションはrails-4.2でなければならないようです。 したがって、目標のアップグレードを実施するには段階を踏んでアップグレードする必要があり、まずはrails-4.1.1をrails-4.2.7.1にアップグレードしなければなりません。

実際のアップグレードの流れは次のとおりです。

  • rails-4.1.1をrails-4.2.7.1までアップグレード
  • ruby-2.1.4をruby-2.4.0までアップグレード
    • ruby-2.4にバージョンをあげたらrails-4.2.7.1では動作しなかったので、railsの4-2-stableにこの時点でスイッチしました。
  • rails-5.0.1までアップグレード

一つのフェーズごとにテストが全てpassすることをゴールと設定しています。

そして、各バージョンにアップグレードする前には、可能な限りdeprecation warningsを潰すようにしました。 これは、将来的に削除される機能などを事前に回避しておくことでトラブルを防ぐ意味合いがあります。

また、この戦略を進めるにあたり、リリースブランチとは別にアップグレード専用のブランチを用意するほか、ruby-2.4でテストするための専用のjenkins jobを用意して、通常のリリースフローとは独立する形で作業を進めました。

変更点

この手の作業はbundle updateを通す作業から始まります。 幸いにして5.0.1は去年の12月リリースだったこともあって、ある程度依存しているgemは対応済みで、gemに対してパッチを当てたりforkしたり、といったことは特にする必要はありませんでした。 新しいバージョンが出たばかりのHotな時期にアップグレードするのもいいですが、数年間アップグレードされてこなかった規模の大きいアプリケーションを対象とする場合は、数ヶ月程度の時間を置いてから進めた方がハマりどころが少なくて助かるかもしれません。

次に実際の変更点について紹介します。 しかしながらあまりに数が多すぎるため、特に影響範囲が大きかった変更と、deprecation warningsなどの中から、アップグレードガイドに記載されていないものを中心にいくつか取り上げようと思います。

rails-4.2.7.1対応

元がrails-4.1.1なので、まずはUpgrading from Rails 4.1 to Rails 4.2に従って粛々と対応しました。 Upgrade guideに書いてあることは割愛するとして、主要な変更点を挙げます。

deprecations & minor changes

Primary keyでないidcolumnを持つテーブルにおいて、取得したレコードをinspectした結果、idがnilと表記される

idというカラムは存在するが、primary keyではないケースで発生する不具合です。 不具合といってもinspectの結果がおかしい程度ではあるのですが、次のようなコードで正常にidが出力されない問題がありました。

Book.find_by(id: 1).inspect #=> "#<Book id: nil, ...>"

これについてはRails側を修正することで対応しました。たまたま見つけた不具合という感じです。

Pull Request: Fix inspection behavior when the :id column is not primary key by namusyaka · Pull Request #27935 · rails/rails · GitHub

t.timestampsのデフォルトがNULLを許可からNOT NULLに変更

次のように指定することで解決しました。

create_table :books do |t|
  t.timestamps null: false
end

従来どおりの挙動にしたければnull: trueとします。 動的にtableを生成するケースなどがもしあれば、要注意といえるでしょう。

JSON.loadnilを渡さないように修正

4.2.7.1では例外が出るようになっていました。 nullが返されることを期待しているわけでもなかったので渡さないように変更しました。

関連: Fixed JSON coder when loading NULL from DB by chancancode · Pull Request #16162 · rails/rails · GitHub

MySQL-5.6以上では時刻のミリ秒を保存するようになった

MySQLにミリ秒を持たせるのは別の意味でしんどくなりそうだったので、モンキーパッチで回避しました。

参考

ActiveRecord/ActiveModelが範囲外の値に対して例外を吐くようになった

例えば、次のようなコードについて考えます。

Book.where(id: -1).first

この処理はrails-4.1と4.2で挙動が異なり、4.1ではnilが返り、4.2以降はActiveModel::RangeError: -1 is out of range for ActiveModel::Type::UnsignedInteger with limit Xといった例外が発生するようになっています。

ActiveModelにtype castingの機構が実装されたお陰だと思います。 ロジックに問題のあるコードがこれで顕在化されることになるため、これを機に既存の実装を見直しました。

ところで、この機構は明示的にlimitが指定されていない場合に、signed int(4bytes)を自動的にリミットとして定めてしまうという問題があります。

参考: Avoid RangeError without explicit limit by kamipo · Pull Request #26302 · rails/rails · GitHub

Adequate Recordの影響で動的にテーブル名をセットしているところが壊れた件

Adequate Recordはactive_record/core.rbに定義されている.find.find_by.find_by_xxx系のメソッドに対して、ActiveRecord::StatementCacheを用いてキャッシュを有効化する機能を指します。 Arelを経由したSQLへの変換を行わなくて良くなるので二倍ほど高速化が見込めるという話のようです。

この機能自体は非常に素晴らしいのですが、これを実現するために内部的に使用されているキャッシュキーはprimary keyをベースにしたものとなっており、テーブル名については一切考慮されていませんでした。 したがって、テーブル名が動的に変わった場合もprimary keyが一致すればキャッシュが効いてしまうので、誤ったSQLを発行してしまうリスクが存在しています。

無論そんな使い方をするなという話もあるかもしれませんが、動きそうなところが動かないのは困るということで、Railsを修正しました。 直し方としてはtable_name=が実行されたらキャッシュをリセットするように修正する、というものです。

Pull Request: Make table_name= reset current statement cache by namusyaka · Pull Request #27953 · rails/rails · GitHub

ちなみにこれを回避するには、Adequate Recordが効かないQueryMethodsを使ってやるだけで良いので、次のように書き換えるのが簡単です。

Book.find_by(id: 1) # before

Book.where(id: 1).first # after

ruby-2.4.0対応

rails-4.2.7.1の対応を終えた後に、ruby-2.1.4からruby-2.4.0にアップグレードしたところ全く動きませんでした。 どうやら4.2.7.1はruby-2.4.0に対応していないようで、ruby-2.4にアップグレードする前に一旦4-2-stableを使うように変更しました。

なお、私が対応していた時点では4.2系のバージョンの中では4.2.7.1が最新版でしたが、つい先日4.2.8がリリースされ、公式にruby-2.4がサポートされています。今後アップグレードする際には4.2.8を使用することをオススメします。

さて、この項ではruby-2.1.4からruby-2.4.0にアップグレードしたことで発生した主要な問題点を挙げてみます。といっても、Railsほど苦労はなかった印象です。

FixnumIntegerに統合

軽く書いてはいますが、native extension系のgemは要注意です。 以下は具体例です。

アップグレードする対象のアプリケーションが依存するgemのruby-2.4 対応状況については事前に洗い出しておいた方が良さそうです。 必要であればパッチを送り、新バージョンのリリースをねだりましょう。

サブクラスの抽出に自前のObjectSpace#each_objectではなくActiveSupportのコア拡張であるClass#subclassesを使うように変更

ruby-2.3.0からObjectSpace#each_objectがシングルトンクラスを含むようになりました。 SakashoではClass#subclassesの自前実装をObjectSpace#each_objectをベースに持っていて、そちらを修正しようとも考えたのですが、ActiveSupportのコア拡張でほぼ同じことをやっていたので、そちらを使うようにして修正しました。

参考

openssl系の標準添付ライブラリにて、keyやivの文字数がvalidでないと例外が発生するようになった

もともとの実装では、文字数がオーバーしている場合は丸められていたようです。 したがって、事前に文字列を適切にsliceするように変更して修正しました。

参考: openssl: make Cipher#key= and #iv= reject too long values · ruby/ruby@ce63526 · GitHub

rails-5.0.1対応

rails-5.0.1にアップグレードする際にも、まずはUpgrade guideに沿って進めました。

5へのアップグレードには設定ファイルの追加やinitializerの追加などを含むため、そのあたりを中心に対応を自動化するための機構として、app:updateというRakeタスクが提供されています。 対話的に大きな差分を自プロダクトに反映していくことになりますが、ある程度既存のコードにも影響のある部分なので、しっかり差分を注視しながら進めた方が良さそうです。

このフェーズについても、Upgrade guideの内容は割愛し、主要な変更点を挙げていこうと思います。

deprecations & minor changes

ActionController::ParametersがHashを継承しなくなった

多くの場所で取り上げられている変更点ですが、例に漏れず対応が困難だったもののうちの一つです。 Hashが持つメソッドや振る舞いに依存しているコードの多くが正常に動作しなくなるため、アップグレードする際には事前にその辺を洗い出しておくと楽かもしれません。

参考: Make AC::Parameters not inherited from Hash by sikachu · Pull Request #20868 · rails/rails · GitHub

datetime_selectに入力された値をDateTimeとして取得する際に、受け取った値をそのままhidden_fieldtext_fieldに渡すとRubyのHashをinspectした値がvalueに埋め込まれてしまう件

例えば次のような要素があるとします。

= f.datetime_select :opened_at, use_month_numbers: true, start_year: default_start_year

このヘルパーは、日付にかかる情報を扱うために複数のselect要素をレンダリングします。 そして、opened_at(1i)opened_at(2i)...といった複数のキーからなるopened_atにかかる情報をサーバに送信し、それらのパラメータをActiveRecord::Baseインスタンスに渡してやることで、よしなにopened_atに時刻情報が代入されるという作りになっています。

これはもとからRailsが持つ機能ではありますが、Sakashoでは、更にこのsubmitされた値を確認画面などでhidden_fieldに埋め込むケースが非常に多いです。

サンプルとしては、次のようなコードになります。

= f.hidden_field :opened_at

form_forなどに代表されるこれらのform methodは、第一引数で受け取ったカラム名を用いて、対象となるインスタンスに対して実行し、その結果をvalue属性に代入します。 今回はこの代入される値に問題がありました。次のようになります。

<input type="hidden" value="{1=&gt;2017, 2=&gt;2, 3=&gt;27, 4=&gt;21, 5=&gt;0, 6=&gt;0}" name="archive[opened_at]" id="archive_opened_at" />

これは、内部的に実行されるxxx_before_type_castという、その値をキャストする前の値を返す機構に起因します。 今回のケースでは、opened_atにはもともとdatetime_selectによって生成された複数のselect要素群から作られたHashが代入されています。 当然、validationを通過した結果の確認画面ではそのHashがopened_atの値として埋め込まれており、確認画面を経て、いざ作成しようとした際にエラーが発生して保存できない、といった問題が発生することになります。

この問題についてはPull Requestを投げてはいるものの、まだレビュー待ちという状態です。

反省点

今回は短期間で一気にアップグレードを実施しましたが、本来であれば計画的に実施すべきだと思います。 Railsを使ってアプリケーションを作って終わりではなく、定期的に依存するgemのアップグレードに追従しようとする姿勢こそがRailsと付き合っていくコツといえるでしょう。 そこでSakashoでは、bundle updateを自動で行い、アップグレードされたgemの差分を表示しつつ、そのGemfile.lockの変更差分をコミットするPull Requestを自動で作成する機構を導入しました。

これにより、普段の業務でも自分たちが採用しているossのリリースを考慮して取り組めるようになると考えています。

おわりに

大規模なRailsアプリケーションで、かつ長らくアップグレードされてこなかったプロダクトを最新バージョンにアップグレードするのは非常に骨の折れる作業でしたが、短期間で集中的に取り組めたことは良い経験となりました。

また、管理アプリケーションとしての特性上複数DBを前提としているなど、一般的なRailsアプリケーションのそれとは異なる構成であり、それをきっかけとして発見したRailsのいくつかの不具合を修正することができました。

このような環境下で運用されるRailsアプリケーションはそう多くはない認識ですし、Sakashoのような環境でしか発生しない不具合はまだ存在するはずです。Railsを利用する立場として、そういった不具合に積極的に対処してコミュニティに還元していく姿勢が今後重要であると考えています。

最後になりますが、Railsを採用される際には、用法用量を守って正しくお使いください。

続きを読む
ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

Rails エンジンによる脱 Microservices 化のススメ

はじめに

こんにちは。ゲーム事業本部開発基盤部の池田です。

アドベントカレンダー4日目の今日は、複数の Rails アプリケーションを「ほどよく」マイクロサービス化する手法として、 Rails エンジンを用いた構成を紹介します。

Rails エンジン自体はそれほど目新しい技術ではありませんが、Ruby, Rails での開発経験のある方や、サーバサイドの開発者にとって、何かの参考になればよいな、と思います。

Rails エンジンとは何か

Rails エンジン は、Rails プラグインの一種ですが、単なるプラグインよりも強力で、 Rails アプリケーションとほぼ同等の機能を提供してくれます。

Rails エンジンは、Rails アプリケーションから次のコマンドで生成することができます。

$ bin/rails plugin new my_engine --full

実行すると、 my_engine/ ディレクトリ以下に、 app/config/routes.rb を含む Rails アプリケーションそっくりのディレクトリツリーが生成されます。

エンジンを生成した Rails アプリケーション側からは、まるで自身のアプリケーションコードの一部であるかのように扱うことができます。

そもそも、 Rails::Application クラス自体が Rails::Engine クラスを継承しており、その振る舞いを多く引き継いでいます。

なぜ Rails エンジンを採用したか

今回、担当した開発案件では、 API や管理画面機能を含む複数の Web アプリケーションが必要になることが、初めからわかっていました。

API と管理画面では、リクエスト・レスポンスのフォーマットも含め、フロントエンドに近いレイヤのニーズが大きく異なります。
従って、 View, Controller のレイヤは完全に切り離した方がよいだろうと考えられました。

一方で、データストアは共有するため、Model 層は共通化することができました。

そこで、Model 層を含む共通部分を Rails エンジンとして実装し、View, Controller を実装するそれぞれの Rails アプリケーションから利用する形を取ることにしました。

これにより、アプリケーションとしては分離しつつ、共通部分を再利用することで、開発を効率よく進めることができるようになりました。

アプリケーション構成

リポジトリとしては、Rails エンジンを共有する全ての Rails アプリケーションを、単一のリポジトリで管理しています。

ディレクトリ構成としては、以下のようになっています。

  • (root)
    • api/ ... API
      • Gemfile
      • app/
      • bin/
      • config/
      • config.ru
      • lib/
      • shared -> ../shared/
      • spec/
    • admin/ ... 管理画面
      • Gemfile
      • app/
      • bin/
      • config/
      • config.ru
      • lib/
      • shared -> ../shared/
      • spec/
    • shared/ ... 共通エンジン
         - app/
         - config/
         - lib/
         - spec/

個々の Rails アプリケーションと Rails エンジンをそれぞれ別々のリポジトリにするやり方も考えられ、実際に試行錯誤もしましたが、結局は今の形になりました。
個々の Rails アプリケーション同士は独立していますが、アプリケーションとエンジンは互いに密結合であるため、特に開発効率の点から、今の形がベストのように思います。

config/lib/ 以下は、全アプリケーションで共有可能なものはエンジン側に置き、アプリケーションごとに実装の異なるものは、アプリケーション側のディレクトリに置くようにしています。

複数アプリケーション同居のつらいところ

上記のようなディレクトリ構成は、開発上便利ではありますが、Rails アプリケーションの標準的なディレクトリ構成でないため、ときどきレールに乗れずにつらい思いをすることがあります。

特につらいのがデプロイです。
Capistrano を使っていますが、基本的に release_path 等の各種パスが「リポジトリルート = アプリケーションのルートディレクトリ」を前提としているケースが多いです。

そのため、利用している Capistrano のプラグインに対して、いくつかモンキー・パッチを当てて問題を回避しています。

しかし、その辺りは一度仕組みを作ってしまえば、普段のアプリケーション開発で意識する必要はないので、エンジンを共有するメリットの方が勝っていると感じています。

終わりに

以上、Rails エンジンを利用した複数のアプリケーション構成について、紹介しました。

エンジンを共有し、単一のリポジトリで完結させるという点では「モノリシック」に近い構成とも言えそうです。
サービスでより多くのアプリケーションが必要になったときは、必ずしも全てをこれに乗せるべきということはないでしょうが、アーキテクチャとして選択肢の一つにはなると思います。

何かの参考になれば幸いです。

明日、5日目は @sairoutine さんです。お楽しみに!

参考

続きを読む
ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る