1
/
5

2021/8/1 HafH リニューアルで発生した障害とその対応記録

こんにちは、HafH で障害対応を行っている @kseta です。最近は Google Cloud と仲良くなるためにドキュメントや資料を読み込んでいる日々です。

2021年8月1日に HafH は大幅なリニューアルを実施しました。

note の冒頭にあるようにリリース時刻にシステムトラブルが発生してしまいました。当日は朝6時から JAL との航空サブスクサービス実証実験の申し込みが始まることも相まって、早朝から多数のユーザーが公開のために待機してくださってました。ユーザーのみなさまには大変なご迷惑をお掛けしました。改めて申し訳ございませんでした。

朝5時半からぶっ続けでパソコンに向き合いながら、HafH、本日リニューアル。8月1日は、とても大きな一日でした。ローンチ時には一部の皆様にアクセスしづらい状況となりご不便とご迷惑をおかけしました。

今回の障害については再発防止策を打ったものの厳密な原因は不明でした。が、調査が進みましたので、どこかの誰かが私たちと同じ失敗を踏まないように情報公開したいと思います。

結論

  • 厳密な原因は正確には不明。Google にもサポートいただきました。
  • GAE で Next.js アプリを Hosting すると SSG を利用した場合に GAE の CDN にキャッシュされてしまう
  • GAE の CDN のエッジキャッシュの仕様は公開されていない
  • GAE には CDN のキャッシュを削除する機能がなく発生するとキャッシュクリアされるまで待つしかない
  • 緊急の暫定対策として `?hoge` 等のクエリパラメーターを URL に付与してアクセスしてもらえばキャッシュ Hit しない
  • 回避するためには revalidate に短い時間に設定する(既にキャッシュされてしまった場合は効果なし)
  • SSG, ISR を利用する場合は Cloud CDN を GAE のフロントに配置する構成がベスト

何がおこったか

HafH は Google Cloud の GAE flexible environment を利用して Next.js を利用したアプリケーションで動作しています。リリース当日は0時頃からメンテナンスモードに切り替え、朝6時にリニューアル版を公開する予定でした。

最初に問題が発生したのは0時にメンテナンスモードに切り替えたときでした。メンテナンスモードへ切り替えるためのプログラムをリリースしましたが、メンテナンスモードに切り替わるユーザーと切り替わらないユーザーが社内で発生しました。限られた時間で対応判断が必要でしたが、以下を材料としひとまず予定していたメンテナンス作業を続行することにしました。

  • ブラウザキャッシュをクリアしたりネットワークをテザリングに切り替える等で解消されるケースがあったこと
  • WEB API は完全停止しておりメンテナンスモードに切り替わってなくてもデータが壊れるなど異常動作することはなかったこと
  • メンテナンス時間に予定していたリニューアル準備を進める必要があったこと
  • 8/1 リニューアルは様々な制約から延期することができないものであったこと

そして朝6時の公開を迎えました。

私は深夜対応担当ではなく朝8時頃に起きて日中待機する担当でしたが朝に電話が鳴りリリース時刻に「切り替わらない」ということを知ったときは長期戦を覚悟したことを覚えています。結局、この事象は確認できた範囲では数日後まで続き、時間経過でしか解決できませんでした。

  • 00:00頃 メンテナンスモードに切り替え(HTMLを差し替え)
  • 06:00頃 リニューアル版をデプロイ完了。一部のユーザーでリニューアル版が表示されない問題を確認
  • 08:00頃 解消されるユーザーが徐々に増えていることを確認
  • 数日後 確認できる範囲では全て問題解消

どのように対応したか

まず初動の15分でブラウザキャッシュ削除、シークレットモードでアクセス、ブラウザ変更、再デプロイ等を試してみました。これらでは解消せず GAE のエッジキャッシュにあたりをつけ、Next.js のキャッシュ設定の影響の可能性と並列で調査を行いました。

暫定回避策として URL に ?hafh-day-2021 のようなクエリパラメーターを付与すると必ずリニューアル版が表示されることに気づき SNS 等でクエリパラメーター付の新しい URL で案内を行いました。

この問題が発生しているのは Next.js で SSG を利用しているページのみであり、GAE の app.yaml に設定可能な expiration 等のパラメーターはこの事象には有効ではないこと等が確認できました。

