Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

Redash上からBigQueryスクリプトを利用してみた

この記事は Akatsuki Advent Calendar 2019 - Adventar 4日目の記事です。

こんにちは! suwahime です。

昨今どの業界を見渡しても、アクティブユーザー数や入会数といったデータは、当たり前のように日々追っているかと思います。 私の所属するチームでは、主にRedashを用いてKPIを可視化しています。

今日は、先月BigQueryにBetaリリースされたスクリプトとプロージャ機能を使って、RedashのQuery作成を、よりシンプルに、管理しやすい形で書いてみたことをお話しさせていただきます。

RedashはQueryごとに定義が分散してしまいがち

例として、「期間内にサービスを訪れたユーザー」の利用デバイスを調べる、以下のようなQueryをRedashに作成していたとします。

SELECT B.device_name, COUNT(DISTINCT A.user_id) FROM
  (SELECT * FROM dataset.login_user_ids
    WHERE timestamp >= TIMESTAMP("{{start_time}}")
    AND timestamp <= TIMESTAMP("{{end_time}}")
    GROUP BY user_id) AS A
  LEFT JOIN
  (SELECT user_id, device_name FROM dataset.user_devices) AS B
  ON A.user_id = B.user_id
GROUP BY B.device_name

テーブルの内容等は割愛しますが、利用者はstart_timeとend_timeをRedash上のGUIから入力することで、数字が見られるというイメージです。

Redashはこのように愚直なSQLを書いても簡単にグラフ化までできる魔法のようなツールなのですが、いかんせん簡単すぎて、考えなしに様々なQueryを作成してしまい、後々の定義変更が大変だったりすることがあります。

たとえば、以下のような要求がきた場合はどうなるでしょうか。

  • 期間ごとの不正なユーザーIDを検知することができたので、その一覧をKPIから削減したい。
  • ログインしたユーザーではなく、サービスをプレイしたユーザーから取るように変えてほしい。

中間テーブルを作成するなど、やり方はいろいろあると思いますが、ここではまた愚直に以下のように修正してみます。

SELECT B.device_name, COUNT(DISTINCT A.user_id) FROM
(SELECT * FROM dataset.play_user_ids -- 参照するテーブルを置き換え
  WHERE timestamp >= TIMESTAMP("{{start_time}}")
  AND timestamp <= TIMESTAMP("{{end_time}}")
  AND user_id NOT IN -- 不正ユーザーIDを弾く
  (SELECT user_id FROM dataset.wrong_user_ids
    WHERE timestamp >= TIMESTAMP("{{start_time}}")
    AND timestamp <= TIMESTAMP("{{end_time}}"))
  GROUP BY user_id) AS A
LEFT JOIN
(SELECT user_id, device_name FROM dataset.user_devices) AS B
ON A.user_id = B.user_id
GROUP BY B.device_name

Redashの埋め込み記法である {{}} が増えて少々見づらくなりましたが、なんとか簡単にできました。Redashはこのフットワークの軽さが良いですね。

では、更に次のような要求が来た場合はどうでしょう?

  • 「期間内にサービスを訪れたユーザー」を元に調査している他の全てのQueryについても、同様に置き換えて欲しい。

…これは少々困りました。Query数が少ないプロジェクトなどは、そんなにコストがかからず置き換え可能なのかもしれません。しかし、誰でも好き勝手にQuery作成が可能なプロジェクトにおいては、どこでこの指標を利用しているのか、検索して見ていくしかありません。

もし「期間内にサービスを訪れたユーザー」の定義が一元管理できていたら、こんな手間は無くなると思いませんか?

RedashとBigQueryスクリプトを組み合わせて使ってみる

ではここで、BigQueryスクリプトを使った解決策を試してみましょう。

まずは「期間内にサービスを訪れたユーザー」をaccess_user_idsというTEMP TABLEに吐き出すプロシージャを、以下のスクリプトをBigQuery上で実行することで登録してみます。

CREATE PROCEDURE dataset.create_access_user_ids (start_date TIMESTAMP, end_date TIMESTAMP)
BEGIN
  CREATE TEMP TABLE access_user_ids AS
  SELECT * FROM dataset.play_user_ids
    WHERE timestamp >= start_date
    AND timestamp <= end_date
    AND user_id NOT IN
    (SELECT user_id FROM dataset.wrong_user_ids
      WHERE timestamp >= start_date
      AND timestamp <= end_date)
    GROUP BY user_id;
END;

これをCALL関数で呼び出すことで、RedashのQueryは以下のように書けるようになります。

CALL dataset.create_access_user_ids(TIMESTAMP("{{start_time}}"), TIMESTAMP("{{end_time}}"));

SELECT B.device_name, COUNT(DISTINCT access_user_ids.user_id) FROM
access_user_ids
LEFT JOIN
(SELECT user_id, device_name FROM dataset.user_devices) AS B
ON access_user_ids.user_id = B.user_id
GROUP BY B.device_name;

見た目がだいぶスッキリしましたね。しかも、今後定義変更があった場合にはプロシージャ側を編集するだけで、access_user_idsを使っているすべてのQueryが同じ指標に置き換わります。

注意すべきは、1クエリではなくスクリプトになるため、区切り文字「;」が必要になることと、プロシージャ内で作られるTEMP TABLEの名前がRedashからだと隠蔽されてしまうため、命名規則などで対応する必要があることです。

RedashからBigQueryスクリプトを使うことのメリット、デメリット 

プロシージャを利用することの最大のメリットは、RedashのQueryごとに指標がバラけることなく一元管理することができることです。他にも、BigQueryからRedashにSQLをコピペしてから、時間範囲の部分だけを {{start_time}} と {{end_date}} で置き換えて…なんていう手間を省くこともできます。最終的にはプロシージャや中間テーブルだけでクエリを作成して、RedashからはCALLするだけという運用にすれば、ダッシュボードツールに縛られることのない未来もありそうです。

デメリットは、BigQueryスクリプト内で書かれたSQLに関して、処理される推定バイト数が実行前にわからないことです。想定外に検索費用がかかってしまうこともあるかもしれません。これに関しては、たとえば先にdry runを実行して処理する推定バイト数を算出し、一定以上の費用がかかりそうなら実行しない、ということができるようになればありがたいですね。(現時点では、コンソール上からdry runをすることはできないようです。)

BigQueryスクリプトで可能になることはまだまだありそうなので、今後も追ってみたいと思います。

参考

cloud.google.com

cloud.google.com

cloud.google.com

RailsでTZ環境変数を設定するハックを不要にした話

TL;DR

環境変数を設定するだけでRuby on Railsサーバが10%高速化する(かもしれない)話』  でRailsを高速化させる素晴らしいハックが紹介されましたが。いまや有効なハックではなくなりました。

 

TZハックさん、ながい間(2日間)おつかれさまでした。

 

