Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

24年新卒から次の新卒に伝えたい、ミラティブに感じた技術的な面白さ

前書き

初めまして。山倉拓也と申します。

私は2022年9月26日から現在(2023年3月)まで、株式会社ミラティブの基盤開発部でインターンをさせていただいており、2024年度に正社員として入社する予定です。この記事では、私が具体的にミラティブで今までにどんな仕事をしたのか、どんなところに技術的な魅力を感じて入社を決意したのかをお伝えします。

目次

私が担当した仕事についてのみ述べても分かりにくいので、簡単に現在のミラティブの技術的な前提について紹介していきます。

ミラティブを支えるバックエンド&インフラの技術的な話

インフラに、インメモリデータベースであるRedisを内製化して使っている

この記事にある通り、ミラティブではRedisを内製化したRadishaというNoSQLサーバーを使っています。RadishaはRedisが元々持つコマンドに加えいくつかの他のコマンドも使えて、ランキング集計機能や一定時間だけデータをためておく機能をもちます。

特にランキング機能について、おおよそ以下のような仕様を持っています:

  • RANKSET rankingA 100 user1 のように命令するとrankingAにuser1の得点を100に設定できる
  • ランキング内部のデータ構造的に、データは常に得点によってソートされている状態となっている
  • RANKGET rankingA user1 のように命令するとrankingAからuser1の順位や得点、Updateされた時刻を取ってくる
  • RANKRANGE rankingA 0 4のように命令するとrankingAの1位から5位までのユーザーの名前や得点、Updateされた時刻をとってくる
  • 以上の操作の計算量について挿入と一点取得がO(log N)、範囲取得がO(M + log N) (ただしNはランキングの長さ、Mは取得範囲の長さ)と、高速に行うことができる

Radishaの構成は次のようになっています。いくつかのノードのうち、1つがLeader、その他がFollowerとして機能し、書き込みがLeaderに行われ、それがFollowerにレプリケーションされます。読み込みは基本的にRouterと呼ばれるサーバー群から行い、何も指定しなければラウンドロビン方式で選ばれたFollowerのうちの一つからしますが、強制的にLeaderから読み込ませることもできます。これはレプリケーション遅延が許されないときに有用です。ちなみにLeaderの選出はRaftというアルゴリズムによって行われます。

https://tech.mirrativ.stream/entry/mirrativ-raft-redis-server より

Go/クリーンアーキテクチャ、に現在移行している

こちらの記事にもあるように、ミラティブのサーバーサイドでは現在Go言語とクリーンアーキテクチャという構成に移行中です。もともとPerl/MVC+Serviceで書かれていたのですが、様々な技術的負債が溜まってきてしまったので別の言語/アーキテクチャに移行する必要が出てきました。

そこで、Perl/MVC+ServiceからPerl/クリーンアーキテクチャへの移行と並行してGo/クリーンアーキテクチャへの移行を進め、その結果2023年3月現時点ではかなり多くのPerlのコードがGoに移譲され、Perl/MVC+Serviceのコードの多くはフリーズすることができました。しかし、それでもPerlで書かれている部分は少なくはないですし、いまだ機能改修などのためにPerl/クリーンアーキテクチャ(一部はPerl/MVC+Service)で書かざるを得ないこともあります。

現状の構成

もう少しアーキテクチャの詳細を見ていきましょう。データの集約や永続化の責務を負うRepository層に、複数のInfra, Datasourceへのトランザクションを管理する部分があります。ただし、このトランザクションは現状MySQLのみに適用することができ、MySQL+RadishaなどRollbackを実装していないミドルウェアをまたいだトランザクションはサポートされていません。しかしRepositoryで1集約単位でUpdateしたいときはそのようなことをしたくなるときもあるかもしれません…そこで、代わりにすべてのMySQLのトランザクションが成功した場合にのみ処理を行うSucceeded()関数にRadishaの書き込みを登録することができます。これに登録された関数のErrorは無視されるので、厳密な整合性を別の機構によって担保する必要があります。これについてはまた後述します。

Succeeded関数の概要

以上に挙げたミラティブの二つの記事は技術的にとても面白いので、お時間があればぜひ読んでみてください。僕自身もまだ完全に理解したとは言い難いですが、学べることが非常に多かったです。ちなみに、本記事の本筋とは若干外れますが「テスト戦略」のカバレッジチェックが個人的な推しポイントです。自動テストで人的なミスを防ぐのは大事!

いままでミラティブでした仕事

