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

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

アカツキのサーバーサイドインターンに参加しました

初めまして、2021年の9/6 ~ 9/24の3週間、アカツキのサーバーサイドインターンに参加させていただきました、竹下といいます。この記事では、私が取りくんだこと、学んだことについて書かせていただきたいと思います。

自己紹介

慶應義塾大学 総合政策学部の学部3年生で、普段は個人や仲間内、アルバイトなどでWebサービス開発を行っています。使う言語などに強いこだわりがあるわけではないですが、Rustで開発をすることがひそかなマイブームです。 過去には趣味でオレオレWAFを実装したりしていました。

今回取り組んだこと

今回のインターンで私が配属させていただいたプロジェクトは、Ruby on Railsによるゲームのサーバーチームでした。その中で、アップデート後に処理が重くなってしまったエンドポイントについて、速度改善をするというタスクにチャレンジしました。

エンドポイントの役割としては、スマホゲームではおなじみの「ミッション機能(一定の条件をクリアすると報酬が貰える)」の一部です。デイリーミッションやコンプリート系のミッションなどといった、いつのまにか達成しているようなミッションを含めてミッション進捗を確認するために叩かれます。

続きを読む

Akatsuki Summer Internship 2021 の Go/GCP コースで運用改善を行いました

はじめに

はじめまして、otofune と申します。

9/6 から 9/24 の3週間、株式会社アカツキでプラットフォームエンジニアリングを行うチームでのインターンシップに参加させて頂きました。

aktsk.jp

といっても祝日が2日あり、新型コロナワクチンの摂取が被って副反応で1日休み、さらに社内イベントもあり、実質2週間でした。

その中で、私は GAE/Go 上で動作するサービスについて、運用に関わる以下の成果を出すことができました。

  1. API レスポンスのリファクタリング
  2. 蓄積されたアクセスログを解析し利用状況を可視化

この記事では関わったサービスについて軽く説明したあと、行った改善について紹介し、終わりに全体を通した感想を書きます。

続きを読む

株式会社アカツキのインターンでお世話になった話

こんにちは。 東京工科大学のコンピュータサイエンス学部というところの3年生をやっております(就職するなら23卒ということになります)。 宮川です。

この度、アカツキのインターンを受けましたのでその記録を残します。 ブログ執筆経験が浅くつたない文章になると思いますがご容赦下さい。

  • エントリーと選考
  • やったこと
    • 練習問題
    • クライアント改善課題
    • 新規機能実装
  • 何を得たか
    • コミュニケーションスキル
    • 作業を管理するスキル
    • 目的を意識するということ
  • 感想

エントリーと選考

アカツキのインターンを知ったのは就活サイトがきっかけです。

プログラミングのインターンを受けたい。でも1day仕事体験とかではなく数週間にわたってやりたい。あとお給料出ると嬉しい。そんな欲張りな条件で探した結果アカツキのインターンに出会いました。 ゲーム開発のインターンということで、私はゲーム開発に興味があるので良さげと思いました。

でもUnityなんだろうな...。私はゲーム制作に興味はあるもののUnityはあまり得意ではないのです。 ですが募集要項を見てみると..... Cocos2d-x & C++!? 私はそれに一目惚れして、アカツキのインターン選考が非常に厳しいらしいことも知らず、募集人数1人程度の「ヤバいインターン」にダメ元でエントリーしました。

────そして何故か選考に合格してしまったのです。(めちゃくちゃ嬉しかったです。)

続きを読む

初めてのRuby on Railsでユーザ数が大規模なゲームのAPIを高速化した話

自己紹介

はじめまして、大阪工業大学 情報科学部  情報知能学科  3年 伊地知翔也と申します。  
普段は、チームや個人でWEBアプリ開発をしたり画像処理についての研究をしています。  
このサマーインターンに参加する前までは、
私のサーバサイドで経験のある言語は、

  • Golang
  • Python(Flask)

しかありませんでした。
しかしながら、ゲームのサーバサイドに携わってみたいという想いから経験のないRuby on Railsを使用する本インターンを志望しました。

 

事前準備

事前の準備として、Ruby on Railsのチュートリアルを概ね完走しました。

といっても、他の言語でWEB開発の経験あったので、この機能を実現するにはこのメソッドを使えば良いなど、飛ばし飛ばし大まかに学び、準備していました。

続きを読む

ユーザー数が大規模なゲームで負荷試験と改善をした話

はじめまして、7月5日から7月30日までアカツキのサーバーサイドでインターンをさせていただいた光枝と申します。 本記事では私が今回のインターンで取り組んだことについて共有させていただきます。

