2018-12-13に更新

SP☆12参考表(地力表)支援サイト を支える技術

読了目安:32分

SP☆12参考表(地力表)支援サイト を支える技術

この記事はcrieitでの 個人開発サービスに用いられている技術 Advent Calendar 2018 の9日目の記事です。
昨日はckoshienさんの野球リーグスコア管理システムに用いている技術についてという記事でした。
明日はiotas𓆡創作支援アプリ運営中𓅬さんの記事です!

SP☆12参考表(地力表)支援サイト とは

ゲームセンター等でプレイできるbeatmania IIDXというゲームの成長支援サイトです。
2014年11月に開発を開始し、運用を始めて4年ほど経過しました。
現時点で登録者数は9200人ほどで、GAのマンスリーアクティブユーザは11k人ほどです。

beatmania IIDXは音ゲーと呼ばれるジャンルのゲームで、楽曲に難易度が設定されています。
楽曲の難易度はおおまかに言うと☆1から☆12までに分類されます。

この分類はシングルプレイ(SP)、ダブルプレイ(DP)といったモードごとに存在しているのですが、本サイトではその中でもSPの☆12についてだけ取り扱ってます。

なお、このサイトはGitHub上でpublic repositoryとして公開しています
参考になったらスターをいただけるとモチベーションになります。

なぜSP☆12だけなのか

ここではゲーム性の説明を行います。
技術的な話を知りたい方はスキップしてもらって構いません。

SP☆12は現時点で(未解禁のものも含めると)300曲を超えています。
そして他の難易度帯と比較すると☆12は易しい曲と難しい曲の難易度差が最も激しくなっています。
☆12に挑戦し始めの人は噂程度に「この曲は☆12の中でも難しいと言われている気がする…」という選曲判断が必要になってきます。
公式にも指標に近いものがあるのですが、割とあてにならないという背景もあります。
※ 人によって得意不得意分野があるため参考表も万人に当てになるわけではないです

また、☆12の攻略には長い時間が必要になります。
自身の例ですと、1つのゴールであろう皆伝という最上位段位の取得に☆12触り始めから1年以上の時間が必要でした。
皆伝という段位は☆12の中でも上位の難しさに位置している楽曲で構成されるコースのため、簡単な☆12からスタートし、段階を踏んで難しい☆12もクリアできるようになる必要があります。

私の元々の開発モチベーションとしては、このステップをモチベを維持しつつ「続けていける」ようにすることでした。
そのため自分がプレイしているSPであり、皆伝を取得するのに避けられない☆12を対象としたサイトを作り始めました。

スタート地点としてはPHPで簡易的な☆12参考表サイトを作成していました。
このサイトは知人等にインターナルに利用してもらっていたのですが、需要がありそうということで公開を前提に作り直し始めたのが2014年11月です。

採用している技術たち

ざっくりと書くと、

といった形になっています。
AWS/GCP等も検討したことはあるのですが、VPSと比較すると月額コストが跳ね上がるため見送っています。

技術的な解説

はじめてRailsを使って作成したアプリだったことによるイマイチコードがいたるところに点在しています。
合間を見て少しずつ改善はしているものの既にユーザが利用していると変更しにくい部分があり、手がつけにくいところもあります。
特にRailsのroutes.rbの記法の修正は既にブックマークをしているユーザへの影響を考えると変更しにくいです。

一度ルーティングの変更をリダイレクトサポートをしながら変更したことがあったのですが、1年半ほどそのページを開くとリダイレクトしつつnoticeでブックマークの変更を促していたにも関わらず、いざサポートを切るとdead linkを参照してしまう方は想像より多かったのを記憶しています。

技術的な要素でいうと、「いたって普通なRailsアプリ」をメインポリシーとして作成しています。
特殊なことをいろいろと行うと、その分メンテナンスコストも上がるため極力Rails Wayに乗っておこうという精神です。
メインで開発し続けているサービスなら多少の獣道も選択としてはありですが、社会人が片手間に行う分には大衆に乗っかるほうが楽でした。