自分が担当したタスクの中で、特に学びの大きかった二つのタスクを紹介します。

エモモランあーるぴーじーで、イベントが切り替わるタイミングでライフを自動回復する機能の実装

背景・概要

ライブゲームのひとつであるエモモランあーるぴーじーにおいて、イベントが切り替わるタイミングは午前11時ですが、ライフは夜中の0時に全回復します。すると、イベントが切り替わったタイミングでもライフを既に消費しきっている人はゲームを遊べず、UXの低下を招いてしまいます。そこで、イベントが切り替わったタイミングで全ユーザーのライフを回復させよう、というのが私に与えられたタスクでした。

ライフ切れの状態

学んだこと

1. メンテナンス期間をいれない、シームレスなライフ管理のバージョン移行

ミラティブはライブ配信アプリであるという特性上、メンテナンス期間を入れてしまうとその分機会損失がどうしても発生してしまいます。従って、このタスクではシームレスにライフ管理の仕方を移行する必要があるのですが、これは次のようなやり方で実現することができました。

まず、現行のライフ管理を担当しているMySQLのテーブル(テーブルv1と呼称します)と、移行先であるライフ管理を担当するテーブル(テーブルv2と呼称します)の大まかな構造は以下通りです:

ライフ管理をするテーブル構造

ユーザーIDとイベントIDを複合キーにすることでイベントごとにライフ管理するようにすれば、イベント切り替え時にテーブルv2をupdateする必要がなくなり不整合や不具合が発生する確率が減ります。ところで、このテーブルを切り替えるタイミングはいつでしょうか?答えはある基準となるイベントの切り替え時で、その時からリポジトリで参照させるテーブル先を変えればよいです:

ライフ管理の切り替え

これで、基準となるイベントに切り替わるタイミングの午前11時から参照するテーブルが切り替わるので、シームレスなライフ管理の移行が達成できました。

2. ミラティブ独自のパーティショニング機能

 先ほどのRadishaの記事でも少し言及していましたが、ライフ機能は日時データであり一定期間をすぎると不要になるデータです。よって、ディスク容量を確保するため必要ないデータから削除していく必要があるのですが、ミラティブでは日付ごとに別々のテーブルを用意して、いらなくなったテーブルを消していくという方針をとっています。

データベースの仕組みはこうなっているのに対して、バックエンド側ではその仕組みが隠蔽されるようになっています。つまり、バックエンドのDataSourceでどの日付のテーブルに接続するかわざわざ手作業で書かずとも、以下のようなテーブル定義を書けばパーティショニングされたテーブルに接続するメソッドを自動生成してくれるのです。便利!

テーブル定義

table_v2:
...
 primary_keys:
   - ユーザーID
   - イベントID
 partitioning:
   type: suffix
   valid_period: [-7, 3]
...

自動生成されるDatasourceのメソッド

func (ds *ds) insert(
   ctx context.Context,
   suffix string,
   rows []*dsmysql.TableV2Row,
) (map[uint8]sql.Result, error) {
   ...
       res, err := txn.ExecContext(ctx, "insert into table_v2_"+suffix+" (`ユーザーID`,`イベントID`,`残りライフ数`) values "+strings.Join(valuesPlaceholders, ","), args...)
   ...
}

実際に本番環境にデプロイして

初めてのユーザーに見える機能の実装だったので、これが本番環境にデプロイされた時は緊張と嬉しさの両方がありました。デプロイされた直後はtwitterで実際に喜んでいるユーザーもいらっしゃって、純粋に嬉しかったです。順調に機能していたように見えましたが、しかし実はバグが潜んでいたのでした…

それはPerl側の実装に起因するバグで、Perl側のテーブルv1を参照していた部分をテーブルv2に切り替える処理を書き換え忘れていたために起きました。この時はメンターさんや様々な社員さんたちが迅速な対応をしてくださったおかげで被害が最小限に抑えられましたが、自分が書いたコードが実際に世に出回ってバグを起こしたことはショックでした…。

GitHubのPRのテンプレートに「Perl実装は考慮しましたか?」というタスクがあったのにも関わらず、動いているからまあ大丈夫だろうと思っていたために、このような事態に繋がってしまいました。この経験から今では網羅的に変更の影響を追跡をするにはどうしたよいだろうか?ということを考えるようになりました。

エモモランあーるぴーじーで、シーズンランキングをMySQL + MemcachedからMySQL + Radishaに移行する

背景・概要

