はじめに
はじめまして!
2023年8月2日〜8月31日の1ヶ月間「CA Tech JOB」というインターンシップに参加しました、瀬尾優人(@geeeeorge)と申します。現在は東京大学の修士1年です。
私が配属されたチームはABEMAのサービスグロース バックエンドチームです。
この記事では開発の流れ、ABEMAのバックエンドエンジニアとしてどのようなタスクに取り組み、どのような学びを得たのかご紹介させていただきます!
インターンシップ参加の目的
最初は「大規模サービス固有の複雑性とそれに対応する具体的な解決策の理解を深め、解決するためのスキルとノウハウを養うこと」を目標として掲げていました。就業していく中で、ABEMAでは大規模リクエストを捌くために様々な工夫がされていることを知り、この抽象的な目標が次のように少し具体的になってきました。
- 負荷を減らす等、定量的に効果が見えるような実装をする
- キャッシュ、NoSQL、Kubernetes(k8s)等使ったことのない技術を理解する
- 小さなタスクでも何か自分から提案して設計、実装する
トレーナーの江頭さんにこの目的を伝えると、それに合ったタスクを私に振ってくださり、非常に成長できたことを実感しております。
開発の流れ
ABEMAのサービスグロースチームでの開発の流れは以下のようになります。
- 設計
- 設計レビュー (全員参加)
- 実装
- 実装レビュー (2 approves required)
- デプロイ、動作確認
まずesaというドキュメント管理ツールで設計ドキュメントを作成します。タスク設計をesaに残すことで、設計の軌跡が残るのでとても良いです!(esaを非常に気に入って、個人でesaを使うようになりました)
設計が終わると、毎日17:30からある共有会で設計レビューを受けます。設計レビューではチームメンバー全員が目を通すので属人化を防げますし、他メンバーの設計をじっくり見れるので勉強になったりします。
実装は概ね設計通りに行い、実装が終わるとgithubで2人からレビューを受けます。この2人は自動でアサインされるように設定されています。approveをもらいマージすると、PipeCDによりデプロイされます。
タスク
1ヶ月と言う短い期間でしたが、たくさんのタスクをこなすことができました。
- PodのZone分散配置
- Redisのバージョンアップ
- キャッシュTTLの調整
- 依存関係の修正
- 開発合宿
- マイリスト追加の負荷対策
- user-stats負荷分散
- リードタイム計測アルゴリズム修正
これらに対して詳しく説明するのではなく、概要をまとめる形でそれぞれ少しずつコメントしていきます!
PodのZone分散配置
最初に担当したタスクは「PodのZone分散配置」というタスクでした。ABEMAではk8s用のリポジトリがあり、そこで全てのマイクロサービスのマニフェストを管理しています。クラウドベンダーはGCPを使用しており、Zoneはasia-northeast1-a、asia-northeast1-b、asia-northeast1-cの3つが使われています。
これまではZone分散配置の設定をしておらず、PodがあるZoneに偏る可能性がありました。偏ってしまうと、Zone障害が起きた際に一時使用不可になってしまうので問題です。
そこで、Podに対してtopologySpreadConstraintsというトポロジー分散に関する制約を設けることにしました。topologySpreadConstraintsでは、どの程度分散させるか、Zone毎に分散させるのか、Region間で分散させるのか等の設定ができます。
私はk8sは未経験だったため、k8s?Pod?Zone分散?という状態でしたが、チームの方々のサポートもあって、かなり理解度高く進めることができました。gcloud、kubectlというCLIツールの使い方にも慣れることができました。k9sというかっこいいツールがありますが、こちらは使いこなせなかったので、今後使えるようになりたいと思います。
Redisのバージョンアップ
ABEMAのuser-statsというマイクロサービスでは、Memorystore for Redisをキャッシュとして使用しています。Redisはバージョン6.xを使用していましたが、2023/07/17にバージョン7.0がGAしたのでバージョンアップすることになりました。
user-statsはユーザー情報を扱うマイクロサービスで、使用頻度が高いのでキャッシュをとっています。キャッシュは1つのRedisサーバーで行うのではなく、36個のサーバーをConsistent Hashingしています。処理の計算量はユーザー数に依存して増えていくため、サーバー数を増やすことで負荷を分散しています。
最初はTerraform上で6_Xを7_0にするだけの簡単なタスクでは?と軽く考えていましたが、甘かったです。実際には以下のようなことを考慮する必要があります。
- 後方互換性はあるか
- 全て一気にバージョンアップするのか
- バックアップは必要か
- バージョンアップのメリット
結論、後方互換性はありました。バージョンアップは数回に分けて行うことにしました。何度もcommit,、applyをする必要がありましたが、キャッシュ機能を一時完全停止させるのは問題と判断しました。バックアップはキャッシュなので必要無しです。バージョンアップのパフォーマンス面でのメリットは無かったですが、こまめにバージョンアップすることは重要なので実施しました。
ただバージョンアップすると言うにも考慮することがたくさんあり、その中で様々あるNoSQLのpros/consについて考えたり、AWSとGCPの似たサービスの違いだったり色々興味が湧いたので、インターンシップを終えた今でもサービスの研究をしています。
キャッシュTTLの調整
ABEMAでは、HTTPのAPIのそれぞれに対し細かくCache-Controlヘッダーを設定しています。Cache-Controlヘッダーでは、CDN等に置くキャッシュのTTL(Time to Live)を設定できます。
例えば、/mediaというURIに対しては、キャッシュTTLを3分に設定するといった具合で設定ができます。これを5分に設定すれば、その分APIを叩かれる回数が減り、負荷軽減につながります。一方でデータのフレッシュさが失われるので、このトレードオフを考慮してAPI毎にうまくTTLを考える必要があります。これまでTTLの設定やトレードオフについて、考えたことがなかったので良い経験になりました。
依存関係の修正
ABEMAでは元々、マイクロサービス毎にリポジトリが別々でした。この時、protobufはabema-protobufというリポジトリに全て集約されていました。各マイクロサービスのリポジトリがabema-protobufに依存しているという構造です。
その後、モノリポ化に伴いabema-backendという一つのリポジトリにコードが集約されました。この時、abema-protobufのprotobufもabema-backendに集約されました。うまく移行されていれば、abema-backend内のマイクロサービス達は、abema-protobufに依存しなくなるはずでした。しかし、abema-protobufに依存しているものが残っていました。
つまり、abema-backendとabema-protobufで同じコードが二重管理されている状態で、とりあえず動けばいいというコードだったので、後々危険になるぞと思い、依存関係の修正を実行しました。この際、protoの名前競合が発生したため、同時に名前の修正も行いました。
このタスクは初めて自分から提案したタスクで、ABEMAのコードをリファクタリングできたのは自信になりました!
開発合宿
サービスグロースチームでは毎月開発合宿を行っています。合宿といってもどこかに泊まりに行くわけではなく、オフィスのある一室を使ってチーム全員で改善や技術的負債などの解消していく内容になっています。
私が参加した回ではLinterの指摘箇所の修正を行いました。ABEMAではGoのLinterとしてgolangci-lintを使用しています。デフォルトでは有効になっていないLinterも一部有効になっています。そのため、goconst、nilnil、forcetypeassert等デフォルトでは有効になってないLinterについての理解が進みました。
開発合宿を経て、Linterの適切な設定はその言語のベストプラクティスに繋がることに気がつき、普段の開発でもgolangci-lintをうまく活用するようになりました!
マイリスト追加の負荷対策
8月の中旬頃にマイリスト追加のAPIが高負荷になることがありました。
マイリストに追加(PUT)すると、その作品の最新話が放送される数分前に通知がいくように設定されます。これはPUTの中でマイリストをDBに登録する操作とPubSubにメッセージをPublishする操作の2つが行われているからです。そのメッセージの処理が詰まっていることが原因でした。そこで、本来マイリストが登録されていれば、Publishする必要はなかったので、DBに登録されているか確認し、されていればearly returnするようにしました。
user-stats負荷分散
このタスクは以下の3つのマイクロサービスが関係してきます。
- user-content: モジュールの配列を返却
- user-targeting: モジュールを生成
- user-stats: ユーザー情報
ここで、モジュールというのはABEMAを開いた最初の画面で、横並びになっている動画群のことを指します。下の写真で言うと、「人気・注目」が1つ目のモジュール、「新作アニメを選んだあなたへ」が2つ目のモジュールといった具合です。
モジュール内の動画順序はユーザーの属性などを元にし、最適化されています。この最適化アルゴリズムには色々なものがありますが、そのうちの一つがuser-targetingです。
user-contentにはuser-targeting等様々な最適化アルゴリズムを呼び出してModuleを返すListModulesというメソッドがあります。ListModulesを一回呼び出すとuser-statsが呼ばれ、更にuser-targetingの中でもuser-statsが呼ばれるため、1回のListModules呼び出しで2回user-statsが呼ばれることになります。これによりuser-statsの負荷が増えてしまっていたので、処理の共通化を行うことで対応しました。
処理の共通化ではListModulesの中で一度user-statsを呼び、返り値をuserと言う変数でもっておきます。user-targetingの引数にこのuserを渡すことで、user-targeting側でuser-statsを呼び出す必要を無くすといった具合です。もちろん後方互換性を保ちながら行う必要があったので、綿密に設計しました。
リードタイム計測アルゴリズム修正
インターンシップ最後の週は新たにタスクをすることはしないで、開発合宿の残りを消化していました。ただ結構単純作業になってきて、他にタスクが無いか探していたところに「リードタイム計測アルゴリズム修正」と言うタスクを見つけました。これは社員の方がTODOにしていたタスクで、自分の方で対応しても良いか聞いてみたところOKが出たので取り掛かりました。
既にGithub ActionsでPRのマージ後にbotがリードタイムを表示する仕組みはできていました。リードタイムの計算アルゴリズムは`PRmerge時 – 最初のcommit時`で、GitHub CLIのgraphqlを用いて計測されていました。
ただ、commitが編集されることでリードタイムが正確ではなくなる可能性があったので、リードタイムを`PRmerge時 – min(最初のcommit, PRcreate時)`とすることでより正確に計測できるようにしました。GitHub CLIやjqコマンド、awkコマンドなどを様々なコマンドを駆使してワンライナー化することができました。
さいごに
ここまでタスクについて書いてきたので、他に印象に残ったことについて触れておきます。
まず渋谷ランチをたくさん堪能できました!ハンバーグ、カツ丼、そば、海鮮、焼肉など毎日楽しみにして出社していました。他の事業の社員さんとのランチにもいけます。担当人事の方やチームメンバーに相談すると、すぐにランチ設定してくださるのでとても助かりました。私は1ヶ月間Abema Towersで業務していましたが、渋谷スクランブルスクエアの見学もできました!
毎週金曜日にはウィンセッションがあります。ウィンセッションでは、1週間の成果を互いに褒め合ったり、メンバーに感謝を伝えたりします。感謝を伝え合うことでモチベーションアップになるのでとても良い習慣だと感じました。この習慣に影響を受けて、普段から「ありがとう」と伝えることが増えたような気がします。
開発で困ったことは周りのチームメンバーにすぐに聞くようにしてました。皆さん自分の作業を止めて、すごく親切に教えてくださるので大変感謝しています。皆さんありがとうございました!