ぽっぺんしゃんにょろりんこ

匿名・非追跡型アダルト動画検索エンジンの設計ノート

プロキシが回転型じゃなかったので、自前でIPローテのコードを書いた話

こんにちは、にょろりんこの備忘録的技術ブログです。

今回は、検索エンジンのインデクシングでよく使われる「回転型プロキシ」について、実際に契約してみたところ、全く回転しなかったという話と、そこから自前でIPローテーションの制御コードを作った過程について書いていきます。

まず大前提として、プロキシは切り替え方式で以下の2つに分けることができます。

  • 回転型プロキシ(Rotating):リクエストや時間ごとにIPが変わる。切り替えを自動でやってくれるので便利。  
  • 固定型プロキシ(Static/Dedicated):同じIPを継続して使う。切り替えは自前でやるしかないが、そのぶん制御の自由度は高い。

これはトレードオフなのですが、私は「IPの切り替え構造は自前では持たないでおこう」と思っていたので、自動で切り替えてくれる回転型を使おうと決め、「Rotating」と明記されていた Stormproxies と契約しました。

しかし、送られてきたのは──ただのIPアドレスの羅列。

「ん?これって回転型じゃなくない?」と思いながらも使ってみましたが、やはり自動では切り替わらない。  つまり、「回転型」と謳っていても、実態は「自分でローテーション制御しないと何も変わらない固定型」に近いものでした。

しかも、補足すると──テストしたIPはすべて接続不能でした(泣)

ここら辺の顛末はこちらにまとめてありますので、よろしければ見ていってください。

www.n-rinko.com

というわけで、今回の記事では「想定と違った回転型プロキシ」に直面した私が、どうやって自前でIPローテーションを実装したかをまとめていきます。

ティアラ_ハートのポーズ

構造としては、MySQLに登録されたプロキシ一覧から「最後に使った時間が5分以上前のプロキシ」を1件取得し、使用後に last_used を更新する、というシンプルなローテーション構造です。

コードは以下のような感じです。

// get_random_proxy.js


/**
 * proxiesテーブルから、status='working' かつ
 * last_usedが5分以上前のプロキシをランダムに1件取得する
 * 
 * ※実際に接続可能かどうかの確認は行わない
 * @returns {Promise<{id: number, ip: string, port: number, username: string, password: string}>}
 */
async function getRandomProxy() {
  try {
    const [rows] = await pool.query(`
      SELECT * FROM proxies
      WHERE status = 'working'
        AND (last_used IS NULL OR last_used < NOW() - INTERVAL 5 MINUTE)
      ORDER BY RAND()
      LIMIT 1
    `);

    if (rows.length === 0) {
      throw new Error(' 使用可能なプロキシがありません(working かつ last_used > 5分前)');
    }

    return rows[0];
  } catch (err) {
    console.error('プロキシ取得中にエラー:', err);
    throw err;
  }
}

この処理は「IPが生きているか、帯域が死んでいないか、403が返るか」といった通信レベルの健全性確認はしていません。

「status = 'working'」 フラグが立っていて、「last_used」が5分以上前という「事前に登録されたプロキシ一覧の中で使ってもよさそうな条件」に基づいて選んでいるだけです。また、このローテーション制御は、以下のような `proxies` テーブルで管理しています:

| カラム名    | 説明 |
|-------------|------|
| `ip`        | プロキシのIPアドレス |
| `port`      | ポート番号(例:8080) |
| `username`  | 認証用ユーザー名 |
| `password`  | 認証用パスワード |
| `status`    | 使用可否(`working` or `blocked`) |
| `last_used` | 最後に使った時刻(5分以上空けて再利用) |

このように、`last_used` により連続使用を避け、`status` によって死んだIPやブロック中のものを除外しているというシンプルな構図です。

// update_proxy_last_used.js


/**
 * 指定したプロキシIDの last_used を現在時刻に更新する
 * @param {number} proxyId - proxies テーブルの id
 * @returns {Promise<void>}
 */
async function updateProxyLastUsed(proxyId) {
  try {
    await pool.execute(
      'UPDATE proxies SET last_used = NOW() WHERE id = ?',
      [proxyId]
    );
    console.log(`プロキシID=${proxyId} の last_used を更新`);
  } catch (err) {
    console.error('updateProxyLastUsed エラー:', err);
  }
}

`last_used` カラムは、直近に使用された時刻を記録するためのものです。  プロキシを使い終わった後に `UPDATE proxies SET last_used = NOW()` を実行することで、  次の選定時に一定の「クールダウン」を設けることができます。

これによって、「生きている/使用可能なプロキシ」を選別し、  一度使用したプロキシを「一定時間(ここでは仮に5分)」休ませるという構造にしています。

なお、この種の管理は JSON ファイルなどでローカルに行うことも可能ですが、  DBで管理したほうが細かい制御(並列処理、再試行の記録、ブロック判定など)がしやすく、運用面でも楽になるため、今回は DBベースの設計を採用しています。

今回のように「Rotating」と書かれていても、実際には回転しないプロキシというのは存在します。  そうした場合でも、自前で制御コードを書けば、ローテーション管理は十分可能です。

特に `last_used` を軸にしたクールダウン管理と、`status` フラグによる生死判定を組み合わせれば、スケーラブルなプロキシ制御が簡単に構築できます。

現行の Sae-Porns では回転型プロキシを採用しているため、今回紹介したローテーション制御コードは実運用では使っていません。

とはいえ、プロキシを多用する用途(インデクシング、スクレイピング、分散クロール、SEO検証など)では、  こういった「自前管理によるIPローテーション設計」が、実は最もトラブルに強い構造だったりします。

ここはあくまで私の推測ですが──  Google、Bing、Baidu、Yandex などの大手検索エンジンサービスは、おそらく固定型IPを大量に保有した上で、使用IPの選定・クールダウンなどを「自前で制御している」のではないかと思います。

それではみなさん、よい開発ライフを。

※このコードが実際に使われている「追跡されないアダルト動画の検索エンジンSae-Porns」はこちら!よかったら見ていってください。(18歳未満の方はご利用できません。)

sae-porns.org