スクレイピング

リトライとバックオフ:スクレイパーはいかにして小さな失敗をBANに変えるか

スクレイパーのBANのほとんどは自業自得だ。単純なリトライループは、サイトが速度を落とすよう求めたまさにそのタイミングで、1件のスロットリング応答に対してトラフィックのバーストで応じてしまう。

Chris Collins

Chris Collins

2026年8月23日 · 1 分で読める

スクレイパーが遅い応答に当たり、リトライする。そのリトライも失敗し、すぐにまたリトライする。同じ壁に同じ瞬間に当たった他のすべてのワーカーも同様だ。数秒のうちに、すでに「量を減らしてほしい」と伝えていたサイトに対してトラフィックのバーストを送りつけることになり、一時的なスロットルだったものが、関係するすべてのアドレスに対するハードブロックに変わってしまう。サイトがエスカレートしたのではない。あなたのリトライループがエスカレートしたのだ。

これは、うまく作られたスクレイパーが自分自身を傷つける最も一般的な方法であり、完全に避けられるものでもある。リトライロジックは負荷分散の仕組みであって、粘り強さの仕組みではない。この二つの考え方の違いが、緩やかに劣化するジョブと、自らをBANに追い込むジョブとの違いになる。より広範な衛生管理についてはavoiding blocksで扱っている。本稿はリトライパスそのものの仕組みを扱う。

なぜ素朴なリトライは事態を悪化させるのか

三つの力学が組み合わさり、それらは互いに増幅し合う。

第一に、リトライは負荷そのものが問題になっている時にまさに負荷を追加してしまう。429や遅い応答は「送信量を減らしてほしい」というリクエストであり、それに対してさらに多くのリクエストで応えることは信号を反転させることになる。第二は同期の問題だ。同じ瞬間に失敗し、同じ固定間隔で待機するワーカーは、同じ瞬間にリトライすることになる。つまり分散するのではなく、協調したバーストとして到達し、そのバーストの各ラウンドが次のラウンドを再び同期させてしまう。第三に、リトライは通常アドレスごとにカウントされるため、同じIPからターゲットを叩き続けると、そのアドレスはスロットル状態からフラグ付き状態へと移行する。これはインシデントよりも長く続く評判の問題であり、そのアドレスは次のジョブにまで付いて回る。

これらが組み合わさると、素朴なループは回復可能な状態を、まさにアンチボットシステムが検知するために作られたトラフィック形状に変えてしまう。同じ失敗でも、抑制を持って対処していれば自然に解決していたはずのものだ。

リトライする前に分類する

第一のルールは、すべての失敗がリトライに値するわけではなく、値するものであっても異なる扱いが必要だということだ。レスポンスを三つのバケツに分類する。

一部の失敗は一時的でリトライする価値がある。接続リセット、タイムアウト、502、503、504、そして429だ。これらは、システムが一時的に「対応不能」であることを表しているのであって、「対応する気がない」ことを表しているわけではない。特に429は、ペーシングに関する明示的な指示であり、拒否ではない。一部は終端的で、決してリトライしてはならない。404、400、401、複数アドレスにわたって持続する403、そしてきれいにパースできたが望むものが何も含まれていなかったページだ。これらをリトライすることは、変わり得ない結果のために帯域と評判を消耗するだけであり、パース失敗はコードのバグであって、リトライは永遠に忠実にそれを再現し続けるだけだ。

第三のバケツが危険なものだ。成功したように見えて実際はそうではないレスポンスである。チャレンジページ、汎用的または空の結果、切り詰められたリスティング、ランディングページへのリダイレクトは、いずれも200ステータスで届く可能性があり、ステータスコードだけを信頼するスクレイパーは、それらを喜んでデータとして記録してしまう。レスポンスを成功としてカウントする前にボディを検証すること。これがdetecting blocked or fake contentの本質だ。ソフトブロックはリトライ可能な失敗だが、それが失敗だと気づいた場合に限る。

指数バックオフと、なぜジッターが必須なのか

リトライ可能なバケツについては、試行間の遅延は増加していくべきであり、標準的な形は指数関数的なものだ。1秒待ち、次に2秒、次に4秒、次に8秒。増加が重要なのは、苦しんでいるターゲットに、一定の連打ではなく、徐々に大きな余地を与えるからだ。