GAE flexible のエッジキャッシュがどのような条件で作成され、既に作成されてしまったそれらをどのように削除するかは発見することができず時間経過での解決を祈るしかできませんでした。

後日、調査結果

Google のサポートの方にエンジニアと繋いでいただき以下の回答をいただきました(意訳)。

  • GAE のエッジキャッシュは Google Frontend に作成される
  • このエッジキャシュを削除する方法はない
  • このエッジキャッシュの作成方法は公開されていないが Cloud CDN と類似している(はずではないか)

Cloud CDN がキャッシュする条件については以下に記載があります。

大筋、キャッシュ作成条件のポイントは以下の理解で良いようです。

  • GET リクエストによるレスポンスであること
  • HTTP Status Code が以下であること
    • 200, 203, 204, 206, 300, 301, 302, 307, 308, 404, 405, 410, 421, 451, 501
  • Cache-Control に以下が設定されていること
    • public が設定されいてる
    • s-maxage, max-age または Expries が設定されている
    • no-store, no-cache, private が設定されていない
  • レスポンスのサイズが一定の容量以下であること(10MB)

Next.js で SSG を行うと Cache-Control: s-maxage=31536000 stale-while-revalidate が必ず設定される実装になります。この場合、必ずエッジキャッシュが作成されてしまいます。そして一度作成されると削除する方法はありません。デプロイしてもサービスを削除してもキャッシュがヒットし続けます。

# https://github.com/vercel/next.js/blob/51559f5c645c6ad8ba1bf2da74bac284015f887e/packages/next/server/send-payload.ts#L35
export function setRevalidateHeaders(
res: ServerResponse,
options: PayloadOptions
) {
if (options.private || options.stateful) {
if (options.private || !res.hasHeader('Cache-Control')) {
res.setHeader(
'Cache-Control',
`private, no-cache, no-store, max-age=0, must-revalidate`
)
}
} else if (typeof options.revalidate === 'number') {
if (options.revalidate < 1) {
throw new Error(
`invariant: invalid Cache-Control duration provided: ${options.revalidate} < 1`
)
}

res.setHeader(
'Cache-Control',
`s-maxage=${options.revalidate}, stale-while-revalidate`
)
} else if (options.revalidate === false) {
# ここでセットされる
res.setHeader('Cache-Control', `s-maxage=31536000, stale-while-revalidate`)
}
}

これを回避するためには SSR でキャッシュされないように Cache-Control をセットするか revalidate に短い時間を設定することです。

return {
props: {},
revalidate: 10
}

注意点としては ISR としては機能せず Cache-Control を強制的にセットするためだけの苦しい使い方です。さらに、エッジキャッシュが複数のエッジロケーションに分散されるらしくキャッシュヒットするものしないものが混在してしまうため長い時間のキャッシュはセットするとそれはそれで問題が発生します。

最終的には Cloud CDN を GAE のフロントに配置するのがベストだと Google のサポートからもアドバイスをいただきました。

このあたりの Cloud CDN + GAE の設定や Vercel 以外での ISR の実現については以下の記事が大変参考になりました。このあたりはとても情報が少なく本当に助かりました。CatNose さんありがとうございます!

まとめ

結論は冒頭にまとめたとおりです。GAE のエッジキャッシュとうまく付き合うためには Cloud CDN + GAE を使いましょう!

GAE はアプリケーションの動作環境やスケーリングをマネージしてくれる大変便利なサービスですが、それゆえに問題発生時には GAE が提供する環境とうまく付き合っていかなくてはいけません。特に今回のようなキャッシュ関連についてはキャッシュ削除機能がないために特に注意が必要です。

HafH では今回の問題に対して Cache-Control の設定によって完全に対策済ですが、よりベストなパフォーマンスと DX (Development Experience) を求めて Cloud CDN の導入を進める予定です。

最後に

HafH にはインフラに強いエンジニアがまだいません。一人目のインフラエンジニアとして興味もっていただいた方や Next.js, Rails 製の API のキャッシュ戦略を考えガンガン改善していくことに興味を持っていただいたエンジニアの方はぜひ Wantedly より応募ください😄

株式会社KabuK Style的招募
4 Likes
4 Likes

本週排名

展示其他排名