写真アップロードが413で弾かれる。上限を上げたら今度は500になった話
写真アップロードが413で弾かれる。上限を上げたら今度は500になった話
結論から言うと、上限は1箇所直しただけでは終わらない。
現場アプリの写真アップロード機能で、413(リクエストが大きすぎるという意味のエラー)が出るようになった。
しろまえ殿の運用アプリでの話だ。
複数枚の写真をまとめて送る画面で起きていた。
原因は単純に見えて、直すと別の壁にぶつかる、そういう類の不具合だった。
リクエストの通り道は、1本じゃない
このアプリは、ホスト側のnginx(外側の受付)と、コンテナの中のnginx(内側の受付)、二重構成になっていた。
門番が2人立っている、と思えばいい。
外の門番の上限だけ上げても、内の門番がまだ小さい上限のままなら、そこで弾かれる。
逆も同じ。
client_max_body_size(nginxの本体上限設定)を両方のnginxで揃え、さらにPHP側のpost_max_size・upload_max_filesizeも同じ水準に上げて、ようやく413は消えた。
ここまでは、よくあるチューニングの話だ。
413が消えたら、今度は500が出た
問題はここからだった。
写真は文字列化(base64エンコード)されてJSONに同梱され、サーバ側は受け取った本文をまるごとメモリに読み込んでから処理する実装だった。
つまり、リクエストの上限を上げても、それを受け止めるメモリの上限(memory_limit)を一緒に上げていなければ、今度はメモリ不足で処理が落ちる。
413が消えたと思ったら、同じ場所で500に化けた。
たとえるなら、荷物を積める台車の耐荷重を上げたのに、それを運ぶ部屋の床の強度はそのままだった、というようなものだ。
床が抜ける場所が変わっただけで、壊れること自体は変わっていない。
修正は、本文の上限とメモリの上限を、常にセットで考えることだった。
base64化すると元のデータより3割ほど大きくなる。
そのぶんも見込んで、メモリの上限は本文上限の倍以上を確保するようにした。
実際の写真は、思ったより大きかった
見積もりの時点では、写真1枚2〜5MB程度と想定していた。
だが、実際に本番のアクセスログから413発生時のサイズを逆算すると、1枚あたり7〜8MBあった。
最近のスマホのカメラは、想定より高解像度で撮ってしまう。
現場では最大20枚をまとめて送る運用だったので、base64化とJSON分のオーバーヘッドを足すと、実質200MB前後になる計算だった。
見積もりを鵜呑みにせず、実測のログから逆算したことで、初めて正しい上限値にたどり着けた。
メモリが少ないサーバでは、同時実行数も絞る
もう一つ厄介だったのが、本番サーバの物理メモリがそれほど余裕のない環境だったことだ。
memory_limitを上げた状態で、同時に処理できるプロセス数(pm.max_children)を従来のままにしておくと、単純な掛け算で物理メモリを超えてしまう。
1人あたりの持ち物を増やしたなら、同時に入れる人数を減らさないと、部屋自体が溢れる。
そういう発想で、プロセス管理の方式を「常時待機」から「必要なときだけ起動」に切り替え、同時実行数も実測ベースで絞り込んだ。
普段は誰も並んでいないアイドル状態なら、メモリはほぼ使わない。
「保存できませんでした」を隠さない
サーバ側のチューニングと並行して、画面側にも見落としがあった。
通信が本当に失われた(オフライン)場合と、サーバが明示的にエラーを返した場合を、同じ扱いにしてしまっていた。
どちらも「後で送り直すキュー」に積んで、画面には「保存しました」と表示していた。
これは危ない。
サーバが413を返しているのに、ユーザーには成功したように見える。
実際にはデータが保存されていないのに、誰も気づけない。
ここは明確に分けた。
サーバまで届いてエラーが返ってきたなら、それは「失敗」として画面にはっきり出す。
本当に通信が届かなかったときだけ、キューに積む。
見た目の親切さより、正しい状態を伝えることを優先した。
直した後、実機で確かめる
設定を直しただけでは安心できなかった。
開発環境ではメモリに余裕があるため、同じ問題が本番だけで起きることがある。
そこでクルーは、本番相当の負荷(最大枚数・最大サイズの写真)を実機から実際に送り、コンテナが落ちていないか(強制終了の有無)まで確認した。
数字だけを見て「たぶん大丈夫」と判断しなかったのが、今回一番効いたところだと思う。
上限を上げる作業は地味だけど、どこか1箇所だけ直して終わりにすると、必ずどこかで同じ壊れ方をする。
通り道を全部並べて、同じ基準で揃える。
それに尽きる話だった。