しかし指数バックオフだけでは十分ではなく、ここが人々が見落とす部分だ。100個のワーカーが一斉に失敗し、全員がちょうど1秒待つなら、1秒後に一斉にリトライすることになる。バックオフは増加したが、バーストは生き残ったままであり、単に同じスパイクをタイムライン上でずらしただけだ。解決策はジッターである。計算された値をそのまま使うのではなく、各遅延をランダム化する。フルジッター、つまりゼロから現在の上限までの間でランダムに選んだ待機時間は、同期した失敗をなめらかなリトライの分布へと広げてくれる。これは2行の変更であり、本稿で最も効果的な単一の対策だ。

これにもう二つのルールが加わる。サイトがRetry-Afterを送ってきた場合はそれに従うこと。なぜなら、このヘッダーはターゲットが「どのくらい待つべきか」を正確に伝えているものであり、自分のスケジュールを優先してそれを無視することは無礼であるだけでなく逆効果でもあるからだ。そして遅延と試行回数の両方に上限を設けること。5回失敗したリクエストが6回目で成功する見込みはなく、無制限のリトライは、すでに失われたものに対していつまでも諦めないだけの遅いやり方に過ぎない。

import random, time
import requests

PROXY = "http://customer-USERNAME-country-us:PASSWORD@p.shifter.io:443"
PROXIES = {"http": PROXY, "https": PROXY}

TRANSIENT = {429, 502, 503, 504}
MAX_ATTEMPTS = 5
BASE, CAP = 1.0, 60.0

def fetch(url):
    for attempt in range(MAX_ATTEMPTS):
        try:
            r = requests.get(url, proxies=PROXIES, timeout=20)
        except requests.RequestException:
            pass                                  # transient: fall through to backoff
        else:
            if r.status_code == 200 and is_valid(r.text):
                return r.text                     # validate the body, not just the code
            if r.status_code not in TRANSIENT:
                return None                       # terminal: do not retry
            after = r.headers.get("Retry-After")
            if after:
                time.sleep(min(float(after), CAP)) # the target told you the answer
                continue

        ceiling = min(CAP, BASE * (2 ** attempt))
        time.sleep(random.uniform(0, ceiling))     # full jitter, not a fixed delay
    return None

ローテーションか待機か:リトライがよく間違える判断

レジデンシャルプロキシを使う場合、もう一つの軸がある。失敗には、時間で応えることも、別のアドレスで応えることも、その両方で応えることもできるが、選択を誤るとどちらかを無駄にすることになる。

信号がレートに関するものなら待機する。429やRetry-Afterは、ターゲットが「あなたのペースが速すぎる」と言っているのであり、同じペースを維持するために新しいアドレスに切り替えることは、まさに回避行動に見える振る舞いであり、1本のルートではなくプール全体を焼き尽くす結果になりかねない。代わりに速度を落とすべきだ。

信号がアドレスに関するものならローテーションする。ブロックページ、持続する403、一本のルートで繰り返し現れるチャレンジは、その特定のアドレスがもはや信頼されていないことを意味し、待機してもそれが回復することはない。そのルートを引退させ、新しいルートで続行する。これがfailoverのパターンであり、代替となるアドレスのreputationこそが、そのリトライが実際に役立つかどうかを決めることに注意してほしい。ローテーションが誤りとなる唯一のケースは、セッション途中の作業だ。フローが保持されたアイデンティティに依存している場合、アドレスを変更するとそれが壊れてしまう。したがってsticky session内での失敗は、既存のセッションの下でアドレスを入れ替えるのではなく、新しいセッションでシーケンスを再開することを意味する。

タイムアウトはこの二つの中間に位置し、反射的な対応ではなく、それ自体の診断に値する。reasons requests time outには、ターゲットの遅さ、健全でないルート、そして自分自身の並行数が高すぎることが含まれるからだ。

バジェットとサーキットブレーカー

リクエストごとのリトライルールだけでは十分ではない。それらにはシステム全体を見渡す視点がないからだ。二つの仕組みがその視点を与えてくれる。

