スマフォアプリゲーム開発における問題と対策

自分が関わっているスマフォゲーム開発で起こる様々な問題と、それをどのようなツールで解決しているのかを共有したいと思います。

同僚と話していたときに、他のチームがやっていて便利な事が共有されたらいいよねという話があったので、まず自分のチームのことから共有したいと思います。 pythonで開発していますが、どこでも起きうる一般的な問題が多いかなと思います。サーバのエンジニアをしているので若干サーバ視点です。

環境

  • クライアントはcocos2d-x
  • サーバは python 2.7 + Django 1.8
  • クライアントはサーバにAPIで接続
  • サーバとクライアント開発は別グループ (それぞれ5人くらいずつ)
  • ソースコード管理はgit

問題と対策メニュー

問題 対策
いつの間にか自分の書いたコードが意図しない形で変更されている コードレビュー
レビューがコーディング規約違反の指摘でうめつくされる コーディング規約に準拠した整形ツール
レビューしていいコードなのか、まだ途中なのかわからない レビュータイトルに[WIP]を追加
開発環境デプロイ後にクライアントの方からサーバがエラーで繋げないと言われる JenkinsでAPIテスト・結合テスト導入
マスターデータの頻繁な変更とデータの不整合 自動モデル作成とJenkinsでバリデーション
クライアントの方が、サーバが作成したAPIの仕様がわからない APIドキュメント自動生成
APIを試しに叩いてみたいが、URLやポストパラメ設定が面倒 APIドキュメントでリクエスト発行
誰がどのタスクを現在やっているのかわからない backlogで管理
将来やるタスクが忘れ去られてしまう backlogで管理
クライアントからサーバにエラーが起きていると言われるがどのようなリクエストを投げたかわからない リクエストの通信ログ取得
リリースされる前に、遅いAPIを洗い出しリリースされるのを防ぎたい 開発環境におけるNew Relicの導入
サーバの性能が出るか確かめたい 負荷試験
本番環境のユーザを開発環境に再現したい 管理画面でユーザexport/import

いつの間にか自分の書いたコードが意図しない形で変更されている

1人や2人くらいでコードを書いているときは常にお互いが連携できており、コミットログを追いやすいのでコードレビューを実施しなくても問題無い場合が多いのです。 しかし多人数(自分のチームだと6−7人)だと、他の人のコードをいじったり利用したりすることも多く、作成者の意図した使われ方をしないで負荷などが高まってしまったり、バグを出してしまいます。 また何より、1人だけがコードを理解している状態だと、障害発生時の対応が遅くなります。

上記の問題を解決するためにコードレビューを導入しています。 Stashを利用しておりStash内ではプルリクエストと呼ばれております。

以下のようにプルリクエストを機能ごとになげて、チームメンバーにレビューしてもらいます。

pr.png

いつマージできるかのタイミングですが、自分のチームでは、レビュワーが2人以上承認したらプルリクエストした本人がマージできるようにしております。 全員がレビュー承認すると、承認までの時間がかかりすぎているのでそのようにしています。

ただ、承認するというプロセス自体に、プルリクエストを投げた人に待ち時間が発生するのは確かなので、小さな変更などはレビューしないでマージできる約束になってます。

以前みた海外のブログでコードレビューはマージした後にしているというのもあり、確かにそういう方法もあるなと思ったのですが、ここらへんはチームの状況によるかなと思います。

レビューがコーディング規約違反の指摘でうめつくされる

チームではPEP8に準拠するようにしております。

その上で、コードレビューを始めると問題点になるのが、コーディング規約に違反していますというレビュー指摘です。 この指摘自体はまっとうなのですが、するほうもされるほうも、時間の無駄の感じがあり不毛です。

レビューする前にコーディング規約に違反していないプルリクエストを上げれば問題は回避できます。

上記を達成するには以下の方法が考えられます。

  1. gitでコミット時にhookをかけてチェックする
  2. Stash側でプルリクエストが行われる前に、コーディングチェックを走らせ、違反したものは弾く
  3. エディタでPEP8違反を自動でチェックしてくれるプラグインを入れる

1番と3番の方法をチーム内では推奨しております。 1番を実施するには、autopep8でコミットした後に、コミットされたファイルでコーディング違反のあるものを自動で修正してくれるようにします。 細かいやり方は別の記事で書きます。

