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

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

運用フェーズのプロダクトの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人の方とお話をしていたのですが、次々といろんな方が加わってきて、「もはや会議室とかで行った方が良いのでは...??」と思える人数となってしまいました。

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

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

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

感想

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

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

3週間就業型インターンでハチナイのクライアントエンジニアを経験した話

はじめまして!9/2〜9/20の3週間、「Akatsuki Summer Internship 2019」に参加したy-shikaです。 八月のシンデレラナインのプロジェクトでクライアントエンジニアとしてガッツリ働かせていただきながら、アカツキの雰囲気や働き方を感じてきました。 本記事ではどんな経緯でインターンが決まったのかや実際に3週間でやったことなどを書いていきたいと思います!

目次

インターン前

インターン選考

自分がアカツキと初めて繋がりを持ったのは4月中旬にあったジースタイラスさんの「逆求人プレミア@大阪」でした。 自分のブースが割り当てられ、そこに来てくれた企業の方に対してプレゼンをしたり、FBをもらったりといったイベントで1日に沢山の会社と繋がりを持てるのが魅力なのですが、丁度その時アカツキの人事の方とエンジニアの方が僕のブースに来てくれました。 もちろん前から社名は知っていたのですが、具体的な事業までは知らなくて、イベントを通して色々と社風や事業について教えてもらいました。

そしてイベント後に面接を受けさせてもらうことになり、面接終了後に技術テストを受けて無事合格との連絡を頂けました。

事前面談

事前面談はインターン開始の1ヶ月前にリモートで行いました。(自分は地方の大学院に通っているため)

面談では実際にインターンで1ヶ月お世話になるメンターさんと1on1で「どんなことがやりたい?」とか「ハチナイ遊んだことある?」なんかをラフな雰囲気で話す感じでした。 自分はハチナイをそれまで遊んだことがなかったので、インターン開始までにプレイしてやりたいことを見つけてくるといった感じになりました。

インターン中

課題決め

さてインターンが始まりました。 実際に遊んでみての感想や「ここはもっとよく出来そうだな」ってポイントをまとめていたので、それをメンターさんとプランナーさんに見て頂き、実現可能性やチーム内に溜まっているユーザさんからの要望などと照らし合わせてみて、3週間での課題を決めました。

結果として課題は「スカウトUIの改修」に決定しました。 スカウトとは他ゲームでいうガチャですね。簡単に説明するとチケットや石などでスカウトを回せるのですが、2年間運営していく中で種類も多くなり現状のUIでは快適とは言い難くなってきたようです。 ということでUIを見直して改善するといった感じです。 詳しくは後述の「インターン成果発表プレゼン」をご覧ください。

1日の流れ

ある日のスケジュール
10:00 出社
10:00 - 10:05 チーム全体朝会
10:05 - 11:00 作業
11:00 - 11:30 メンターさんと1on1面談
11:30 - 12:30 作業
12:30 - 13:30 話してみたい社員の方とランチ
13:30 - 14:00 人事の方と1on1面談
14:00 - 18:00 作業
18:00 - 18:10 アウトゲームチーム夕会
18:10 - 19:00 作業
19:00 退社

こんな感じでした。

「1on1面談」は「楽しく仕事やれてるか?」とか普段執務室では中々聞きづらい質問などをメンターさんや人事の方に聞ける面談で、3週間を通して合計5回ありました。 エンジニアリング的な部分や、もしアカツキに入社した場合の待遇の話など、かなり突っ込んだ内容をお聞きすることができたのでとてもありがたかったです。

また「話してみたい社員の方とランチ」はインターン生の特権でインターン期間中に毎週1, 2回会社が費用を出してくれて、話を聞いてみたい社員の方とランチに行くことができる制度です。(タダ飯最高) 自分は他者からアカツキに転職してきた方や、R&D部門でAR研究をしている方とのランチを設定して頂きました。 どの社員の方もとても優しくて、楽しくランチをしながら聞きたいことを聞かせて頂きました!

成果発表会

とうとうインターン最終日です。 ここで待ち受けているのが3週間で関わった社員の方やメンターさん、人事の方、同期のインターン生たちが集まる前で行う最終成果発表会です。 とはいっても学会とか研究室発表みたいな重苦しい感じではなくて、どちらかとLT会といった雰囲気で3週間の成果を話そう!といった感じです。

多分つらつら概要を語るよりもプレゼンを見て頂いた方がわかりやすいので、自分が発表したプレゼンを置いておきます。

speakerdeck.com *speakerdeckには代理で登録しています

インターンを振り返って

このブログを執筆しているのはインターン最終日なのですが、いざ振り返ってみると真っ先に出てくるのは「楽しかった」という感情です。(ありきたりですいません笑)

そもそも学生なので週5定時勤務もこのようなインターンで初めて体験するわけで「社会人って大変だなぁ」と痛感しながらも、その一員にあと1年半くらいで仲間入りする予定なわけで若干ツラくもあるんですが、それ以上にアカツキで働くという体験はこれまで経験してきたインターンと違って新鮮で”楽しかった”です。

また長期インターンとして参加したわけですが、モチベーションとしてはエンジニアリング的な部分を学ぶというよりも、アカツキでの働き方や実際に入社したときのことを想像することに重きを置いていました。 結果的にこの3週間でアカツキの良い面や悪い面、チーム内の人間関係なんかも含めて何となく見えてきたかなといった感じで、今後もしも入社した際にはどんな生活が待っていてどんなキャリアの積み方をしていけば良いのかなんかもざっくりわかったと思います。なので本当に有意義な3週間だったと思います!

