New Relicを使って手軽にローカル開発環境のrailsアプリのプロファイル

いまのところRailsで動かしたNew Relic Ruby Agentでしか確認していないのですがNew Relicのプロファイラがlocalで利用できます。

まずはスクリーンショットを確認してみます。

pro_1.png pro_2.png pro_3.png pro_4.png

初めてみたとき すげー と思いました。 いままでNew RelicのWeb上でしか確認できなかったものが、ローカルで手軽にできます。 ただ、注意として直近の100リクエストしか履歴がありません。

使い方としては以下があるかなと思います。

  • ローカルの開発環境で使用し、開発中のAPIなどのプロファイラとして使う。パフォーマンス改善に役立つ
  • 新しくメンバーに入った人にプロファイラみてもらって関数呼び出しなどを確認してもらう
  • ステージング環境(MySQLのサーバがAppサーバと分かれているとよりパフォーマンスがわかりやすい)で使用し、SQL文とそれぞれのパフォーマンスを確認
  • ローカルでのみ実行(New RelicのWebにはパフォーマンスを送らないように設定できる)

Developer Modeの設定の仕方

newrelic.ymlで、以下の項目が含まれている事を確認

monitor_mode: false
developer_mode: true

rails sなどでサーバを起動し、 http://example.com/newrelic など通常のURLの下に、/newrelicでアクセス

便利です。pythonのAgentには無いのかな。。。 Railsははほかにもそれっぽいgemがありそうな気はします。

参考

長い時間かかるバッチを並行実行し何倍も早くする

何倍も早くなるかどうかは処理している内容によりますが、並行に実行することにより長い時間かかっていたバッチがはやくなることがあります。

並行に実行できそうな例

  • ゲームの設定などが入っているマスターデータを順番にBigQueryにインポートしているが遅い
  • MySQLのテーブルをRedshiftに順番にコピーしているが遅い
  • 複数SQLMySQLに投げ、すべて取得し終わってから処理をしている

まだまだたくさんありますが、ひとまずこれくらいに。 上記を実現できそうなモジュールとして、geventがあったので、試してみました。 (ちなみにgeventは1プロセスのイベントループで並行に処理するものなので、CPUバウンドな並列化の場合は、他にも組み合わせる必要があります)

geventを使わず直列で実行した場合

serial.png

通常、何もやらずに処理を書くと以下のように書くことになります。

import time
from datetime import datetime


def task(n):
    print('task {} start at {}'.format(n, datetime.now()))
    time.sleep(1)
    print('task {} end at {}'.format(n, datetime.now()))

map(task, xrange(3))

結果    
task 0 start at 2015-12-09 23:22:34.381534
task 0 end at 2015-12-09 23:22:35.381949
task 1 start at 2015-12-09 23:22:35.381989
task 1 end at 2015-12-09 23:22:36.384658
task 2 start at 2015-12-09 23:22:36.384715
task 2 end at 2015-12-09 23:22:37.385449

1秒かかるタスクが3つあるので3秒ほどかかってます。 CPUを専有しているタスクだったらいいのですが(1 coreなら)、I/Oで待たされているだけの場合は無駄に時間がかかってます。

geventで並行処理を書く

concurrent.png

以下のようにgeventを使って並行処理を書くことができます。 関数をそのまま、使って並行処理が書けるので楽です。

from datetime import datetime
import gevent


def task(n):
    print('task {} start at {}'.format(n, datetime.now()))
    gevent.sleep(1)
    print('task {} end at {}'.format(n, datetime.now()))

gevent.joinall([
    gevent.spawn(task, n) for n in xrange(3)
])

結果
task 0 start at 2015-12-09 23:22:59.650820
task 1 start at 2015-12-09 23:22:59.651468
task 2 start at 2015-12-09 23:22:59.651491
task 0 end at 2015-12-09 23:23:00.655431
task 1 end at 2015-12-09 23:23:00.655526
task 2 end at 2015-12-09 23:23:00.655598

処理が並列で実行できるので、約1秒で終わってます。

他のタスクが終わるのをまって実行する

event.png

1つのタスクを複数のタスクが待つこともできます。

from datetime import datetime
import gevent
from gevent.event import Event


first_finish_event = Event()
second_finish_event = Event()


