終わりなき戦い

株式会社マイネットでは、人と人とを結び付けるサービスを提供することで
お客様へ価値をお届けする『オンラインサービスの100年企業』を目指しています。

現在、市場の中でもオンライン化が最も進んでいるスマートフォンのゲーム市場において、
「買収・再生」というビジネスモデルに取り組んでおります。

エンジニアとしては、既に存在しているシステムをいかに要素分解して最適化、
そして再配置してゆくかが鍵になっており、日々引き出しの多さや提案力が求められます。

――そんな弊社の「とあるゲームタイトル」での”終わりなき戦い”のなかから、
　つい最近直面した話題を届けします。

- 稼動を維持しながらノーメンテでDB容量削減
- バッチの実行にやたら時間がかかる！8時間って何だ？！
- Jenkinsでのリリースがある日突然エラーで進めなくなった！？
<br><br>

***

# 稼動を維持しながらノーメンテでDB容量削減
mysql5.6系innodbにて、innodb_file_per_table 設定をONとし、
テーブルごとに.ibdファイル等が作成されています。

DBサーバーのHDD容量は有限で、容量逼迫の危機と隣り合わせ。
→「メンテナンスに入れずに、サービス継続しながら容量も削減したい」
　やってみたことと起きたことを記録に残します。

　※スレーブ4台のレプリケーション構成です。

（1）DELETE文発行だけでは効果はない
　　DELETE文を発行しても.ibdファイルの容量が減ることはありません。

　　ダンプ＆リストアすると容量削減はできますが、
　　大量のデータを取り扱うには膨大な時間とメンテナンス入りが必須。

（2）HDD容量の開放をするには
　　```mysql> ALTER TABLE <テーブル名> ENGINE INNODB;  ```
　　または
　　```mysql> OPTIMIZE TABLE <テーブル名>;  ```
　　を実行することで容量を開放できるものの、
　　このプロセスの間テーブルはロックされてしまうとのこと。

　　だがmysql5.6系以降では書込みもできるようになったとのことで、
　　試験環境のマスターに対して実験。
　　→　確かにOPTIMIZE TABLE している間に、insertができた。

（3）結果・・失敗・・・
　　そこで本番マスターに対し実行してみたところ、レプリケーション遅延が発生。
　　形上はうまくいく操作なものの、テーブルが巨大で、
　　非常に重い処理になってしまったため。

（4）回避方法
　　2点とその順序が重要。

　　・先にスレーブ全台で
　　　```--set-vars="sql_log_bin=0"```
　　　で変更binlogに出さないようにしつつ、
　　　```ALTER TABLE <テーブル名> ENGINE INNODB;```
　　　を実行しておく。

　　・マスターで
　　　```--set-vars="sql_log_bin=0"```
　　　で変更binlogに出さないようにしつつ、
　　　```ALTER TABLE <テーブル名> ENGINE INNODB;```
　　　を実行。

■MySQL 5.6で本当にオンラインでDDLが実行できるか検証してみた
　http://oinume.hatenablog.com/entry/online-ddl-on-mysql-5.6

■innodb_file_per_tableが有効な時にディスク容量を開放するには
　http://yakst.com/ja/posts/68

■無停止でALTERできるPercona-Toolkitのonline-schema-change
　http://d.hatena.ne.jp/fat47/20140418/1397811745

■大人のためのInnoDBテーブルとの正しい付き合い方。
　http://nippondanji.blogspot.jp/2010/09/innodb.html

***

# バッチの実行にやたら時間がかかる！8時間って何だ？！
　　とあるゲームタイトル内にはユーザー様の**”強さ値”**に応じたカテゴリが数十あり、
　　明け方に以下のような処理を実行していました。

　　「カテゴリ内の全ユーザー様の強さ値の平均、を全カテゴリ分計算」

　　明け方に実行開始するのに、終了が昼ごろになっていた、というお話です。

　　調べたところ、このようなコード（JAVA7）になっていました。
　　（かなり変数名変更、簡易化しています）

```java
List<略> categoryList = ＜DBから全カテゴリリスト取得＞;
for ( 略 category : categoryList){
  List<略> categoryUserList = ＜DBからそのカテゴリ内全ユーザー様のステータスリスト取得＞;

  // 初期化
  long totalRating = 0;
  int userCount = 0;

  for ( 略 categoryUser : categoryUserList ) {
    // そのユーザー様の強さ値を取得
    ＜ユーザー様データの型＞ userRatingTable = ＜DBからそのユーザー様の強さ値を取得＞; //（★）
    Integer rating = userRatingTable.getRating();

    totalRating += rating;
    userCount++;
  }
  long average = totalRating / userCount;
  ＜このカテゴリ内の全ユーザー様の強さ値の平均、をDBに記録＞;
}
```

　　結論、（★）の先の実装に、newによるオブジェクト生成があるため、
　　ユーザー様一人一人ひとりの処理毎にメモリ消費が蓄積される形になっており
　　メモリを食いつぶしていました。

　　さらに調査した結果、、、
　　非常に大きくユーザー様が偏ったカテゴリが存在しており、そこで長大な時間に。

　　**ループ内でインスタンス生成しない！**
　　上記コードのループに入る前に、全ユーザー様の強さ値を HashMap に投入。
　　ループ内で引き出すよう変更したところ・・・

