はじめに
この記事はScala Advent Calendar http://qiita.com/advent-calendar/2014/scala の10日目です。
ほんとはMyFleetGirlsの宣伝だけしとこうかと思ったのですが、Scala Advent Calendarらしくもうちょっと技術的な話します。
ちなみにサイトは http://myfleet.moe/ です。つい出来心でmoeドメイン取りました。
MyFleetGirlsとは
MyFleetGirlsは艦これツールの1種で、艦これのデータをサーバで公開し、自分や他の人が見れるようにするツールです。主に艦これの進捗とか嫁艦とかを自慢したくて作りました。
全体としては、艦これの通信をProxyして艦これデータを取得してサーバに流すクライアント側と、艦これデータを受け取って溜め込み、表示するサーバ側に分かれています。
使ってるのはクライアント側がScala + Finagle(Proxy用)、サーバ側がScala + Playframeworkです。
開発の規模としては、Scalaが約8000行、CoffeeScriptが約1600行、HTML(Playframeworkのテンプレート)が約2300行ぐらいです。
Scalaという選択
サーバ側がScalaになったのは必然というか、当時唯一マトモに使えたFrameworkがScala + Playだったからという理由です。クライアント側がScalaになったのも似たような理由ですが、一応最初に最大の技術的懸案だった「Proxyしつつ通信の中身を見る」というのが達成できたのがFinagleで、それがたまたまScalaだったという側面もあります。
ScalaはJVM上でマトモに(?)開発できる数少ない言語の1つで、Webのサーバサイドなら非常に使い易い言語の1つだと思ってます。理由は、Jetty、Nettyなどの非常に高速かつ高機能な、アプリケーションサーバを作る為のFrameworkがJVM上に存在し、そういったJava製ツールを現代的な設計の言語で扱えるというのは特筆すべき特徴でしょう。DBから取り出したデータを簡単に加工できる充実したCollectionも見逃せないですね。
ただし欠点として、バイナリ互換性がマイナーバージョン間でしか存在しない、という特有の問題があります。Scalaコンパイラのメジャーバージョンを上げる(例えば2.10から2.11に上げる)と、ライブラリも全て2.11でコンパイルされたものを揃える必要があります。これが割とやっかいで、長期間に渡って保守運用する場合は、定期的に(年に1度か2年に1度ぐらい)アプデしたり、場合によってはサポートの切れたライブラリを自前でアプデするなり置き換えるなりする必要があります。
場合によってはクライアント側をC#で書くという選択肢もあります。実際多くの艦これツールがC#で書かれています。理由はIE Componentを使うことで、Proxyという原始的な手法を用いずに通信を読むことができるからです。そして、これこそがC#を使わなかった理由です。作者はいつも使っているLinuxで動く艦これツールを作りたかったのです。
FinagleとCrossVersion
(3/14 UPDATE) FinagleがScala2.11に追従したので、CrossVersionは無くなりました。
「Proxyしつつ中身を見る」というが最も今回面倒くさかった部分で、これが一番簡単にできそうだったのでFinagleは選ばれました。ただし、このtwitter社の開発したFinagleがScala2.11に対応していないせいでScala特有の複数バージョンを保守する必要がある、という問題に直面します。
現在MyFleetGirlsは主にScala2.11を使っていますが、ClientはScala2.10、ServerとClient双方から呼び出されるLibraryと呼ばれるSubProjectではScala2.10と2.11の併用をしています。サーバではScala2.11のみ対応したPlay2.3を使いたい一方で、Finagleを使用するクライアント側がScala2.11にできないため、結果として両方から呼び出されるLibraryが両方に対応する必要があるのです。
このためにLibraryはsbtでcrossScalaVersionsを設定しています。
Libraryはjson4s以外に依存していないため、割とCrossVersionしやすい方なのですが、いちいちコンパイル時に+付けるのも面倒なので、Finagleの代替があるなら置き換えてしまいたいと思っていたりします。
とかくTwitter社のScalaツールはScalaバージョンの追従が遅いのです。
クライアントサーバ間の通信
クライアントとサーバの通信はJSONで行なってます。これはよいのですが、Scalaに限らず、型付き言語ではSerializeするときにmodelとか言われる型定義が必要です。ここで使うmodel定義なのですが、実はLibraryにあり、ClientとServerで共通化されています。
最初これを考えたときは名案だと思ったものですが、今となってはMyFleetGirls最大の設計ミスと言って良いでしょう。
サーバは開発者が直接管理しているので頻繁にアップデートされますが、クライアントは必ずしもそうではないのです。従って、クライアントのデータ形式にデータを追加する際には、全てOption型にするか、新しくURLを設定して別のmodelを作るか…、とにかく改修コストが著しく高くことになりました。
最終的にはクライアントの強制アップデートの仕組みを導入することになりました。
ちなみに、艦これの通信もJSONであるため、艦これの通信を直接投げるという方法もあります。しかしこれは一方で、艦これの認証に関わるデータを作者のサーバに投げることになります。この部分は制御した方が良いと思ったのと、艦これ通信の解析をサーバ側計算リソースでやりたくなかったので現在の形になりました。今思えばそこら辺フィルターしてそのまま投げた方が良かったように思えます。
クライアント強制アップデート
仕組みとしては単純で、クライアントを起動する前にアップデータを起動し、最新のクライアントがあるかどうかチェックし、あれば落としてくる仕組みです。バイナリサイズ肥大化を防ぐためにJavaで作ってあります。
wgetの再実装をするのは心苦しいところですが、curlもwgetもないWindows環境を考慮すると自作せざるを得ませんでした。実はPowerShellを使えばできることは分かっていましたが、配布されたpsファイルに署名が必要ということを知り面倒くさくなってやめました。Javaで書けるならJavaで書いた方が早いのではないでしょうか。
ちなみにScalaマンセーな作者ですが、バイナリサイズを落とすという観点で言うならJavaで書くのは良い選択だと思います。さきほどのアップデータの例で言うならば、数KBで収まります。JVMはデカいものですが、30億のデバイスで既に動いているのですから問題ないでしょう。Macにだって最初から付いてますよ(ただしMyFleetGirlsはJava7以降を必要とし、MacのJavaは未だに6です)
認証
MyFleetGirlsで2番目に酷い失敗を挙げるとすれば認証系でしょう。
当初、クライアントからサーバへの通信に、なにか1つパスワード的な要素が必要と考えました。そこで、おそらく誰も知らないであろうnickname_idを使って認証を掛けることにしました。
この時点まではこの独自認証でもさほど問題なく動きましたが、その後、サーバ側実装が複雑化し、ユーザを指定する必要がでてきたので、Webでも認証するようにしました。これは普通のWebがやってる認証と全く変わりません。
しかし普通のユーザがnickname_idを知っている筈もないので、別でパスワードらしきものを予めパスワードとして使っているnickname_idと別に設定する必要があります。クライアントが。
このときに思い付いた実装が、クライアント側の設定ファイルにパスワードを書くというもので、とても名案に思えました。それ自体はそこまで悪くなかったように思えますが、それを独自実装で組み上げてしまったのは大変な失敗でした。この時点で既にそれは普通のWebにあるような認証になっているのですから、独自実装するべきではなく、PlayのPluginに任せるべきでした。
独自実装であったがために、
- 実装コストが高く付く
- 現在ログインしているのかどうかクライアント側からは分からない
- 多くの場合独自実装はSecureではない
- ユーザがパスワードをリセットする方法がない(パスワード以外にユーザが本人あることを保証する方法がない
という多くの問題を抱えることになりました。
ScalikeJDBC
DBとのやりとりはScalikeJDBCが全て担っています。ScalikeJDBCはその名の通りドライバのように薄いラッパーで、ORMよりも生のSQLに近いものになってます。ただDSLも用意されているので、そこそこプログラムっぽく書けます。ORMほど短かく簡潔に書けないですが、特定RDBMS依存のSQLとかも割と楽に書けるので、「おれがカスタマイズしたSQLで高速に」という用途にはうってつけでしょう。もう1つのウリは作者が日本人で、とりあえずScalikeJDBCの疑問をTwitterに日本語で投げれば丁寧な回答が来ることでしょうか。
速度にそこまで注力していないMyFleetGirlsでは若干宝の持ち腐れな感じもしますが、SQLの勉強になるという点では大変良かったと思います。
MariaDB
MySQLでも良かったのですが、なんとなくMariaDB + Aria Engineを選んでみました。とはいえMariaDBらしいことはなにもしてないので、MySQLと然程変わらないでしょう。今思えばTransactionも外部キー制約もないAriaを使うのは時期尚早だった感が否めませんが。
Deploy
Deployが良く分かってなかったので、Herokuを参考に、gitのpushされるのをhookしてShellScriptを立ち上げるようにしています。ShellScriptからsbt startを叩いています。
今の課題はDeploy時はサービスが10秒ぐらいストップすることで、割と頻繁にDeployすることもあり、別の方法を考えているところです。
(3/14)updateされて、No STOPでいけるようになりました。nginxでbalancerを定義してport番号を9003と9004に振り分け、deploy時には起動してない方のportで起動して、起動したら古い方を落とす、というやり方にしました。sessionまわりに問題がある可能性が高いですが、Not Foundを表示するよりも遥かにマシだと思ってこうしてます。
Migration
非スキーマレスDBのウリを挙げるなら「non nullであることが保証される」、欠点は「Migration」、そしてRDBMSのウリを挙げるなら「Migrationツールが揃ってて楽だから」を挙げるでしょう。
MyFleetGirlsのMigrationはplay-flywayを使ってます。なお、ベースになっているflywayはupのみ書く形式ですが、個人的にはdownも記述すべきだと思っていて、今思うとPlay標準のMigrationツールで良かったかと思わないでもないです。(downがあるとテスト時に重宝するのです)起動時にMigrationファイルがあれば自動で実行する設定にしています。
最後に
ユーザ数1000人ぐらいを目標にしてますが、最近200人突破したので割と現実味を帯びてきました。GitHubでコード公開しているので、みんなPullRequestとかIssueとかもっと出しましょう。
アイコンかわいい