Lobiスピンオフ!トーナメント開催サービスをDocker+CircleCIで開発したはなし

 はじめまして(?)。taiyohと申します。
 幾つか事業部を渡り歩いた後、現在はLobiにて新規機能の開発などを行っております。
 皆様は3/3発売のNintendo Switchの予約はお済みでしょうか。僕は早速1/21の午前中に近所の電器店に駆け込んで予約をしました。ゼルダ、Splatoon2、ゼノブレイド2と、既にやりたいソフトがいくつかあるので大変楽しみです。2017年も何とか生きていけそうです。どうぞ宜しくお願いします。

 さて、昨年の2016年末ですが、Lobiアカウントを利用したトーナメントサイトというものをリリースしましたので、そのちょっと技術っぽい観点についてお話させていただきます。なお、技術っぽいと書きながら実装やインフラ成分は低めです。その点予めご了承ください。

 Lobiトーナメントはゲーム大会をより簡単に開催・参加できるようにしたサービスです。大会のエントリーから大会終了まで、スマートフォンさえあればすぐに使うことができます。トーナメント表、対戦の組み合わせ、相手とのチャットコミュニケーションなど、オンライン、オフラインに限らず、ゲーム大会に必要なさまざまな機能が揃っています。1/28の土曜には12月に次いでハースストーンの第二回大会も行われ、今回も進行不可能になる状況もなく、参加者の方々には引き続きお楽しみいただけたようです。

vs.lobi.co

f:id:sun-basix:20170126182707p:plain

第一回ハースストーンLobi杯 - トーナメント表 - Hearthstone - Lobiトーナメント

目次:

トーナメントの作り方について

 トーナメントといえば、1回戦から徐々に勝ち上がり、決勝戦で勝つと優勝という、あのトーナメント表が真っ先に浮かぶと思います。

(こちらの写真はイメージです)
 実はこのサイトの実装に入る前に、どこかにトーナメント表をプログラムとして作った人がいないか、Googleで検索するところから始めたのですが、自分の求めるサイトや記事が見つかりませんでした。なのでこのエントリでは、作り方というか、データ構造の考え方について多少多めに文字数を割きたいと思います。
 さて、このトーナメント表をデータとしてどう表現するかといいますと、まず考えるべきは木構造です。ただ、安易に木構造をDBに保存することを考えてしまうと、SQLアンチパターンの第二章に記述されている「ナイーブツリー」に該当してしまいます。
 SQLアンチパターンに記載されている解決方法は、以下のことが考慮されているはずです。

  • 任意のノードに新たにノードが追加・削除されることを想定する
  • 任意のノードの親や子のノードをリストアップできる
  • 任意のノードから見て別のノードの深さを調べることができる

ただ、トーナメントの木構造は以下の特徴があります。

  • 木構造の中でも二分木に該当する
  • あとからノードが追加・削除されることがない
  • 条件が揃うと完全二分木になる

 完全二分木であれば、SQLアンチパターンに記載された方法でなくても、配列でデータを保持しながら考慮すべき点を全てクリアすることができます。というのも、配列のインデックス番号を使えば、任意の2つのノード間に親子関係があるかどうかや、同じ深さのノードかどうかも簡単な計算で算出することができるからです。設計がシンプルにできるだけでなく、データ量も大幅に抑えることができます。これを使わない手はないと思いました。
 ただ、一つ問題があり、トーナメントの参加者が完全二分木を構成できる人数になるとは限らないということです。参加者を2n人にするために抽選で落とすなどしたくはありません。
 とはいえ、完全二分木を採用することのメリットはとても大きいので、今回このトーナメントサイトでは、ダミーのプレイヤーを大会毎に紛れ込ませておいて、足りない分の人数はそのダミーのプレイヤーで埋めることで、必ず完全二分木の構造となるように調整することにしました。ダミーのプレイヤー同士の対戦は表示する側で調整すればよく、シードも同様に表示方法を工夫してもらうことで、この問題を乗り切ることにしました。

