파서나 봇이 하나의 IP에서 분당 수백 개의 요청을 보낼 때, 서버는 빠르게 비율 제한을 활성화하고, 그 다음에는 CAPTCHA 또는 차단이 발생합니다. 해결책은 요청을 작업자와 프록시 간에 분산하는 작업 큐입니다. 이 기사에서는 Celery, RQ 및 BullMQ에서 이러한 아키텍처가 어떻게 구성되어 있는지, 그리고 각 IP 주소에 대한 요청 제한을 어떻게 효과적으로 관리할 수 있는지 살펴보겠습니다.
프록시 작업 시 작업 큐가 필요한 이유
만약 작업이 Wildberries에서 50,000개의 상품 카드를 파싱하거나 Instagram에서 10,000개의 계정 상태를 확인하는 것이라면, 순차적으로 요청을 실행하는 데 몇 시간이 걸리며, 제어 없이 병렬 실행하면 IP 차단을 빠르게 당할 수 있습니다. 작업 큐는 두 가지 문제를 해결합니다: 대량의 작업을 작은 작업으로 나누고, 이를 여러 작업자에게 분산시키며, 각 작업자에게 개별 프록시를 할당할 수 있게 합니다.
아키텍처는 일반적으로 다음과 같습니다: 메시지 브로커(주로 Redis 또는 RabbitMQ)가 작업 큐를 저장하고, 작업자들이 큐에서 작업을 가져와 실행하며, 각 작업자는 자신의 프록시를 사용하거나 풀에서 프록시를 회전시킵니다. 이는 전체 요청량이 방대하더라도 각 IP에 대한 부하를 안전한 범위 내로 유지할 수 있게 합니다.
이러한 접근 방식은 파서 개발자뿐만 아니라 Facebook Ads 광고 계정 확인을 자동화하거나 Ozon의 가격을 모니터링하거나 동시에 수십 개의 사이트에서 데이터를 수집하는 팀에서도 사용됩니다. 큐와 IP에 대한 적절한 분산 없이 이러한 시스템은 첫날부터 차단될 운명에 처하게 됩니다.
하나의 IP에 대한 요청 제한 문제
대부분의 웹사이트와 API는 과도한 부하에 대한 보호 장치가 있습니다. 이는 여러 형태로 나타날 수 있습니다: 서버는 HTTP 429(Too Many Requests)를 반환하거나 CAPTCHA를 표시하고, IP를 10-60분 동안 임시로 차단하거나 영구적으로 블랙리스트에 추가합니다. 다양한 플랫폼마다 제한이 크게 다릅니다. 예를 들어, Wildberries API는 하나의 주소에서 초당 제한된 수의 요청만 허용하며, Instagram은 15-20개의 연속적인 행동 후에 의심스러운 활동에 반응합니다.
큐를 구축할 때의 주요 실수는 모든 작업자를 동일한 IP를 통해 실행하는 것입니다. 작업이 20명의 작업자 간에 분산되어 있더라도 모두가 동일한 프록시를 사용하거나 아예 사용하지 않는다면, 이 IP에 대한 제한은 순차 실행과 마찬가지로 빠르게 발생합니다. 따라서 작업자 수는 프록시 풀에서 사용할 수 있는 IP 주소 수와 비례해야 합니다.
하나의 도메인에 대해 높은 요청 빈도를 가진 작업에는 데이터 센터 프록시를 사용하는 것이 좋습니다. 이들은 낮은 지연 시간을 제공하며, 속도가 중요하고 일반 사용자로 가장하는 것이 아닌 대규모 병렬 파싱에 적합합니다. 만약 목표가 마켓플레이스나 소셜 미디어의 더 강력한 보호를 우회하는 것이라면, 일반 사용자와 유사한 IP, 즉 레지던트 또는 모바일 IP가 필요합니다.
Celery: 아키텍처 및 코드에서의 프록시 회전
Celery는 Python 생태계에서 가장 성숙하고 인기 있는 작업 큐입니다. 이는 브로커(주로 Redis 또는 RabbitMQ) 위에서 작동하며, 경쟁성, 재시도 및 작업 우선 순위의 유연한 구성을 지원합니다. 프록시 회전을 위해 Redis에 IP 풀을 저장하고 큐에 작업을 추가할 때 각 작업에 프록시를 할당하는 것이 편리합니다.
from celery import Celery
import requests
import redis
import random
app = Celery('tasks', broker='redis://localhost:6379/0')
redis_client = redis.Redis(host='localhost', port=6379, db=1)
PROXY_POOL = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
def get_proxy():
# Redis를 통한 라운드 로빈, 하나의 IP를 치지 않기 위해
idx = redis_client.incr("proxy_cursor") % len(PROXY_POOL)
return PROXY_POOL[idx]
@app.task(bind=True, max_retries=3, default_retry_delay=30)
def fetch_url(self, url):
proxy = get_proxy()
try:
response = requests.get(
url,
proxies={"http": proxy, "https": proxy},
timeout=15
)
if response.status_code == 429:
raise self.retry(exc=Exception("비율 제한"))
return response.status_code
except requests.exceptions.RequestException as exc:
raise self.retry(exc=exc)
핵심 포인트는 get_proxy() 함수로, Redis 카운터를 통해 풀에서 다음 프록시를 순환하여 제공합니다. 이는 부하가 모든 IP에 균등하게 분산되도록 보장하며, 하나에 집중되지 않게 합니다. max_retries 매개변수와 429 코드 처리로 인해 현재 IP가 제한을 받으면 작업이 다른 프록시로 자동으로 재전송됩니다.
Celery는 작업자 수준에서 경쟁성을 제어하기 위해 작업자 시작 시 --concurrency 매개변수를 통해 동시에 실행되는 작업 수를 제한할 수 있으며, 엄격한 비율 제한이 있는 느린 작업을 위한 별도의 큐와 같은 다양한 우선 순위 큐를 사용할 수 있습니다.
RQ (Redis Queue): 간단한 대안
RQ는 Celery보다 더 가벼운 라이브러리로, RabbitMQ 브로커가 없고 최소한의 구성으로 사용할 수 있습니다. 프로젝트가 작고 복잡한 작업 라우팅이 필요하지 않으며, 단순히 작업자와 작업에 프록시를 연결할 수 있는 큐가 필요한 경우에 적합합니다.
from redis import Redis
from rq import Queue
import requests
import itertools
redis_conn = Redis()
q = Queue(connection=redis_conn)
PROXY_POOL = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
proxy_cycle = itertools.cycle(PROXY_POOL)
def fetch_with_proxy(url):
proxy = next(proxy_cycle)
response = requests.get(
url,
proxies={"http": proxy, "https": proxy},
timeout=15
)
if response.status_code == 429:
# 지연 후 큐에 재배치
q.enqueue_in(60, fetch_with_proxy, url)
return None
return response.status_code
# 큐에 작업 추가
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
q.enqueue(fetch_with_proxy, url)
RQ에서 itertools.cycle을 통한 회전은 작업자 프로세스 내에서 작동합니다. 이는 Redis 카운터보다 간단하지만, 많은 병렬 작업자가 있을 경우 확장성이 떨어집니다. 왜냐하면 각 작업자가 처음부터 사이클을 시작하기 때문입니다. 1-5명의 작업자가 있는 작은 프로젝트에는 충분하지만, 더 큰 프로젝트에서는 Celery 예제와 같이 Redis에서 중앙 집중식 카운터로 돌아가는 것이 좋습니다.
RQ는 이미 프로젝트에서 Redis를 사용하는 개발자에게 적합합니다. 별도의 메시지 브로커를 설정할 필요가 없습니다. 단점은 Celery에 비해 내장된 우선 순위 지정 및 모니터링 메커니즘이 적기 때문에, 하루에 수십만 개의 작업을 처리하는 프로덕션 시스템에서는 주로 Celery가 선택됩니다.
BullMQ: IP 제한이 있는 Node.js 큐
BullMQ는 Node.js 생태계에서 작업 큐의 표준으로, Redis 위에 구축되어 있으며 내장된 비율 제한기가 있어 IP 요청 제한을 제어하는 데 특히 유용합니다. 수동으로 요청 수를 계산하는 대신 큐 수준에서 직접 제한기를 지정할 수 있습니다.
const { Queue, Worker } = require('bullmq');
const axios = require('axios');
const connection = { host: 'localhost', port: 6379 };
const proxyPool = [
'http://user:[email protected]:8000',
'http://user:[email protected]:8000',
'http://user:[email protected]:8000',
];
const queue = new Queue('fetchQueue', { connection });
// 제한기: 큐당 초당 최대 5개의 작업
const worker = new Worker('fetchQueue', async job => {
const proxy = proxyPool[job.id % proxyPool.length];
try {
const response = await axios.get(job.data.url, {
proxy: false,
httpsAgent: new (require('https-proxy-agent'))(proxy),
timeout: 15000,
});
return response.status;
} catch (err) {
if (err.response && err.response.status === 429) {
throw new Error('비율 제한, 다른 프록시로 재시도');
}
throw err;
}
}, {
connection,
limiter: { max: 5, duration: 1000 },
concurrency: proxyPool.length,
});
for (let i = 0; i < 100; i++) {
queue.add('fetch', { url: `https://example.com/item/${i}` }, {
attempts: 3,
backoff: { type: 'exponential', delay: 5000 },
});
}
BullMQ의 limiter 매개변수는 전체 시스템에서 요청 제한을 준수하는 데 유용한 내장 메커니즘입니다. concurrency 매개변수는 프록시 풀의 크기와 같아, 동시에 작동하는 작업자의 수가 자유로운 IP 수와 정확히 일치하도록 보장합니다.
이러한 접근 방식은 TikTok이나 Telegram에서 작업을 자동화하기 위해 자체 봇을 작성하는 팀에서 자주 사용됩니다. BullMQ는 나머지 Node.js 스택과 잘 통합되며, 단일 작업을 위해 Python으로 전환할 필요가 없습니다.
Celery, RQ 및 BullMQ 비교
| 매개변수 | Celery | RQ | BullMQ |
|---|---|---|---|
| 언어 | Python | Python | Node.js |
| 브로커 | Redis / RabbitMQ | 오직 Redis | 오직 Redis |
| 내장 비율 제한기 | 없음 (수동으로 작성) | 없음 | 예, 기본 제공 |
| 진입 장벽 | 중간 | 낮음 | 중간 |
| 적합한 용도 | 대규모 프로덕션 시스템 | 소규모 프로젝트, 프로토타입 | Node.js 스택, 봇, 자동화 |
IP에 따른 부하 분산 전략
위의 예에서 사용된 라운드 로빈은 가장 간단한 전략이지만 항상 최적은 아닙니다. 다음은 작업에 따라 고려할 가치가 있는 몇 가지 옵션입니다:
- 라운드 로빈 — 각 다음 요청은 순환하여 다음 프록시를 통해 진행됩니다. 모든 IP가 동등하고 제한이 동일할 때 적합합니다.
- 스티키 세션 — 하나의 프록시가 특정 계정이나 세션에 고정되어 작업하는 동안 유지됩니다. 소셜 미디어와 마켓플레이스에서 IP 변경이 의심스럽게 보일 수 있기 때문에 중요합니다.
- 가중 분배 — 요청의 일부는 더 비싸고 신뢰할 수 있는 IP(예: 모바일)로, 일부는 저렴한 데이터 센터 IP로 전송되며, 처리되는 도메인에 따라 달라집니다.
- 적응형 스로틀링 — 오류 429 또는 CAPTCHA 비율이 증가하면 작업 전송 속도가 자동으로 감소하고, 오류가 사라지면 복원됩니다.
실제 사용자로 가장하는 것이 중요한 작업(예: Instagram 또는 TikTok에서 계정을 따뜻하게 하고 확인하는 작업)에는 모바일 프록시와 함께 스티키 세션을 사용하는 것이 좋습니다. 이들은 안티봇 시스템의 의심을 덜 받으며 일반 모바일 인터넷 사용자의 행동을 따릅니다.
속도와 양이 중요하고 알고리즘의 신뢰성이 덜 중요한 웹사이트에서 대량의 데이터를 수집할 때는 레지던트 프록시와 함께 라운드 로빈이 안정성과 가격 간의 균형을 제공합니다: IP는 실제 사용자처럼 보이지만 특정 세션에 대해 자주 변경할 수 있습니다.
오류 429 및 차단 모니터링
모니터링 없는 작업 큐는 블랙 박스입니다: 일부 작업이 실패하는 이유와 어떤 IP가 의심받고 있는지 이해하기 어렵습니다. 최소한 추적해야 할 메트릭 세트는 다음과 같습니다:
- 각 프록시 별로 429 및 403 코드의 응답 비율
- 평균 응답 시간 — 지연 증가가 종종 차단에 앞서 발생합니다
- 작업에 대한 재시도 수 (retries)
- 실시간 작업 큐 길이 — 큐가 증가하면 작업자가 처리하지 못하고 있다는 신호입니다
Celery에서는 Flower를 사용하여 작업과 작업자를 실시간으로 모니터링하는 웹 인터페이스를 제공하는 것이 편리합니다. BullMQ에는 유사한 기능을 가진 Bull Board가 있습니다. RQ에는 RQ 대시보드가 있습니다. 이 세 가지 도구는 작업 상태, 실패한 시도 수를 보여주고, 풀에서 어떤 프록시를 일시적으로 회전에서 제외해야 하는지 빠르게 이해할 수 있게 합니다.
좋은 관행은 429 오류가 연속으로 발생한 후 15-30분 동안 프록시를 "격리 중"으로 자동으로 표시하고, 성공적인 테스트 요청 후에만 다시 풀로 반환하는 것입니다. 이는 차단된 IP가 계속해서 작업을 받고 작업자의 시간을 낭비하는 상황을 방지합니다.
결론
작업 큐는 요청의 확장성 문제를 해결하지만, IP 제한으로부터 스스로를 보호하지는 않습니다. 이를 위해서는 Celery, RQ 또는 BullMQ 내에서 프록시 회전을 신중하게 설계해야 합니다. 도구 선택은 스택에 따라 다릅니다: Python 프로젝트는 일반적으로 프로덕션을 위해 Celery를 선택하고, 간단한 작업에는 RQ를 사용하며, Node.js 개발자는 내장된 비율 제한기가 있는 BullMQ를 선택합니다.
선택한 큐와 관계없이 결과는 프록시 풀의 품질에 크게 의존합니다. 빠른 대량 파싱이 목표라면 속도를 위해 데이터 센터 프록시를 고려하고, 소셜 미디어 및 마켓플레이스에서 안티봇 시스템에 대한 충성도가 중요한 경우 레지던트 또는 모바일 IP가 적합합니다. 이들은 429 오류 및 차단 비율을 줄여주며, 이는 재시도 수를 직접 줄이고 전체 작업 큐의 실행 속도를 높입니다.