最近まではフロントエンドとの連携に自作のgem使ったりしながら特殊なことをしていたのですが、
そのあたりはwebpackerに乗り換え、スタンダードRailsへの変身しました。
(この変身コストはとても大きかったです。。)

webpacker 4.0.0.pre.3

webpackerのstable releaseとしては3系ですが、このプロジェクトでは4系を利用しています。
理由としては最新のts-loaderを使いたかったためです。

ts-loaderの最新を利用するためにはwebpack4系である必要があるのですが、stable releaseのwebpackerではwebpack3系までしか対応していなかったためです。

webpackerを採用した理由としては「現在はRailsの中でそれが主流」という理由以上のことはありません。
疎なwebpackインテグレーションを自作しても良かったのですが、メンテナンスを続けるモチベが持てなさそうなので敬遠しました。
実際使ってみると細かいところで不満はあるものの、トータル的には体感は悪くないという印象を受けました。

webpackerでは下記のように少しだけ設定を変えています。

process.env.NODE_ENV = process.env.NODE_ENV || 'production'

const environment = require('./environment')

module.exports = environment.toWebpackConfig()
module.exports.devtool = 'source-map'

といった設定にしてproductionでは devtoolの nosources-nource-mapsource-map に置き換えていることです。
source-map自体を切る修正を入れる人が多そうな印象ですが、自分の環境ではあえて設定しています。
理由はsentryでのエラートラッキングが快適になるためです。

sentry-source-map.png
このような形でエラー部分の元ソースコードを表示することができます。
また raven-for-redux というnpmを利用することでそのときのactionやstateの状態までわかります。

そのエラーになるまでのactionの履歴
action-history.png

そのエラーが出たときのクライアント側のstate
reducer.png

OSSなので特にこのサービスに関しては隠蔽する必要はないのですが、mapファイルはsentryサーバ以外からアクセスできないようにしています。
自前でsentryを立てているので特定IPからのアクセスは弾くようになっています。

location ^~ /packs/ {
  gzip_static on;

  set $response_file 1;
  if ($request_uri ~ 'map$') {
    set $response_file 0;
  }
  if ($remote_addr = 'xxx.xx.xxx.xxx') {
    set $response_file 1;
  }
  if ($response_file = '0') {
    return 403;
  }
}

typescriptに置き換えてからまだ一度もエラーが出ていなかったため、この画面は意図的にエラーを仕込んで発生させました…w

今のところ、webpacker4系に対する不具合は踏んでおらず問題なく運用できています。

react-rails

Reactとのつなぎ込みにはreact-railsというgemを利用しています。
react_on_railsというgemのほうが多機能なのですが、相容れないところがありreact-railsを採用することにしました。

react_on_railsのほうが優れている点としてはrailsContextというオブジェクトが付与されており、
この中にtimezone/localeなどを始めとした開発者がほしいであろうメタ情報が入っています。
またRailsにxhrリクエストをする際にcsrf-tokenをheaderにセットするAPIも用意されています。

ただcomponentをrenderするためのhookに turbolinks:render を利用しており、このhookではページがチャタるケースがあったため、採用しませんでした。

react-railsはreact_on_railsと比べると非常にシンプルで、コンポーネントをrenderする以上のサポートはあまりないようです。
ただそれを行ってくれるだけで自分の要件的には十分だったため、採用しています。
react-railsではcomponentをrenderするhookに turbolinks:load を採用しています。

チャタるとは何かをもう少しわかりやすく言うと、一瞬古いページが表示された後に新しいページが表示される現象のことを指しています。
chattering.gif

コンテンツが一瞬表示される->白くなる->再度表示されるというループになっていることがわかると思います。
一瞬表示された後と再度表示された後でAPIリクエストが2回走ってしまうという問題やUXの観点から許容できない、という判断になりました。

上記はturbolinks的には「仕様」の挙動です。
GitHub - turbolinks/turbolinks: Turbolinks makes navigating your web application faster から抜粋すると以下のように書かれています。

turbolinks:renderfires after Turbolinks renders the page. This event fires twice during an application visit to a cached location: once after rendering the cached version, and again after rendering the fresh version.

react_on_railsのgem自体にはissueを投げてみたのですが、少し私の想像を超えた範疇の問題があるらしく、今回はreact-railsを採用することにしました。