自己紹介

神戸大学理学部の学部3年生です。 普段は主にWebアプリケーションの開発をやっていて、サーバーは Ruby on Rails、フロントは React/Next (TypeScript) を書くことが多いです。 最近は Go や Kubernetes の勉強などもしています。

インターンの題材

今回のインターンで取り組んだ題材は「負荷試験」です。

私が携わらせていただいたサービスはユーザー数が非常に多いゲームでイベントや新しいガチャが実装された際にはサーバーにかなりの負荷がかかります。

そんな中で当ゲームはメジャーバージョンアップを控えています。 その際に

  • 今までの機能が重くなっていないか
  • 新機能が重くないか

を検証する必要があり、そのタスクにアサインいただきました。

検証方法

検証にはAWS上に構築した本番環境の数十分の1スケール(以後stress環境と呼びます)を使用しました。 手順は以下の通りです。

1. シナリオの準備

負荷試験には LOCUST という負荷試験ツールを使用しました。 LOCUST は locustfile.py というファイルにシナリオを書くことで、その通りに負荷をかけてくれるツールです。 (他にもアクセスするユーザー数や Hatch rate を設定し、結果を確認するためのGUIを提供しています。)

シナリオは例えば

ログイン → お知らせ確認 → ログインボーナス取得 → ...

のようにユーザーの動きを再現するように、各APIを順番に叩く処理を記述します。

バージョンアップに際して今回着目した新APIを叩く処理をシナリオに追記しました。 (ここではそのエンドポイントを POST /hoge として置きます。)

2. ローカル環境での実行

まず新バージョンがすでに動いている環境のデータベースをコピーしてきて、LOCUST をローカルで実行します。 ここで失敗し続けているAPIがないか確認します。

(例えば特定のAPIが失敗していた場合、そこで処理が止まってしまい想定していた負荷をかけることができなくなったり、その後に続く処理が実行されなくなってしまうのでここで確認する必要があります。)

3. AWS の stress環境に反映して実行

手元で確認が取れたら実際にstress環境で実行します。 stress環境にかかる負荷は

インフラの負荷 → Amazon CloudWatch

アプリケーションの負荷 → New Relic, LOCUST

で確認します。

実行結果

上記の 1 ~ 3 で負荷試験を実施してデータをみたところ、新APIに問題が見つかりました。

平均レスポンスタイム (ms)
POST /hoge 2090
1リクエストあたりのSQL実行回数
ActiveRecord Model2 find 396
ActiveRecord Model3 find 396
ActiveRecord Model4 find 18.3

これは N + 1 問題が起きていると考えられます。 なのでこれを修正して再度負荷試験を行い、どの程度改善されたかを計測します。

実は負荷試験を実施するまでにもいくつか苦労したところ (e.g. 「外部API のモック。これをやらないと外部サービスに対して負荷をかけてしまう」) がありましたが、今回は負荷試験の結果と改善に焦点を当ててお話させていただきます。

発生している問題

まず 「N + 1 問題とは何か」を簡単に説明します。

例えばユーザー(User)というモデルがあり、ユーザーがユーザーHoge (UserHoge)というモデルと関連があるとします。 また1ユーザーにつき、通常複数の UserHoge が関連しているとすると以下の図のような関係になります。

f:id:itsuki_mi:20210730180407p:plain

これらのデータが全て必要な場合にUserHoge が N 個あるとすると、このデータを取得するためにデータベースに N + 1 回 (1回は最初に User を取ってくるため) 問い合わせてしまうことを N + 1 問題と呼びます。

これらは本来、工夫することで 2 回の問い合わせですべてのデータを取得することができます。

今回の POST /hoge の問題も N + 1 問題だと予想されます。

調査

まず実際にローカル環境で API を実行して発生したクエリを再現し、該当コード部を読みました。 その結果以下のような構造になっていることが判明しました。

f:id:itsuki_mi:20210730181502p:plain

User が複数の Model1 と関連していて、Model1 は複数の Model2 と Model3 と関連があります。Model3 は 1 対 1 で Model4 と関連があります。 またこのサービスはデータベースを水平分散しており、「User, Model1, Model2, Model3」は同一のデータベース(Shard1)にありますが、Model4 だけは別のデータベース(Shard2)に保存されています。

これらのデータをすべて取得してくる必要があったため対策がない場合は

  • 1 ユーザーあたり N1 個の Model1 と関連
  • Model1 は N2 個の Model2・N3 個の Model3 と関連
  • Model3 は N4 個の Model4 と関連