エモモランあーるぴーじーにおいて、ランキング期間中にゲームをプレイすると自分が所属しているグループでのハイスコア順位が出ます。

ランキングの表示

これをシーズンランキングと言いますが、これに対するデータストアがMySQL + Memcachedという構成でした。しかし、Memcachedにはユーザーが所属するグループ全体のデータをバイナリ形式でキャッシュしていて、ある1ユーザーの順位を取得するためにはグループのデータを丸ごと引っ張ってきてソートする必要があったため、通信量や計算量が大きく無駄になっていました。そこで、Memcachedを先ほど述べた内製のミドルウェアであるRadishaに移行しよう、というのがこのタスクの概要です。

現行のデータストアの構成

今までの実務経験の中で、一番難しかったかもしれないです。

最終的な構成

最終的な構成の落とし所は次の図のようになりました:

最終的な構成

このような構成となった経緯を説明します。

すこし事情がややこしいのですが、MySQLにはシーズンランキング以外のシーズン記録も保存しています。なので完全にMySQLを無くすということはできず、ランキングをUpsert/SetするときはRadishaとDouble Writeをしています(1)。しかし先程も述べたとおり、1集約単位でRepositoryでUpdateするとなるとミドルウェアを跨いでトランザクションを張れないので、Succeededメソッドを使うことになります。つまり、

  • MySQLのUpsertについてはトランザクションを張る
  • そのCommitが上手くいった場合はRadishaにSetをする
  • もしRadishaからエラーが返ってきても無視される

という流れです。 エラーが無視されてしまうので、もしかしたら不整合が発生してしまうかもしれません。そこで15分に一度RadishaのデータをMySQLのデータにSyncさせることにしました(3)。最後に、ランキングデータが欲しい時はRadishaから必要な分だけReadすれば、既に順位付き/ソート済みのデータが返ってくる(2)というわけです。

ここからは(1), (2), (3)のそれぞれについて詰まった点、工夫した点について簡単に述べていきます。

詰まった点、工夫した点

(1) ハイスコアをMySQLとRadisha同時にWrite

まず上記に上げたSucceededメソッドによるフックの工夫が一つ挙げられます。それから、Banされたユーザーの扱いに対して、MySQLではBanのフラグをUpdateするのに対して、RadishaではランキングからDeleteする必要があり、その扱いの差に苦慮しました。なるべく不整合が出ないコードを組むよう気をつけました。

(2) ハイスコアのRead

ゲームが終了してシーズンランキングにWriteした直後そのユーザーをReadしようとすると見つからず、以下のように0位が出てしまいました。

0位…!?

これは、レプリケーション遅延が原因でした。先ほど述べた通りRadishaはLeaderノードに書き込まれ、デフォルトではFollowerノードから読み取ります。LeaderからFollowerにレプリケーションされる時間差を考慮しなかったため、このようなバグが発生してしまいました。このケースでは強制的にLeaderノードから読み取らせることで、この問題を解決できました。

(3) バッチ処理で15分に一度RadishaをMySQLにSync

15分に1回処理を行うので、なるべくRadishaに対するSet/Deleteが少なく済むようにする必要があります。そこで、MySQLのランキングデータを全てRadishaにSetするのではなく、それらの差分データだけSet/Deleteすることで負荷を最小限に抑えるようにしました。 また、ここではRadishaの方のみUpdateするようなRepositoryが必要なわけですが、これは1集約単位の更新の原則に反してしまうため、リポジトリ名を「UpdateToSync…」とすることであくまでSync専用だよ!と伝えることにしました。

ミドルウェアに触れることができた!

このタスクに取り組むにあたってRadishaの機能が若干足りないということがありました。

それは “RANKGET rankingA user3”とRadishaに問い合わせたとき、rankingAはあったけどuser3はそこにいなかった時、”ERR ranking_group not found”と返ってしまうことでした(本当はuser not found in the ranking groupのようなメッセージを返してほしい)。

機能改修前のRadisha

これをインフラチームの方に相談したところ、なんと「山倉くんが改修してもいいよ」と提案していただきました!

私は今までバックエンドしかやってこなかったのですが、このような低レイヤーな分野にも興味があったので、喜んで実装しました。機能改修はあまり大きいものではなかったですが、それでも自分がやったことのない分野に足を踏み込めたのは大きな経験だと思っています。

機能改修後のRadisha

このタスクでどういう力が身についたか

