LoginSignup
3
1
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

Cloud Run で APIサーバー を動かす時の初歩的な落とし穴

Last updated at Posted at 2024-01-08

概要

Cloud Runにサービスを作成し、FastAPIでリクエストを処理する。これはその時の失敗談です。
Cloud Runを使い慣れている方にとっては当たり前かもしれません。。

APIの仕様

リクエストを受け取ってスクレイピング処理などを実行します。
スクレイピングに時間がかかるため、 処理が完了する前に クライアントへレスポンスを返します。

from fastapi import FastAPI
import asyncio
from stock_scraping_service import stock_scraping_service
from dividend_scraping_service import dividend_scraping_service
from spreadsheet_update_service import spreadsheet_update_service
from market_update_service import market_update_service
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = ["http://localhost:3000"]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET"],
    allow_headers=["*"],
)

async def async_update_stock_info(
    stock: bool, dividend: bool, market: bool, spreadsheet: bool
):
    if stock:
        await stock_scraping_service.stock_scraping()

    if dividend:
        await dividend_scraping_service.dividend_scraping()

    if market:
        await market_update_service.update_market_and_industries()

    if spreadsheet:
        await spreadsheet_update_service.spreadsheet_update()

@app.get("/update_stock_info")
async def update_stock_info(
    stock: bool = True,
    dividend: bool = True,
    market: bool = True,
    spreadsheet: bool = True,
):
    try:
        print(
            f"stock: {stock}, dividend: {dividend}, market: {market}, spreadsheet: {spreadsheet}"
        )
        # 非同期でこのタスクが実行される
        asyncio.create_task(
            async_update_stock_info(stock, dividend, market, spreadsheet)
        )
        # 上記タスクの完了を待たずにレスポンスを返す
        return {"message": "保有株情報を更新中です"}
    except Exception as e:
        return {"message": "エラーです"}

何があったか

ローカルのDocker上では毎回正常に動くのに、Cloud Run上では安定せず、正常に動いたり失敗したりします。特に失敗しやすいのはスクレイピング処理で、Chrome Driverが起動せずエラーになります。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options


def boot_driver():
    # Chrome オプションの設定
    chrome_options = Options()
    chrome_options.add_argument("--no-sandbox")  # Chrome の保護機能を無効化する(Docker環境で動かすため)
    chrome_options.add_argument("--headless")  # ヘッドレスモードを有効にする
    chrome_options.add_argument("--disable-gpu")  # GPUを無効にする
    chrome_options.add_argument("--disable-dev-shm-usage")  # 共有メモリ使用を無効化

    # Docker環境専用の記述
    chrome_driver_path = "/usr/local/bin/chromedriver"

    # ドライバーの起動
    driver = webdriver.Chrome(
        options=chrome_options,
        executable_path=chrome_driver_path,
    )

    return driver

下記のいずれかのエラーが出ます。

selenium.common.exceptions.SessionNotCreatedException: Message: session not created: DevToolsActivePort file doesn't exist
selenium.common.exceptions.WebDriverException: Message: chrome not reachable

原因と解決方法

Chromeのオプション

エラー文でググると下記のChromeのオプション設定を追加しろ、という内容の記事がたくさん出てきます。
DockerでSeleniumを使う際は、確かにこれらの記述は必要なようです。

chrome_options.add_argument("--no-sandbox")  # Chrome の保護機能を無効化する(Docker環境で動かすため)
chrome_options.add_argument("--headless")  # ヘッドレスモードを有効にする
chrome_options.add_argument("--disable-gpu")  # GPUを無効にする
chrome_options.add_argument("--disable-dev-shm-usage")  # 共有メモリ使用を無効化

しかし私のコードには既にこれらのオプションは存在します。

Cloud Runの設定

原因がわからず困り果てていたところ、Stack Overflowにこのような投稿を見つけました。
そう、原因はコードではなく、Cloud Runの設定にあったのです!

リビジョンの設定を確認すると、CPUの割り当て設定が「リクエストの処理中にのみ CPU を割り当てる」になっていました。

これを「CPUを常に割り当てる」に変更すると処理が毎回成功するようになりました。

スクリーンショット 2024-01-08 14.29.35.png

なぜCPUを常に割り当てる必要があるのか

おそらくリクエストの処理中とは リクエストを受け取ってからレスポンスを返すまで なのでしょう。
私のAPIは 処理が完了する前にレスポンスを返す 仕様だったため、処理が完了する前にCPUの割り当てがなくなってしまうのです。

なぜ時々成功していたのか

「リクエストの処理中にのみ CPU を割り当てる」の設定の時でも、処理によっては成功していました。
おそらくレスポンスを返してからCPUの割り当てが無くなるまでに少し猶予があり、その間に処理が完了していたのだと思います。

最後に

当時の私はローカルとCloud Runで実行環境が異なるために起こる問題だと思い込んでいたため、解決までにかなりの時間を費やしてしまいました。Cloud Run上でしか再現できないためデバッグも大変でした。。
この記事が同じように悩んでいる人の役に立てば幸いです。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1