ローテーティングプロキシを設定し、10件のリクエストを送信したところ、すべてが同じ出口アドレスを報告してくる。当然の結論としてはローテーションが壊れているように思えるが、たいていはそうではない。ほとんどの場合、ゲートウェイは要求どおりに新しいアドレスを割り当てているのに、コードとゲートウェイの間にある何かがそれを妨げている。そして最も多い原因は、処理を速くするために存在するHTTPクライアントの機能だ。
以下、確認すべき原因を順番に、それぞれの対処法とともに挙げる。
まず、正しく確認する
原因を診断する前に、テスト自体が妥当であることを確認しておこう。呼び出しごとに1件のコマンドラインリクエストを送るのが最もクリーンな検証方法だ。各実行は共有状態を持たない独立したプロセスになるからだ。
for i in 1 2 3 4 5; do
curl -s -x customer-USERNAME:PASSWORD@p.shifter.io:443 https://ipinfo.io/json \
| python3 -c "import sys,json; print(json.load(sys.stdin)['ip'])"
done
5つの異なるアドレスが返ってくれば、ローテーションは機能しており、問題はアプリケーション側にある。5つとも同じであれば、設定に問題がある。この一点を見分けるだけでデバッグ時間の大半を節約できるので、まずこれを実行しよう。
原因その1: ユーザー名内のセッション識別子
最も単純な説明だ。ユーザー名にsidフラグが含まれている場合、明示的にスティッキーセッションを要求していることになり、アドレスが保持されるのは不具合ではなく正しい挙動だ。
customer-USERNAME-country-us-sid-abc123 # スティッキー: 意図的に同一IP
customer-USERNAME-country-us # ローテーティング: リクエストごとに新しいIP
これは、ドキュメントのサンプルをそのままコピーした人や、デフォルトでセッションを追加するヘルパーでユーザー名を組み立てた人によく起きる。sidフラグを削除し、TTLはセッションとセットでのみ意味を持つため、ttlも一緒に削除しよう。それぞれの意味についてはスティッキーとローテーティングの違いとセッションのドキュメントを参照してほしい。
もう少し分かりにくいケースとして、変化させるつもりだったセッション識別子が実は固定されている場合がある。ジョブごとに1つ生成しているつもりでも、そのジョブが多数のリクエストを実行するなら、そのジョブ内のすべてのリクエストが同じアドレスを共有することになる。これは正しい挙動だが、意図とは異なるかもしれない。
原因その2: コネクションの再利用、誰もが引っかかるやつ
上記のcurlループがローテーションするのにコードがしない場合、たいていこれが答えだ。
現代のHTTPクライアントはコネクションを維持し、再利用する。リクエストごとに新しいTCPとTLSのコネクションを開くのは遅いからだ。クライアントがプロキシへの既存トンネルを再利用すると、リクエストはすでに確立されているコネクションを通って流れる。そしてそのコネクションにはすでに出口アドレスが紐づいている。ローテーションはコネクション単位で発生するのであって、既存のコネクションを通じて送られたリクエストごとには発生しない。したがって、コネクションプールを保持するセッションオブジェクトは、すべてのリクエストを忠実に同じ出口経由で送ることになる。
対処法はどこまで制御したいかによる。キープアライブを無効にするか、リクエストごとに新しいコネクションを強制するか、論理的なリクエストごとに新しいクライアントを使うかのいずれかだ。
import requests
PROXY = "http://customer-USERNAME:PASSWORD@p.shifter.io:443"
PROXIES = {"http": PROXY, "https": PROXY}
# コネクションを1つ再利用: すべてのリクエストで同じ出口アドレス
s = requests.Session()
for _ in range(5):
print(s.get("https://ipinfo.io/json", proxies=PROXIES).json()["ip"])
# リクエストごとに新しいコネクション: 期待どおりローテーションする
for _ in range(5):
r = requests.get("https://ipinfo.io/json", proxies=PROXIES,
headers={"Connection": "close"}, timeout=20)
print(r.json()["ip"])
同じことは他の環境にも当てはまる。Nodeのキープアライブ付きhttp.Agent、.NETやJavaで共有されるHttpClient、Goのコネクションプールなどだ。使用している言語にコネクションをプールするデフォルトクライアントがあるなら(実際どの言語にもある)、これがアプリケーションコードでまず確認すべき点になる。
はっきり言っておくと、これはバグというよりトレードオフだ。コネクションの再利用は速く、コストも低い。もし本当にリクエストごとに新しいアドレスが必要な処理であれば、毎回新しいコネクションを開くコストを払うことになる。そうでなければ、再利用のままで問題なく、むしろ望ましいことが多い。
原因その3: チェックが速すぎる、あるいはそのフィルタに対してプールが思ったより小さい
関連する2つの効果がある。
フィルタを厳しく絞り込んでいる場合、たとえば小さな都市や特定のASNに限定している場合、あなたにサービスを提供できるアドレスの集合は全体のプールよりもはるかに小さくなる。そのため、同じアドレスが正当に何度も再登場する。これは失敗ではなく、単なる算数の結果だ。フィルタを広げれば、この繰り返しは減っていく。
また、レジデンシャルプールは実際の接続で構成されており、それらは絶えず出入りしていることも念頭に置いてほしい。したがって、多数のリクエストの中で同じアドレスを2回見ることは、疑わしいことではなく、むしろ想定内だ。ローテーションとは、次のリクエストが独立して選択されることを意味するのであって、あるアドレスが二度と現れないことを意味するわけではない。あるワークロードで確実な重複排除が必要なら、それは自分のコード側で扱うべき設計上の制約だ。
原因その4: 上流の何かがキャッシュしている
ブラウザ、拡張機能、システムレベルのプロキシ設定、あるいは企業ネットワーク経由でテストしている場合、リクエストは想定している場所に行っていないかもしれない。特にブラウザはコネクションを積極的に維持し、タブをまたいで再利用するため、ローテーションを検証する環境としては最悪だ。まずコマンドラインでテストし、次にアプリケーションで、そして最後にブラウザでテストしよう。
同様に、コードが明示的な設定に加えてプロキシの環境変数も設定している場合、どちらかがもう一方を上書きし、設定したはずのゲートウェイとは別のものにトラフィックを送っている可能性がある。
原因その5: アドレスは同じだがリクエストがそもそも出ていっていない
これは無愛想だが確認する価値のある点だ。プロキシが実際には使われていない場合、すべてのリクエストは同じアドレス、つまりあなた自身のアドレスを報告する。見えているアドレスが自分のサーバーのアドレスそのものではないか確認しよう。もしそうなら、プロキシ設定がまったく適用されていないということであり、これは別の問題だ。多くの場合、httpエントリと並んでhttpsエントリが欠けているか、使用しているスキームに対してプロキシ設定を無視するクライアントを使っていることが原因だ。
原因その6: スティッキーセッションがまだ期限切れになっていない
意図的にスティッキーセッションを使っていて、しばらくしたらローテーションすることを期待している場合、TTLが切れるまでアドレスは保持され続けることを覚えておこう。デフォルトの有効期間は120秒で、ttlを明示的に設定していない限りこれが適用される。もっと早く新しいアドレスが欲しいなら、待つのではなくセッション識別子を変更しよう。新しい識別子は新しいセッションを意味する。
また、スティッキーなアドレスは、根底にある接続が切れた場合にはTTLより前に失われることがある点にも注意してほしい。これらは専用インフラではなく、実際の家庭用接続だからだ。スティッキーとは、要求された期間についてベストエフォートで保持することを意味し、保証ではない。
手早い判断の道筋
curlループを実行する。それがローテーションするのにコードがしない場合、コネクション再利用の問題であり、原因その2だ。どちらもローテーションしない場合は、ユーザー名にsidフラグがないか確認し、次に自分自身のアドレスを見ていないか確認し、その後に狭い地理フィルタを広げてみよう。期待するほど頻繁にはローテーションしないが一応ローテーションはしている場合は、不具合ではなく、そのフィルタに対するプールサイズの問題を見ていることになる。
それ以外については、周辺の挙動はローテーションの仕組みに説明があり、繰り返しではなくリクエストが失敗している場合は、リクエストがタイムアウトする理由と407エラーの修正が最も一般的な2つの失敗モードをカバーしている。
結論
ローテーションの問題は、たいていローテーションの問題ではない。まず独立したプロセスでテストし、ゲートウェイがそもそもローテーションしているかどうかを確かめよう。ローテーションしているなら、次にHTTPクライアントを見ること。コネクションの再利用が圧倒的に多い原因だからだ。すでに開いているトンネルを通じて送られたリクエストは、そのトンネルがすでに持っている出口アドレスを保持し続ける。それでも解決しない場合は、紛れ込んだsidフラグを確認し、自分自身のアドレスを見ていないか確かめ、非常に狭い地理フィルタはずっと小さい集合から選ばれるため繰り返しが正常であることを思い出そう。スティッキーセッションがアドレスを保持し続けているのは設計どおりの動作であり、識別子を変更すればすぐに新しいものが得られる。
ローテーションのモデル自体と、それを制御するフラグについてはレジデンシャルプロキシネットワークにまとめられており、そこではセッションの挙動はプランの設定ではなくリクエストごとのパラメータとなっており、ローテーションの頻度に関わらずGB単位で課金される。