ts-routes

Railsとフロントの繋ぎ込みには GitHub - bitjourney/ts_routes-rails: Exports Rails URL helpers to TypeScript, inspired by js-routes というgemを利用させていただきました。
似たようなgemにjs-routesと呼ばれるものがあり、そちらを採用していたのですがts化にあたって載せ替えを行っています。
パラメタ不足などの不正なルーティングに関して検知しやすくなるため、tsとrailsを組み合わせるならお薦めです。

Sidekiq

兄弟サイトのIST(Iidx Score Table)からのデータ取り込みを始めとした外部サービスの連携や定期処理(sidekiq-cron)などに利用しています。

先述した通り☆12の譜面はかなり数が多く、1年を通して「解禁」され数が増え続けていきます。
これらを全て人手によって捕捉し続けるのはコストが高い、と感じ1日に1回IST側のデータを利用して新規の☆12が解禁されたら自動追加するようにしています。
ただ☆12の中での難易度自体は議論によって決められるもののため、難易度が決定した後の反映処理は人手で行っています。

当サイトはVPS1台で運用しているので、バックアップ等の日次処理も必要になってきます。
これらの処理もSidekiq側に任せています。
abilitysheet/service_dumper.rb at master · 8398a7/abilitysheet · GitHub というlibを作り、

  • DBのdump(pg_dump)
  • carrierwaveで保存された画像等の収集

を行った後にtar.gzに固めてS3に送る処理です。
これらの処理をjobにして朝の5時過ぎに行うようにしています。
また、手元でproduction環境の再現を行いたいときにもS3からデータを取ってきています。

運用しているいくつかのサービスは全てこのような形でバックアップを取っており、いつVPSが死んでしまってもすぐにサービスを復旧できるようにしています。

CI

CircleCIを使ってテストを行っています。
テストフレームワークはrspecでjs部分のテストはjest等では書いていません。
代わりにsystem specとしてchromedriverを利用したE2Eで担保しています。
昔に書いたコードなので、コントローラ系のspecも多いですがこのあたりも粛清したいところです…w

system specではjsを使うところ使わないところで以下のように条件分岐を書いています。
systemであっても js: true でないところは rack_test で行うようにしているところと、NO_HEADLESS という環境変数を定義して実行するとchromeが実際に立ち上がるようにしています。

  config.before(:each, type: :system) do |example|
    if example.metadata[:js]
      if example.metadata[:iphone6]
        display_size = [375, 667]
        args = %w[--headless --disable-gpu --user-agent=iPhone]
      else
        display_size = [1920, 1080]
        args = %w[--headless --disable-gpu]
      end
      args.shift if ENV['NO_HEADLESS']
      caps = Selenium::WebDriver::Remote::Capabilities.chrome(chromeOptions: { args: args })
      driven_by :selenium, screen_size: display_size, options: { desired_capabilities: caps }
    else
      driven_by :rack_test
    end
  end

実行する際には NO_HEADLESS=true bundle exec rspec spec/systems といったコマンドで実行します。
spec内に binding.pry を仕込むことでElementも触れるのでspecがうまくいかないときの不具合調査などに利用しています。

e2e.gif

運用サポート

死活監視にはmackerelとgodを利用しています。
Mackerel(マカレル): 新世代のサーバ管理・監視サービス はSaaS型のサービスでプロセスが死んでいるか生きているかの監視もできます。

死活監視の肝としては God - A Process Monitoring Framework in Ruby を採用しています。
sidekiq/puma/redis/postgresql/nginxなどのサービス継続に必要なプロセスは全てgodに監視させています。
再起動時などにもgodに任せて起動させているため、再起動するとサービスが落ちたままになっているということもないです。

特にsidekiqは知らない間にメモリの使いすぎでプロセスが落ちていることもあったため、godに監視させて死んでいたら起こすようにするのは精神的安定感が増し増しでした。
デプロイには capistrano を採用していますが、こいつはsidekiqのgod監視とやや相性が悪いです。
pumaと違ってgraceful restart時にプロセスがいない瞬間ができてしまうため、
capistranoがsidekiqを起こすのとgodがsidekiqを起こすのが衝突してしまい、二重でsidekiqが起動してしまうケースが有るためです。
capistranoと併用する場合は素直にcapではsidekiq:stopだけして起こすのはgodに任せたほうが良いという感覚を持っています。
abilitysheet/deploy.rb at master · 8398a7/abilitysheet · GitHub