あとはインターン中の給与もそうなんですが、待遇が他企業と比べてもとても良くて、企業単位でインターン生に投資(つまり未来への投資?)してくれている感じがして、より一層頑張ろう!という気持ちになりました。 (社員の福利厚生制度も一部利用させて頂けて、自分は「書籍購入制度」を使ってお高い技術書を3冊買って頂きました。本当にありがとうございました!)

と、この辺りで筆を置きたいと思います。 今後もアカツキでは毎年インターンが開催されると思うので、少しでも興味のある方は是非!おすすめです!

クライアントエンジニアインターンでUX/UI改善した話


こんにちは、ritoという者です。

この度、クライアントエンジニアとして3週間の就業型インターンシップに参加し、八月のシンデレラナインのUX/UI改善のために尽力させていただきました。

このインターンシップでは、僕が希望したこともあり、既に挙がっている要件を実装するのではなく改善策を提案するところから経験することができました。
本稿では僕が取り組んだ課題を決めるまでの流れと、改善案の実装について書ける範囲で書いていこうと思います。

課題決めと改善案

自分で遊んでみないことには改善案なんか出せません。

僕はハチナイをプレイしたことがなかったので
インターンが始まるまでの2週間強、暇さえあればアプリを起動して遊んでいました。

初心者ながら遊んでいてこのゲームの中で一番面白いと感じたのがデレストでした。

デレスト(シンデレラストーリー)とは
ハチナイにおいてキャラにスキルを覚えさせるためのカードゲームで、
ゲーム内で取得したポイントが所定の値に達したらスキルを習得させられるといったものです。
種々のストーリーにはそれぞれ特殊なギミックがあり、
個人的には各ギミックに応じてデッキの編成や手札を切る戦略を考えるのが、このゲームの醍醐味だと思いました。

しかし、遊んでいて思ったのが

スキルを覚えさせたいキャラが決まっているときに、
そのキャラに合わせてデッキ(オーダー)を編成しデレストを始めるまでの画面遷移が煩雑だ

ということです。

画面遷移が煩雑なあまり、
戦略を練って編成して挑戦し、また戦略を練って編成し…というサイクルを回すのが億劫になってしまいました。

そこで今回のインターンシップではこの画面遷移を簡潔化するという課題を設定しました。

細かい説明は省かせていただいて、図でお見せすると

f:id:ritoaktsk:20190828215903p:plain

デレスト開始までの画面遷移(改修前)

↑これを…

f:id:ritoaktsk:20190828220039p:plain

デレスト開始までの画面遷移(改修後)

↑こうしよう…という提案をしました。

オーダー確認画面を撤廃し画面遷移を簡潔化するのに伴って、オーダー編成画面のデザインや機能にも変更を加えました。
この変更により、ゲーム開始までの必要タップ数、画面遷移数が減少し、オーダーの一覧性が向上します。

f:id:ritoaktsk:20190828220256p:plain

オーダー編成画面の変更点

実装

話し合いを重ねて改善案の仕様を決めるのと並行して、2日ほどかけてじっくりソースコードを読み込みました。

「画面遷移を簡潔化する」

言うは易し、とはまさにこのことだと痛感しました。

画面間のデータの渡し方ひとつ取っても、
同一シーンでのビューの切り替えなのか、はたまたシーンを新たに読み込んでいるのかで
処理は大きく異なりましたし、

デレストへはホーム画面からだけでなくチーム強化からも来られるので、1つの変更が全く別の画面にまで波及することも十分にあり得ました。

とはいえ技術的にものすごくチャレンジングなことをしているわけではなかったので、
最終的にはなんとか形にはできました。
技術的な挑戦があったというよりかは、可読性・保守性に配慮したコーディングができているかといったエンジニアとしての基礎力を磨くことができたと思います。
エンジニアとしてまだペーペーの僕には非常にありがたい経験でした。

インターンを終えた感想

今回のインターンシップでは
ひとりのユーザとしての感想や仕様書、ToDoリストの内容を踏まえて自分で改善案を考え、プレゼンし、仕様を詰め設計して実装するという一連の流れを経験することができ、
大変有意義な時間を過ごせたと思います。

特に改善案のプレゼンでは好評をいただき、自分で取り組むことになった案以外にも多数、今後やるべきタスクとして採用していただけました。
大学での研究で培った経験が活きたのを実感することができ嬉しかったのを覚えています。

ただやはり上手くいかなかったことも多くあり、
中でも一番の反省点は全体的にあまりにも消極的あるいは受け身だったことです。

周りの社員さん方が忙しそうなのであれば、積極的に自分なりにできることを探したり、インターン中にはあまり関わることがない別部署の見学をさせていただいたり、色々とやりようはあったと思います。

そんな引っ込み思案な僕に対しても皆さん快く接してくださりましたし、
開発チームのメンバーで行ったお食事や1on1ミーティングなどでは、
趣味や雑談、ゲーム業界や技術の話はもちろんのこと、
僕の就職や将来についての悩み・相談事にも親身に付き合ってくださりました。
おかげで朧げだったやりたいこと・これからすべきことがはっきりしてきました。

初めてのインターンで、始まる前は不安もありましたが、ここに来れてよかったです。
お世話になったメンターさんをはじめ、サポートしていただいた開発チームの皆さんや、人事・総務の皆さんに、この場をお借りして改めてお礼申し上げます。

3週間ありがとうございました。