f:id:sun-basix:20170126171654p:plain

 こちらが、出来上がったテーブル構成の一部です。試合テーブルの「シーケンス番号」カラムは、決勝戦(=根ノード)を1として、幅優先の順序で番号を振っています。また、対戦相手1と対戦相手2がそれぞれダミーのプレイヤーだった場合は試合のステータスは「未決定」となり、片方がダミーのプレイヤーだった場合は「シード」というステータスとなります。
 Wikipediaのトーナメントのページを見てみますと、よく見知ったトーナメントだけでなく、実は様々な方式があることがわかります。これらの方式を全て網羅するようなトーナメントサイトを作ることはできません。まずはPO(プロダクトオーナー及びプロジェクトオーナー)達とよく相談をして、どういうトーナメント方式にするのかを決めるということが何より重要です。今回の場合「勝ち残り方式」で「シングルイリミネーション」であったからこそ、ここまでの設計上の割り切りができました。

テスト環境について

 CIはCircleCIを使用しております。現在弊社にて稼働中のプロジェクトの多くはJenkinsを使用しており、実績もありますが、規模がそこまで大きいサイトではないので、試験的な意味も兼ねてCircleCIを選択することにしました。ここでは、ちょっとハマったポイントについて共有したいと思います。
 本番のRDBはAmazon Auroraを使用することにしていたので、できればクライアント側のDBのドライバはバージョン5.6としてビルドしたもので統一したいという気持ちがありました。

Amazon Aurora は MySQL 5.6 と互換性を持つように設計されている

ただ、調べてみたところ、このエントリの執筆時点でCircleCIで提供しているMySQLは5.7のみとなっています。検索してみて解決方法を書いている方が何人かいましたが、基本的には「既にあるものをアンインストールして必要なものをインストールし直せ」というものです。ただ、自分の場合、どうにも食い合わせが悪かったのか、1日2日では解決しそうにありませんでした。
 散々ハマった挙句最終的に採用したのが、dockerのMySQL5.6イメージをCircleCI上でdocker pullしておき、テスト実行時にそのコンテナを起動して接続する、という方法でした。諸事情で今回のサイトはPerlを使って構築したのですが、PerlであればTest::Docker::MySQLというCPANモジュールがあるので、dockerでMySQLコンテナを立ち上げる際の障壁は低かったです。ただ、これを案件にフィットする形で一部改修を加えて使用しています。
 実際に使用しているcircle.ymlの中から、テスト実行前の依存モジュール等のセットアップ部分を切り出しました。

dependencies:
  cache_directories:
    - ~/perl-5.24
    - ~/docker
    - local
    - assets/node_modules
  pre:
    - |
      sudo apt-get remove -y 'mysql-*'
      sudo apt-get autoremove -y
      sudo apt-add-repository -y 'deb http://ppa.launchpad.net/ondrej/mysql-experimental/ubuntu precise main'
      sudo apt-get update
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libmysqlclient-dev
    - |
      mkdir -p ~/docker
      if [ -e ~/docker/mysql-56.tar ]; then
        docker load < ~/docker/mysql-56.tar
      else
        docker pull mysql:5.6
        docker save mysql:5.6 > ~/docker/mysql-56.tar
      fi
    - |
      mkdir -p ~/perl-5.24
      if [ -z "$(ls -A ~/perl-5.24/)" ]; then
        wget https://raw.githubusercontent.com/tagomoris/xbuild/master/perl-install -O ~/perl-install
        chmod +x ~/perl-install
        ~/perl-install 5.24.0 ~/perl-5.24
      fi
      carton install --deployment

 CircleCIはまっさらなコンテナから都度環境を作成するので、データやツール等が次のテストに持ち越されることがないので、依存が少なく済むのでとてもよいですね。ただこれは諸刃の刃で、テストの度に構築処理が走るので、テスト自体は一瞬で終わるのに構築に数分かかる、ということもあります。そこが気になる方は、テスト用のイメージを作成しておけばその分のオーバーヘッドは減るかと思います。ちなみに今回のプロジェクトではそこまでは行っておりません。現状、proveによるテストはおよそ3分半ほどに対して、上記YAMLによる環境構築も同じくらいかかっています。

