はじめに

はじめまして!慶應義塾大学環境情報学部3年の上野 大和です!2026年の4月の1ヶ月間、CA Tech JOBにバックエンドエンジニアとして参加させていただきました。 普段はTypeScriptでバックエンドやフロントエンドを開発しています。

 

CA Tech JOBは、現場のチームに実際に配属され、社員のエンジニアの方々と同じ環境で開発に取り組める、就業型のインターンシッププログラムです。私はABEMAの広告配信に関わるチームに配属され、Go言語で書かれたマイクロサービスのパフォーマンス改善と、その前提となる負荷試験・依存関係の整理に取り組みました。

 

参加したきっかけ

サイバーエージェントに興味を持ったきっかけは、以前参加した「Go College」という、Go 言語を中心にサーバーサイド開発を学べるインターンでした。そこで Goに触れたのはもちろんですが、それ以上にサイバーエージェントのエンジニア文化や、大規模サービスを支える技術への関心が強くなりました。

 

普段スタートアップで開発をしていると、どうしても「いかに早く作って出すか」が中心になりがちです。一方で、数千万人規模のユーザーが日々利用する大規模サービスを「いかに安定して、速く、長く運用し続けるか」という観点は、なかなか経験する機会がなく、大規模サービスならではの技術と、それを支えるカルチャーを肌で感じたい、というのが今回応募した一番の動機でした。

 

目標について

インターンに臨むにあたり、私は次の3つの目標を立てました。

 

  1. 大規模サービスを支える技術を学ぶ — マイクロサービス、Kubernetes、Redis など、普段あまり触れない技術に触れて理解して使えるようにする
  2. 自分のタスクをドメイン・エンジニアリングの両面からきちんと理解する — タスクをAIでこなすだけでなく自分の言葉で説明できるようになる
  3. サイバーエージェントのカルチャーを知る — チーム開発の進め方や、技術的な意思決定のプロセスを体感する

プロダクトと使用技術

私が配属されたのはABEMAの広告配信を支えるバックエンドチームです。広告配信のシステムは複数のマイクロサービスで構成されており、私は主にABEMAのVOD視聴周りの以下の3つのサービスに関わりました。ABEMAではリアルタイムで放送されてるコンテンツとは別で、コンテンツを楽しむことができます。VODとは「ビデオ・オン・デマンド(Video On Demand)」の略で、過去の番組やライブ映像、ドラマやアニメなどのコンテンツを好きなタイミングで視聴できる機能のことです。VOD視聴では、コンテンツの種類や視聴者の年齢・利用状況などに応じて、配信する広告を決定する必要があります。そのため、視聴開始時や広告挿入のタイミングで、コンテンツ情報やユーザー属性を含むリクエストが広告配信システムに送られます。広告の選定やログ送信によってレスポンスが遅くなると、動画の再生開始や広告再生までの待ち時間につながるため、高いスループットを維持しながら、できる限り低いレイテンシで処理することが求められます。

 

構成図

 

  • 広告フィルタリングサービス:  配信候補の広告に対して、事前に配信可能な条件に基づいた広告のフィルタリングを行うサービス
  • 広告データをRedisに書き込むサービス:  コンテンツごとに配信可能な広告データをあらかじめ Redis に書き込んでしておくサービス
  • 広告配信サービス: コンテンツ情報やユーザー情報をリクエストとして受け取り、クライアントへ配信可能な広告を返すサービス

 

使用した主な技術は以下の通りです。 普段の私のスタックとは大きく異なり、特に Go,gRPC,マイクロサービス,Kubernetes はほぼ初めて本格的に触れる領域でした。

 

分類 技術
開発言語 Go
API通信 gRPC
アーキテクチャ マイクロサービスアーキテクチャ
インフラ Kubernetes / GCP(Memorystore for Redis, Artifact Registry, GKE)
データストア Redis
CI/CD・監視 pipeCD, Datadog, Grafana, k6
データ形式・メッセージング Protocol Buffers, Avro, Pub/Sub

取り組んだこと

背景にある課題 — フィルターサービスのRPSが低すぎる

私が取り組んだ中心的なテーマは、広告フィルターサービスのパフォーマンス改善でした。

 

過去に実施された負荷試験で、このサービスの RPS(秒間リクエスト数)が想定よりも低いことが判明していました。すでに負荷試験からある程度の改善はされていたものの、まだボトルネックが残っている状態でした。

 

まずは現状を正しく把握するため、改善に入る前に改めて負荷試験を行いました。このサービスはCPUの上限が2コアに設定されているため、CPU 使用量が2コアに到達した時点を性能の限界と定義しました。

 

結果、限界は 約700rpsでした。プロファイリングしてみると、CPU時間を大きく食っていたのは主に2つの処理だと分かりました。

 

  • Redisから取得したprotobufのUnmarshal(バイト列を構造体に戻すデシリアライズ処理)
  • Pub/SubへのPublish処理