としたとき合計で

N1 × (N2 + N3 + N4) 回

のSQLクエリを発行することになります。(ただし N1 個の Model1 はすでに取得済みとします。)

これが原因で POST /hoge は非常に重くなっていました。

修正に当たっての問題 その1

本サービスのサーバーサイドは Ruby on Rails で書かれています。 通常の N + 1 問題であれば includes メソッドを使うなどで解決できますが、今回の Model4 はシャードが異なるためワンライナーで取ってくることはできません。 Model3 から関連する Model4 を取る際にシャードを変更する必要があります。

この部分は以下のようにして解決しました。

Octoball.using(shard1) do
  ActiveRecord::Associations::Preloader.new.preload(model1s, [:model2s, :model3s])
end

model3s = model1s.flat_map(&:model3s)

Octoball.using(shard2) do
  ActiveRecord::Associations::Preloader.new.preload(model3s, :model4)
end

まず Octoball の部分でシャードを切り替えています。 以下のように記述することで #block の部分を指定の shard に接続した状態で実行することができます。

Octoball.using(shard) do
  # block
end

そこでまず Model2, Model3 まで shard1 に接続した状態で preload (ActiveRecord::Associations::Preloader.new.preload(model1s, [:model2s, :model3s])) しておきます。 そして preload した model3s だけ変数に参照渡しして、shard2 に切り替えてから残りの Model4 も ActiveRecord::Associations::Preloader.new.preload(model3s, :model4) で読みます。

このように記述することでシャードを跨いだ関連に対しても N + 1 を回避させることができます。

修正に当たっての問題 その2

全データの取得はこれで問題ありませんでしたが、それでも model4 だけ N + 1 問題が解決しませんでした。 理由としては、その後の model4 を使用する箇所で model3 のアソシエーションを使用していなかったためです。 実装は model4_id を用いて model4find_by するコードになっていました。

class Model3 < ApplicationRecord
  belongs_to :model4
.
.
.

  def model4
    unless defined? @model4
      @model4 = 
        if model4_id
          Model4.using(shard2).find_by(id: model4_id)
        end
    end
  end
end

なお model4 の関連付けがあるのに model4 メソッドが用意されているのは、毎回シャードを指定しなくてもよくするためです。 (また shard2 は動的に決まっています。shard2 は master/slave 構造を取っており、環境変数のAZを見て最適な slave を返すようになっています。)

上記の記述だとアソーシエーションを利用せずに model4 を取得するため preload したデータを使いません。 そこで以下のように条件を書き加えました。

class Model3 < ApplicationRecord
  belongs_to :model4
.
.
.

  def model4
    unless defined? @model4
      @model4 = 
        if model4_id
          if association(:model4).loaded?
            association(:model4).target
          else
            Model4.using(shard2).find_by(id: model4_id)
          end
        end
    end
  end
end

association(:model4).loaded? ですでに読み込まれているか確認して、true だった場合はその中身 association(:model4).target を返すようにしました。

改善後の結果

上記の改善を入れて、再度負荷試験を実施したところ以下のようになりました。

改善前の平均レスポンスタイム (ms) 改善後の平均レスポンスタイム (ms)
POST /hoge 2090 296
改善前の1リクエストあたりのSQL実行回数 改善後の1リクエストあたりのSQL実行回数
ActiveRecord Model2 find 396 1
ActiveRecord Model3 find 396 1
ActiveRecord Model4 find 18.3 1

完全に N + 1 が消えて、各 Model がそれぞれ1クエリで取れるようになったのがわかります。 また POST /hoge 自体もかなり高速化されました。

インターンでの学び & 感想

インターンに参加する前は負荷試験について言葉だけ知っている状態でした。

しかし実際に

負荷試験の準備(シナリオ作成、APIの失敗がないかのチェック及び修正、ローカル実行で確認)

stress環境で負荷試験実施

CloudWatch, New Relic, LOCUST でメトリクスなどを確認

問題があった場合は改善

再度負荷試験を実施して改善効果を検証

という一連の流れを体感することができました。

またシャードを跨いだ関連付けの取得やActiveRecord 内部の仕様についても理解を深めることができました。

実は事前面談で「インフラ系のタスクをやってみたい」とお話していてこのタスクを用意していただきました。

なので私がやりたいと言ったことをやらせていただき、また手厚いサポートのもとたくさんのことを学ばせていただきました。

約3週間本当にお世話になりました。ありがとうございました!!!