def task_first():
    print('task_first start at {}'.format(datetime.now()))
    gevent.sleep(2)
    first_finish_event.set()
    print('task_first end at {}'.format(datetime.now()))


def task_second():
    print('task_second start at {}'.format(datetime.now()))
    first_finish_event.wait()
    gevent.sleep(1)
    second_finish_event.set()
    print('task_second end at {}'.format(datetime.now()))


def task_last():
    print('task_last start at {}'.format(datetime.now()))
    second_finish_event.wait()
    gevent.sleep(1)
    print('task_last end at {}'.format(datetime.now()))


gevent.joinall([
    gevent.spawn(task_first),
    gevent.spawn(task_second),
    gevent.spawn(task_last),
])

結果
task_first start at 2015-12-09 23:25:40.381855
task_second start at 2015-12-09 23:25:40.382446
task_last start at 2015-12-09 23:25:40.382479
task_first end at 2015-12-09 23:25:42.383815
task_second end at 2015-12-09 23:25:43.388675
task_last end at 2015-12-09 23:25:44.391731

並行で動作するタスクを制限する

pool.png

バッチを実行しているサーバのスペックが足りなかったりして、同時実行するタスクを制限したいときはPoolを使います。

from datetime import datetime
import gevent
from gevent.pool import Pool


pool = Pool(2)


def task(n):
    print('task {} start at {}'.format(n, datetime.now()))
    gevent.sleep(1)
    print('task {} end at {}'.format(n, datetime.now()))


pool.map(task, xrange(3))

結果
task 0 start at 2015-12-09 23:27:06.978898
task 1 start at 2015-12-09 23:27:06.978962
task 0 end at 2015-12-09 23:27:07.979837
task 1 end at 2015-12-09 23:27:07.979910
task 2 start at 2015-12-09 23:27:07.980069
task 2 end at 2015-12-09 23:27:08.984811

まとまったタスクごとに処理する

group.png

from datetime import datetime
import gevent
from gevent.pool import Group


first_group = Group()
second_group = Group()


def task(n):
    print('task {} start at {}'.format(n, datetime.now()))
    gevent.sleep(1)
    print('task {} end at {}'.format(n, datetime.now()))
    return n * 10

for i in first_group.imap(task, xrange(2)):
    print(i)

for i in second_group.imap(task, xrange(2, 4)):
    print(i)

結果
task 0 start at 2015-12-09 23:27:57.212093
task 1 start at 2015-12-09 23:27:57.212149
task 0 end at 2015-12-09 23:27:58.214046
task 1 end at 2015-12-09 23:27:58.214113
0
10
task 2 start at 2015-12-09 23:27:58.214336
task 3 start at 2015-12-09 23:27:58.214371
task 2 end at 2015-12-09 23:27:59.216035
task 3 end at 2015-12-09 23:27:59.216134
20
30

実際に多数のファイルをマスターデータ => BigQueryに入れるスクリプトに導入してみて

Poolを使ったのですが、プールの数の分だけ平行化して高速に処理ができました。 ただ手元のMacで動かした感じだと時々、エラーがでるのでそこらへん今度さぐってみたいと思います。

スマフォゲームアプリの負荷試験いろは

スマフォゲームアプリの負荷試験をしたので、どのように実施したのか共有したいと思います。 今回はDAU10万人を想定して負荷試験を行いました。

使ったツール

  • 負荷試験クライアント
  • 性能監視
    • New Relic (通常の監視に加え、MySQL Pluginを入れるといい)
    • AWSの標準のモニタ

単純な構成で負荷試験

まず疎通確認の意味も込めて、単純な構成で負荷試験を行います。 今回は、渡されたjsonをechoするだけのAPIを作って、locustを使い負荷試験を実施しました。 いきなり複数台で行うと、パフォーマンスが出なかったときに、どこを調整していいかわからなくなります。 まずAppサーバ自体でDjangoフレームワークがどのくらいパフォーマンスが出せるのか確認します。 (本来だったら、素のDjangoでやるのがいいのですが、今回は開発しているアプリサーバのDjangoAPIを足す形にしてあります)

server_1.png

結果として、DjangoのAppサーバが70%ほどのCPU使用率で、600RPSほど捌けました。 Django自体のベンチマークと比較したりして極端に遅くなっていないかなど見直すといいと思います。

またこの試験を行う利点として、負荷クライアントのlocust自体も性能を発揮できるか確認できます。