k8s

k8sもローカル環境での動作自体はサポートしています。
VPSを2台所持しているため、2台でなんとか運用できないか検討していたのですが、結局2台では割と厳しいということと、
capistranoに代わるデプロイツールを自作し、メンテし続けるコストを払える自信がなかったため見送りました。

一通りproductionとしてローカル環境で動作させるところまでは確認しているのでrailsでのk8sに参考にしたい方はどうぞ。
abilitysheet/kubernetes at master · 8398a7/abilitysheet · GitHub
ただ運用もしていないコードなので穴はたくさんあると思います…

BigQuery

nginxのアクセスログをfluentd経由でBigQueryに投げるようにしています。
SSDで容量が厳しく、割と細かめにログを削除しているため後でまとめて見るためにBigQueryを利用しています。

元々はS3に置いてAthenaで集計していたのですが、BigQueryのほうがいろいろと捗りそうなので移管しました。
GitHub - kaizenplatform/fluent-plugin-bigquery というプラグインを利用させていただき、bqにログを送っています。

主にARのconnection poolがタイムアウトになっていたり、急激な負荷によるアラートの調査などに使っていたりします。
railsユーザであればnginxのltsvログに 'request_id:$sent_http_x_request_id\t' を追加しておくことで、調査が捗ることがあるためおすすめします。
nginxのログから問題のrequest_idがわかればproduction.logをrequest_idでgrepすることで該当行のログに辿り着きやすくなります。

おまけ IE対応

最近webpackerベースのreactにリプレースを行っていたのですが、その際に困った問題と解決した手法についても記載しておきます。

tsconfig.json ではtargetをes5としていましたが、babel-polyfillを仕込み忘れていたことが1点目です。
記法自体はes5まで落としてくれるのですが、polyfillがないと Promise などがIEにはないため正常に動作しません。
polyfillの入れ方に関しては、headerに https://cdn.polyfill.io/v2/polyfill.min.js を追加するのがおすすめです。
ブラウザのUAを見て必要なpolyfillを行ってくれます。
試しにIEで見ると結構な量をpolyfillしてくれますが、最新版のchromeではほぼ空であることが確認できると思います。
自分でbundleすると配布するjsファイルが巨大化するため、cdn経由で入れることにしました。

2点目はes5にトランスパイルされていないライブラリを利用してしまっていることでした。
query-stringというライブラリでquery-string/index.js at master · sindresorhus/query-string · GitHubの行でアローファンクションを使っているのですが、これがIEだと解釈できないという問題です。
webpackerのbabel-loaderはexcludeで node_modules が指定されているため、使っているライブラリまではes5にトランスパイルされません。
よくよくドキュメントを見るとレガシーブラウザは5系を使え(最新は6系)ということが記載されていたので、5系を利用することで解決しました。

当初は上記2点の問題がわからぬまま、IEでは見れないといった声が上がっていたため、最初の対応としてはelectronでサイトをラップしたexeを配布していました。
それにはGitHub - jiahaog/nativefier: Make any web page a desktop applicationというライブラリを利用したのですが、幅広くの場面で役に立つので困ったら利用してみてください。

まとめ

今回はSP☆12参考表(地力表)支援サイト を支える技術をご紹介しました。
他にもいくつかサービスがありご紹介したい技術もあったのですが、割と長くなってしまったので今回はこのような形で締めたいと思います。

サービスを運用している方の参考になれば幸いです。

GitHub - 8398a7/abilitysheet: This app is ability sheet for beatmania iidx music of level 12.

P.S. 作者は2018年5月に念願の冥ハードを成し遂げ、ゆるふわ離脱モードに突入しました。


8398a7

vim/vscode, typescript/ruby/golang, react/chef/rails/docker/aws/gcp

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!

ボードとは?

関連記事

コメント