Article

音楽配信サービス開発事例:スケーラブルなストリーミングで新規ユーザー獲得

高田晃太郎
音楽配信サービス開発事例:スケーラブルなストリーミングで新規ユーザー獲得

統計によると、2022年時点で世界の音楽売上の67%超がストリーミング由来となり¹、再生開始の遅延が2秒を超えると以降1秒ごとに離脱率が約5.8%増えるとする研究報告もある²。国内でも音楽配信市場の拡大とともにストリーミングのシェア拡大が続いている⁴。さらにピークタイムの瞬間同時接続はキャンペーン時に平時の8〜12倍まで跳ね上がることが珍しくない。こうした需給ギャップに備えながら、獲得の転換点である初回体験の品質を安定させることが、成長を左右する。本稿では、HLSとCDN(コンテンツ配信網)を軸にマイクロサービス化と観測性を統合し、再生開始の短縮やオリジンオフロード率(CDNでの配信比率)の向上、獲得効率の改善に寄与しうる設計・実装パターンを、設計判断と実装コード、運用指標の三点から分解して共有する。数値は一般的なレンジや公開資料に基づく目安として解説する。

スケール設計の全体像と獲得KPIの接続

対象とするのは、サンプリング可能な無料視聴をフックに獲得を進める音楽配信サービスの想定アーキテクチャである。事業側の主要KPIの例として、初回セッションの再生開始時間、初回トラックの30秒到達率、トライアル登録率を置き、技術側の主要SLOとして再生開始p95(95パーセンタイルの開始時間)、エラー率、オリジンオフロード率を定義する。キャンペーンの急増トラフィックに耐えるため、配信はHLSのfMP4(CMAF、再生互換と低遅延の両立を狙うコンテナ規格)を採用し³、マニフェスト(再生リスト)は圧縮・強キャッシュ、セグメント(媒体の細片)は短尺化しつつ先読みで初動を稼ぐ方針にする。エッジ(CDN側)での署名認証とトークン検証を導入し、オリジンはオブジェクトストレージを主としてスケールを確保、配信用APIは軽量なGoサービス群として分離する。A/BテストやレコメンドのためのイベントはKafkaに集約し、ClickHouseで遅延の少ない分析を行う。設計判断の要点は配信のボトルネックをエッジに寄せ、オリジンの仕事を極小化すること、そして観測性でKPIとSLOの因果を日次で検証できるようにすることに集約される。

HLS+CMAFとエッジ署名の実装要点

アダプティブ配信はHLSを中心に据え、ブラウザ互換と実装容易性を優先した。セグメント長は2秒程度を標準とし、低速回線向けのビットレートを細かく刻むことで初回再生の成功率を狙う。エッジでのアクセス制御はクエリ署名(署名付きURL)を用い、期限とパスバインディングで不正ダウンロードを抑止する。以下は署名URLを発行するGoの最小実装で、HMAC-SHA256と短寿命トークンを組み合わせた。

package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "fmt"
  "log"
  "net/http"
  "net/url"
  "os"
  "time"
)