単純な構成でうまくいったら、次にテストシナリオを作成します。

テストを行うユーザの定義とテストシナリオ作成

負荷試験を行う際に、想定されるユーザの定義と、達成すべき目標を決めます。

アプリの特性にもよると思いますが、今回はユーザを以下のように、 ヘビーユーザ , 一般ユーザ , 新規ユーザ として定義します。

  • ヘビーユーザ: ゲームにのめり込んで遊んでくれているユーザ
  • 一般ユーザ: ゲームを遊んだことがある既存ユーザだが軽めに遊ぶユーザ
  • 新規ユーザ: その日に、新しくゲームをインストールして遊んでくれるユーザ

DAU10万人を想定するといっても新規ユーザが多いのか、ヘビーユーザが多いのかによって、叩かれるAPIの数や種類が違い、負荷が全然違ってきます。 ヘビーユーザが多いとゲームのコアの部分、例えばクエストやPvPが多く遊ばれます。一方で、新規ユーザが多い場合、サインアップ後の登録処理のAPIが多く叩かれます。

ユーザを特徴的なセグメントにわけれたので、次にどのようなAPIがそれぞれ叩かれるのかを以下のように定義しました。

user_1.png

  1. 各セグメントが叩くAPIを定義する
  2. 各セグメントがそれぞれのAPIをどのくらい叩くか定義する
  3. 各セグメントの1ユーザが1日に叩くAPIの数を算出する (図だとヘビーユーザは340)
  4. 3に各セグメントのDAUをかけて、足しあわせ、システムにくる1日のリクエスト数を出す (図だと13,510,000)
  5. 4により平均RPSが算出できる
  6. いままでの経験値などからピーク値は3倍ほどと見積もり469RPS(Request per Seconds)をシステムが出す必要がある

上記のように定義したものを、実際にlocustでシナリオを作成し、負荷試験をしました。

シナリオを使って1台のサーバに負荷試験

いきなり複数台のAppサーバを使って負荷試験をせずに、1台だけでやります。狙いとしては、テストシナリオの疎通確認という意味と費用削減です。 いきなり複数台のAppサーバをたてると、エラーがあっていろいろ試行錯誤している間にもお金がかかりよくありません。 以下のような構成でMySQL, Redisも含めたテストシナリオの試験をします。

server_2.png

想定の負荷に耐えるように負荷試験

最後に、以下のようにサーバをセットアップし、目標であるRPSを達成できるまで負荷試験を実施します。 Appサーバの負荷が高く(CPUが70%以上など)なればAppサーバを増やすか、スペックをよくします。MySQL, Redisに関しても同様です。

server_3.png

まとめ

自分のプロジェクトでどのように負荷試験をしたか書いてきましたが以下がポイントでした。

  • 最小構成から大きくしていくこと
  • 想定ユーザからテストシナリオを作る

TableauでRedshiftに実行されたクエリを確認する

TableauでRedshiftにつなぎにいっていろいろ確認しているときに、Tableauが発行しているSQLを確認したいときがあります。例えば以下の場合です。

  • psqlでRedshiftにつなぎにいくときに、長ったらしい、JOINをかくのが面倒なので、TableauでGUI上で作成してものを流用したい
  • Tableauが意図した通りのSQLを発行しているか確認したい

上記のような場合にポイントとなるのは、 どこでRedshiftは発行したクエリを保持しているのか と、 Tableauはどのようにクエリを発行しているか ということです。

どこでRedshiftは発行したクエリを保持しているのか

svl_statementtext のテーブルにシステムで実行されたすべての SQL コマンドが入っています。 ただ、このテーブルは長いSQLは200文字で分割されて別々のレコードに格納されているので、文字列を結合する必要があります。

Tableauはどのようにクエリを発行しているか

Tableauはcursorを発行してクエリを取得しています。例えば、SQL_CUR0x7ffeb2f68c60という名前のカーソルを宣言し、fetch文を発行しレコードを取得しています。以下が例です。

fetch 1000 in "SQL_CUR0x7ffeb2f68c60"

CursorはDECLAREで宣言するので、その場所を特定できれば発行しているSELECT文が分かりそうです。

結局どのようにすればいい?