レビューしていいコードなのか、まだ途中なのかわからない

初期のころは、コードレビュー時のプルリクエスト一覧を見た際に、レビューしていいものなのか、一時的にレビュー前にあげたものなのかが、タイトルではわかりませんでした。

これを解消するために、レビューしていいものには[Review]、まだレビューしてほしくないやつには[WIP]とつけるようにしました。 WIPはWork in Progressの略です。

開発環境デプロイ後にクライアントの方からサーバがエラーで繋げないと言われる

コードレビューをしても、バグは入り込むものです。これを防ぐために、マージ後にJenkinsでテストが走ってOKなら開発サーバにデプロイするようになっています。

以下の環境で社内に別マシンのMac上でJenkinsをたてて、Stashでのdevelopブランチの変更をポーリングして、15分おきにデプロイしてます。StashからWeb Hookで変更されたらすぐにテストを走らせたいのですが、セキュリティ上やコストの関係で社内にあるので、ポーリングしています。

server_pptx.png

テストは以下のテストをpy.testで走らせてます。

  • APIテスト: クライアントに公開されているAPIをテストする。サーバエンジニアは新しいAPIを作ったら必ず疎通確認以上のAPIテストを作成してもらってます。 数分で終わるようなテストです。
  • 結合テスト: MVCで言う、Model以下などロジック部分をテストしてます。項目が多いのと、py.testでDjangoをテストするといちいちデータを消去してからテストするので40分以上かかってます。並列化する必要がありますがまだしてません。

結合テストは時間がかかりすぎるので、APIテストが通ったら開発サーバにデプロイしてます。

マスターデータの頻繁な変更とデータの不整合

ゲームのマスターデータというのはユニットのパラメータだったりクエストのパラメータだったりするデータです。 もとのファイルはExcelですが諸事情により複雑になっています。以下が概要となっております。

masterdata.png

サーバコードがマスターデータを読み込み、コード上で使うようになっております。 いちいち更新されるたびに、手動でサーバコードを更新しているのは時間の無駄なので、Jenkinsで自動で更新するようになってます。

マスターデータで他に、問題となるのは、頻繁に、マスターデータにカラムが増えたりする事です。例えば、ユニットのマスターデータにスキルがつきスキルIDが増えたみたいな感じです。 対策として、サーバ側では、1つのマスターデータに1つのモデルソースコードを対応させており、機能拡張する場合はそれをラップしてメソッドを書くようにしてます。

例えば以下のような形です。

application/module/unit/models/unit.py  # ユニットマスターデータ用のモデル(Unit)
application/module/unit/models/unit_wrapper.py  # ユニットモデルのラッパ(UnitWrapper)
application/module/unit/models/__init__.py 

init.pyは以下のように定義します

# -*- coding:utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
from .unit import UnitWrapper as Unit

そうすると、外部から参照するときに、以下のようにすれば、UnitWrapperがあたかもUnitとして読み込めます。

from module.unit.models import Unit 

このunit.pyの部分はスクリプトを走らせれば自動で作ってくれるようになっているのでマスターデータの更新に煩わされる事はありません。 この部分はJenkinsで自動化はさせてませんが、もちろん自動化も出来ます。

クライアントの方が、サーバが作成したAPIの仕様がわからない

サーバ側がAPIつくってその仕様(リクエストパラメータやレスポンスパラメータ)が確認できないと困ります。 Confluenceなどに書くパターンもありますが、常に更新されていないと意味をなさないので、ソースコードと連動していることが好ましいです。

Swaggerなどがありますが、自分のチームでは、自前で管理画面上で実現しています。

APIが一覧表示されています。新しいAPIを作ると自動で追加されます。 api_1.png

player/signupのAPIドキュメント例

api_2.png

api_3.png

上記のように、リクエストとレスポンスがわかるものはチームメンバーが作ってくれました。

APIを試しに叩いてみたいが、URLやポストパラメ設定が面倒

開発している際にAPIを叩いてレスポンスを確認したいときがあります。 ChromeプラグインでAdvanced REST Clientなどがありますが、日々APIが追加されていくなかでメンテナスするのも大変です。

arc.png

さきほどあった管理画面のAPI一覧でリクエスト発行もできるようになっております。

