初めに
2023年10月の1カ月間、CA Tech JOB に参加させていただきました、東京工業大学の学部4年の阿部翔太(@kyosu-1) です。大学ではサイバーセキュリティ分野の研究を行っており、普段はアルバイトや業務委託でバックエンド・クラウドインフラ領域を中心とした開発を行っています。
私が配属されたチームは ABEMA の Backend チームです。
インターンシップでは主に以下のようなタスクに取り組みました。
- 検索レスポンスへの縦サムネイル追加
- Go のバージョンアップと monorepo 移行
- マイクロサービスの通信効率化・負荷軽減
- トランクベース開発のリードタイム改善(GitHub Actions による CI の改善)
この記事ではインターンシップの前半に行った「マイクロサービスの通信効率化・負荷軽減」に関する取り組みについて紹介させていただきます。
背景
ABEMA の Backend は Kubernetes (GKE) 上の多数のマイクロサービスから構成されています。各マイクロサービス間の(同期)通信では主に gRPC が利用されており、APIインタフェースは protobuf で定義されています。マイクロサービスはそれぞれ独立した単位としてスケールアウトさせたり、デプロイすることが可能であり、大規模サービスのスケーラビリティ、アジリティの向上に寄与しています。一方で、マイクロサービス間のネットワーク通信はパフォーマンス上のボトルネックとなることもあります。また、特に複数の上流のマイクロサービスからリクエストが来るような下流のマイクロサービスは負荷が増大し、障害点ともなりやすいです。
一般に、サービス間通信は可能であれば少ない方がレイテンシや複雑性の観点から望ましいとされています。ABEMA では参照系のパフォーマンス向上のため、キャッシュの活用なども多く行われています。
今回のタスクの背景として、過去のインターンシップ生の取り組み の中の「user-stats負荷分散」というものがあります。記事の中で紹介されている user-content のマイクロサービスは、各ユーザーに対して最適なコンテンツを返すためにレコメンド基盤を利用しています。また、ABEMA では Dragonと Yatagarasu と呼ばれる二つのレコメンド基盤が開発されており、この二つで提供されるAPIが user-content の中で呼び出されています。記事の中ではユーザー情報を返す user-stats のマイクロサービスの呼び出しを user-content に共通化し、Dragon側では user-stats のAPIを呼び出さないことで通信の効率化を実現しました。この改善が適用されたのがちょうど私がインターンに参加したタイミングの少し前だったのですが、実際に Grafana ダッシュボードによる呼び出し状況を適用前後で見たところ、通信回数が大きく減り改善されていることが確認できました。
そこで、Yatagarasu 側についても同様の改善が行えないかと考え、実装及び適用後の効果検証を行ったのが今回の取り組みになります。
今回登場するマイクロサービスと通信の様子を整理したものが以下の通りです。
user-content:ユーザーのホーム画面に対応するデータを返すマイクロサービス
yatagarasu-gateway:レコメンドのマイクロサービス
user-stats:ユーザー属性の取得のためのマイクロサービス
- user-content は user-statsの GetUser を呼び出しユーザー属性(User)を取得します。この情報は他の user-content の中の様々な処理で利用されます。
- user-content はレコメンド情報取得のため、yatagarasu-gateway の /user-to-content API を呼び出します。(この呼び出しの中には User は含まれていません。)
- yatagarasu-gateway の処理の中では user-stats の GetUser が呼び出され、取得したユーザー属性がレコメンド処理の中で利用されます。
このように、user-content と yatagarasu-gateway の二つのマイクロサービスから、ユーザー情報取得のために user-stats の GetUser が呼び出されています。
方針
前述の背景で紹介した記事の取り組みと同様に、上流のサービスである user-content 側で GetUser の呼び出しを共通化する方針としました。yatagasu-gateway のレコメンド API 呼び出し時に User を渡すようにすることで、yatagarasu-gateway から GetUser の呼び出しをする必要をなくしています。
これにより、動作は変わらないまま yatagarasu-gateway から user-stats の呼び出しがなくなり、通信の効率化および user-statsの負荷軽減が図れます。
一つ注意点として、yatagarasu-gateway によるレコメンドAPIは歴史的経緯から gRPC ではなく、JSON APIとして実装がなされていました。UserStats の GetUser は gRPC メソッドであり、 User は protobufによって定義されたものです。そのため、User の情報をリクエストボディに含めて送るのには以下の二つの選択肢がありました。
- User を JSON Object に変換してリクエストボディのフィールドに追加
- protobuf によるバイナリを base64 エンコードした文字列をリクエストボディのフィールドに追加し、yatagarasu-gateway側でデコード時に解釈
はじめはJSON APIとして単純かつスキーマが明示的な前者の方針で設計を行いました。
しかし、メンバー全員で行われる設計レビューの際には、
- User の定義は protobuf によって行われ、各アプリケーション内では protobuf によって生成されたコードを利用するため、いずれの方針でもスキーマ追従や複雑性の観点で変わらない。
- User 情報はレコメンドに必要な情報を含む比較的大きなデータのため、後者の方法であればサイズ縮小化の効果が得られる。
といったレビューをいただきました。そこで、実際に後者の方針でリクエストボディのサイズが1/2以下になることや変換が問題なく行えることを手元実装で検証した上で、最終的に後者の方針で行くことに決定しました。
実装・リリース
ABEMA Backend ではトランクベース開発が採用されており、main ブランチにマージされると開発環境と本番環境で同時にデプロイパイプラインが走るようになっています。(ABEMA Backend のトランクベース開発については、こちらのインターン生による記事で紹介されています。)そのため、実装したAPIについてはその日のうちに本番環境にリリースが行われるため、後方互換性を必ず保ちながら行う必要があります。
先ほどの改善方針のもと以下の手順で順に実装を行い、コードレビューを受けた上で動作確認と本番リリースまで行いました。
- yatagarasu-gateway の実装
a. Userフィールドはnot requiredとして、ボディに含まれない場合については以前と同様の処理を行い、後方互換性を保つように。ボディに User が含まれる場合は、base64 decode した上で、proto.Unmarshal() によりインスタンス化し、それを後続の処理で利用するよう(GetUserを呼び出さないよう)にする。 - user-content の実装
a. yatagarasu-gateway の API (/user-to-content) 呼び出し時にGetUserで取得された User をフィールドに含めるように。 - user-content-v2 の実装
a. yatagarasu-gateway の API (/user-to-content) 呼び出し時にGetUserで取得された User をフィールドに含めるように。
効果検証
本番環境にリリースから数日後、GetUser の API 呼び出しをGrafana ダッシュボードで確認したものが以下のものです。
グラフから分かる通り,yatagarasu-gatewayから多い時には 500回/秒 もの呼び出しがありましたが、改修後は呼び出しがほとんど発生しない形に改善しました。
GetUser の呼び出しは複数のマイクロサービスから行われているため、負荷の軽減に繋がったと考えられます。
おわりに
本インターンシップでは、ABEMA Backend チームで様々な開発に取り組ませていただきました。インターンシップを通して、これまで経験のなかった大規模サービスを支える技術についての理解を深めることができました。
Abema Backend チームはメンバーのほとんどが20代のメンバーで構成されながら、トランクベース開発の手法などを取り入れながら高速に信頼性の高いシステムを構築しており、まさに最高水準のパフォーマンスを出す開発チームを体現していると感じました。また、新卒1、2年目のメンバーの方々がオーナーシップを持ちながら設計や開発に取り組んでいる姿はとても刺激になり、自分もより頑張ろうと思うようになりました。そして、そのようなチームの一員として一緒に開発を経験したことが何よりの良い経験となったと思います。
最後になりますが、インターンシップ期間中様々なサポートをしていただいたトレーナーの重政さん、(deep-dive-into-go というイベント参加後に推薦していただき)インターンシップ参加のきっかけとなり、後半のCI改善のタスクでもお世話になった上田さんをはじめ、インターンシップを受け入れていただいた Backend チームの皆さんには感謝を申し上げます。このような貴重な機会をいただき本当にありがとうございました!