配信画面を壊さないための技術
スコラボ という配信者・VTuberがもっと気軽に、もっと楽しく配信ができるように、配信をサポートするサービスを作っています。
実際どんなサービスかについては、利用が基本無料ですので触っていただけると嬉しいです。
『配信画面を壊す』とは?
スコラボは、OBSなどの配信ソフトのブラウザソースに対して特定のURLを入れてもらうことで、特定のサイズのスクリーンに配信画面をレンダリングすると言う方法で配信画面を構築することのできるサービスです。
表示部分だけをすごく簡単に言い換えると「ブラウザ上で画像や文字・コンポーネントを特定の位置に配置しているだけ」のサービスです。
このサービスの性質から、以下のような状況下で配信画面が表示できない・配信画面上によくわからないもの、邪魔なものが表示されてしまうという状況に容易に陥ります
- なんらかの原因でスコラボのバックエンドのAPIから正常なレスポンスが返らなかった
- なんらかの原因でコンポーネントのレンダリングでエラーが発生した
- なんらかの原因で表示部分にバグが紛れ込んだ状態でフロントエンドをデプロイした
- 依存しているSaaSにて障害が発生した
これらを原因としてユーザーの配信画面上になんらかの影響が出てしまう状態を『配信画面を壊す』・『配信画面を壊してしまう』と呼称しています。
この状況は配信者さんの配信自体に影響があり、同時にその配信を視聴している視聴者さんにも影響があることになるため、スコラボにおいて最も避けなければいけない状況であり、サービスのメンテナンス・デプロイを行う場合にも配信画面については可能な限り表示側だけでもサービスを止めないようにしてメンテナンス計画を立てています。
この記事では、『配信画面を壊す』と言う状態に陥るタイミングとそうならないように・なったときのためにスコラボがとっている対策についてご紹介します。
スコラボのAPI関係で壊れる場合
スコラボのバックエンドのAPIから正常なレスポンスが返らなくなった(ここでは2xx系、304以外のレスポンス)場合に壊れるタイミングは以下のタイミングが想定できます。
- ユーザーが表示・再表示しようとしたタイミング
- ユーザーが編集を行ったタイミング
ユーザーが表示・再表示しようとしたタイミング
この場合はレンダリングする最初の情報がないと表示するものすら分からないため、正直に言うと取れる対策がありません。
ユーザーが表示しようとしたタイミングは基本的にユーザーが配信ソフトを起動したタイミング・新たにブラウザソースを追加したタイミングであるため、Error表示をあえて表示することで利用できないことを伝えています。
ユーザーが編集を行ったタイミング
この場合はすでに一度レンダリングされており、編集前の情報は存在していますので表示を崩さないことを最低条件としつつ、復旧後に再取得するタイミングがあればそれで再度レンダリングされる必要があります。
現在スコラボはデータフェッチにSWRを利用しているため、この点の対応はとても簡単です。
SWRのOptionにonErrorRetryという便利なoptionがあるため、Retryの条件を記述するだけです
デフォルトの挙動では指数バックオフを使用してRetryを行いますが、404発生時の挙動を変更したかったため記述しています。
この部分は改修の余地(単純に常時15秒に一度Retryするのはバックエンドの障害復旧時の負荷を考えても良くない)があるため、指数バックオフとJitterを用いたアルゴリズムに変更を予定しています。
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
if (error instanceof HTTPError) {
// 404では30秒に1回・10分間だけ再試行する
if (error.response.status === 404) {
if (retryCount < 20) {
setTimeout(() => revalidate({ retryCount }), 30000);
} else {
return;
}
}
}
// 15秒後に再試行
setTimeout(() => revalidate({ retryCount }), 15000);
},
また、SWRが返しているerrorについても諸事情があり利用しておらず、SWRがfetcherによってエラーを受け取った際にも取得できているデータがあればそれをdataに格納し続けると言う機構を有効活用している形になります。
コンポーネントのレンダリングでエラーが発生した場合
コンポーネントのレンダリングでエラーが発生するタイミングは現状では以下のタイミングが想定できます。
- チャット機能で意図しないデータが取得されてしまった
他にも
- 画像・動画が取得できなくなった
などがあるにはあるのですが、コンポーネントのレンダリング上のエラーではなく、「依存しているSaaSにて障害が発生した」ケースに近いためそちらで記載させていただきます
チャット機能で意図しないデータが取得されてしまった
チャット機能は現状では唯一、他社(Youtube, Twitchなど)のサービスのデータをAPI経由で取得して表示している機能です。
そのため、他社のAPI仕様の変更・障害にどうしても巻き込まれる場合があります。
間にスコラボが管理するバックエンドを置き、なるべく差分を吸収する機構は整えてはいますが、急な変更や意図されていない変更があると意図しないデータが取得されてしまう場合があります。
この際、たとえばメッセージが空のコメントが取得された際に空のArrayが返ること
{
"data": [
{
"content": []
}
]
}
を期待しているものの、null
{
"data": [
{
"content": null
}
]
}
が返るケースがある可能性がないとは言い切れません。(分かりやすい例ですので実際に起こると言うわけではない場合もありますし、実際のレスポンスとは大きく異なります)
これが発生した場合、
type ChatContentProps = {
content: string[];
}
const ChatContent: FC<ChatContentProps> = ({content}) => {
return (
<div>
{content.map((value, i) => {
return (<span key={i}>{value}</span>)
})}
</div>
)
}
のようなコードでレンダリングしていた場合、Uncaught TypeError: Cannot read properties of null (reading 'map')
が発生し、Next.jsではそのままでも_error.tsx
にて吸収されます。
これ自体はオプショナルチェーン演算子(?.
)を使ってcontent?.map()
のように記述することで回避することも可能ですが、複雑な構造のjsonになるにつれて、オプショナルチェーンが続くことになりコードの可読性があまりよくない状態になります。
また、_error.tsx
にて吸収されるということは、画面全体がエラー時の表示になるということに等しく、チャットを接続中ということは配信中の利用である場合が多いため、配信中の配信画面に影響が出てしまうということになります。
そこで、レンダリングしているコンポーネント(各アイテム)ごとにErrorBoundaryを設置し、影響範囲をそれぞれのアイテム内に封じ込めるようにしています。
エラー発生時に行うことはレンダリングをやめること(空のdivをレンダリングすること)とSentryにエラー情報を送信することのみにしており、配信画面上への影響はそのチャットが表示されなくなるのみに抑えています。
ありがたいことにSentryが公式にErrorBoundaryを提供しているため、それを利用することでエラーの早期発見と対処が行えるようにしています。
表示部分にバグが紛れ込んだ場合
これは正直救いようがないようなミスですが、テストをすり抜けてしまった場合や開発環境での確認漏れなどのケースで発生する可能性はあるものです。(ない方がいいのは確かではあるのですが人間がしていることですので...)
これによって影響を受けるタイミングはシンプルで
- ページをロードしたタイミング
のみです。
ページをロードしたタイミング
「ユーザーが表示・再表示しようとしたタイミング」と同様にレンダリングする最初の情報がない場合は表示するものすら分からないため、取れる対策がありません。
しかし、表示は正常に行えているものの、一部の機能の表示のみに影響があるケースがこちらには存在します。
この場合、修正バージョンを再度ロードしてもらえれば必ず解消しますが、OBSなどのブラウザソースで表示している場合、表示上に何か異常がない限りリロードを行なってもらえる機会はOBSの再起動時のみです。
シーンがアクティブになった際にもロードされる可能性もありますが、これはブラウザソースの設定の「表示されていないときにソースをシャットダウンする」か「シーンがアクティブになった時にブラウザの表示を更新する」にチェックを入れている場合に限り、通常利用時はシーン切り替え時にLoading表示が出ないようにするためにもOffにすることを推奨しています。(ブラウザソースが多くOBSの動作が重い場合やスタジオモードを利用しているなどの特殊なケースを除きます)
そのためスコラボでは、特定のバージョンへのアップデートを必ず行なってもらう必要がある場合に限り、Version Skewの解消を目的として該当のブラウザソースが存在しているシーンが非アクティブ・非表示であるか、配信状態ではない場合に自動的にリロードを行う場合があります。
これは基本的にはユーザーの意識しないところで行われるようになっており、何かしらの副作用が起こることは基本的にありません。
また、これを一部拡張しWebSocket経由でリロードを行えるタイミングで行うように一括で指示する機構についても実装を考えていますが、未だ実装していません。
依存しているSaaSにて障害が発生した
これは最も制御しにくい部分であり、一番起こる可能性が高いケースです。
なるべくクリティカルな問題につながる部分はエンタープライズ契約を行いSLAの確保を行なっていますが、障害が発生することがないわけではありません。
依存しているSaaSは多岐に渡りますが、インフラであるGCPやCloudflareにて障害が発生した場合は「スコラボのAPI関係で壊れる場合」と同様になるため、今回は
- 画像・動画配信にて障害が発生した場合
- 更新通知にて障害が発生した場合
に限定します。(フォント配信元にて障害が発生したケースなども考えられていますが、配信にすぐにクリティカルな影響が出ないためここでは割愛します)
画像・動画配信にて障害が発生した場合
スコラボにおける画像・動画はかなり重要な立ち位置にあり、配信背景や装飾などスコラボが提供している機能以外はすべて画像・動画で配信されています。
スコラボ側で取れる対策はあまり多くなく、Cacheの有効期限を長めに設定することや、SLAの確保、サービスステータス通知の取得は行っていますが、それ以上の対策は表示に失敗した際に「画像がないことを表すマーク」が表示されないようにすることぐらいしか取れる対策がありません。
「画像がないことを表すマーク」を表示しないようにすることに関しては
を参考にさせていただきました。
更新通知にて障害が発生した場合
スコラボでは表示側への編集通知やカウンターの数値変化のリアルタイム性確保のためにWebSocketを利用しています。
障害発生時にはこれを使わないという方法を取る以外の選択肢がありません。
WebSocketでerrorイベントが発生した場合とWebSocketのチャンネルを開くことに失敗したケースでは、以前とっていたポーリングを行って変更を反映する方法に切り替えます。
ポーリングについてもSWRに便利なrefreshInterval
というオプションがあるため、これを利用しています。(isPollingがtrueになった場合に30秒に一度リクエストするという形です)
const {data} = useSWR(path, fetcher, {
refreshInterval: isPolling ? 30000 : undefined,
focusThrottleInterval: 1250,
dedupingInterval: 1250,
refreshWhenHidden: true,
...
})
これによって障害が発生した場合にも更新の反映に時間がかかってしまうものの、更新されないという状況は避けています。(バックエンドへの急激な負荷の増大を避けるために更新頻度を大きく下げています)
また、WebSocketのチャンネルが復旧した場合にはPollingをやめ、WebSocket側に戻るようになっています。
おわりに
スコラボが『配信画面を壊さない』ためにとっている対策についていくつかピックアップしてご紹介しました。
この他にもOBSのバージョンの差異によって表示にズレが起きる場合があることも把握しており、利用しているOBSバージョンとChrome Embedded Framework(通称CEF、ブラウザソース)のバージョンについてもエラー発生時・アクセス時に記録し、シェアの多いバージョンの動作確認を行うなどの確認作業や、バグへの対処につなげています。
途中で『配信画面が壊れる』という恐怖を絶対に感じさせてはいけないため、これらの対策で最も重視していることはすでに表示できているものを壊さないことです。
編集側はある程度挑戦的なことを行っている部分もありますが、それとは対象的に表示側はなるべく安全側に倒しています。
なにか他にもこんな対策が取れるんじゃない?みたいな気になる部分があればぜひコメントなどでお教えください!
また、この記事から弊社に興味を持っていただいた副業や転職先を探している方は私までご連絡ください!
Discussion