はじめに
皆様、Seleniumでスクレイピング生活してますでしょうか?
Webサービスを持っている会社さんであれば、自社のサイトの正常性を定期的に確認するべく、
EventBridge + Lambdaで定期的に監視することもあるでしょう。
※CloudwatchSyntheticsを使え という話は一旦置いときましょう。笑
私は、業務上LambdaでSelenium定期実行することが多いのですが、
掲題の通り稀にWebDriverがハングする事象を発見しました。
今回はそれに対する考察と対策について記載したいと思います。
※本記事の文章には生成AIを一切使用しておらず、すべて自分の言葉で表現しています
構成図
Dockerコンテナ型Lambdaでデプロイします。
今回は事象の再現が難しいので、インフラを構成するためのコードは載せません。
事象
言語:Python3.12
OS:AmazonLinux2
Lambdaスペック:メモリ2GB
コード:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
# オプション設定
options = Options()
options.add_argument("--headless")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--single-process") # これがないとLambdaで動かない
service = Service(executable_path="/opt/chrome/chromedriver")
# WebDriverの起動
driver = webdriver.Chrome(service=service, options=options)
# タイムアウト設定(秒)
driver.set_page_load_timeout(10)
driver.set_script_timeout(10)
try:
url = "https://hotel-example-site.takeyaqa.dev/ja/"
driver.get(url)
except Exception as e:
pass
finally:
driver.quit()
この条件下の時に、たまーに(100回に1回くらい)driver.get(url)
のところでハングします。
Lambdaの最大実行時間は15分ですが、15分いっぱいまでハングします。
set_page_load_timeout
、set_script_timeout
でタイムアウトを指定していますが、まったく意味を成しません。
体感ですが、傾向としては重たいページで頻度が多いイメージです。
考察
ローカルやEC2上で動かしているときに遭遇したことはないので、
おそらく--single-process
が原因だと思います。
シングルプロセスで動かしているので、Webリソースの取得で通信不可になった際に割り込みでタイムアウト処理(=切断処理)が入らないと予想しています。
※当てずっぽうです。WebDriver詳しい方居れば教えてください。
対策
結論としては、timeout_decoratorを使いましょう。
pip install timeout-decorator
でインストールしたうえで、以下のように実装します。
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
import timeout_decorator
import os
import signal
import subprocess
# オプション設定
options = Options()
options.add_argument("--headless")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--single-process") # これがないとLambdaで動かない
service = Service(executable_path="/opt/chrome/chromedriver")
# WebDriverの起動
driver = webdriver.Chrome(service=service, options=options)
# タイムアウト設定(秒)
driver.set_page_load_timeout(10)
driver.set_script_timeout(10)
@timeout_decorator.timeout(10)
def access():
url = "https://hotel-example-site.takeyaqa.dev/ja/"
driver.get(url)
driver.quit()
try:
access()
except Exception as e:
# Chromeプロセスが残存している可能性もあるので、killする
result = subprocess.run(['pgrep', '-f', 'chrome'], stdout=subprocess.PIPE, text=True)
pids = result.stdout.strip().split('\n')
for pid in pids:
if pid.isdigit():
os.kill(int(pid), signal.SIGKILL)
-
@timeout_decorator.timeout(10)
を付与したメソッドにラップすることで、ハングしてもtimeout_decoratorが割り込みしてException発生させてくれます。 -
driver.get(url)
でハングした場合は、driver.quit()
まで行かないので、Chromeのプロセスがゾンビで生き残ってしまいます。
そのため、except Exception as e:
の後にkillする仕組みを導入しています。 - 余談ですが、Lambdaは内部的に同じインスタンスが使い回されることがあります。(サーバレスとは言っても中身はEC2インスタンス(と思われます)。)
前回実行時に/tmp配下に保存したファイルや、プロセスが残存している可能性があるので、後処理を必ず実装することをお勧めします。
(余談)
ECS Fargateを使用するという手もあります。
ただしLambdaよりは高額になるのと、タスクとして定義するので起動レイテンシが若干ネックです。
以上