🤯

Zennで発生した障害の原因と行なった対策のまとめ

2021/02/25に公開
4

2021/02/24の11時頃〜1時間ほどzenn.devにアクセスしづらい・アクセスできない問題が発生していました。その後も3時間ほど一部のページへのアクセスができない状況となっていました。Zennに投稿してくれた方、見に来てくれた方、ご迷惑をおかけしてすみませんでした。

今回の障害は学びが多かったので、個人の記事として残しておくことにします。

原因

今回の障害は、使用しているクラウドサービスではなく、Zenn自体に原因がありました。

1. KaTeX記法により生成されるHTMLが思った以上に大きかった

ZennのマークダウンエディターではKaTeX記法をサポートしています。例えば、$a\ne0$と書くとa\ne0と表示されます。

KaTeXはサーバーサイドレンダリングをサポートしており、KaTeX記法からの数式のHTMLへの変換はサーバーサイドで行なっていました。DBにはマークダウンだけではなく、変換後のHTMLもあらかじめ保存する形にしていました。

ここで見落としていたのが、KaTex記法がHTMLへ変換されたときにサイズが膨れ上がるという点です。例えば、以下のように行列を書くと…

$$
\begin{pmatrix}
a_{11} & a_{12} & a_{13} & a_{14}\\
a_{21} & a_{22} & a_{23} & a_{24}\\
a_{31} & a_{32} & a_{33} & a_{34}\\
a_{41} & a_{42} & a_{43} & a_{44}
\end{pmatrix}
$$

このように表示されます。

\begin{pmatrix} a_{11} & a_{12} & a_{13} & a_{14}\\ a_{21} & a_{22} & a_{23} & a_{24}\\ a_{31} & a_{32} & a_{33} & a_{34}\\ a_{41} & a_{42} & a_{43} & a_{44} \end{pmatrix}

この数式のHTMLを見ると…


これでも行列を構成するHTMLの一部

タグがネストされてまくっており、インラインでスタイルが指定されている部分も多く見られます。容量(圧縮なし)を見ると、マークダウンの時点では200バイト程度ですが、HTMLでは20キロバイト近くなっています。数式が増えてくると、DBの1つのフィールドにかなりのサイズの文字列を突っ込むことになります。

この事実に気づけていなかったことが、今回の障害の根本的な原因です。

2. サイズの大きいレコードをまとめて扱っていた

バックエンド(APIサーバー) の本の表示に関するロジックで、このような巨大なテキストを含むレコードをまとめて扱っている部分がありました。今回の障害では、このエンドポイントへのアクセスが集中し、メモリの使用率が急増していたことが分かりました。

3. オートスケールの設定をケチっていた

Zennはもともと個人開発のサービスで、インフラ周りのコストをかなりケチっていました。アクセスが増えてきた最近もオートスケールの設定を見直すのをすっかり忘れていました。

具体的には最大インスタンス数を少なく設定しており、今回の障害ではすぐに天井にぶつかってしまいました。

4. Vercelのペイロード上限を超えてしまっていた

フロントエンドのアプリ(Next.js)を動かしているVercelでもエラーが発生していることが分かりました。

[ERROR]...LAMBDA_RUNTIME Failed to post handler success response. Http response code: 413.

ステータスコード413Payload Too Largeという意味です。全く気にかけていなかったのですが、VercelのSeverless Functionsでは5MB以上を超えるペイロードを扱うことができません。

Serverless Function Payload Size Limit →

Next.jsのgetStaticPropsgetInitialPropsgetServerSidePropsはどれもこの制限の対象となります。

getStaticPropsの中でAPIへリクエストを送った際、数式のHTMLが含まれる巨大なデータが返ってきたため、この上限に達してしまっていたというわけです。

このエラーが発生したことにより、リクエストが何度もリトライされ、APIサーバーへの負担が急増しました。

以上が今回の障害の主な原因です。KaTeX記法によるHTML肥大問題と、Vercelのペイロード問題については完全に見落としていたため、遅かれ早かれどこかで起きていた障害だと言えます。

暫定対応

複数の要因が重なっていたため、一つずつ対策をしていく必要がありました。

バックエンド(APIサーバー)のオートスケール設定を見直し

まずはGAE(Google App Engine)のオートスケール設定を見直しました。GAEのFlexible環境ではapp.yamlの設定を見直すだけで設定を変更できます。

今回は、最大インスタンス数を数倍に変更し、memoryの値も挙げました。これにより、APIサーバーは復旧できました。

Vercelのペイロード制限を超えていた部分をCSR(クライアントサイトレンダリング)に変更

Vercelのペイロード制限を超えてしまうコンテンツについて、著者の方にお願いをして一時的に投稿を非公開にさせていただきました。申し訳ない気持ちでいっぱいでしたが、快く了承いただき、落ち着いて障害への対応ができました。改めてありがとうございました。

問題となっていたのは、Next.jsのgetStaticPropsからのAPIリクエストです。この処理がVercel上で行われる限り、ペイロード制限は免れられないので、一時的にブラウザから直接フェッチするように変更しました。

ユーザー体験はやや損なわれましたが、暫定対応にはなりました。これでサービス全体が使えるようにはなりました。

根本的な解決策

KaTeXの変換をサーバーではなくブラウザ上で行うように

復旧はできたものの、ブラウザから投稿データを読みに行くのはユーザー体験的にもSEO的にもあまり良くありません。一方で文字数制限を厳しめに設定しても、数式が大半を占める投稿では同じ問題が発生します。

そこで根本的な対策としては、以下の2つを検討しました。

  1. Vercelから他のクラウドサービスへ移行する
  2. KaTeXをサーバーではなくブラウザ上で変換する

少し悩みましたが、KaTeXのサーバーでの変換は、アプリへの負荷が大きいことから、いずれにせよ見直す必要があると感じ、(2) に取り掛かることにしました。

KaTeXのブラウザ上での変換はZennのようなSPA的な動きをするアプリでは変換処理をかけるタイミングの調整が難しいのですが、Web Components の Custom Elements を取り入れることで、Next.js側の処理をほとんど変えることなく移行ができました。

以下の記事と同じようなことをやっているので、詳細が気になる方はチェックしてみてください。

https://zenn.dev/steelydylan/articles/zenn-web-components

これでVercelのペイロード制限に引っかかることは無くなったため、暫定対応としてCSRにしていた投稿の表示をSSRに戻しました。

複数のレコードを分割して取り出すように

また、本の複数のチャプターの本文をまとめて扱っていた部分を分割し、一度に負荷がかからないように変更しました。


この1日で行なった対応は以上になりますが、サーバーやデータベースの設定の見直しが必要だと強く感じています。幸いこれから一緒にやっていくことになった開発メンバーはこのあたりの経験が豊富そうなので、力を借りてやっていきたいと思います。

Discussion

スースー

お疲れ様でした!
自分もアクセスが増えてきたときのオートスケール設定を見直すというところを忘れずにやろうと思います!(自社サービスにて・・・)

catnosecatnose

ありがとうございます!今回の件で冷や汗かいたので、サーバー監視などもして異変に早めに気づけるようにしておこうと思ってます(そのあたり詳しいメンバーがいるのでありがたい)