0. はじめに
Cloud Functionsを使用したAPI実装での予期せぬ503エラーに遭遇し、思わぬ見落とし(無知)に時間を取られてしまったため、戒めとして原因特定までの過程と解決(回避)方法をまとめた。
※コーディング内容などについては随時割愛。
1. 結論
原因はレスポンスヘッダの設定に問題があり、ファイル名にマルチバイト文字が設定できないためだった。少なくともCloud Run Functionsでは🙅♂️
# タイムスタンプを取得
timestamp = datetime.now().strftime('%Y%m%d%H%M%S%f')[:17]
filename = f"{timestamp}_オンラインレポート.zip" # 問題個所
# バイトデータをレスポンスに入れて返す
return https_fn.Response(
response=zip_binary,
status=200,
headers={
"Content-Type": "application/zip",
"Content-Disposition": "attachment",
"filename": filename}
)
以降は実施内容や解決方法について記載していく。
2. 開発環境
- 言語: Python 3.12 + Firebase-tools
- 実行環境: Cloud Functions for Firebase
(v2 APIで実装しているので厳密にはCloud Run Functions) - API管理ツール: Apidog
3. 問題
実装したAPIはjsonデータを受け取り、加工した内容をテキストに出力してzip圧縮したデータを返却するようにしている。
先述のAPIをfirebase deployを実行しApidogで確認したところ、次のレスポンスが返ってきた。
503 Service Unavailable
まさかのFirebaseで障害発生!?かと思いきや、その後時間を空けて実施しても再現しているのでそういうことではなかったようだ。🧐
4. 確認したこと
まずはGCPのログエクスプローラを確認した。リクエストは無事受け取れていたようだが、
その後に1件のみエラーログが出ており、内容は以下の通り:
The request failed because either the HTTP response was malformed or connection to the instance had an error.
Additional troubleshooting documentation can be found at:
https://cloud.google.com/run/docs/troubleshooting#malformed-response-or-connection-error
要するにレスポンス形式に不備があるかAPIインスタンスに起因するエラーとのこと。
該当リンクがあったので内容確認しにいったところ、公式の対処法としてはざっくり次の4点:
- Cloud Logging を確認する。 Cloud Logging を使用して、ログでメモリ不足エラーを探します。コンテナ インスタンスがメモリの上限を超えているというエラー メッセージが見つかった場合は、この問題を解決するための推奨事項をご覧ください。
- アプリレベルのタイムアウト。 リクエストが Cloud Run で設定されたリクエスト タイムアウトに達する前にエラーコード 503 で終了している場合は、言語フレームワークのリクエスト タイムアウトの設定の更新が必要になることがあります。
- [CRITICAL] WORKER TIMEOUT Python デベロッパーは Gunicorn のデフォルトのタイムアウトを更新する必要があります。
- ダウンストリーム ネットワークのボトルネック。 場合によっては、負荷テスト中などに、ダウンストリーム ネットワークのボトルネックの間接的な結果として 503 エラーコードが返されることもあります。たとえば、サービスがサーバーレス VPC アクセス コネクタ経由でトラフィックをルーティングする場合、次の手順を行い、コネクタがスループットのしきい値を超えていないことを確認します。
- 1 つのコンテナへのインバウンド リクエストの上限。 コンテナあたりのリクエスト レートが高いことが 503 エラーの原因となる、既知の問題が存在します。コンテナ インスタンスが 1 秒あたり 800 件を超えるリクエストを受信すると、使用可能な TCP ソケットが枯渇する可能性があります。この問題を是正するには、次のいずれかを試してください。
(公式ドキュメントより抜粋)
ということなので、いろいろ試した結果を以下にまとめる。
①インスタンスのリソース確認
一部スクショを抜粋、ここではスループットとCPU稼働率を確認したが、数B/s程度と稼働率10%未満なのでこれが原因ではなさそう。
②APIのコンテナインスタンスを変更
メモリ不足エラーログはでていないが、コンテナインスタンスを1段階上げて確認。
from firebase_functions import https_fn
from firebase_functions.options import MemoryOption
# Cloud Run Fucntionsではデフォルトは256MBのインスタンスになる
@https_fn.on_request()
def hoge_hoge(req: https_fn.Request) -> https_fn.Response:
(以下略)
# memoryプロパティで指定
@https_fn.on_request(memory=MemoryOption.MB_512)
def hoge_hoge(req: https_fn.Request) -> https_fn.Response:
(以下略)
①でも確認したようにやはりインスタンスのリソース不足というわけではなかったので、エラーは解消されず。
③レスポンスヘッダの見直し
最終的にはレスポンスヘッダの設定を見直すために、Content-Disposition
の filename
を半角英数字で書き換えて動作確認を行ったところエラーが解消された。調べていくとどうやらもともとマルチバイト文字を指定すると文字化けする仕様* (語弊を招く言い方) らしい。
# 問題のあるコード
filename = f"{timestamp}_オンラインレポート.zip"
# 修正後のコード
filename = f"{timestamp}_OnlineReport.zip"
そのため、RFC8187/RFC5987 を参考に次のような方法が可能か試してみたが、同じく503エラーとなってしまった。このヘッダの設定の仕方が間違っているのか、Firebase-tools側のモジュールが対応していないのかまでは確認できていない。
# encode_to_urlはfile_nameをutf-8でURLエンコーディングするメソッド
"filename*": f"UTF-8\'\'{encode_to_url(file_name)}"
5. 最後に
今回のAPIの要件としては、出力されるファイル名にマルチバイト文字を使わない方向で調整できたからよかったものの、FirebaseにてAPIを使用する際にはこの問題は根本的に解決できていないので、どなたか有識者がいたらご教示いただきたい。🙇♂️(他力本願)