トレンド1位の折り返し翻訳辞書、フロント(Mithril)とコンテンツ配信(nginx)の話

先日、[折り返し翻訳辞書](http://orikaeshi.com/)というウェブサービスをリリースしました。Twitterのトレンド1位を1日継続し、NHK様のつぶやきビッグデータに取り上げられたりしました。

このサービスは[Mashup Awards 11の応募作品](http://hacklog.jp/works/3809)として作っています。チームメンバーは3人で、発案者でありAPIサーバ構築をされた方、デザインをされた方がそれぞれ別にいます。
チームメンバーはみんなそれぞれ別の会社で、このサービスは完全に趣味開発です。MAの賞金くらいでしか稼ぐつもりはなかったのですが、想定以上にバズってしまって運用費がやばくなったので、各方面から協力頂いたりアド貼ったりしてます。

私は**フロントエンド開発**と**コンテンツ配信**を担当したので、ここではその話をサラッとしたいと思います。
2015/12/06追記: [発案者でありAPIサーバ担当された方の記事はこちら](http://qiita.com/kyanro@github/items/5b04f335daff08f5246d)

# フロントな話

## 構成

いわゆるシングルページアプリケーションです。

* HTMLはindex.htmlのみ
    * nginxの設定でどんなパスでもindex.htmlを返すようにしている
* JavaScriptはwebpackを使ってbundle.jsにひとまとめ
* CSSはindex.cssのみ。こちらもnode-sassを使ってビルド

## ビルド環境

package.json を晒すのが早そうです

```json
{
  "name": "orikaeshi-front",
  "version": "1.0.0",
  "description": "折り返し翻訳のフロントです",
  "scripts": {
    "build-init": "mkdirp build",
    "build:html": "jade src -o build",
    "build:js": "webpack -p --progress --colors",
    "build:css:sass": "node-sass --functions src/css/_helper.js --include-path bower_components src/css/index.sass build/css/index.css",
    "build:css:prefix": "postcss --use autoprefixer --autoprefixer.browsers 'last 2 versions' -o build/css/index.css build/css/index.css",
    "build:css:min": "csso build/css/index.css build/css/index.css",
    "build:css": "npm-run-all build:css:sass build:css:prefix build:css:min",
    "build:img": "ncp src/img/ build/img/",
    "build": "npm-run-all -s build-init -p build:*",
    "watch:html": "jade src -o build -w",
    "watch:js": "webpack --watch --colors",
    "watch:css": "chokidar src/css -c 'npm run build:css'",
    "watch:img": "chokidar src/img -c 'npm run build:img'",
    "watch": "npm-run-all -p watch:*",
    "serve": "browser-sync start --reload-delay 500 --files 'build/**/*' --server build",
    "start": "npm-run-all -s build -p watch serve",
    "publish:build": "NODE_ENV=production PUBLISH=true npm run build",
    "publish:vps": "gulp vps-publish",
    "publish:s3": "gulp s3-publish",
    "publish": "npm-run-all publish:build publish:s3 publish:vps"
  },
  "author": "macoshita",
  "devDependencies": {
    "autoprefixer": "^6.0.3",
    "babel": "^5.8.23",
    "babel-core": "^5.8.25",
    "babel-loader": "^5.3.2",
    "browser-sync": "^2.9.11",
    "chokidar-cli": "^1.1.0",
    "csso": "^1.3.12",
    "gulp": "^3.9.0",
    "gulp-awspublish": "^3.0.1",
    "gulp-gh-pages": "^0.5.3",
    "jade": "^1.11.0",
    "merge-stream": "^1.0.0",
    "mkdirp": "^0.5.1",
    "ncp": "^2.0.0",
    "node-sass": "^3.3.3",
    "npm-run-all": "^1.2.12",
    "postcss-cli": "^2.2.0",
    "webpack": "^1.12.2"
  },
  "dependencies": {
    "mithril": "^0.2.0"
  }
}
```

なんか、見直せそうな箇所がちょこちょこありますが。。

基本は `npm start` を叩いて開発し、`npm run publish` でデプロイしてます。
デプロイまわりはgulpにいいツールが揃ってるので、gulpで組んでます。gulp-awspublish とか　gulp-gh-pages がそれですね。assetsはS3に、index.htmlはVPSにデプロイするようになっています。
また、この開発中にはじめて知ったのですが、[npm-run-all](https://github.com/mysticatea/npm-run-all)がめっちゃ便利です。特にパラレルとシーケンシャルをしっかり制御できる辺り。

JSのビルドにはwebpackを使っています。バベって結合してるだけなので端折ります。

CSSのビルドにはnode-sassを使ってますが、3.0からの機能で関数をJSで作れるようになってまして、ローカル開発中は `/img/hoge.png` 本番デプロイ時は `https://s3-url/img/hoge.png` になるような `assets-url` という関数を追加しています。便利。

## Mithril

JSのフレームワークにはMithrilを採用しましたが、サービスを見て頂ければ分かる通り、大した処理はしていません。まぁ趣味アプリですので、

*   jQueryで書くのはダルい
*   Angular, Reactはよく知らんし大げさ過ぎる
*   Vueはいつも使ってるし……
*   じゃあんまり書いたこと無いMithrilだな！

みたいな、学習したいもの優先で選択してます。今回、Mithrilがかなりのお気に入りになりました。Vueも好きですけどね;)

その他、採択した理由としては、routerとajaxライブラリが入ってるのに容量がとても小さいことが挙げられます。このサイトのメイン機能のJSはgzip後で9.5Kb程度と非常に小さいものになっています。jQueryの容量削減版のZepto.jsで、9.1k when gzippedとのことなので、本当に小さいですね。

### 私のmithrilの歩き方

- [日本語ドキュメント](http://mithril-ja.js.org/)がすごいしっかりしてて、英語の方はたまにしか見てません
- [ガイド](http://mithril-ja.js.org/getting-started.html) も一通り見とくと良いですが、むしろ[APIドキュメント](http://mithril-ja.js.org/mithril.html)に結構重要なことが書かれているので、しっかり読んだ方が良いなと感じました。特に[ウェブサービスのリクエスト完了前のレンダリング](http://mithril-ja.js.org/mithril.request.html#rendering-before-web-service-requests-finish)なんかは必須かと。

### ソースコード（一部）

MAが終わってない中で全公開するのは手間なので、フォーム部分をコンポーネント化した部分だけ公開します。

```js
import m from 'mithril';

export default {
  controller(args) {
    this.translate = () => {
      m.route('/' + encodeURIComponent(args.text()));
      return false;
    };
  },
  view(ctrl, args) {
    return m('form.main', { onsubmit: ctrl.translate }, [
      m('input[type=text]', {
        onchange: m.withAttr('value', args.text),
        onclick: function() { this.setSelectionRange(0, this.value.length); },
        value: args.text()
      }),
      m('button', '翻訳')
    ]);
  }
};
```

Enterキーでも翻訳できるように、formのonsubmitで翻訳が走るようにしています。

このサイトは orikaeshi.com/ほげほげ でリクエストがあったら即「ほげほげ」で翻訳をかける仕様になっているため、翻訳メソッド(translate)の中身は単純にpushStateでページ遷移しているだけです。

ちなみにES2015を使っているのにclassを使っていないのですが、mithrilでクラスを使ってどんな感じに組むべきかイマイチわからなかったためです。Fat ArrowやTemplateくらいしか活躍してません。

# コンテンツ配信な話

## サーバ構成

下記のような経緯で構成は変わっていっています。

1.  途中まではDTIの月1,000円くらいのVPS
2.  NHKで放送前、オートスケールのためにAWS Beanstalkのnodeサーバを慌てて書いて移行
3.  翌日、nginxの調整でかなりいい感じにさばけるようになったのでVPSに戻す　←今ここ

戻したことですし、以降Beanstalkの話はしません。
また、APIサーバについては私の担当ではなく、よく分かってないので書きません。Azure Web Appsの課金勢だとは聞いております。
2015/12/06追記: [発案者でありAPIサーバ担当された方の記事はこちら](http://qiita.com/kyanro@github/items/5b04f335daff08f5246d)

## アクセス数など

Google Analyticsでしか計測していません。
最もはねたのが10/21(水)で、76万PV、55万UUという数字でした。
この日のピーク時、リアルタイムで6,000という数字が出ていました。

## 配信するコンテンツ

*   index.html -> VPSサーバ
*   画像、CSS、JS -> S3

バズってからhtml以外全てS3に外出ししました。その結果、VPSのnginxが配信してるのは1Kb程度のindex.htmlのみです。

何故index.htmlはS3にしなかったのかというと、 [http://orikaeshi.com/ほげほげ](http://orikaeshi.com/%E3%81%BB%E3%81%92%E3%81%BB%E3%81%92) ←こういうURLの実現が難しかったからです。バズるのをそもそも狙っていたこともあり、URLにはある程度こだわっていました。
[http://orikaeshi.com/#!/ほげほげ](http://orikaeshi.com/#!/%E3%81%BB%E3%81%92%E3%81%BB%E3%81%92) ならS3だけでも実現可能ですが、先日[GoogleのAJAXクロールの仕様が廃止](http://googlewebmastercentral-ja.blogspot.jp/2015/10/deprecating-our-ajax-crawling-scheme.html)されたばかりで、そこではシバン形式のURLがどうなるかは明言されていなかったので、なら余ってるVPSがあるしnginx使ってHistory API使うことにしようとなったわけです。

その結果nginxの調整に追われることになったわけです。。

## Nginxの設定

`worker_processes`や`worker_connections`などを調整し、それでもやっぱり詰まり、時間切れで一旦Beanstalkに逃げました。

その後、決め手の設定**`keepalive_timeout 0;`**が見つかりました。
画像やらCSSやらはすべてS3に外出ししていたので、keepaliveなんてしなくて良かったのですね；　たしかにnginx再起動直後は必ずすんなり繋がっておりました……。
これを書いた途端、一気につまりが無くなってつながりやすくなりました。

10/21の昼にはVPSに戻して、毎時40,000アクセス(毎秒10アクセス)くらいがさばけているのを確認し、ひと安心して今に至ります。

可用性の面では1台で運用するのは非常に微妙なので、いずれ何とかしたいところではありますが、運用費の問題があるので当面は落ちたらごめんなさい運用になりそうです><

それにしても……、10月末の[ISUCON](http://isucon.net/)本戦に参加するのですが、一足先にこんなリアルISUCONが待ち受けていようとは……。いい予行練習になりました。

## S3

S3についてはお金のことしか話すことがないのですが、NHKで放送された前後1〜2日間でDataTransferの金額が$47です。
ちなみにリクエスト数での課金は$3くらい。こっちはCSS Spriteにするなりでもう少し工夫出来そうですし、そんなに痛くはないです。結局高いのはDataTransferなんですねー。

# まとめ

今回、MithrilによるSingle Page Appとしてnginx配布をしたわけですが、結果としてはうまくいったかなと思います。今後趣味サービスでこういうバズる可能性のあるサービスを作っても、コンテンツ部分はnginxサーバ1台+S3で余裕だという実績が得られましたし、APIは今ならAPI Gatewayとかで勝手にスケールして放置できるやつが[こんな感じで作れる（自分のqiitaの記事）](http://qiita.com/macoshita/items/10bc707a019431817952)ので。そして何より、いちいちページ遷移しないので違うワードをどんどん試せるというのがいいかなと。

ですが、S3とかいろいろ便利なものが出てきてサービスが止まりにくくなった世の中でも、運用費には抗えないということもわかりました（知ってた）。
10/21(木)単体で見ても個人としては結構な額が飛んでいってまして、色んな方面からカンパを募って何とかMA終了までは運用しようと頑張っています。
「そんな厳しいの？」と思われるかもですが、翻訳APIに非常にお金がかかります。そこにかかるお金に比べたら、上記のDataTransfer代がゴミのようです。

そんなわけで、Mashup Awardsの賞金が下りないと赤字な状況なので、みなさん是非[こちらの作品ページ](http://hacklog.jp/works/3809)の右側のお気に入りをクリックして応援して下さい。そして、そんな運用費のことは気にせずガンガン遊んでやってください。

最後に、この記事を見ているかわかりませんが、**遊んで下さったみなさま、ありがとうございます！**