はじめに

 アカツキさまで技術顧問をさせていただいている小崎です。

 

このエントリは『環境変数を設定するだけでRuby on Railsサーバが10%高速化する(かもしれない)話』をRubyコミッタが読んだらこうなったというアンサーソングになっています。合わせてお読みください

 

TZ環境変数でTime.newが10倍近く速くなるのは素晴らしい発見ですが、コミッタとしてはTZなしでも速くなって欲しいなと思いました。だってめんどうだし。

 

現状分析

まず問題のテストプログラムを軽く分析してみましょう

 

% strace -c ruby ../t.rb
% time  seconds     usecs/call  calls     errors    syscall
------ ----------- ----------- --------- --------- ----------------
99.86     4.248080          11    400076         1 stat
0.03      0.001304           3       458       319 openat
0.02      0.000786           2       362         7 lstat
0.02      0.000682           4       184           read
0.01      0.000407           3       141           close
0.01      0.000390          11        34           mmap
...

Time.new 10万回に対し、statが40万回、つまりTime.new1回あたり4回のstatが呼ばれていることが分かります。

 ltraceでも確認してみます

% ltrace -c ./miniruby ../t.rb 
% time   seconds    usecs/call     calls function
------ ----------- ----------- --------- --------------------
 30.97   47.608101         158    300000 tzset
 25.24   38.804739         129    300000 localtime_r
 16.28   25.027547         120    207016 malloc_usable_size
 10.51   16.163021         161    100000 mktime
 8.31    12.781192         123    103912 calloc
 8.26    12.698693         123    103204 strlen
 0.18     0.282174          77      3645 memcpy
 0.13     0.204240          76      2674 malloc
 0.08     0.129064          80      1601 memcmp
..

 

おや?tzset(), lcoaltime_r() 呼び出しが30万回しか呼ばれておらず数が合いません。

 が、よく訓練された低レイヤー開発者は glibcの__tzfile_read() が呼ばれるのはtzset(3)が呼ばれたときだけだけども、tzset(3)はmktimeが呼ばれたときにも内部的に呼ばれていることを知っています。

つまり、Time.newするたびに、tzsetを3回、mktimeを1回呼んでおり、それがstat 4回につながります。

だいたい計算があってきました。

 

余談

 

tzset()とはTZ環境変数にもとづいてlibcのタイムゾーン初期化する関数ですが、TZ環境変数が未設定の場合は処理系依存です。

glibcの場合は /etc/localtimeのファイルに従いタイムゾーン情報を使って初期化します(まあ妥当ですよね)。ところがお節介なことに、glibcは /etc/localtime があとから変更された場合に備え、tzset()が呼ばれるたびに、 /etc/localtimeをstatして変更がないかチェックし、変更があれば再初期化するという実装になっています。

ほとんど使い道のないお節介のために遅くなっているのですから、余計なことをしてくれたものです。

 

 

localtime_r 問題(すでに解決済みでした)

 

さて、Rubyのtime.c をみると

#define LOCALTIME(tm, result) (tzset(),rb_localtime_r((tm), &(result)))

のような行があり、ようするに localtime_r()を呼ぶときには毎回 tzset()を呼んでいることがわかります。

これはlibcのマニュアルの

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/ctime.3.html

POSIX.1-2004 によると、 localtime() はあたかも tzset(3) が呼ばれたかのように振舞うことが要求されているが、 localtime_r() にはこの要件はない。 移植性が必要なコードでは、 localtime_r() の前に tzset(3) を呼び出しておくべきである。

 

の記述に従っているためです。

・・・・と言いたいところですが、別に毎回呼べとは書いてありませんね。最初に一回だけ呼べばいいのでは?

 

とここまで調べたところで、実はこの問題はRuby 2.6で解決済みであったことに気がつきます。一番悔しいパターンです。元blogにあわせて Ruby 2.5で調査していたための不覚でした。

修正は以下のコミットです。

 

commit 4bc16691279e98ecdb3e19ff23902be671d46307
Author: nobu <nobu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
Date: Wed Jul 18 10:30:41 2018 +0000
reduce tzset

* time.c (rb_localtime_r): call tzset() only after TZ environment
variable is changed.

 

 

ところで、さきほどの localtime_r の man を注意深く読まれた読者は、あれ? localtime_r も tzset()を内部的に呼びだすっぽいこと書いてあるけど?と思うかもしれません。賢いですね。

 

関数 localtime() は、カレンダー時刻 timep を ユーザーが指定したタイムゾーンでの時刻要素別の表現へ変換する。 この関数は tzset(3) を呼び出したかのように振舞い、 大域変数 tzname に現在のタイムゾーンの情報を設定する。 また、timezone に協定世界時 (UTC) とローカル標準時との 時差の秒数を設定し、 一年の一部で夏時間が適用される場合は daylight に 0 が設定される。 返り値は静的に確保された構造体を指しており、この後で 日付や時刻に関する関数のいずれかが呼び出されると 上書きされる可能性がある。 localtime_r() も同様だが、 データはユーザーが用意した構造体に格納される。 この関数は tzname, timezone, and daylight を設定する必要はない。

 

実は glibc は localtime() は毎回 tzset() を呼び出すが、localtime_r() は最初の1回だけtzset() を呼び出すという最適化をしています。

 

具体的には glibcのソースコード中の time/tzset.c の以下のあたりで切り分けています。

tzset_internal()の引数が0になると最初の一回のみ初期化という意味、tp == &_tmbuf はlocaltime()のとき1、 use_localtime はlocaltime(),localtime_r()どちらでも1です(gmtimeなどのときに0になります)

 

/* Return the `struct tm' representation of TIMER in the local timezone.                                                                                  
   Use local time if USE_LOCALTIME is nonzero, UTC otherwise.  */