以下でいけます。SQL_CUR0x7ffeb2f68c60の部分は適宜、Tableauが発行するクエリをAWSコンソールのGUIから確認すれば出来ます。

SELECT
    *
FROM
    (
    SELECT
        DISTINCT
        xid,
        pid,
        starttime,
        LISTAGG(text) WITHIN GROUP (ORDER BY sequence) OVER (PARTITION BY starttime) AS sql_text
    FROM
        svl_statementtext
    GROUP BY
        xid,
        pid,
        starttime,
        sequence,
        text
    ) as t
WHERE
    sql_text LIKE '%SQL_CUR0x7ffeb2f68c60%' AND sql_text LIKE '%declare%'

廃止予定の実装であることをスマートにお知らせする - debtcollerctor

廃止予定(deprecated)なクラスやメソッドをモジュールのユーザに通知するために python では warnings があります。

以下をコードを実行すると

# -*- coding: utf-8 -*-
import warnings


def deprecated_method():
    warn_msg = "`deprecated_method` is deprecated and will be removed in v0.5"
    warnings.warn(warn_msg, UserWarning)
    print "I'm old"


if __name__ == '__main__':
    deprecated_method()

以下のようにwarningsが出ます。

$ python warning_sample.py 
warning_sample.py:7: UserWarning: `deprecated_method` is deprecated and will be removed in v0.5
  warnings.warn(warn_msg, UserWarning)
I'm old

今回は、この deprecated メッセージを作りやすくしてくれるdeptcollectorの紹介です。

debtcollectorの使い方

ほぼ、Examplesを見ればいいのですが、自分でもやってみます。

class, calssmethod, method, functionのdeprecated

クラス内のメソッドが廃止予定の場合は以下のように@removals.removeのデコレータを使います。

# -*- coding: utf-8 -*-
from debtcollector import removals
import warnings

warnings.simplefilter('always')


class Car(object):
    @removals.remove
    def start(self):
        pass


if __name__ == '__main__':
    Car().start()

実行結果は以下。

$ python removals_sample.py 
removals_sample.py:15: DeprecationWarning: Using function/method 'Car.start()' is deprecated
  Car().start()

コードの中にある、warnings.simplefilter('always')はなぜ必要なのでしょう。

@removals.removeはデフォルトだと、DeprecationWarningがwarings.warnに渡されて実行されてしますので、表示されません。 warningsの警告カテゴリに詳しくのってます。

コード内に書かずに、コマンドラインでも-Wオプションを指定することにより警告を表示できます。 さきほどのソースコードから、warnings.simplefilter('always')を削除し、以下のように実行します。

$ python -Wd removals_sample.py
... 省略
  file, filename, etc = imp.find_module(subname, path)
removals_sample.py:15: DeprecationWarning: Using function/method 'Car.start()' is deprecated
  Car().start()

Car().start()のエラーに混じって他の警告も表示されました。 自分でライブラリなどを作ったら一回やってみるといいかもしれないですね。

クラスへの警告は以下のように書きます。

@removals.remove
class Pinto(object):
    pass

クラスメソッドの場合は、@classmethodデコレータの上につけます。

 class OldAndBusted(object):
    @removals.remove
    @classmethod
    def fix_things(cls):
        pass

method, property, class, keywordなどが、他の名前に変わった時

function名が変わった時はmoves.moved_functionを使います。

# -*- coding: utf-8 -*-
from debtcollector import moves
import warnings
warnings.simplefilter('always')


def new_thing():
    return "new thing"

old_thing = moves.moved_function(new_thing, 'old_thing', __name__)


if __name__ == '__main__':
    print new_thing()
    print old_thing()

実行すると、old_thing()を呼んでも新しいメソッドが呼ばれている事がわかります。

$ python moving_sample.py 
new thing
moving_sample.py:15: DeprecationWarning: Function '__main__.old_thing()' has moved to '__main__.new_thing()'
  print old_thing()
new thing

methodの場合は、デコレータでできます。

class Cat(object):
    @moves.moved_method('meow')
    def mewow(self):
        return self.meow()
    def meow(self):
        return 'kitty'

その他の例は、最初に紹介したリンクを見てください。

将来的に置き換えられるバージョンを指定する

現在のバージョンと、置き換えれるバージョンを指定するには、以下のようにversionとremoval_versionのキーワードを指定します。