対応その1 依存関係の整理 — 変更の影響範囲を閉じる

まず最初に取り組んだのはリファクタリングです。 今回のCPU時間を減らす変更の影響範囲を閉じるため、マイクロサービスのコンポーネント間の依存関係を剥がす必要がありました。具体的には、広告フィルターサービスが広告配信サービスへ返すレスポンスの中に、他のサービス由来の広告データがそのまま含まれており、これだとprotoの変更がレスポンスを通じて広告配信サービスにまで波及してしまうという問題がありました。 そこで、フィルターサービス内でデータを完結させてから返すように変更し、変更の影響範囲を意図した2サービスの内側に閉じ込めました。

対応その2 — 重かったPub/SubのPublishを非同期化する

次に行ったのは、ログのPublishの非同期化です。広告フィルタリングサービスでは、リクエストを処理する中で、分析用途(BigQuery への送信など)のログを Pub/Sub に同期的に Publish していました。

ライブラリに関して調査したところ、すでに使われていたPub/Subのクライアントライブラリ(cloud.google.com/go/pubsub)が非推奨になっていたため、まずv2系へのアップデートを行いました。

 

その上で、ログ送信のPublishを非同期化しました。Pub/Sub SDKのPublishメソッド自体は、内部でメッセージをバッファリングし、複数のメッセージをまとめて非同期に送信する仕組みを持っています。今回の変更では、そのSDKの仕組みはそのまま活用しつつ、リクエスト処理のgoroutineから直接Publishを呼び出すのではなく、アプリケーション側のキューにログを積むだけにしました。実際のPublish呼び出しは背景で動く別のgoroutineに任せることで、ログ送信処理をリクエスト処理から分離しています。

処理フローは次のようになります。

  1. リクエスト処理のgoroutineがログ送信用の関数を呼び出す
  2. 送信したいデータを、アプリケーション側のbuffered channel(バッファ付きチャネル)に積む
  3. 背景で動くgoroutineがチャネルからデータを取り出し、Avro形式に変換する
  4. goroutineがPub/Sub SDKのPublishを呼び出す
  5. SDKがメッセージを内部でバッファリングし、複数のメッセージをまとめてPub/Subへ送信する

 

ここでポイントになったのが、キューが詰まったときの挙動をどう設計するかでした。今回扱うのは分析用途のログなので、「多少送信が遅れてもよいが、リクエスト処理そのものは絶対にブロックしたくない」という性質があります。そこで、ノンブロッキングキューとして次の仕様にしました。

 

  • キューに空きがあれば即座に積む
  • 満杯であれば待たずに破棄する(リクエスト処理を止めない)
  • パブリッシャー停止後に来たものも破棄する

 

結果、RPSは向上したのですが、高負荷時にはキューがすぐに上限へ達し、想定以上のログが欠損してしまいました。下記の対応をしてリリースまで行いたかったのですが、期間の関係で対応できませんでした。

 

  • キューのバッファサイズ(AsyncBufferSize)を拡大する
  • キューが満杯になった場合、ごく短時間だけ空きを待つことで、軽いバックプレッシャーをかける

対応その3 — protoライブラリを変えてUnmarshalを高速化する

次に取り組んだのが、もう一つのボトルネックだったUnmarshalの高速化です。

 

GoでProtocol Buffersを扱う場合、一般的には google.golang.org/protobufproto.Marshalproto.Unmarshal を使ってデータを変換します。しかし、プロファイリングの結果、今回はこの処理が想像以上にCPUを消費していることが分かりました。

 

その理由の一つが、さまざまなメッセージ型を共通の仕組みで扱うためのProtocol Buffersのリフレクション機構です。 ここでいうリフレクションは、Go の reflect パッケージを使って構造体の型情報を扱うことではなく、protoreflect を通じて、Protocol Buffers のメッセージ定義に含まれるメッセージ名、フィールド番号、フィールドの型、繰り返し可否、ネスト構造といった情報を、実行時に参照する仕組みを指します。proto.Marshal / proto.Unmarshal は、メッセージが持つ型情報やフィールド情報をもとに、「このフィールドは文字列か」「繰り返しフィールドか」「ネストしたメッセージか」といった構造を確認しながら encode / decode を進めます。これにより、型ごとに処理を実装しなくても、共通のロジックでさまざまなメッセージを扱えます。

 

一方で、実行時に構造を確認して処理を分岐するぶん、型専用のコードを直接実行する場合と比べてオーバーヘッドが生じます。一回あたりの差は小さくても、大量のリクエストを処理する環境では積み重なり、CPU使用率やメモリアロケーションを押し上げる要因になります。

 