func sign(path string, expires int64, secret []byte) string {
  mac := hmac.New(sha256.New, secret)
  mac.Write([]byte(fmt.Sprintf("%s:%d", path, expires)))
  return hex.EncodeToString(mac.Sum(nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
  path := r.URL.Query().Get("path")
  if path == "" {
    http.Error(w, "missing path", http.StatusBadRequest)
    return
  }
  exp := time.Now().Add(2 * time.Minute).Unix()
  sig := sign(path, exp, []byte(os.Getenv("SIGN_SECRET")))
  q := url.Values{}
  q.Set("exp", fmt.Sprint(exp))
  q.Set("sig", sig)
  u := url.URL{Path: path, RawQuery: q.Encode()}
  w.WriteHeader(http.StatusOK)
  w.Write([]byte(u.String()))
}

func main() {
  http.HandleFunc("/sign", handler)
  log.Fatal(http.ListenAndServe(":8080", nil))
}

プレーヤーからのマニフェスト取得時に署名を付与し、CDNはクエリをキーとしてキャッシュする。エッジで期限切れを弾きつつ、同一署名間ではディスクヒットを最大化できる。

キャッシュ戦略とヘッダー設計

マニフェストは変更頻度が高いがサイズが小さいため、Brotli圧縮を強制し、短いTTLで更新の即時性を担保する。一方でセグメントは不可変とし、1日単位の長TTLでキャッシュを効かせる。エッジキャッシュの鍵はパスとバリアントの組み合わせで十分だが、ドラフト・ローンチ中はバージョンをパスに埋めて衝突を避ける。Nginxをオリジンに使う場合の最小設定は以下のとおりで、m3u8は短TTL、mp4は長TTLを付与する。

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=hls:100m max_size=10g inactive=24h use_temp_path=off;

server {
  location ~* \.(m3u8)$ {
    gzip on;
    gzip_types application/vnd.apple.mpegurl;
    add_header Cache-Control "public, max-age=15, stale-while-revalidate=60";
    proxy_cache hls;
    proxy_cache_valid 200 15s;
    proxy_pass http://origin;
  }
  location ~* \.(m4s|mp4|aac)$ {
    add_header Cache-Control "public, max-age=86400, immutable";
    proxy_cache hls;
    proxy_cache_valid 200 24h;
    proxy_pass http://origin;
  }
}

この設計により、ピーク帯のCPU飽和を避けやすくなり、CDNヒット率の向上とともにトータルの再生開始p95の短縮に寄与することが多い。

ストレージと配信の最適化でp95を短縮

オブジェクトはリージョン間レプリケーションで可用性を確保し、ホットパスのプレフィックスはハッシュ分散してリスト操作の局所性を高める。マニフェストはBrotli圧縮前提でContent-TypeとContent-Encodingを明示し、CDN側の自動圧縮を補助する。配信レイヤではビヘイビアを分離し、マニフェストとセグメントで異なるキャッシュポリシーを適用する。インフラをコード化して回帰を防ぐ具体例として、CloudFrontの設定断片を示す。

resource "aws_cloudfront_distribution" "music" {
  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  origin {
    domain_name = aws_s3_bucket.origin.bucket_regional_domain_name
    origin_id   = "s3-origin"
  }
  ordered_cache_behavior {
    path_pattern     = "*.m3u8"
    target_origin_id = "s3-origin"
    viewer_protocol_policy = "redirect-to-https"
    compress = true
    cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id
  }
  ordered_cache_behavior {
    path_pattern     = "*.m4s"
    target_origin_id = "s3-origin"
    viewer_protocol_policy = "redirect-to-https"
    compress = false
    cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id
  }
}

再生開始時間の改善は、セグメントの粒度だけでなく、プレーヤーの先読み戦略と接続再利用の影響が大きい。ブラウザではHTTP/2の多重化でマニフェストと初期セグメントを同一コネクションで取得できるため、コネクション確立コストを抑える工夫が効く。多くの環境で、先読みと接続再利用の組み合わせにより、初回セッションのp95再生開始を数秒台前半に収めることが現実的な目標になる。リバッファ率(再生中の読み込み待ち)も適切なABR設定とキャッシュ最適化により低減が期待できる。

配信コストの最適化とオフロード率

キャッシュのヒット率を高めることでオリジンの転送量は大きく減り、オフロード率は一般に80〜95%程度を狙う設計が多い。ピーク帯でも高いオフロード率を維持できれば、配信コストは月次で二桁%の削減余地が生まれ、浮いたコストを獲得のクリエイティブ実験に再投資できる。制作側の運用負荷を抑えるため、アーティストの音源アップロードは直書き込みの事前署名URLを使う。Node.jsでの最小実装は以下の通りだ。

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import express from "express";

const s3 = new S3Client({ region: process.env.AWS_REGION });
const app = express();

app.get("/upload-url", async (req, res) => {
  try {
    const key = `uploads/${Date.now()}-${req.query.name}`;
    const cmd = new PutObjectCommand({ Bucket: process.env.BUCKET, Key: key, ContentType: req.query.type });
    const url = await getSignedUrl(s3, cmd, { expiresIn: 300 });
    res.json({ url, key });
  } catch (e) {
    console.error(e);
    res.status(500).json({ error: "failed to issue url" });
  }
});

app.listen(3000);

アップロードはクライアントからS3へ直行し、メタデータのみをAPIに送ることでコントロールプレーンのスループットを確保する。マニフェスト生成は非同期に行い、公開時点でキャッシュプリウォームの対象として登録する。

観測性とレコメンドが獲得を後押し

技術的な高速化が事業KPIに効いているかを検証するため、再生イベントを粒度の細かいスキーマで収集する。イベントはKafkaに集約し、Flinkでセッション化と特徴量抽出を行い、ClickHouseに投入してダッシュボードからほぼリアルタイムで確認できる。配信失敗の検知や地域別の品質低下はPrometheusとOpenTelemetryでメトリクス・トレースを結合して追跡する。実装の雰囲気を伝えるため、Pythonの非同期コンシューマでイベントを整形してClickHouseにバルク投入する例を示す。

import asyncio
from aiokafka import AIOKafkaConsumer
from clickhouse_driver import Client
import json

async def main():
    consumer = AIOKafkaConsumer(
        "playback",
        bootstrap_servers="kafka:9092",
        value_deserializer=lambda x: json.loads(x.decode()),
        enable_auto_commit=False,
    )
    await consumer.start()
    ch = Client(host='clickhouse', database='music')
    try:
        while True:
            msgs = await consumer.getmany(timeout_ms=500, max_records=1000)
            rows = []
            for tp, batch in msgs.items():
                for m in batch:
                    v = m.value
                    rows.append((v["user_id"], v["track_id"], v["ts"], v.get("buf_ms", 0)))
            if rows:
                ch.execute("INSERT INTO playback (user_id, track_id, ts, buf_ms) VALUES", rows)
                await consumer.commit()
    finally:
        await consumer.stop()

asyncio.run(main())

データ基盤の整備によって、初回のおすすめ精度を段階的に引き上げられる。コールドスタートでは人気度とジャンルのシンプルな二次元でランク付けし、数日以内にユーザー固有の埋め込みを学習して差し替える。A/B実験では、初回セッションの上部に「30秒で分かるあなた向けミックス」を配置する案のように、短時間で魅力が伝わる導線が効果的なことが多く、新規登録率やDay1リテンションに有意な改善が見られるケースもある。ファネルの健全性はClickHouseのクエリで監視し、閾値を下回る地域は自動でプレーヤー設定を保守的に切り替える。以下は簡易なファネル計測のクエリだ。

WITH s AS (
  SELECT user_id,
         minIf(ts, event = 'play_req') AS t_req,
         minIf(ts, event = 'play_start') AS t_start,
         minIf(ts, event = 'sec_30') AS t_30
  FROM events
  WHERE ts > now() - INTERVAL 1 DAY AND cohort = 'new'
  GROUP BY user_id
)
SELECT
  count() AS req,
  countIf(t_start > 0) AS start,
  countIf(t_30 > 0) AS sec30,
  round(start / req * 100, 1) AS req_to_start,
  round(sec30 / start * 100, 1) AS start_to_30
FROM s;

観測性そのものの設計や実装は、本稿の想定ではトレースのspanにCDNのcache-statusや署名有効期限などのタグを載せ、プレーヤーの再生失敗とネットワーク層の事象を一続きで追えるようにすることで、障害時の平均修復時間の短縮が狙える。

信頼性設計とオートスケールの現実解

ピークの前後でスケールの揺れが激しいため、再生APIはCPUとRPSの両指標でスケールする。外部依存の鋭いスパイクに備え、サーキットブレーカー(連鎖的な失敗を止める制御)と指数バックオフを組み合わせ、ユーザー体験を損ねない範囲でフォールバックを返す。SLOは月次99.95%といった高い可用性を目標に置き、エラー予算の消費速度が閾値を超えた場合は新規機能のロールアウトを自動停止する。KubernetesのHPAは以下のように構成し、予測可能な範囲で前倒しにスケールする。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: playback-api
spec:
  minReplicas: 4
  maxReplicas: 60
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: playback-api
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 55
    - type: Pods
      pods:
        metric:
          name: rps
        target:
          type: AverageValue
          averageValue: "120"

障害対応では、CDNの地域障害を想定してセカンダリ配信網へのフェイルオーバーを準備し、DNSのヘルスチェックと加重ルーティングで段階的に振り分ける。プレーヤー側にはマニフェストのミラーURLを埋め、第一取得が失敗した場合にバックアップへ切り替えるロジックを実装する。設計全体は再現可能な形で整理しており、導入期間は既存のクラウド環境と組織体制に依存するが、概ね数週間から数カ月のレンジが一般的だ。

ベンチマークとROIの整理

参考までに、検証環境での一般的なレンジを整理する。エッジキャッシュヒット時のマニフェスト取得は数十ms、配信APIのp95は数十〜数百ms、プレーヤーの初回セグメント取得完了までの合算は環境により概ね1.5〜3.0秒に収まることが多い。キャッシュ最適化と帯域の適正化により、月次コストは二桁%の削減余地が生まれるケースがあり、その再投資によってクリエイティブの実験数を増やし、トライアル登録やLTVの向上に間接的に効く二層のROI(短期の獲得単価改善と長期の継続率・単価改善)を狙える。

まとめ:ユーザー体験を軸にスケールと成長を同時に達成する

本稿が示すのは、再生開始の数秒を短縮する地道な改善が、獲得とリテンションの双方に効くという事実である。配信の安定化、キャッシュの最適化、観測性と実験の高速サイクルを一本の糸で結ぶことで、トラフィックの急増を恐れずに施策を打てるようになる。もし今、初回再生のp95が2秒を超えているなら、マニフェストの圧縮とセグメントの短縮、エッジ署名の導入から手を付けてほしい。観測基盤を整えたうえで、プレーヤーのUIに小さな仮説を重ねれば、数字は確実に反応するはずだ。あなたのサービスにとって、まず一番小さく確実に動かせるボトルネックはどこだろうか。今日のデプロイ一回分の時間を、ユーザーの最初の一曲のために投じることから始めてみてほしい。

参考文献

  1. IFPI Global Music Report 2022: Global recorded music revenues grew 9% in 2022. https://www.ifpi.org/ifpi-global-music-report-global-recorded-music-revenues-grew-9-in-2022/
  2. 2秒超で離脱が増えるという研究を紹介する記事(Impress Web担当者Forum, 2012年11月20日). https://webtan.impress.co.jp/e/2012/11/20/14177
  3. Ant Media: CMAF Streaming (CMAFの概要と利点). https://antmedia.io/cmaf-streaming/
  4. PR TIMES: 2023年第三四半期の音楽配信売上・ストリーミング動向(プレスリリース). https://prtimes.jp/main/html/rd/p/000000520.000010908.html