開発環境について(docker周り)

 テスト環境でdockerを使い始めたので(CircleCIが元々dockerベースなのはおいといて)、開発環境も手を入れようと思い、docker-composeで環境を組んでみることにしました。Docker Hubperlのイメージが存在していたので、これを導入して開発用のコンテナを作成しています。開発スピードがだいぶ優先されていたので、Dockerfileにはモジュールのインストール周りの記述はせず、docker-composeでの起動時に volumes を使ってgit cloneしたディレクトリをそのままマウントする形にしています。
 以下に、プロジェクトで使用しているdocker-compose.ymlを当たり障りのない範囲で載せます。

version: "2"
volumes:
  mysql:
  redis:
services:
  mysql:
    image: mysql:5.6
    ports:
      - 3306:3306
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
    volumes:
      - mysql:/var/lib/mysql
  redis:
    image: redis:3.2
    ports:
      - 6379:6379
    volumes:
      - redis:/data
  node:
    image: node:6.9.1
    working_dir: /app
    volumes:
      - .:/app
  web: &app
    build: .
    image: hoge
    working_dir: /app # /app 以下にリポジトリを設置している
    ports:
      - 5050:5000
    volumes:
      - .:/app
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - mysql
      - redis
    links:
      - "mysql:hoge-mysql"
      - "redis:hoge-redis"
    stdin_open: true
    command: "plackup ****"
  admin:
    <<: *app
    ports:
      - 5000:5000
    command: "plackup ****"

 docker周りで個人的に一番驚いたのは volumes の挙動です。散々調べた結果、これはホストマシンのパスだけでなく、Linux環境の元イメージのパスも共有できるということでした。Windows上でdocker-composeを動かした時、 /var/run/docker.sock がちゃんと共有されて内部からdockerコマンドが問題なく動いたのを見た時は、目を疑ってしまいました。当然、Windows環境には /var/run/docker.sock は存在しておりません。知らないと怖いですね。(「Dockerでホストを乗っ取られた」という恐ろしい投稿もあるので、取扱に注意が必要です)
 docker-composeを組んだ副産物として2点ほどよかったことがあります。

  • フロントエンドエンジニアの使うnodejsのバージョンもdocker-composeに収めることができた
  • Docker for Windows上でだいたい動く

 前者については、フロントエンドを担当しているエンジニアがgulpによってCSSやJSのビルドをを行っている都合でnode.jsの環境構築が必要だったのですが、nodeのイメージをdocker-compose.ymlに入れておいたので、ローカルの環境を変更することなくビルドの作業ができるようになりました。
 後者は、Windows10だとPro版でないと動作しません。また、「だいたい動く」と書いたのは、「 docker-compose run が動かない」という問題があり、仕方なくコンテナを起動しっぱなしにしておいて、 docker exec で中に入る必要があります。ただ、どうやらこれはdocker-composeのバージョンが1.8までの話で、1.9では解消するらしいという話をこのエントリを書いている最中に見つけました。

qiita.com

 Docker for Windowsユーザはご参考までに。
 現状では開発環境しかDocker化されていないですが、ゆくゆくはECSに置き換えていきたいと考えている今日このごろです。

まとめ、というかおまけ

  • トーナメント表を実装する際は、仕様を適切にシュリンクさせないと死亡するので注意しましょう
  • CircleCIは環境構築の時間が長いのでなんとか頑張りましょう(?)
  • Docker for Windowsは思った以上にイケる

 最後に、昨年の12/23と今年は1/28にこれまで2回大会が行われたのですが、大会開催中は特に不正もなく、ユーザからシステム不具合や運営方法でクレームをつけられることなく、和やかに終了しました。終了後アンケートをとってみたところ、「また参加したい」という反応が多く、サイトのシステムに対しても「クレーム」ではなく「改善案」を挙げてもらえるという、とても貴重な体験ができました。この場を借りて、改めて参加者の方々に御礼申し上げます。