そこで導入したのが vtprotobuf でした。vtprotobufは、型ごとに専用のMarshal / Unmarshalコードをあらかじめ生成するツールです。「このフィールドはstring、次はint32、ここはrepeated message、ここはnilチェック」といった処理がコードとして展開されるため、実行時にメッセージの構造をたどる処理を減らせます。その結果、CPUとメモリの消費を大きく抑えられます。

 

似たライブラリに easyprotoもありますが、vtprotobuf の方がパフォーマンスが高く、かつ導入が楽だったため採用しました。導入の手間が小さいのは、既存の protoc-gen-go が生成するprotoの型はそのまま使い、vtprotobuf は MarshalVT / UnmarshalVT / SizeVT を含む別ファイル(_vtproto.pb.go)を追加で生成する形になるからです。既存コードを置き換えるのではなく、呼び出すメソッドを差し替えるだけで済みます。

 

呼び出し側の違いは次のようなイメージです。

 

// 標準の proto(リフレクションを使う)

 

b, err := proto.Marshal(msg)

 

err = proto.Unmarshal(b, msg)

 

// vtprotobuf が生成するメソッド(リフレクション不使用)

 

b, err := msg.MarshalVT()

 

err = msg.UnmarshalVT(b)

 

この変更を行ったところMarshal関連がCPU時間の約32%程度を占めていたのが、約16%程度まで低下したのです。狙い通り、デシリアライズ周りの CPU消費は確実に削減できました。

 

振り返り

技術的な学び

今回のインターンで、Go・gRPC・マイクロサービス・Kubernetes・Redis といった、普段あまり触れない技術にまとめて触れられたのは、純粋にとても楽しい経験でした。

 

特に大きかったのは、負債解消や運用・監視という観点が身についたことです。これまでの私は新規開発がほとんどで、「動くものを作る」ことに意識が向いていました。しかし今回は、すでに動いているサービスのボトルネックを計測し、ライブラリの選定や設計を見直し、影響範囲を見極めながら改善する、という一連の流れを経験することができました。

 

非推奨になったライブラリを更新したり、不要になったフィールドを削除したり、依存関係を整理したり——こうした地道な作業の一つひとつが、サービスを長く健全に保つために必要なのだと実感しました。技術選定や設計の良し悪しが、数ヶ月・数年後の運用コストに直結する。その重みを、コードと数字の両方から学べたと思います。

チーム・文化の学び

サイバーエージェントのエンジニア文化で印象的だったのは、インプットの機会の多さです。過去のリサーチや調査結果が社内ドキュメントとしてきれいにまとまっていて、自分のタスクに関連する背景知識をすぐにたどることができました。「誰かが調べたことが、ちゃんと次の人の資産になっている」状態は、組織として強いなと感じました。

 

また、1on1の機会も非常に多く、同じチームのエンジニアだけでなく、他チームのエンジニアやデザイナー、営業、PMなど、職種やチームを越えて話を聞くことができましたそれぞれの立場からプロダクトをどう見ているのか、普段どのような課題に向き合っているのかを知ること自体が、大きなインプットになりました。

 

組織でプロダクトを運用し、さまざまな立場の人と協力しながら継続的に改善していくとはどういうことか。そのイメージを、今回のインターンを通してかなり具体的に持てるようになったと思います。

反省

一番の反省は、タスクの見積もりが甘く、期間内にやり切れなかったことです。やりたいことを盛り込みすぎて並行に進めてしまい、結果として全てのタスクを完了してリリースまで持っていくことができませんでした。 まず一つの改善にしっかり集中し、計測 → 改善 → 検証 → リリースのサイクルを最後まで回し切ることを意識したいです。 改善施策はやることよりも、やり切ることが大事だと学びました。

 

もう一つは、もっと積極的に相談・進捗共有をすべきだったという点です。せっかく頼れる先輩エンジニアが周りにいる環境だったのに、まずはAIを使って自分でもう少し調べてからと考え、問題を抱え込んでしまう場面がありました。

 

AIは一般的な技術知識を調べたり、考えを整理したりするうえで役立ちます。一方で、既存実装の意図や過去の経緯、プロダクト固有の制約については、実際にそのプロダクトに関わってきたエンジニアに聞く方が、早く正確な答えにたどり着けることも少なくありません。早い段階で状況を共有していれば、より短い時間で解決できた問題がいくつもあったと思います。

 

さいごに

1ヶ月というかなり短い期間でしたが、自分にとって大きな学びになりました。

 

まず、エンジニアの経験が浅い私を温かく受け入れ、丁寧にサポートしてくださったトレーナー、メンターの方々、チームの皆さんに心から感謝しています。

 

これだけの大規模なサービスに、インターン生として実際に手を入れさせてもらえる環境は本当に貴重だと思います。

ありがとうございました。