api_4.png

新しいAPIが加わったときも自動で追加されます。さらにパラメータ候補も5つまで保存できるので異なったパラメを試したいときも重宝します。一度設定したらブラウザに自動保存されます。 複数のパラメを試せる部分はメンバーが追加開発してくれました。

誰がどのタスクを現在やっているのかわからない

自分は現在、サーバチームのリードとしてやってますが、その上で問題になるのはメンバーのタスク管理です。

タスクはバックログ上になれべられており、ガントチャートを毎朝見て管理しています。

backlog_1.png

基本タスクは作っても担当者が振られていない状態にしておき、タスクが終わったメンバーに、担当者の振られていないタスクを実装してもらいます。

作成されたタスクはメンバーに振り以下の状態遷移となります。

ステータス 説明
デフォルト(赤) タスクが作られた状態。担当がふられてもこのまま
処理中(青) 担当者がタスクを開始しだしたら、担当者が処理中にする
処理済み(緑) 担当者がタスクを完了した場合、担当者が処理済みにする
完了 処理済みのものを、確認し、リーダが完了にする。担当者は実装した本人にし、変えない

上記のようにすれば、後からメンバーがどのタスクを処理したかは、担当者別でフィルタすれば確認できるので、振り返りにも便利です。

将来やるタスクが忘れ去られてしまう

忘れないために、実行する必要のあるタスクはすべてbacklog上にのせて、さらに将来の適当な日付をうって、backlogのガントチャート上にのるようにしております。 これで忘れない!はず。

クライアントからサーバにエラーが起きていると言われるがどのようなリクエストを投げたかわからない

よくあるのがクライアントがサーバレスポンスが原因で、アプリが落ちた(500とかがかえってきた)けど調べてくださいというパターンです。 デバッガからの報告などの場合もあり、どうしても時間差があるので、どのリクエストかわからなくなってしまいます。 ユーザIDをもらっても多くのリクエストから探すのは骨がおれます。

そのための対策としては以下が考えられます。

  • 管理画面でユーザIDごとに、リクエストとレスポンスが見れるようにする
  • SentryでユーザIDで検索できるようにする
  • NewRelicでErrorsを確認する

管理画面でユーザIDごとに、リクエストとレスポンスが見れるようにする

もともと他のチームで使っていたものが便利そうだったので移植してチームのメンバーに改良してもらいました。

サーバに来たリクエストを、ユーザIDで絞り込んで検索できます。 またサーバ処理時間も記録しているので、レスポンスが遅いリクエストもフィルタリング出来ます。

api_res_1.png

開くメニューを押すと、リクエストとレスポンスの内容が見れます。

api_res_2.png

上記はクライアントの方もリクエスト・レスポンスを直感的に見れるので重宝しております。

SentryでユーザIDで検索できるようにする

会社ではSentryを例外を捕捉して記録するサービスとして使っています。

このSentryのログを送る際に、ユーザIDもカスタムメトリックスとして送れるようにしております。

class ErrorHandleMiddleware(object):
    ...
    def process_exception(self, request, exception):
        if not self._can_emit(exception):
            return
        data = self._gen_data(request)
        if getattr(request, 'device_id', None):
            tags = {'device_id': request.device_id}
        else:
            tags = None
        client.captureException(data=data,
                                tags=tags,
                                exc_info=sys.exc_info())
    ...

これにより、ユーザIDで検索すれば該当のエラーが発見できます。

例外が発生すると各種ログがSentryに送られ、以下のように見れます。

sentry_1.png

sentry_2.png

NewRelicでErrorsを確認する

NewRelicでもここのようにすればユーザIDを送ることができます。

リリースされる前に、遅いAPIを洗い出しリリースされるのを防ぎたい

以前書いたようにNewRelicを使うといいと思います。

サーバの性能が出るか確かめたい

負荷試験をすればいいと思います。 locustがpythonで書かれており、いい感じです。ここらへんのノウハウは別途書こうと思います。

本番環境のユーザを開発環境に再現したい

開発環境でテストする際に、本番環境と同じユーザ情報を使いたいときがあります。 例えば、レベルが上がった状態、指定のユニットが所持された状態などです。

上記は管理画面で実現できるようになっており、jsonでimport/export出来るようになっております。