　　実行時間　8時間　→　30秒に　！

# Jenkinsでのリリースがある日突然エラーで進めなくなった！？
　　上記同じくとあるゲームタイトルのお話。  
　　試験環境、本番環境共に、Jenkinsによるリリースの自動化がなされています。
　　大まかな流れは以下の通り。

　　（1） ゲーム本体を動かすためのJAVAをビルド、war化
　　（2） 画像やJS/CSSは別途CDNへ転送
　　（3） warは数十台のフロントサーバーへ配置し、tomcat再起動
　　　→　突然エラーを起こすようになったのは（2）でした。

　　**まずやったこと**
　　　・画像の点数を減らしてみる
　　　　エラーが起きるようになる数時間前、画像を管理するSVNに500枚ほど
　　　　画像の追加コミットが行われていたので、いったん削除。

　　　→　無事（2）のジョブは通るようになりました。
　　　　このため、チーム内で急遽不要なファイルを洗い出し、
　　　　1万点弱をリポジトリから外しました。
　　　　これで多少の時間は稼げたとはいえ、画像は定常的に増えていくものなので、
　　　　またすぐに同じことは起きてしまいます。

　　**一時的対策**
　　　・全部一気に処理しない
　　　　大量のファイルを一度に処理しないで、おおよそ10分割するようにしました。
　　　　テストで強引に画像点数を今の倍にしても、処理は通るようになりました。
　　　　（このままでも数年持つ計算です）

　　**根本対策**
　　　・Jenkinsが何をしているのか解体する
　　　　設定を調べていくと、処理の実態は Node.js + CoffeeScript で実装されていました。
　　　　大まかな流れは以下の通り。

　　　　（2-1）SVNと手元のディレクトリ「A」を同期（svn update）
　　　　（2-2）ディレクトリ「A」から、CDNへ配置する必要なファイルを選別して、
　　　　　　　ディレクトリ「B」にコピー
　　　　（2-3）「B」に対し、連番になっているファイル名は難読化しておく。
　　　　（2-4）「B」を非同期処理でCDNへ配置
　　　　→　問題が起きていたのは（2-2）でした。

　　　　　　出力されていたエラーは
　　　　　　```Maximum call stack size exceeded```
　　　　　　です。


　　　・さらにNode.jsで何をしているのか解体する

```
    async = grunt.util.async
    options = this.options
      concurrency: 2

    async.forEachLimit this.files, options.concurrency, (file, nextFile) ->
      async.forEach file.src, (src, nextSrc) ->
        ＜「A」から、「B」に必要なファイルを選別してコピー＞
        nextSrc err
      , nextFile
    , (err, result) ->
      ＜略＞
      done err
```
　　「async.forEachLimit」「async.forEach」を調べると、
　　Async.js（caolan/async）に行き着きます。

　　さらに、コールバックを利用しているので再帰処理であること、
　　またこれらの関数を使えばそれだけで非同期になるのではないこと、
　　がわかりました。

　　結論、nextSrc、nextFile を呼び出している部分に「setTimeOut」記述を追加。

　　■Async.js は自動的に非同期処理や並列処理にしてくれるライブラリじゃないよ
　　　http://qiita.com/VoQn/items/c68e6472029ebd8b69e8

　　■JavaScriptでスタックオーバーフローを起こさない方法
　　　http://south37.hatenablog.com/entry/2014/02/22/JavaScript%E3%81%A7%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E8%B5%B7%E3%81%93%E3%81%95%E3%81%AA%E3%81%84%E6%96%B9%E6%B3%95
<br><br>
***

　　3点目の解決の仕方に触れつつ、あとがき

　　・目的を忘れない
　　　目的は「エラーなく期待されている処理が動くようにする」というシンプルなものです。

　　　「なぜNode.js + CoffeeScriptで実装されているのか」
　　　「なぜasyncのメソッドを使っているのか」
　　　と頭をよぎりますが、
　　　重要なのは「いかに既存の実装記述を利用できるか」です。

　　・速度も大事
　　　工数は他の新規施策等に向け、より価値を高める方向に持っていくほうが望ましく、
　　　エラーなど突然発生する後ろ向きな事態はスピーディーに解決することが求められます。

　　・判断者への提案の仕方
　　　「全てを解決できます、ただし1月かかります」
　　　という提案ではなく
　　　「まずA案ですばやく時間を稼ぎ、
　　　　B案という、完全には解決しなくても数年安心な状態に手早く修正します。
　　　　根本対策はC案という方法が立っていて、必要になった場合実施します」

　　　というように、分解して案を出せるようにしておきたいものです。
　　　そのためには知識の幅を広げて、引き出しを多く持っておかねばなりません。

　　・終わりなき戦い
　　　色々な種類の解決しなければならない問題は、時間と種類を選ばずやってきます。
　　　しかも技術はどんどん新しくなります。

　　　引き出しの中身はほうっておけば古くなっていく一方なので、
　　　何歳になっても勉強を止めることはできず「終わりなき戦い」となります。

　　　お客様に気持ちよくサービスを利用していただけるよう、
　　　今後も不断の勉強を欠かさず、日々邁進していきます！