リトライバジェットは、リトライを全トラフィックに対する割合として上限を設ける。例えば、特定のターゲットに対するリクエストのうち、リトライは最大でも10パーセントまでとする、といった具合だ。通常の状態では、このバジェットは手付かずのままだ。何かが広範囲に壊れた場合、バジェットは即座に使い切られ、余分なリトライは単純に発生しなくなる。これこそが求めている性質だ。リトライは孤立した失敗には役立つが、一般的な障害の最中は明確に有害であり、バジェットはその違いを自動的に見分けてくれる。

サーキットブレーカーはさらに踏み込む。ターゲットごとの失敗率を追跡し、それがしきい値を超えたら、そのターゲットへの送信を完全に停止し、クールダウン期間を設ける。見込みのないリクエストを少しずつ送り続けて探り続けるのではなく。クールダウンの後、少数のリクエストを通過させ、それらが成功すれば再開する。これはターゲットをあなたの集中攻撃から守ると同時に、あなたのアドレスが、今誰にも応答していないサイトに対する失敗を蓄積することからも守ってくれる。両方の仕組みはグローバルではなくターゲットごとであるべきだ。なぜなら、一つの壊れたサイトが、他の50サイトからデータを収集しているジョブを止めてしまうべきではないからだ。

リトライは金がかかり、データの中に隠れる

はっきり述べておくべき二つの帰結がある。

すべてのリトライは、あなたが料金を払うリクエストだ。帯域幅課金のレジデンシャルプロキシでは、リトライの嵐は明確な費用項目となり、午後中ずっとダウンしているサイトに対して静かに5回リトライを続けるジョブは、何の成果もなく驚くほどの量のデータを動かしてしまう可能性がある。これはcutting proxy bandwidth costsにおけるコスト構造への、あまり目立たない寄与要因の一つだ。

そしてリトライ率は、ダッシュボードに載せるべき先行指標だ。特定のターゲットに対するリトライ率の上昇は、その防御機構が変化したか、あなたのペーシングがずれたことを示す最も早い警告であり、成功率が崩れるよりもずっと早く現れる。したがってmonitoring the pipelineは、最終結果だけでなく、試行と結果の両方を追跡すべきだ。静かにリトライを重ねて見かけ上正常な成功率にたどり着くスクレイパーは、問題を隠しているのであって、解決しているのではない。

最後に、リクエストが試行回数を使い果たした時、それを捨ててはならない。デッドレターキューに積み、現在のバーストの中ではなく、次回の実行時か長いクールダウンの後に、ずっと後になってリトライすること。インシデント中に失敗したほとんどの項目は、1時間後には自然に成功する。キューはハードな失敗を、先延ばしにされた失敗へと変えてくれる。

結論

リトライロジックは一時的な失敗を吸収するために存在するのであって、押し通すためのものではない。行動する前に失敗を分類し、真に一時的なものだけをリトライし、レスポンスボディを検証してソフトブロックをそのまま失敗として扱うこと。指数関数的にバックオフし、必ずジッターを伴わせ、Retry-Afterに従い、遅延と試行回数の両方に上限を設けること。待機を求めるレート信号と、ローテーションを求めるアドレス信号を区別し、同じペースを維持するためにアドレスを切り替えることでスロットルに応えることは決してしないこと。リトライバジェットとターゲットごとのサーキットブレーカーを追加し、広範な障害が自己誘発の洪水に変わらないようにすること。これらを実践すれば、チームがアンチボットのエスカレーションのせいだと責めているBANのほとんどは単純に起こらなくなる。なぜなら、それらはそもそもエスカレーションではなかったからだ。

もう半分は、フェイルオーバーする先を持っておくことであり、それがresidential proxiesが提供するものだ。実在する家庭級のIPの大規模なプールにより、引退したルートは同じアドレスが再挑戦するのではなく、クリーンなものに置き換えられる。per-GB pricingもまた、規律あるリトライが自らの元を取る理由だ。実際に行ったリクエストの分しか支払わないため、決して送らなかった嵐は、決して購入しない帯域幅だからだ。

始める準備はできていますか?

Shifterのレジデンシャルプロキシをお試しください。IP 205M+件、195+カ国、$0.75/GBから。

始める