# -*- coding: utf-8 -*-
from debtcollector import moves
import warnings
warnings.simplefilter('always')


def new_thing():
    return "new thing"

old_thing = moves.moved_function(new_thing, 'old_thing', __name__,
                                 version="0.5", removal_version="0.7")


if __name__ == '__main__':
    print new_thing()
    print old_thing()

実行結果は以下です。

$ python moving_version_sample.py 
new thing
moving_version_sample.py:16: DeprecationWarning: Function '__main__.old_thing()' has moved to '__main__.new_thing()' in version '0.5' and will be removed in version '0.7'
  print old_thing()
new thing

その他の方法

以下のようにしてメソッド内でメッセージを出す事もできます。

import debtcollector
debtcollector.deprecate("This is no longer supported", version="1.0")

まとめ

deptcollectorを使うと、デコレータを使うだけでスマートに廃止予定のメソッドをユーザに通知できそうです。

コードレビューでコーディング規約違反ですと言われないためにgit hookでコミットするときに自動で直す

チームでコードレビューを実施する際に、コーディング規約違反ですというのを何十個もつけた事があって、これを不毛な作業だし、時間がもったいないという事で解決するのに以下の方法を実施しました。

  • gitのコミットフックを利用して、コミットする際にコーディング違反を自動修正する

コミットフックは、pre-commit(コミットする前に実行)とpost-commit(コミット後に実行)があるのですが、以下の理由でpost-commitを選びました。

  • 自分の書いたコードに対して、自動で変えられたものをコミットしたくない
  • どこが規約違反なのか認識したい

それでは設定の仕方を記します。

前提

  • pythonで開発
  • コーディング規約はPEP8
  • autopep8でコーディング規約を修正

手順

autopep8をインストールします。

$ pip install autopep8

まずリポジトリを作ります。

$ mkdir your_proj
$ cd your_proj
$ git init

次に、gitのpost-commitフックに使用するスクリプトを作成します。実行権限も付与する必要があります。

$ vi .git/hooks/post-commit
$ chmod 755 .git/hooks/post-commit

以下の内容を書き込みます

#!/usr/bin/env bash

PROJ_ROOT="$( git rev-parse --show-toplevel )"

${PROJ_ROOT}/hooks/scripts/post-commit
hook_status=$?
if test ${hook_status} -ne 0
then
    echo " git post-commit hook error"
    exit 1
fi

hookスクリプトを作成します。post-commit内に書くてもありますが、.git/hooks/post-commitの内容を変えたくなったときにいちいちメンバー全員に変更してもらうのはナンセンスなので、ソースコード自体にコミットしておきます。

$ mkdir -p hooks/scripts
$ vi hooks/scripts/post-commit
$ chmod 755 hooks/scripts/post-commit 

以下の内容にします。 --ignoreの部分はチームのコーディング規約の状況に応じてかえてください。

#!/usr/bin/env bash

FILES=$(git diff HEAD^ HEAD --name-only --diff-filter=ACM | grep -e '\.py$')

for f in ${FILES}
do
    # auto pep8 correction
    autopep8 --ignore=E309,E501,W291,E303 -i ${f}
done

hookスクリプトをコミットしておきます.

$ git add .
$ git commit -m'first commit'

準備ができたので何かコーディング規約に違反しているpythonスクリプトを作ります。 改行がありすぎたり、=(イコール)との間にスペースがなかったりします。

$ cat test.py 
print 'Hello World!'

def my_method():
    a=1+2


    return a



$

ただし正しく実行はできます。

$ python test.py 
Hello World!

それではコミットしてみます。

$ git add .
$ git commit -m'add test'

するとコミットした直後にもかかわらず変更点がでています。

coding_—_bash_—_bash.png

git diffでみてみると以下のようになります。

coding_—_lv_—_bash.png

commit後に自動で修正されているのがわかります。 これでさらにコミットしてpushすれば、もうコードレビューで規約違反ですと言われる事がなく、本質のレビューに集中できます。

今回はpost-commitにしますが、チームの状況に応じて、pre-commitにすれば、後からコミットしなくてもすみます。 何がPEP8違反なのかわかっているチームならpre-commitでもいいと思います。

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

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

同僚と話していたときに、他のチームがやっていて便利な事が共有されたらいいよねという話があったので、まず自分のチームのことから共有したいと思います。 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出来るようになっております。