はじめに
2024年1月11日から1月31日までJOBインターンとしてABEMAバックエンド開発に携わりました。@mewutoといいます。よろしくお願いします。
この記事では、ABEMAバックエンドのDB移行プロジェクトと開発合宿(1day)を紹介します。
本題の前に
ABEMAバックエンドの体制が色々すごかったので、本題の前に、まずチーム構成とオンボーディングに焦点を当てて紹介します。
ABEMAバックエンドは1人のマネージャーと4つのユニット(1ユニット約5人)で構成されています。ユニットのリーダーはいないので、ユニット毎で意思決定をします。ユニット毎の担当ドメインもないため、タスクの責務領域がかなり広く、大きいプロジェクトを何個も同時並行でやっていました。また、そのうち1ユニットは機械学習(ML)チームとなっており、検索や推薦体験の向上を目指しています。しかし、決して分断されているわけではなく、他ユニットのメンバーがMLチームに混ざることもあり、バックエンドエンジニアがMLを積極的に利用しています。バックエンド×MLをやっているのはとても新鮮に感じました(おそらく業界的にも珍しい)。
また、オンボーディング周りも、ドキュメント等が適切に整備されており、入社2日目でFlaky Testを修正したプルリクエストを投げるスピード感でした。もちろんコミット規約系のドキュメントもあったので、安心して開発できました。
マスターデータ移行プロジェクト
本題です。まず、課題の説明のために非リレーショナルDBの特性を簡単に説明します。
非リレーショナルDB
非リレーショナルDBは、コレクションという単位でデータを格納しています。ベストプラクティスな利用としては、関連度が高いデータでひとまとまりのコレクションにすること(例えば、ユーザーコレクション、動画コレクションなど)です。
課題
ABEMAの現状では、マスターデータのコレクションが過度に正規化されているため、副次的なものも合わせて、以下の問題が発生しています。
- toCで必要なデータを取得する際に複数のコレクションから取得する必要があるため、APIのレイテンシーが悪化している
- toC で必要なデータはGoogle Cloud Storage で管理しているため、各マイクロサービスでそれを取り込む実装が必要になっている
- どのコレクションに何のデータがあるのか分かりづらい
そのため、複数のコレクションを見に行くAPIへのアクセスを極力抑えるために、Redisを使ったキャッシングを行うことや、都度Google Cloud Storageを使ったデータ取得の実装をしなければならず、実装の複雑度が上がっていました。
したがって、私がジョインしたユニットは、このAPIに関係するデータをAlloyDBへ移行する対応を行っていました。
AlloyDB
今回解決したい本質的課題は正規化されたコレクションの複雑な構造なので、RDBMSを使うことで自然に課題を解決できますが、数あるRDBMSの中でAlloyDBを選定した理由を以下に述べます。
なぜAlloyDB?
AlloyDBはPostgreSQL(エンタープライズ規格のOSS DB)の互換DBサービスなので、要求の厳しいワークロードに対応可能なことや各種プラグイン機能が充実しています。また、超高速キャッシュなどの利点があります(参考: https://cloud.google.com/alloydb?hl=ja )。
特に、私は1番の特徴はAlloyDB AIだと思います。VertexAIと統合することで、エンベッディングされたデータを利用して構築されたモデルを通じて、自然言語でのクエリ処理が可能になる点は大変興味深いです(参考:https://cloud.google.com/alloydb/docs/ai/configure-vertex-ai )。
前述の通り、ABEMAではバックエンド×MLの体制を取っているため、かなり相性が良いDBと言えます。
実装
インターンでは、AlloyDBへのデータ移行に関係するAPIの実装を行いました。実装設計は以下の通りです。
- 既存のロジックと新しいロジックをインフラ層で素早く切り替えるためにFeature Flagを用いる
- DB書き込みはダブルライトを採用しているため、読み取りの際にAlloyDBからデータを取得できなかった場合はfallbackし、マスターデータからデータを取得する
- AlloyDBの利点によりRedisやGoogle Cloud Storageからの取得ロジックが不要になる
ロジックだけですが、以下のように実装しました。
go var res *Response var ok bool if featureFlag { var err error res, err = GetFromAlloyDB() if err == nil { ok = true } } else { res = GetFromCache() } // fallback if !ok { res = GetFromMaster() } return res
実装例からは省きましたが、データ構造も変更したのでデータマッピングなども行いました。DB周辺ということもあり、新旧のコードが乱立していました。これのテストコードをいくつか追加する際に、ドメイン知識不足で難しいところもありましたが、新卒1年目の方にサポート頂きながら数箇所リリースできました。
開発合宿
ABEMAバックエンドでは月1で開発合宿を行います。重要度は高いが、緊急度が低い課題の対処をします。私がアサインされたのはLinter 指摘解消です。(過去のインターン生も似たようなことをやっていました)
ABEMAバックエンドではLinterとしてgolangci-lintを使っています。以下のコードは本番でも使っているものです。理由付きで書いてくれており、最高の開発者体験でした。
yaml linters: enable-all: true Disable: - cyclop # https://github.com/bkielbasa/cyclop Use gocyclo - deadcode # deprecated - depguard # https://github. com/OpenPeeDeeP/depguard - dupl # https: //github.com/golangci/dupl - exhaustivestruct # https://github.com/mbilski/exhaustivestruct - exhaustruct # https: //github.com/GaijinEntertainment/go-exhaustruct - funlen - gci # https://github.com/daixiang0/gci - godot # https://github.com/tetafro/godot - godox # https://github. com/766b/godox - golint # deprecated
Pull Request(PR)更新のCI時にreviewdogを利用してリンターチェックをしており、PRの変更箇所のみをチェック対象としています。あまり更新しない昔のコードが負債となってしまうところを、開発合宿で回収します。
具体的な作業は、以下のようにローカルのターミナルで自分のスコープに対して、lintをかけて、ベストプラクティスな実装にしていくということをしました。
shell-session $ golangci-lint run thelper hogehoge $ golangci-lint run // 何も無くなったらOK $
この開発合宿はベストプラクティスな実装を学ぶとても良い機会でした。当然ですが、動作保証をするためにテストは変えずに行いました。
また、開発合宿の雰囲気はというと、待ちに待ったタスクなどもあったので、みんな作業に没頭して休憩を忘れているほどでした。しかし、お菓子の差し入れなどもあり、合宿らしい楽しい雰囲気でした。
感想
とてもありがたい経験をさせていただいたので、その時の感想をいっぱい書きます。
ランチや1on1などを人事、メンターなどさまざまな人に組んでもらい、バックエンド(MLチームも含めて)の人のみならず、クラウドプラットフォーム(クラプラ)やエキスパートの人と話しました。
バックエンドチームの第一印象はとにかく若かったという印象です。平均年齢26ぐらいだと思います。自分のメンターは新卒3年目で、タスクを一緒に進めてくれた人は新卒1年目でした。しかし、自分が分からない箇所はちゃんと教えてくれるし、今回のタスクの背景なども事細かく教えていただき、技術的に成長できる環境なんだなと実感しました。また、今回のAlloyDBの件も然り、新しいことをどんどん取り入れていく環境とも感じました。さらに、このチームにジョインできたことで、ML×バックエンドに強く興味を持ちました。今後もかなり面白そうな領域になりそうだと感じたのでML初心者ですが積極的にキャッチアップしていきたいと強く決意しました。
クラプラとエキスパートの人たちは自分のことを〇〇の人と言える人ばかりで、技術的にかなり尖った人たちでした。自分のキャリア的にも目指したい姿だったので、かなり良い刺激をもらい、現在精進中です。
総じてとても良いインターンでした。関係者の皆様ありがとうございました!