このタスクは、Infraの構成を差し替えたときに如何にUsecases/Interactorの外から見た動きを不変にできるか、そしてできればRepositoryまでで変更を吸収できるか、というクリーンアーキテクチャのお手本のような課題でした。もちろんそれは理想的な話でInteractorの関数内部まで変更が及ぶこともあったのですが、それでもコアとなるロジックの変更を最小限にするよう努めました。

そして、MySQLやMemcache、Radishaの元となったRedisの特性を知ることができました。それに基づいてどのようにインフラを構成すると信頼性が高くなり、そして効率が良くなるかなども検討する力がついたと思います。わずかですがミドルウェアに触れることもできて、この一ヶ月半で大きく成長することができました。

私が感じたミラティブの技術的な魅力、課題

魅力

私が感じたミラティブの技術的な魅力はいくつもあって悩んだのですが、ここでは3点ほど紹介します。

1. Goやクリーンアーキテクチャといったモダンな技術を使った開発ができる

前述した通り、ミラティブのサーバーサイドでは主にGo言語を使っていますが、これによってコードの品質が均一に保たれやすかったり、エディタ支援が手厚かったり、静的解析がしやすいので、ストレスフリーに開発を進めることができると感じています。また、クリーンアーキテクチャをベースとした構成を採用していることで、例えばData Storeの変更にも強く、実際にそのようなタスクを経験できました。

2. 大規模な開発を体感できる

あまり規模の大きくない開発だと、そもそもそのData Storeの変更の必要性があまりないことも多く、クリーンアーキテクチャがただ煩雑としか思えないことがあります。タスクをこなしているうちにクリーンアーキテクチャの威力を実感できたのは、規模の大きさゆえなのかなと思います。それから、パフォーマンス向上やUX向上のために、Radishaなど独自のミドルウェアを開発したり、その他にもインフラでは動画配信サービスならではの独自の工夫を凝らしていることも多く()、このようにインフラレイヤーの開発も活発で面白いです。

3. 意欲があれば新しい分野に挑戦できる

ミラティブではエンジニアの方々がGoogle Meet上で定期的に勉強会を開催しており、興味があればインターン生でも参加することができます。私はRedisを使ったことがないのに(!)Radishaの勉強会に参加して、前述した通りRadishaに関するタスクも任せてもらえました。いままでバックエンドしか実務経験のなかった私も、わずかではありますがミドルウェアのRadishaを書かせていただいたのは非常に貴重な経験でした。

課題

一方で、私が感じたミラティブの技術的な課題についても二点ほど述べさせていただきます。

1. どんどんGo移行を進めているが、Perlで書かれたor書かれる部分もまだまだ少なくなく、過渡期なので構成が複雑

どうしてもPerlのような比較的古い技術が残ってしまっているので、全面的にフリーズするまでうまく付き合い続けるしかないとは思っています。ただ、個人的にはそこも面白い部分で、魅力でもあるとも感じています。複雑な問題にこそ技術的に成長するチャンスがあるとも捉えられるので!

そもそもリファクタリングは資本となる古いコードが存在していて、開発者がそれを刷新しようという意志を持っていないとできず、いまこのような貴重な経験ができる環境にいることはありがたいともいえます。いま持て囃されている言語やアーキテクチャがいつまでも続くとは限らないので、このような経験は後々必ず生きてくると思っています。

2. ドキュメンテーションされていない部分が多い

ドキュメンテーションがあまりされていないと、ドメイン知識をつけるのに時間がかかってしまうor説明してもらうために時間をかけさせてしまうことがあり、更には既にやめてしまった人しか知らないことが稀にあり困る、なんてケースもありえます…急成長中で仕様がしばしば変わる忙しいサービスでは仕方ないことなのかもしれないですが、だからこそ、自分は自分が書いたコードのドキュメンテーションを大切にしていきたいと考えています。このように様々な考え方の人を包容する土壌も魅力のひとつと言えるかもしれないです。

最後に

ここまで読んでいただき本当にありがとうございました。そして、私を半年間フォローしていただいたメンターさん、並びに他の社員さんの方々に感謝いたします。まだまだ未熟者ですが、何卒これからもよろしくお願いいたします。

いまこれを読んでミラティブを受けてみようかな?と思っている方が一人でもいたならば幸いです。

We are hiring

ミラティブでは新卒およびインターンを募集しています!

興味を持った方、是非エントリーお待ちしています。

www.mirrativ.co.jp

speakerdeck.com

mirrativ.notion.site