struct tm *
__tz_convert (__time64_t timer, int use_localtime, struct tm *tp)
{
  long int leap_correction;
  int leap_extra_secs;

  __libc_lock_lock (tzset_lock);

  /* Update internal database according to current TZ setting.                                                                                            
     POSIX.1 8.3.7.2 says that localtime_r is not required to set tzname.                                                                                 
     This is a good idea since this allows at least a bit more parallelism.  */
  tzset_internal (tp == &_tmbuf && use_localtime);

 

コメントに理由が書いてありますね。つまり、tzset() で毎回 /etc/localtimeをstatするのはタイムゾーン処理のためで、localtime_r()はタイムゾーンまわりの処理が不要だから、ということのようです。

 

 mktime問題

残った mktime 問題ですが、grepすると呼び出し箇所は簡単に見つかります。 

 

time.c 

static const char *
find_time_t(struct tm *tptr, int utc_p, time_t *tp)
{
(snip)

#if defined(HAVE_MKTIME)
    tm0 = *tptr;
    if (!utc_p && (guess = mktime(&tm0)) != -1) {
        tm = GUESS(&guess);
        if (tm && tmcmp(tptr, tm) == 0) {
            goto found;
        }
    }
#endif

 

これはラッキーです。mktime() があれば使うが、なければないで特に困らないという実装になっています。このような実装になっている理由については 

 

APIデザインケーススタディ ~Rubyの実例から学ぶ。問題に即したデザインと普遍の考え方』 

 

という本に解説があるので興味がある人は一読するといいと思います。このケースに限らずRubyのいろいろなAPIデザインの知見がつまったとてもいい本です。

 

要約すると mktime() があれば使うが、なかったり信用できなかったりした場合は、64bit integerの全空間を二分探索でlocaltime_r() を使って力業で探索するということです。

 

 ふつうに考えれば、前者のほうが明らかに速いのですが、今回我々は、mktimeは毎回tzset() を呼ぶが、localtime_r () は最初の一回のみ tzset() を呼ぶ。そして tzset() はとても遅い、ということを学びました。

つまり、毎回力業探索をしたほうが速いと言うことは十分ありえるとことです。やってみましょう!

 

 結果測定

t = Time.now; 100000.times { Time.new(2019) }; Time.now - t

 

のケースで
Ruby 2.6:                                          0.387sec
Ruby 2.6 + TZ環境変数:                   0.197sec
Ruby 2.7開発ツリー:                        0.162sec
Ruby 2.7開発ツリー+TZ環境変数:   0.165sec

 

と約2倍の高速化を達成し、かつTZ環境変数ありなしで速度が同等になりました。

 

 さらに検討をすすめる

 

じゃあ、削ろうかなと準備していたところに、オリジナル作者のakrさんから

 

mktime が tzset する副作用は意図して使っていたわけじゃないけど、ユーザから見ると /etc/localtime の変化に自動的に追従する挙動を提供していたと思う。
追従しない挙動に変えるのなら、代替手段が存在した方が説明しやすいと思う。
誰も気にしない可能性もあるけど。

 

というコメントをいただきました。その後しばらくコミッタ同士で議論して、

ENV['TZ'] = nil

したら、次のTime.newはtzsetを呼びなおすという挙動にすることにしました。これはRuby 2.6でのlocaltime_rの高速化をした

 

commit 4bc16691279e98ecdb3e19ff23902be671d46307
Author: nobu <nobu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
Date: Wed Jul 18 10:30:41 2018 +0000
reduce tzset

* time.c (rb_localtime_r): call tzset() only after TZ environment
variable is changed.

 

において、TZ環境変数を設定したらtzset()を呼びなおすという実装がすでに入っていたため、それを素直に拡張して、ENV['TZ'] = nil のときは元々nilだったとしてtzset()をしなおす(/etc/localtimeが変更されていればtimezoneは変わる)という挙動にしました。

  

  

まとめ

今回議論した mktime削除パッチは

https://github.com/ruby/ruby/commit/4d7a6d04b2c71aabb9d6e619f4405887806a5be8

としてコミット済みのため、とくに問題なければRuby 2.7に含まれるはずです。

 

よって要約すると

  • Ruby 2.6ではTime.newが3倍ぐらい速くなった
  • Ruby 2.7ではさらに2倍ぐらい速くなる(予定)
  • そのため、2.7以降では、TZ環境変数を設定する必要はまったくない

 ということになります。

 

アカツキさまのおかげで全世界のRailsアプリが高速化されました。このような改善につながるバグ報告は大変ありがたく、とても素晴らしい貢献だと思います。

 

運用フェーズのプロダクトのCocos2d-xバージョンを3.2から3.17に一気に上げた時の知見

この記事は https://adventar.org/calendars/3952 3日目の記事です。

 

はじめに

 こんにちは。クライアントエンジニアのRiyaaaaaです。

 私が担当しているプロダクトではゲームエンジンとしてCocos2d-xを採用しています。リリース当初のバージョンは3.2で、それ以降4年間1度もアップデートされることはありませんでした。しかし、今年、関係各所の調整の結果、とうとうCocos2d-xを最新バージョンまで上げる大規模な改修バージョンのリリースに成功し、こうして記事の執筆に至った次第です。

 断っておきますが、執筆者である私はCocos2d-xバージョンアップ作業において、先行的な技術調査、最低限のゲームを動かすためのコンパイル作業や基幹システムへの修正等しか行なっておりません。Cocos2d-xバージョンアップという大きな改修は、多くのエンジニアの協力あって実現しました。この場を借りてお礼申し上げます。

 

Cocos2d-xバージョンアップを実行に移した経緯

 品質向上のためにもゲームエンジンのバージョンを上げたいという話は以前から上がっていましたが、プロダクトの視点から見ると、リスクの割にベネフィットが薄い点が否めません。そのため、問題意識はありながらも、開発項目に具体的に上がることはありませんでした。

 しかし、スマホアプリ業界に2つの大きな激震が走り、Cocos2d-xのバージョンアップはほぼ必須というところまで追い込まれてしまったのです。業界の方ならよくご存知でしょう。

 1つ目は、2018年6月ごろAppleが発表した、iOSのOpenGL ESの利用をdeprecatedとする発表です。つまり、将来的にOpenGL ESはサポートされなくなるので、Metalへ移行すべしというお達しです。

 2つ目は、2019年1月ごろGoogleが発表した、Google Playアプリは2019年8月をもって64bit対応を必須とする発表です。これはiOSの件よりも深刻でした。なぜなら、数年後といったスパンではなく、残り半年強という期限で対応を迫られていたからです。*1

 この2つの発表が決め手となり、ついにプロジェクトリーダー、版元様、ディレクター陣の合意を取り、現場のエンジニア、テスターなどのスタッフの大規模なリソースを注いでの、バージョンアップ作業が始まりました。

 

バージョンアップ作業黎明期、とりあえずゲームが動くまで

 さて、私はというと、AppleのOpenGL ESのdeprecated化の発表より、秘密裏にCocos2d-xバージョンアップ計画を進めておりました。私は将来的に必ず、ゲームエンジンのバージョンアップを迫られる日が来るという直感があったからです。*2 しかし、マイナーバージョンを2から17に上げるというアグレッシブな改修には、そもそも「可能である」ことの証明が必要です。そのため、開発の空き時間などを利用し、地道にローカルでバージョンアップ作業を進めていました。

 もちろんコンパイルすら通らないところからのスタートです。gitで専用のworkspaceを切って、毎日少しずつコンパイルエラーを減らしていきました。ようやくコンパイルが通ったところで、起動時クラッシュに直面。そこから数多くのファイルシステム、ネットワークシステム、その他クラッシュを引き起こす不具合を修正し、「とりあえず動く」状態に持って行きます。

 

Cocos2d-xバージョンアップの開発本格始動

 そして2019年初ごろ、Googleの64bit対応必須化発表の折、「Cocos2d-xアップデートなしには対応は不可能」という見解をエンジニアチームは出します。ndk-buildのabiFilterにarm64-v8aを追加する、そんな単純な話ではありません。特に問題になるのが、Third-pirty製静的ライブラリです。アプリ側のライブラリはもちろん、Cocos2d-xが依存する外部静的ライブラリもほぼ全てが64bitサポートされていませんでした*3。エンジンはアップデートすることなくライブラリだけアップデートする? そんなことが不可能なのは火を見るより明らかでした。

 しかし功を奏して、私の手元にはとりあえず一通り動作している最新Cocos2d-xリプレイス済みバイナリがありました。もちろん、遷移するだけでクラッシュする画面もあれば、どこもかしこもレイアウト崩れが起きまくっていましたが... 「バージョンアップ、イケると思います」

 こうして、Cocos2d-xバージョンアップ計画は、新規開発チーム全体を巻き込んで始まったのでした。

 ちなみに、私はCocos2d-xバージョンアップのメイン開発チームには加わっていません(!?)。弊プロダクトは定期的に大規模なイベントを開催しておりますが、その重要機能の開発に追われていたからです。残念なことです。しかし、ちょうどその時に社内から異動してきたエンジニアの方が低レイヤーなんでも御座れの超ベテランエンジニアだったので、代わりに担当していただき、結果的に素晴らしいアウトプットになりました。最後に紹介しようと思いますが、単なるCocos2dxアップデートに留まらない劇的なビルド環境改善に取り組んでくれたのです。
 

起こったバグ、修正方針まとめ

 以降は、事象、修正方針ベースで項目化して紹介していきます。あまりにも量が膨大だったため、漏れがあるかもしれませんが、参考にしてください。免責事項として以下に紹介する内容はあくまで弊プロダクトの修正方針であり、全てのプロジェクトで動作を保証するものではありません。また、私が実際に修正したわけではないものも多くありますが、誰かの役に立つことを願って、代理として紹介させていただきます。

1. ファイルシステムの大規模なコンフリクト

 弊プロダクトはCriFsというCRIWARE様のファイルシステムを採用しています。そのため、Cocos2d-xのFileUtilsをはじめとするファイルシステム関連クラスには独自拡張を大量に含んでおり、バージョンアップの際に大規模なコンフリクトを起こしました。気合で直しました。初っ端から参考にならない事例で申し訳ない... 

2. HttpClientクラスのレスポンスコードの取り扱いの変化

 元のcocos2d/cocos/network/HttpClientには、responseCodeが200以上、300未満ではない場合、タスクの失敗とする実装がありましたが、3.17のHttpClient-apple/HttpClient-androidクラスでは、それに該当するコードが無くなっていました。つまり、コールバックの取り扱いが変化していたのです。すると、アプリ側でFailureコールバックとしてハンドリングしていた部分が途端に動かなくなりました。

 そのため、挙動を元に戻すためにエンジンに手を加えました。例えば、HttpClient-apple::processTaskの末尾に3.2当時のと同等動作のコードを加えます。

static int processTask(HttpClient* client, HttpRequest* request, NSString* requestType, void* stream, long* responseCode, void* headerStream, char* errorBuffer)
{

  // 中略

  + if (!(*responseCode >= 200 && *responseCode < 300)) {
  +   return 0;
  + }

  return 1;

}

androidも同様です。

3. OpenSSL EVP_CIPHER_CTXのインスタンスの確保方法の変化

 aes_encrypt/aes_decryptに使う暗号情報のコンテキストの型EVP_CIPHER_CTXが、動的確保に変化しました。具体的には、EVP_CIPHER_CTXは前方宣言のみ提供され、今までのようにスタック変数として宣言することができなくなり、EVP_CIPHER_CTX_nex(), EVP_CIPHER_CTX_free()インターフェースで動的確保、解放することを要求されるようになりました。

4. FileUtils::fullPathForFileNameのファイルが見つからなかった時の挙動の変化

 以前の実装ではファイルが見つからなかった時の挙動が「ファイル名」を返す、という挙動でしたが、空文字列が返るように変更されています。妥当な修正ですが、以前の挙動を前提としたコードがあったので元に戻しました。

5. Spriteのレンダー用のコマンドクラスがQuadCommandからTrianglesCommandへ変化

 独自のSpriteを継承した描画クラスが、draw関数をオーバーライドして実装していましたが、その辺りが旧_quadCommandメンバにアクセスしていたため、_trianglesCommandに変更しました。インターフェース等は抽象クラスが同じなため変数差し替えだけで動作しています。

 

6. RefPtrが不完全な型を受け付けなくなった

 RefPtrにコンパイル時型検査が追加されました。

static_assert(std::is_base_of<Ref, typename std::remove_const<T>::type>::value, "T must be derived from Ref");

 std::is_base_ofは完全型を要求するので、既存のコードで前方宣言のみされた型をRefPtrとして定義しているところのコンパイルが通らなくなったのです。これはこのstatic_assertをコメントアウトするだけでも解決しますが、アプリ側の該当箇所が1箇所だったのでそちらを修正しました。

class A {

  class B {

    cocos2d::RefPtr<A> _ptr;

  };

};

 
 このように、インナークラスが外側のクラスのRefPtrを保持するような設計でしたが、これだとBの定義段階ではAは不完全型なので、インナークラスをやめて分離することで解決しました。

7. Scene::_childrenにカメラインスタンスが追加され、_childrenのインスタンスのインデックスが変化した

 これはかなり頭を悩ませました。なにせクラッシュを引き起こす上に、不正なstatic_castを起こして意味不明な場所でバグるのです。そんなコードが何十箇所も!!

 これははっきり言ってお見せするのも恥ずかしい典型的なアンチパターンのコードなのですが、画面の構築順序上「このインスタンスはchildrenのX番目にある」という危なげな根拠による、childrenメンバへのインデックスによるアクセスコードが大量にあったのです。
  

// X番目のChildはHogeクラスだからstatic_castするぜ

auto hoge = static_cast<Hoge*>(view->getChildren().at(X));


 これは最悪なコードです。まず前提としてこんなコードを書くべきではありません。ですが、開発黎明期に書かれたレガシーコードには多くのこういったコードが含まれていました。*4 しかも、たちの悪いことに、static_castを使用しているのです! 嗚呼、これがdynamic_castだったなら、即時にnullptrアクセスで落ちるのに。しかし、static_castは違うインスタンスポインタだろうが問答無用でキャストします。その結果、アクセスの仕方によって様々な挙動を見せます。これは低レイヤー言語の趣深い挙動ですね。クラッシュすればいい方です。

これはgrepで該当コードを探し出してひたすら修正しました。修正方針はそこの文脈で都度判断。ですが、原因の多くはSceneクラスを定義する時、Layerを継承して空のSceneにaddして返す、という古きCocos2d-xのお作法によるものが多かったです。つまり、あるSceneをcreateした時、返ってくるinstanceは目的のクラスではなく、空のSceneクラスだったのです。そのため、childrenから目的のクラスインスタンスを探すコードが多くありました。

auto scene = MyScene::create();

auto myClass = static_cast<MyScene*>(scene->getChildren.at(0)); // 最新バージョンでは、0番目はカメラのインスタンスのためNG

 
 実際にはSceneクラスを定義する時にそんな面倒なことをする必要はなくシンプルにSceneを継承すればいい話です。公式のサンプルコードも、すでにその黒歴史を修正しています。
don't use Layer by minggo · Pull Request #17048 · cocos2d/cocos2d-x · GitHub


上記修正と同じことを既存のSceneクラスに取り込めば、大体は解決しました。

 

8. いくつかのクラス、関数が引数にnullptrを受け付けなくなっていた

CCLuaStack::luaLoadBuffer、CCProgresTimer::initWithSpriteなどです。大抵は呼び出しもとでnullptrハンドリングして終了です。

9. setColorを使ったグレーアウトが動作しなくなっていた

setColorの内容を子供に伝播させるにはNodeのcascadeColorEnabledをtrueにしなくてはならないのですが、様々なCocos2d-x関連クラスのデフォルト値がfalseに変更されました。そのため、setColorを使ったグレーアウトなどの処理が途端に動かなくなったため、急ぎ様々なViewクラスでsetCascadeColorEnabledを呼び出して止血しました。現象次第はシンプルなのですが、如何せん影響範囲が広く、一つの原因に紐づくバグチケットの数がかなり多かったですね。。

10. Androidで一部通信のstyleがunknown_formatになる

 Cocos2dxはAndroidでHttp通信にcurlではなくjava.net.HttpConnectionを使うようになりました。この時、デフォルトヘッダの挙動が変わり Accpet: */* が追加されなくなりました。この時、サーバーサイドのAPIのroutesでレスポンスのMIMEタイプが指定されてないときの挙動の違いにより、レスポンスの解釈がiOSとAndroidで変化するようになってしまいました。というか、基本的にはjson以外受け付けてないんですけどね...

これはアプリ側のリクエストクラスでデフォルトヘッダの差を吸収するようにしています。サーバーサイドでも必ず全てのAPIのroutesで{format: json}を指定するようにしてもらいました。

11. Android4.4未満で通信がバグる

お気の毒ですがCocos2d-x 3.17では4.4未満をサポートしなくなりました

13. ImageViewのアルファチャンネルが無効になるバグ

これはシンプルにcocos2d-xのバグで、ImageViewを2回同じファイル名でloadTexuteを呼び出すとアルファチャンネルが効かなくなるバグです。
これは担当エンジニアがすでに本家にプルリクを送っており、マージされました。
github.com

14. Labelの末尾の改行が無視されなくなった

バグだったのかはわかりませんが、末尾の改行がちゃんと適用されるようになり、それによりレイアウト崩れが発生していました。

15. 9sliceを有効にしたImageViewが特定条件下でバグる

fix_bottomとfix_topが実際の画像の縦幅を超えるというデザインバグがあり、なぜか今まで動いていた状態だったが、バージョンアップによって普通に表示が崩れるようになってしまいました。

16. SceneをaddChildしているところが動かなくなった

そんなことをしてはいけません。おそらくVisittingCameraとかそういうアクティブなシーンのカメラみたいな概念が追加された影響で動かなくなったのかと。replaceSceneやpushSceneを使いましょう。

17. 勝手にボタンがグレーアウトする

ui::Button::setEnableにfalseを渡すと、Buttonクラスの内部で勝手にグレーアウトするようになりました。仕様ということにしました。

18. ScrollViewでクリッピングがバグる

Cocos2d-xのバグです。社内で独自のパッチが当てられましたが、本家では別の形で修正されているようでしたので、そちらの方のリンクを貼っておきます。
https://github.com/cocos2d/cocos2d-x/pull/20352

19. LabelLetterを使っているところでバグる

Cocos2d-x側のバグです。本家にPRを送りました。
Fix: LabelLetter::isVisible always returns false by Riyaaaaa · Pull Request #18975 · cocos2d/cocos2d-x · GitHub

20. Labelの返すgetContentSize().hegihtが半分になっている

これはかなーーり大変な挙動の変化でした。半分になった、というよりはもともとバグっていて2倍になっていたのが元に戻った、というのが正しいです。
しかし、これを前提として構築されたレイアウトがとんでもなく多く、ハードコーディングされたオフセット、italicでアフィン変換された時の位置、様々な箇所に止血対応が入りました。heightサイズバグを再現するLegacyLabelクラスの爆誕などもしました...。この挙動の変化の影響で、とんでもない数の画面の表示がおかしくなっていたのです。。。
全体の工数の10%くらいは、この不具合の対応に追われていたような気もします。笑
 

21. Cocos2d-xのLuaBindingの強化

3.17では、任意のCocos2d-x APIをLuaから呼び出せるように、大規模な改修がされています。ただし、その影響でエンジンのいたるところに、ScriptBindingが有効な時にのみ走る処理が追加されています。弊プロダクトでは、Cocos2d-x Lua-bindingを使用していません。これらの処理は無用なオーバーヘッドなので、CC_ENABLE_SCRIPT_BINDINGを無効にしました。*5


こんなところでしょうか。数が多すぎて網羅できた気がしない...。

おわりに

Cocos2d-xバージョンアップの歴史を一気に振り返りました。この記事を執筆する上で、大量のバグチケットやプルリクを漁り、懐かしい気持ちになりながら、当時の慌ただしさを感じていました。どうせエンジンをアップデートするなら、他のシステムも改善したいというエンジニアの要望もあり、同バージョンには多くのシステム改善がされました。

・Android NDKビルドシステムのndk-buildから、CMakeへの移行
・サウンド系システムの大改修
・CocoaPodsの導入
・様々なThird-pirty製ライブラリのアップデート
・その他パフォーマンス改善など

トータルで半年以上の開発期間(検証含む)を要したこのバージョンは、無事Android 64bit対応期限に間に合い、本番環境で深刻な不具合を出すことなくリリースされました。機能追加を含まないバージョンを、半年以上も開発し続けることができるリソースがチームにあったことはとても喜ばしいことです。これが実現できたのは、現場の開発者だけではなく、マネージャー、運用サイドの担当者などの多くの方のご協力があってのことです。

ちなみにOpenGL ESが本格的にiOSでサポートされなくなり、Metal対応が必須になった時、きっとCocos2d-x v4へのバージョンアップが迫られることでしょう...。メジャーバージョンアップには、今回の規模を遥かに超える改修が必要になりそうです、が、このチームなら乗り越えられる気がしています。多分。

ここまで読んでくださりありがとうございました。


 

*1:しかし、この記事を執筆する上で再度調査していると、実際には2017年には告知されており、弊プロダクトは技術に対するアンテナが細かったために急な対応を迫られてしまった説があります。反省ですね。

*2:この時は、まだAndroid 64bit対応の発表はされていませんでした。

*3:iOSライブラリだけは、64bit対応が2014年くらいに実施されていたので対応されていました

*4:もちろん私はこんなコード書きません。

*5:なぜ有効になっていたのかというと、Cocos2d-xとは関係のないオリジナルのLuaEngineのバックエンドに、CCLuaEngineを使用していたためです。本来はtolua++等のピュアなLua Bindingライブラリを使用すれば必要のない設定でした。

環境変数を設定するだけでRuby on Railsサーバが10%高速化する(かもしれない)話

この記事は Akatsuki Advent Calendar 2019 1日目の記事です。

はじめに

アカツキでは Ruby on Rails を使ったゲームサーバを開発・運用しています。ゲームの体験を向上するために、レスポンスタイムは一つの重要な要素となるため、種々のパフォーマンスチューニングを行なっています。今回はその一例として、環境変数を1つ設定するだけで、あるAPIのレスポンスタイムが10%も改善した例をご紹介します。

TL;DR

多数の時刻を含むレコードを扱う Ruby on Rails サーバでは、 TZ 環境変数を設定することで、デフォルトタイムゾーン設定ファイル /etc/localtime へのアクセスが減り、高速化が図れるかもしれません。

効果は Time オブジェクト1個あたり数μsの短縮といったオーダーですが、チリも積もれば山となり、数千個のレコードを処理するAPIではレスポンスタイムが10%近く改善する例もありました。

f:id:NeoCat:20191105185907p:plain

APIのレスポンスタイムが10%も改善!

きっかけ

ある日、モバイルゲームのAPIサーバの負荷テストを大きめのインスタンスサイズのサーバで実施したところ、小さい構成のサーバでは問題なかったAPIのレスポンスタイムが、なぜか数秒から数10秒と、とんでもなく遅くなるという事象が起きました。
その時のシステムの状態を top を確認してみると、CPU使用率の user は数%なのにも関わらず、sys が 95% といった高い値を示しており、アプリケーションではなく OS (Linux) 周りの問題であるようでした。

環境

問題が発生した環境は、AWS EC2の c5.9xlarge インスタンス (36 core) の上にDockerで ruby:2.5 コンテナ(Debian Stretchベース)を立て、その中でRailsサーバーを動かしており、そこに多数並列でHTTPリクエストを送り込んでいた、という状況です。

Railsのサーバとして、unicorn が複数プロセス動いており、各プロセスはシングルスレッドで動作する構成です。つまり、スケーラビリティの問題の原因になりやすい Ruby の GIL (グローバルインタプリタロック) のせいではありません。

詳しく分析してみる

このセクションでは、 Linux カーネルや Ruby の動作を調べた内容を説明していきます。低レイヤーに踏み込む話になるので、早く結果を知りたいという方は、次のセクションの「対策とおまけ効果」まで読み飛ばして構いません。

プロファイラをかけてみる

こういったときには、どこでCPUが使用されているかをプロファイラで調べてみると有効です。そこでまず、 Linux のプロファイラである perf を使用してみます。

問題発生中に sudo perf top -g を実行すると、CPU が使われている場所のプロファイルが C の関数単位でリアルタイムに表示されます。

以下は95%以上を占める高い __xstat64 から下位の関数を展開していったところです。なお __xstat64 はファイルの変更日時などの情報を取得するglibcの関数です。

Samples: 30K of event 'cycles', Event count (approx.): 32976632083
  Children      Self  Shared Object       Symbol
-   95.21%     0.02%  libc-2.24.so        [.] __xstat64
   - __xstat64
      - 95.04% entry_SYSCALL_64
         - do_syscall_64
            - 94.14% __do_sys_newstat
               - 94.03% vfs_statx
                  - 92.21% filename_lookup
                     - 92.14% path_lookupat
                        - 66.85% link_path_walk.part.39
                           - 63.60% walk_component
                              - 22.86% lookup_fast
                                 - 22.75% __d_lookup
                                    + _raw_spin_lock
                              - 21.11% path_parent_directory
                                 - 19.89% dget_parent
                                    - lockref_get_not_zero
                                       + _raw_spin_lock
                                 + 1.23% dput
                              - 19.53% dput
                                 + 18.02% _raw_spin_lock
                                   1.38% lockref_put_return
                           + 3.00% inode_permission
                        - 22.98% walk_component
                           - 20.49% dput
                              + 19.17% _raw_spin_lock
                                1.14% lockref_put_return
                           + 2.41% lookup_fast
                        + 1.70% trailing_symlink
                        + 0.57% terminate_walk
                  + 0.88% path_put


詳細は割愛しますが、path_lookupat 関数を始め、主に Linux カーネル内のファイルパスを辿る関数群で時間を消費しているようです。さらに辿っていくと、主要因としていろんなところで _raw_spin_lock が出てきました。これは、 Linux カーネルのスピンロック (他のCPUと排他が取れるまで無限ループしながら待つロック方式) の関数で、1つのコアしか同時に処理を行えません。何らかの同じリソースの取り合いが全コアで発生し、コア数が多い分、より激しく競合が発生した結果、アプリケーションの処理が遅々として進まないほどに性能が低下してしまったと考えられます。

では一体何を奪い合っているのでしょうか?

システムコール呼び出しを見てみる

アプリケーションがこの時何をしているのかを調べてみます。

strace -p <rubyのPID>

を実行して何のシステムコール呼び出しているかを調べてみると、どのプロセスも、

stat("/etc/localtime", ...)
stat("/etc/localtime", ...)
stat("/etc/localtime", ...)
...

と、ひたすら /etc/localtime というファイルに対して stat システムコールを呼び出していることがわかりました。同じファイルに対して全コアでロックを取りあっているため、激しく競合が発生しているようですね。

このファイルは、システムのデフォルトのタイムゾーン情報を格納するものです。

ファイルと言いましたが、正確にはrubyコンテナではシンボリックリンクになっています。

UTCをデフォルトタイムゾーンとして使用していますので、リンク先は /usr/share/zoneinfo/Etc/UTC を指しており、さらにこれもシンボリックリンクで ../UCT を指しています。

また同様に ltrace をアタッチしてみると、tz_set, localtime_r などの時刻関連の関数がひたすら呼び出されていました。この中で先ほどの stat システムコールが呼び出されています。

stat システムコールということは、このファイルの内容を読んでいるわけではありません*1。もしかしたらいつの間にかデフォルトのタイムゾーンが前回から変更されているかもしれないので、再読み込みか必要かどうかを調べるために、更新日時を調べているのです。( → glibc__tzfile_read() )

Ruby on Railsはその時何をしていたのか?

では、なんでこんなに localtime_r が呼ばれているのでしょうか?
実は、負荷テストのシナリオの中に、数千件のレコードを MySQL から取得して読み込む API がありました。これらのレコードにはそれぞれ数個の日時が含まれていました。加えて、ActiveRecordの慣例に従って、各レコードには created_at, updated_at カラムがあり、作成・更新時刻を格納しています。
そしてこれらを ActiveRecord が Time(WithZone) クラスとしてインスタンス化する際に、 localtime_r が呼び出されていたのでした。

f:id:NeoCat:20191105191132p:plain

読み込んだレコードには日時が多数含まれていた

localtime_rTime.new ごとに1回呼ばれるだけではなかった

しかも、 localtime_r は Time のインスタンス化で一回呼び出されるだけではありませんでした。

strace ruby -e 'puts; p Time.new'

などと実行してシステムコールの呼び出され方を見てみると、以下のようになりました。*2

  • Time.new → statが1回呼ばれる
  • Time.new(2019,7,1,0,0,0,"+09:00") → なぜかstatが2回呼ばれる
  • Time.new(2019,7,1,0,0,0) → なぜかstatが4回くらい呼ばれる(timezoneを取得するため?)

Ruby の time.c を見てみると、 find_time_t 関数にてシステムの localtime_r に指定時刻の2時間前後の時刻を与えて、何が返ってくるかを調べています。これはどうやら、夏時間の切り替わりの際には同一の時刻が2回ある場合があるので、そういうときに必ず決まった側を返すための処理のようでした。

それにしても、このコードには多数の #ifdef があり、Ruby を様々な環境で動かすための苦労が窺われます……。

対策とおまけ効果

man tzset を見てみると書いてあるのですが、 環境変数 TZ に値が設定してあれば、 /etc/localtime は読まれなくなり、この問題も発生しなくなります。
そこで、例えば TZ=UTC と設定すると、 stat 等のファイルアクセスはなくなります。

これは今回きっかけとなった負荷テストで見られたような大規模サーバでのスケーラビリティの解決はもちろんですが、たとえ小さなサーバであっても、システムコールの発行自体がなくなることで性能向上を期待できます。

具体的に、先ほどのruby:2.5.1コンテナ内で、シングルプロセスのみで試してみましょう。

$ irb
irb(main):001:0> t = Time.now; 100000.times { Time.new(2019) }; Time.now - t
=> 1.027663412

# 環境変数を設定
irb(main):002:0> ENV['TZ'] = 'UTC'
=> "UTC"

irb(main):003:0> t = Time.now; 100000.times { Time.new(2019) }; Time.now - t
=> 0.135658217

なんと7〜8倍の高速化です!

Linuxディストリビューションによっては、 /etc/localtime はシンボリックリンクでなく実ファイルであることもあります。
例えばAmazon Linux 2では /etc/localtime は実ファイルとなっていますが、このケースでも測定してみました。

irb(main):001:0> t = Time.now; 100000.times { Time.new(2019) }; Time.now - t
=> 0.57196811

TZ を設定すると先の例と同程度の時間になり、約4倍の改善となりました。シンボリックリンクのパスを辿らなくて済む分、軽微な影響となるようです。

もちろんこれは Time.new のみの倍率であって、1回1回は高々数μsといったオーダーの効果です。しかし、数千レコードを扱うようなAPI*3だと、積もり積もってAPI全体のレスポンスタイムが10%程度も改善するケースが実際にありました。

まとめ

Ruby on Railsで大量の時間を含むレコードを扱う際、 TZ 環境変数が設定されていないと、Time オブジェクトの初期化のたびに複数回 /etc/localtime に対して stat システムコールが呼び出されるため、性能が低下します。特に /etc/localtime がシンボリックリンクである場合や、コア数の多いサーバ環境ではカーネル内のスピンロックのために影響が顕在化しやすくなります。この現象は特にRuby 2.5までにおいて顕著です。

TZ 環境変数を適切な値に設定することで、これを避けて、時刻を扱う処理を高速化することができます。

 

今後のRubyへのフィードバック

さて、これで本問題は一段落なのですが、 こうした手間をかけなくともRuby自体で高速に動作してくれたら嬉しいですよね。アカツキに技術顧問として加わって頂いた小崎資広さんに、この現象についてRubyのアップストリームで根本的な改善ができるのではないかと、コミュニティで議論していただいています。

 詳しくは別の記事でご紹介したいと思いますので、乞うご期待。 
→ 記事が公開されました! ぜひ併せてお読みください。

hackerslab.aktsk.jp

*1:ファイルの内容は、起動直後に1度だけ読み込まれています。

*2:なお、この挙動はRuby 2.6で少し変更され、stat(2)の呼び出しは 1回となっているようです。

*3:ゲームの運用を続けていくにつれてデータが増えていき、数千件のレコード処理の読み込みが必要になることはままあります。

アカツキ就業型インターン9月の部

こんにちは!アカツキの就業型サマーインターンに参加させていただいたyutaroです。

当インターンでは15日間にわたり、サーバーサイドエンジニアとしてお邪魔させていただきました。

ここでは、その参加報告をさせていただきます。

実際に開発に携わったプロダクトは「八月のシンデレラナイン」です。担当することが決まってから初めてプレイしましたが、登場人物がカワイイですね!

初めて触れるジャンルでしたので、とても刺激的でした。

自己紹介

ざっくりまとめるとこんな人です↓

  • 大学では雑多な学問を学ぶ(情報系は独学)
  • 3年次からサーバーサイドの言語を学び始める
  • 2年次までは夜な夜な遊び散らかしていた

同じ時期に来ていたインターン生と比べると、エンジニアとしての経歴は浅い方ですね。

他の方は、かなり前からコードを書いていたようなので、ずっとビビっておりました。笑

今までに触れた技術

深掘りはせず、気になったものばかり学んでいたので、全てにおいて広く浅いです。

  • Web系の高級言語(PHP、Ruby、Python)及びその主要FW(Django、Rails)
  • Webフロントエンド及びその主要FW(React、Vue)
  • インフラ(AWS、GCP、Heroku)
  • iOSアプリ(Swift)
  • その他(Docker、CircleCI)

参加までの経緯

他社さんの逆求人イベントで、初めてアカツキさんとコンタクトを取りました。

志望後に面接 → コーディングテスト → 合否連絡 といった流れです。

参加前に面談が組まれ、そこで「どういった業務を行いたいか」の話し合いを行います。「インターン生がやりたいこと」を重視してくれるので、すごく嬉しかったのを覚えています。

他にも行ってみたい企業は複数あったのですが、以下のような理由で志望を決めました。

ゲーム業界の特徴的な実装をみてみたい

サマーインターンに参加する以前から、長期のインターンとしてサーバーサイドエンジニアの経験を積んでいました。

ただ、今までの経験で触れた分野は「メディア」のみです。Web業界の王道(?)分野ですね。

ゲーム業界はユーザー関連の情報がかなり多く、またトラフィックの変化も激しいので、また一味違う経験が積めるだろうと思っていました。

なので、「これを機にゲーム業界を経験したい」という思いで志望しました。

お賃金が高い

これは僕の主観ですが、他のインターン生にも少なからずある下心だと思います。笑

高い給与を提示している企業で、「僕自身が通用するのか」試してみたいチャレンジマインドが奮い立たせられました。

相当レベルの高い学生しか志望しないと思っていたので、「選考が通ればラッキーだな」ぐらいの感覚でしたね。受かってよかった...

何をやったのか

大きく分けて3つのタスクを行いました。

  • ランキングマッチ上位者の詳細なポイント取得履歴を出力するRake Taskの追加
  • 管理者画面にOne Loginでログインできるようにする
  • 消費元気n倍で試合をし、報酬をn倍にする機能の追加

以降は、これらをそれぞれ具体的に記述していきたいと思います。

ランキングマッチ上位者の詳細なポイント取得履歴を出力するRake Taskの追加

長期のインターンで入社時に毎回行なっているのは「簡単なタスクを片付けてみる」です。

理由としては↓

  • 入社した直後はあまりコードに慣れておらず、大きなタスクをこなすのは非常にストレスを感じる行為だと考えている
  • メンターが僕自身の実力をまだ把握できていない

なので今回も例外なく、単純なタスクを行いました。


ハチナイの試合形式として「ランキングマッチ」というものがあります。

これは、全国のユーザー同士が一定期間内に試合を行い、順位を競い合う形式となっているのですが、ランキング上位者には特別な報酬が与えられます。

現状の方法として、ランキング上位者を算出するために、エンジニアの方が毎回手動でスクリプトを書いています。しかし↓

  • 毎回スクリプトを書くのがめんどくさいので、楽にできないか
  • 詳細なポイント取得履歴も同時にみて、より妥当性を検証できるようにしたい

このような要望があったので、双方を満たすようなスクリプトを追加しました。

着手するまでRailsのRakeの存在すら知らなかった僕ですが、すでに手打ちで書かれたコードが存在していたので、すんなり実装できました。

One Login機能の追加

着手前の段階で、「次のタスクは何をやるか」を決めるミーティングが開かれました。

というのも、具体的なタスクは入社後に決める方針だったからです。

僕はせっかくなので、「難しそうなやつをやってみたいっす!!」と申し出たところ、要望通りのモノが来ました。笑


ハチナイ(サーバーサイド)の管理画面へのアクセスはIP制限をかけてはいますが、ログイン手法はかなり単純で「これってセキュリティ的にあまり良くないよね??」という話は前々からあったようです。

ただ、みんなやりたがらない領域だったので、僕に白羽の矢が立った(というか、僕が立たせた)。といった経緯です。

アカツキでは全社的にOne Loginを利用しています。One Loginについてざっくり説明すると↓

  • SSO(シングルサインオン)用のサービス
  • One Loginにログインするだけで、他のサービスへのログインが不要になる

こんなところです。

つまり今回のタスクは「管理画面へのログインにOne Loginの機構を取り入れる」というものになります。

ひたすらインプット

着手時点で、「そもそもOne Loginってなんですか??」という状態だったので、最初のうちはとにかく調べまくりました。

認証方式の実体はSAMLだったことや、実際に組み込んだ際の認証フロー、One Login側の操作方法等、調べていくうちに実装のイメージが明確となっていきました。

幸いにもOne Loginの方々が、Rails向けのライブラリをすでに実装してくださっていたので、比較的ラクに実装できたと思います。

既存のライブラリとうまく組み合わせる

一番ツラかったのが、この部分ですね。

管理画面にはActive Admin、ユーザー管理にはdeviseというライブラリを用いていたのですが、既存の実装への影響を最小限にするためには、これらのライブラリとOne Login用のライブラリをうまく組み合わせる必要があります。

ライブラリ同士の組み合わせで情報を探そうとすると、そもそも全体数が少なく、情報が古かったりして、トライ&エラーの繰り返し...

根気よく粘りつつ、なんとか実装することができました。

消費元気n倍で試合をし、報酬をn倍にする機能のプロトタイプ作成

次は「ゲーム会社ならではのタスクを行ってみたい!!」と申し出ました。

前回行ったタスク(One Login機能)は、他の業界でも行うことができるものだったからです。


「自分で課題を見つけて、それを解決する機能を実装してみて」と言われたので、ハチナイで利用される元気を数倍にし、報酬をその倍数分増加させる機能をサーバー側で実装してみました。

プロトタイプですので、リリースされるかは未定です

イベント等で同じステージを周回するのに、毎回同じ操作を行うのはUX的な観点でツラいだろうと思い、それを解決する機能となります。

どの要素に対してn倍を適応するのか、逐一メンターの方に相談しつつ、実装を進めました。

ハチナイの中でそこそこコアな部分の実装に触れることができ、非常に楽しかったです。

学べたこと

ゲーム開発(サーバーサイド)の実情

ソーシャルゲーム特有なのかもしれませんが、仕様の微妙な変更が多いだろうなと感じました。

仕様からみたら微妙な変更でも、実装から見ると大きな変更を伴うケースはあるはずです。

実装中、聞いた方が確実に早いときはメンターの方に聞くのですが、現在利用されていないコードもいくつかあるそうで...

そういったところから、ゲーム開発のツラそうな一面を垣間見ることができました。

フラットなチーム

所属するチームや担当するプロダクトによって異なるかもしれませんが、どの方もアグレッシブに働いている印象を受けました。

僕自身、エンジニアを目指していない時から4社ほど長期インターンを経験していますが、このような組織は初めてです。

すごく記憶に残っている一面は、僕のタスクの仕様を決めるために、メンターや他の社員とお話をしている時ですね。雑談ライクに1~2人の方とお話をしていたのですが、次々といろんな方が加わってきて、「もはや会議室とかで行った方が良いのでは...??」と思える人数となってしまいました。

どの方もすごく楽しそうに話されていて、良い人ばかりでした。

飲み会にも参加させていただいたのですが、終始笑いが絶えず。「あ、とてもいいチームなんだな」と思いました。

また、このようなチームを将来作れる人になりたいとも思えました。これは未来の宿題ですね。

感想

就業型の短期インターンシップは初めてで、アルバイトと同じものだと考えていましたが、それにとどまらない体験をさせていただけたと思います。

また、貢献度としてはまだまだ低い結果だと思うので、短期間でも成果を出せるよう、これからも精進していきたいと思います。