블로그로 돌아가기

프록시를 통한 로드 밸런싱: 10000+ RPS로 파싱 및 API 요청 확장하는 방법

프록시 풀을 통한 부하 분산에 대한 완벽한 가이드: 마켓플레이스 파싱, API 작업 및 자동화를 위한 아키텍처, 로드 밸런싱 알고리즘 및 설정에 대한 실용적인 예.

📅2026년 2월 7일
```html

마켓플레이스의 수천 개 페이지를 파싱하고, 대량의 API 요청을 보내거나, 수백 개의 계정을 자동화할 때, 프록시를 통한 적절한 부하 분산이 매우 중요해집니다. 올바른 균형이 없으면 차단, 타임아웃 및 성능 저하에 직면하게 됩니다. 이 가이드에서는 부하 분산 아키텍처, 균형 알고리즘 및 고부하 시스템을 위한 실용적인 전략을 살펴보겠습니다.

이 자료는 데이터 파싱, API 요청 자동화 또는 비즈니스 문제를 위한 대규모 프록시 풀을 관리하는 개발자 및 기술 전문가를 대상으로 합니다.

프록시를 통한 부하 분산의 필요성

프록시를 통한 부하 분산은 고부하 시스템의 여러 중요한 문제를 해결합니다. 첫 번째이자 가장 중요한 문제는 차단으로부터의 보호입니다. 동일한 리소스(마켓플레이스, 소셜 미디어 API, 검색 엔진)에 수천 개의 요청을 보낼 때, 대상 서버는 하나의 IP 주소에서 비정상적으로 높은 활동을 감지하고 이를 차단합니다. 수십 개 또는 수백 개의 프록시 간에 요청을 분산시키면 귀하의 활동이 일반 사용자와 유사하게 됩니다.

두 번째 문제는 성능입니다. 하나의 프록시 서버는 제한된 대역폭(일반적으로 100-1000 Mbps)을 가지며 동시에 처리할 수 있는 연결 수가 제한적입니다. 분당 10,000 페이지를 파싱하거나 대량의 API 요청을 보낼 때, 하나의 프록시는 시스템의 병목이 될 수 있습니다. 부하 분산은 새로운 프록시를 풀에 추가하여 수평적으로 대역폭을 확장할 수 있게 합니다.

세 번째 문제는 신뢰성입니다. 만약 하나의 프록시가 고장나거나(기술적 결함, 차단, 임대 기간 만료)면, 시스템은 자동으로 작동 중인 프록시로 트래픽을 재배분합니다. 부하 분산 및 상태 점검 메커니즘이 없으면 하나의 프록시의 실패가 전체 시스템을 중단시킬 수 있습니다.

실제 예시: 경쟁자의 가격 모니터링을 위해 Wildberries를 파싱할 때, 매시간 50,000개의 상품을 처리해야 합니다. 이는 초당 약 14개의 요청에 해당합니다. 하나의 프록시는 이러한 부하를 견딜 수 있지만, Wildberries는 하나의 주소에서 100-200개의 요청 후에 IP를 차단합니다. 20개의 주거용 프록시 간에 요청을 분산시키면 각 IP에 대한 부하가 초당 0.7 요청으로 줄어들어, 일반 사용자의 활동처럼 보이게 됩니다.

네 번째 이유는 지리적 분산입니다. 많은 서비스는 사용자의 지역에 따라 다른 콘텐츠나 가격을 표시합니다. 다양한 국가와 도시의 프록시 간의 균형을 맞추면 모든 목표 지역에서 동시에 데이터를 수집할 수 있습니다. 예를 들어, Ozon의 가격 모니터링을 위해서는 모스크바, 상트페테르부르크, 예카테린부르크 및 기타 대도시의 프록시가 필요합니다.

부하 분산 시스템 아키텍처

프록시를 통한 부하 분산 시스템의 전통적인 아키텍처는 여러 구성 요소로 이루어져 있습니다. 최상위에는 부하 분산기(load balancer)가 있습니다. 이는 들어오는 작업(파싱 요청, API 호출)을 수신하고 사용 가능한 프록시 간에 분산시키는 소프트웨어 모듈입니다. 부하 분산기는 별도의 서비스로 작동하거나 애플리케이션에 내장될 수 있습니다.

두 번째 구성 요소는 프록시 풀 관리자(proxy pool manager)입니다. 이 구성 요소는 IP 주소, 포트, 프로토콜(HTTP/SOCKS5), 지리적 위치, 유형(주거용, 모바일, 데이터 센터), 현재 상태(활성, 차단됨, 점검 중)와 같은 특성을 가진 모든 사용 가능한 프록시의 목록을 저장합니다. 풀 관리자는 새로운 프록시 추가, 작동하지 않는 프록시 삭제 및 주기적인 가용성 점검을 담당합니다.

세 번째 요소는 모니터링 및 상태 점검 시스템입니다. 이 시스템은 각 프록시의 작동 상태를 지속적으로 확인합니다: 테스트 요청을 보내고, 응답 시간을 측정하며, 연결의 성공 여부를 확인합니다. 프록시가 응답하지 않거나 오류를 반환하면 시스템은 해당 프록시를 사용 불가로 표시하고 일시적으로 회전에서 제외합니다.

구성 요소 기능 기술
부하 분산기 프록시 간 요청 분산 HAProxy, Nginx, Python/Node.js 라이브러리
프록시 풀 관리자 프록시 목록 관리 Redis, PostgreSQL, 인메모리 저장소
상태 점검 시스템 프록시 가용성 점검 예약 작업, Celery, cron
요청 속도 제한기 요청 빈도 제어 토큰 버킷, 리키 버킷 알고리즘
모니터링 및 메트릭 성능 메트릭 수집 Prometheus, Grafana, ELK 스택

네 번째 구성 요소는 요청 속도 제한기(rate limiter)입니다. 이 구성 요소는 각 프록시가 목표 리소스에 대한 허용된 요청 빈도를 초과하지 않도록 모니터링합니다. 예를 들어, Instagram을 파싱하고 플랫폼이 분당 60개의 요청 후에 IP를 차단한다는 것을 알고 있다면, 속도 제한기는 하나의 프록시를 통해 분당 50개의 요청 이상을 보내지 않도록 합니다.

다섯 번째 요소는 메트릭 및 분석 시스템입니다. 이 시스템은 각 프록시의 성능에 대한 데이터를 수집합니다: 성공적인 요청 수, 오류 수, 평균 응답 시간, 차단 비율. 이러한 데이터는 균형 알고리즘을 최적화하고 문제 있는 프록시를 식별하는 데 사용됩니다.

균형 알고리즘: 라운드 로빈, 최소 연결, 가중치

라운드 로빈 알고리즘은 가장 간단하고 널리 사용되는 균형 방법입니다. 이 알고리즘은 목록의 프록시를 순차적으로 순회합니다: 첫 번째 요청은 프록시 №1을 통해, 두 번째는 프록시 №2를 통해, 세 번째는 프록시 №3을 통해 진행됩니다. 목록이 끝나면 부하 분산기는 처음으로 돌아갑니다. 이 알고리즘은 모든 프록시가 동일한 성능을 가질 경우 부하를 고르게 분산합니다.

class RoundRobinBalancer:
    def __init__(self, proxies):
        self.proxies = proxies
        self.current_index = 0
    
    def get_next_proxy(self):
        proxy = self.proxies[self.current_index]
        self.current_index = (self.current_index + 1) % len(self.proxies)
        return proxy

# 사용 예
proxies = [
    "http://proxy1.example.com:8080",
    "http://proxy2.example.com:8080",
    "http://proxy3.example.com:8080"
]

balancer = RoundRobinBalancer(proxies)
for i in range(10):
    proxy = balancer.get_next_proxy()
    print(f"요청 {i+1} → {proxy}")

최소 연결 알고리즘(Least Connections)은 현재 활성 연결 수가 가장 적은 프록시를 선택합니다. 이는 요청의 실행 시간이 서로 다를 때 유용합니다. 예를 들어, 하나의 요청은 100ms 동안 처리될 수 있지만, 다른 요청은 5초가 걸릴 수 있습니다(페이지가 느리게 로드되는 경우). 최소 연결 알고리즘은 자동으로 새로운 요청을 덜 부하가 걸린 프록시로 보냅니다.

class LeastConnectionsBalancer:
    def __init__(self, proxies):
        self.proxies = {proxy: 0 for proxy in proxies}
    
    def get_next_proxy(self):
        # 최소 연결 수를 가진 프록시 선택
        proxy = min(self.proxies.items(), key=lambda x: x[1])[0]
        self.proxies[proxy] += 1
        return proxy
    
    def release_proxy(self, proxy):
        # 요청 완료 시 카운터 감소
        self.proxies[proxy] -= 1

# 컨텍스트 관리자와 함께 사용
balancer = LeastConnectionsBalancer(proxies)

def make_request(url):
    proxy = balancer.get_next_proxy()
    try:
        # 선택한 프록시를 통해 요청 수행
        response = requests.get(url, proxies={"http": proxy})
        return response
    finally:
        balancer.release_proxy(proxy)

가중 라운드 로빈(Weighted Round Robin)은 고전적인 라운드 로빈을 확장하여 각 프록시에 성능이나 품질에 따라 가중치를 부여합니다. 더 높은 가중치를 가진 프록시는 더 많은 요청을 받습니다. 이는 품질이 다양한 프록시가 있을 때 유용합니다: 예를 들어, 높은 속도의 프리미엄 주거용 프록시와 제한이 있는 저렴한 데이터 센터 프록시가 있을 때입니다.

class WeightedRoundRobinBalancer:
    def __init__(self, weighted_proxies):
        # weighted_proxies = [(proxy, weight), ...]
        self.proxies = []
        for proxy, weight in weighted_proxies:
            # 가중치만큼 프록시를 리스트에 추가
            self.proxies.extend([proxy] * weight)
        self.current_index = 0
    
    def get_next_proxy(self):
        proxy = self.proxies[self.current_index]
        self.current_index = (self.current_index + 1) % len(self.proxies)
        return proxy

# 가중치와 함께 사용
weighted_proxies = [
    ("http://premium-proxy1.com:8080", 5),  # 높은 품질
    ("http://premium-proxy2.com:8080", 5),
    ("http://cheap-proxy1.com:8080", 2),    # 낮은 품질
    ("http://cheap-proxy2.com:8080", 1)     # 매우 낮은 품질
]

balancer = WeightedRoundRobinBalancer(weighted_proxies)

랜덤(Random) 알고리즘은 사용 가능한 목록에서 프록시를 무작위로 선택합니다. 이는 라운드 로빈보다 구현이 간단하며, 많은 수의 프록시(100개 이상)에서 잘 작동합니다. 무작위 분산은 충분한 요청량이 있을 때 자동으로 균형을 맞춥니다. 단점은 불균형한 부하가 짧은 기간 발생할 수 있다는 것입니다.

IP 해시(IP Hash)는 대상 URL 또는 다른 매개변수를 기반으로 프록시를 선택하는 알고리즘입니다. 이는 동일한 리소스에 대한 요청이 항상 동일한 프록시를 통해 이루어지도록 보장합니다. 이는 세션을 사용하는 사이트나 동일한 IP에서의 요청 순서를 요구하는 경우(예: 인증 + 데이터 수신)에 유용합니다.

프록시 풀 관리: 회전 및 상태 점검

프록시 풀을 효과적으로 관리하려면 각 프록시의 상태를 지속적으로 모니터링하고 자동으로 회전해야 합니다. 상태 점검은 테스트 요청을 보내어 프록시의 가용성을 주기적으로 확인하는 것입니다. 일반적으로 신뢰할 수 있는 서비스(예: httpbin.org 또는 자체 엔드포인트)에 대한 간단한 GET 요청이 사용되어 IP 주소에 대한 정보를 반환합니다.

import requests
import time
from datetime import datetime

class ProxyHealthChecker:
    def __init__(self, test_url="http://httpbin.org/ip", timeout=10):
        self.test_url = test_url
        self.timeout = timeout
    
    def check_proxy(self, proxy_url):
        """프록시의 작동 여부를 확인합니다."""
        try:
            start_time = time.time()
            response = requests.get(
                self.test_url,
                proxies={"http": proxy_url, "https": proxy_url},
                timeout=self.timeout
            )
            response_time = time.time() - start_time
            
            if response.status_code == 200:
                return {
                    "status": "healthy",
                    "response_time": response_time,
                    "timestamp": datetime.now(),
                    "ip": response.json().get("origin")
                }
            else:
                return {
                    "status": "unhealthy",
                    "error": f"HTTP {response.status_code}",
                    "timestamp": datetime.now()
                }
        except requests.exceptions.Timeout:
            return {
                "status": "unhealthy",
                "error": "timeout",
                "timestamp": datetime.now()
            }
        except Exception as e:
            return {
                "status": "unhealthy",
                "error": str(e),
                "timestamp": datetime.now()
            }

# 사용 예
checker = ProxyHealthChecker()
proxies = ["http://proxy1.com:8080", "http://proxy2.com:8080"]

for proxy in proxies:
    result = checker.check_proxy(proxy)
    print(f"{proxy}: {result['status']} ({result.get('response_time', 'N/A')}s)")

풀 관리 시스템은 작동하지 않는 프록시를 자동으로 사용 불가로 표시하고 주기적으로 다시 확인해야 합니다. 일반적인 전략은 서킷 브레이커 패턴입니다: 연속으로 세 번의 실패한 점검 후에 프록시는 5-10분 동안 풀에서 제외되며, 이후 다시 점검됩니다. 점검이 성공하면 프록시는 활성 풀로 돌아갑니다.

class ProxyPoolManager:
    def __init__(self, health_checker, max_failures=3, cooldown_seconds=300):
        self.health_checker = health_checker
        self.max_failures = max_failures
        self.cooldown_seconds = cooldown_seconds
        
        self.proxies = {}  # {proxy_url: ProxyInfo}
    
    def add_proxy(self, proxy_url, metadata=None):
        """프록시를 풀에 추가합니다."""
        self.proxies[proxy_url] = {
            "url": proxy_url,
            "status": "active",
            "failures": 0,
            "last_check": None,
            "cooldown_until": None,
            "metadata": metadata or {}
        }
    
    def get_active_proxies(self):
        """활성 프록시 목록을 반환합니다."""
        now = datetime.now()
        active = []
        
        for proxy_url, info in self.proxies.items():
            # 프록시가 쿨다운 상태인지 확인합니다.
            if info["cooldown_until"] and now < info["cooldown_until"]:
                continue
            
            if info["status"] == "active":
                active.append(proxy_url)
        
        return active
    
    def mark_failure(self, proxy_url):
        """프록시 사용 실패를 기록합니다."""
        if proxy_url not in self.proxies:
            return
        
        info = self.proxies[proxy_url]
        info["failures"] += 1
        
        if info["failures"] >= self.max_failures:
            # 쿨다운 상태로 전환
            info["status"] = "cooldown"
            info["cooldown_until"] = datetime.now() + timedelta(seconds=self.cooldown_seconds)
            print(f"프록시 {proxy_url}가 {info['cooldown_until']}까지 쿨다운 상태로 전환되었습니다.")
    
    def mark_success(self, proxy_url):
        """프록시 사용 성공을 기록합니다."""
        if proxy_url not in self.proxies:
            return
        
        info = self.proxies[proxy_url]
        info["failures"] = 0
        info["status"] = "active"
        info["cooldown_until"] = None

프록시 회전은 특정 간격마다 또는 특정 요청 수 후에 프록시를 자동으로 변경하는 전략입니다. 여러 접근 방식이 있습니다: 시간에 따른 회전(5-10분마다 변경), 요청 수에 따른 회전(100-500 요청 후 변경), 세션에 따른 회전(하나의 파싱 세션에 하나의 프록시 사용). 전략 선택은 대상 사이트의 요구 사항에 따라 달라집니다.

팁: 마켓플레이스(Wildberries, Ozon)를 파싱할 때는 요청 수에 따른 회전이 최적입니다: 하나의 프록시당 50-100 요청 후 변경합니다. 소셜 미디어 API(Instagram, Facebook)와 작업할 때는 세션에 따른 회전을 사용하는 것이 좋습니다: 하나의 프록시가 전체 인증 → 작업 → 로그아웃 사이클을 담당합니다.

요청 속도 제한 및 요청 빈도 제어

요청 속도 제한은 부하 분산 시스템의 중요한 구성 요소입니다. 이는 목표 리소스에 대한 허용된 요청 빈도를 초과하는 것을 방지하여 프록시가 차단되는 것을 방지합니다. 두 가지 주요 알고리즘이 있습니다: 토큰 버킷(Token Bucket) 및 리키 버킷(Leaky Bucket).

토큰 버킷은 다음과 같이 작동합니다: 각 프록시에는 요청을 하나 수행할 수 있는 권한을 부여하는 가상의 "버킷"이 있습니다. 버킷은 설정된 속도(예: 초당 10개의 토큰)로 점차적으로 토큰으로 채워집니다. 버킷의 최대 용량은 제한되어 있습니다(예: 50개의 토큰). 요청이 들어오면 시스템은 토큰의 존재를 확인합니다: 토큰이 있으면 하나의 토큰을 빼고 요청을 수행하며, 없으면 새로운 토큰이 나타날 때까지 기다립니다.

import time
from threading import Lock

class TokenBucketRateLimiter:
    def __init__(self, rate, capacity):
        """
        rate: 초당 토큰 수
        capacity: 버킷의 최대 토큰 수
        """
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity
        self.last_update = time.time()
        self.lock = Lock()
    
    def _add_tokens(self):
        """경과된 시간에 따라 토큰을 추가합니다."""
        now = time.time()
        elapsed = now - self.last_update
        new_tokens = elapsed * self.rate
        
        self.tokens = min(self.capacity, self.tokens + new_tokens)
        self.last_update = now
    
    def acquire(self, tokens=1):
        """지정된 수의 토큰을 얻으려고 시도합니다."""
        with self.lock:
            self._add_tokens()
            
            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            return False
    
    def wait_and_acquire(self, tokens=1):
        """토큰이 나타날 때까지 기다리고 얻습니다."""
        while not self.acquire(tokens):
            # 대기 시간을 계산합니다.
            wait_time = (tokens - self.tokens) / self.rate
            time.sleep(wait_time)

# 각 프록시에 대한 사용
proxy_limiters = {
    "http://proxy1.com:8080": TokenBucketRateLimiter(rate=10, capacity=50),
    "http://proxy2.com:8080": TokenBucketRateLimiter(rate=10, capacity=50)
}

def make_request_with_limit(url, proxy_url):
    limiter = proxy_limiters[proxy_url]
    limiter.wait_and_acquire()  # 토큰의 가용성을 기다립니다.
    
    response = requests.get(url, proxies={"http": proxy_url})
    return response

리키 버킷(Leaky Bucket)은 다르게 작동합니다: 요청은 "흘러나오는" 일정한 속도로 대기열(버킷)에 배치됩니다. 버킷이 넘치면 새로운 요청은 거부되거나 대기 상태로 전환됩니다. 이 알고리즘은 시간에 따른 부하를 보다 고르게 분산하지만, 활동이 급증할 때 지연을 초래할 수 있습니다.

서로 다른 대상 플랫폼에는 서로 다른 제한이 필요합니다. Instagram API는 하나의 IP에 대해 시간당 약 200개의 요청을 허용합니다(분당 약 3.3 요청). Wildberries는 하나의 IP에서 분당 100-200개의 요청 후에 차단합니다. Google 검색은 분당 10-20개의 요청을 허용합니다. 귀하의 요청 속도 제한 시스템은 이러한 제한을 고려하고 안전 여유(일반적으로 20-30%)를 추가해야 합니다.

플랫폼 요청 제한 추천 설정
Instagram ~200 요청/시간 분당 2-3 요청(여유 포함)
Wildberries 분당 100-200 요청 분당 60-80 요청
Google 검색 분당 10-20 요청 분당 8-12 요청
Ozon 분당 50-100 요청 분당 30-50 요청
Facebook API 시간당 200 요청 분당 2-3 요청

성능 모니터링 및 자동 확장

효과적인 부하 분산 시스템은 주요 메트릭을 지속적으로 모니터링해야 합니다. 첫 번째 메트릭 그룹은 프록시의 성능입니다: 평균 응답 시간(response time), 성공적인 요청 비율(success rate), 타임아웃 수, 4xx 및 5xx 오류 수. 이러한 데이터는 문제 있는 프록시를 식별하고 균형 알고리즘을 최적화하는 데 사용됩니다.

class ProxyMetrics:
    def __init__(self):
        self.metrics = {}  # {proxy_url: metrics_dict}
    
    def record_request(self, proxy_url, response_time, status_code, success):
        """요청 메트릭을 기록합니다."""
        if proxy_url not in self.metrics:
            self.metrics[proxy_url] = {
                "total_requests": 0,
                "successful_requests": 0,
                "failed_requests": 0,
                "total_response_time": 0,
                "timeouts": 0,
                "errors_4xx": 0,
                "errors_5xx": 0
            }
        
        m = self.metrics[proxy_url]
        m["total_requests"] += 1
        
        if success:
            m["successful_requests"] += 1
            m["total_response_time"] += response_time
        else:
            m["failed_requests"] += 1
            
            if status_code == 0:  # 타임아웃
                m["timeouts"] += 1
            elif 400 <= status_code < 500:
                m["errors_4xx"] += 1
            elif 500 <= status_code < 600:
                m["errors_5xx"] += 1
    
    def get_success_rate(self, proxy_url):
        """성공적인 요청 비율을 반환합니다."""
        m = self.metrics.get(proxy_url, {})
        total = m.get("total_requests", 0)
        if total == 0:
            return 0
        return (m.get("successful_requests", 0) / total) * 100
    
    def get_avg_response_time(self, proxy_url):
        """평균 응답 시간을 반환합니다."""
        m = self.metrics.get(proxy_url, {})
        successful = m.get("successful_requests", 0)
        if successful == 0:
            return 0
        return m.get("total_response_time", 0) / successful
    
    def get_report(self, proxy_url):
        """프록시의 전체 보고서를 반환합니다."""
        m = self.metrics.get(proxy_url, {})
        return {
            "proxy": proxy_url,
            "total_requests": m.get("total_requests", 0),
            "success_rate": self.get_success_rate(proxy_url),
            "avg_response_time": self.get_avg_response_time(proxy_url),
            "timeouts": m.get("timeouts", 0),
            "errors_4xx": m.get("errors_4xx", 0),
            "errors_5xx": m.get("errors_5xx", 0)
        }

두 번째 메트릭 그룹은 시스템의 전체 성능입니다: 초당 총 요청 수(RPS — requests per second), 평균 지연(latency), 요청 대기열의 크기, 활성 프록시 수, 프록시 풀의 사용 비율. 이러한 메트릭은 시스템이 부하를 처리하고 있는지 또는 확장이 필요한지를 보여줍니다.

자동 확장(auto-scaling)은 부하에 따라 프록시를 동적으로 추가하거나 제거할 수 있게 해줍니다. 간단한 전략: 만약 풀의 평균 부하가 5분 동안 80%를 초과하면 시스템은 자동으로 새로운 프록시를 추가합니다. 만약 부하가 15분 동안 30% 미만으로 떨어지면 시스템은 여분의 프록시를 제거하여 자원을 절약합니다.

모니터링 설정 예시: Wildberries에서 시간당 100,000개의 상품을 파싱하려면(초당 약 28개의 요청) 최소 30-40개의 프록시가 필요합니다. 프록시당 요청 제한이 60개인 경우, 알림을 설정하세요: 성공률이 85% 미만으로 떨어지거나 평균 응답 시간이 3초를 초과하면 시스템은 자동으로 10-15개의 예비 프록시를 풀에서 추가해야 합니다.

메트릭을 시각화하려면 Grafana와 Prometheus 또는 ELK 스택을 사용하세요. RPS 시간에 따른 그래프, 응답 시간 분포, 가장 빠른/느린 프록시 상위 10개, 오류 유형별 오류 맵 등의 대시보드를 생성하세요. 이를 통해 문제를 신속하게 식별하고 시스템을 최적화할 수 있습니다.

Python 및 Node.js에서의 실용적 구현

인기 있는 라이브러리를 사용하여 Python에서 부하 분산 시스템을 완전히 구현하는 방법을 살펴보겠습니다. 이 예제는 설명된 모든 구성 요소를 통합합니다: 부하 분산기, 풀 관리자, 상태 점검, 요청 속도 제한 및 모니터링.

import requests
import time
import random
from datetime import datetime, timedelta
from threading import Lock, Thread
from collections import defaultdict

class ProxyLoadBalancer:
    def __init__(self, proxies, algorithm="round_robin", rate_limit=10):
        """
        proxies: [{"url": "...", "weight": 1}, ...] 형식의 프록시 목록
        algorithm: round_robin, least_connections, weighted, random
        rate_limit: 프록시당 초당 최대 요청 수
        """
        self.proxies = proxies
        self.algorithm = algorithm
        self.rate_limit = rate_limit
        
        # 부하 분산기의 상태
        self.current_index = 0
        self.connections = defaultdict(int)
        self.rate_limiters = {}
        self.metrics = defaultdict(lambda: {
            "total": 0, "success": 0, "failed": 0,
            "response_times": [], "last_check": None
        })
        
        # 속도 제한기 초기화
        for proxy in proxies:
            proxy_url = proxy["url"]
            self.rate_limiters[proxy_url] = TokenBucketRateLimiter(
                rate=rate_limit,
                capacity=rate_limit * 5
            )
        
        self.lock = Lock()
        
        # 백그라운드 상태 점검 시작
        self.health_check_thread = Thread(target=self._health_check_loop, daemon=True)
        self.health_check_thread.start()
    
    def get_next_proxy(self):
        """알고리즘에 따라 다음 프록시를 선택합니다."""
        with self.lock:
            if self.algorithm == "round_robin":
                return self._round_robin()
            elif self.algorithm == "least_connections":
                return self._least_connections()
            elif self.algorithm == "weighted":
                return self._weighted_random()
            elif self.algorithm == "random":
                return random.choice(self.proxies)["url"]
    
    def _round_robin(self):
        proxy = self.proxies[self.current_index]["url"]
        self.current_index = (self.current_index + 1) % len(self.proxies)
        return proxy
    
    def _least_connections(self):
        min_conn = min(self.connections.values()) if self.connections else 0
        candidates = [p["url"] for p in self.proxies if self.connections[p["url"]] == min_conn]
        return random.choice(candidates)
    
    def _weighted_random(self):
        weights = [p.get("weight", 1) for p in self.proxies]
        return random.choices(self.proxies, weights=weights)[0]["url"]
    
    def make_request(self, url, method="GET", **kwargs):
        """부하 분산기를 통해 요청을 수행합니다."""
        proxy_url = self.get_next_proxy()
        
        # 속도 제한의 가용성을 기다립니다.
        self.rate_limiters[proxy_url].wait_and_acquire()
        
        # 연결 수 카운터 증가
        with self.lock:
            self.connections[proxy_url] += 1
        
        try:
            start_time = time.time()
            response = requests.request(
                method,
                url,
                proxies={"http": proxy_url, "https": proxy_url},
                timeout=kwargs.get("timeout", 30),
                **kwargs
            )
            response_time = time.time() - start_time
            
            # 메트릭 기록
            self._record_metrics(proxy_url, response_time, True, response.status_code)
            
            return response
        
        except Exception as e:
            self._record_metrics(proxy_url, 0, False, 0)
            raise
        
        finally:
            # 연결 수 카운터 감소
            with self.lock:
                self.connections[proxy_url] -= 1
    
    def _record_metrics(self, proxy_url, response_time, success, status_code):
        """요청 메트릭을 기록합니다."""
        with self.lock:
            m = self.metrics[proxy_url]
            m["total"] += 1
            if success:
                m["success"] += 1
                m["response_times"].append(response_time)
                # 최근 1000개의 값만 저장
                if len(m["response_times"]) > 1000:
                    m["response_times"].pop(0)
            else:
                m["failed"] += 1
    
    def _health_check_loop(self):
        """프록시의 작동 여부를 점검하는 백그라운드 작업입니다."""
        while True:
            for proxy in self.proxies:
                proxy_url = proxy["url"]
                try:
                    response = requests.get(
                        "http://httpbin.org/ip",
                        proxies={"http": proxy_url},
                        timeout=10
                    )
                    with self.lock:
                        self.metrics[proxy_url]["last_check"] = datetime.now()
                        proxy["status"] = "healthy" if response.status_code == 200 else "unhealthy"
                except:
                    with self.lock:
                        proxy["status"] = "unhealthy"
            
            time.sleep(60)  # 매 분 점검
    
    def get_stats(self):
        """모든 프록시에 대한 통계를 반환합니다."""
        stats = []
        with self.lock:
            for proxy in self.proxies:
                proxy_url = proxy["url"]
                m = self.metrics[proxy_url]
                
                avg_response_time = (
                    sum(m["response_times"]) / len(m["response_times"])
                    if m["response_times"] else 0
                )
                
                success_rate = (
                    (m["success"] / m["total"] * 100)
                    if m["total"] > 0 else 0
                )
                
                stats.append({
                    "proxy": proxy_url,
                    "status": proxy.get("status", "unknown"),
                    "total_requests": m["total"],
                    "success_rate": round(success_rate, 2),
                    "avg_response_time": round(avg_response_time, 3),
                    "active_connections": self.connections[proxy_url]
                })
        
        return stats

이 부하 분산기를 사용하여 마켓플레이스를 파싱하는 방법은 다음과 같습니다:

# 부하 분산기 설정
proxies = [
    {"url": "http://proxy1.example.com:8080", "weight": 5},
    {"url": "http://proxy2.example.com:8080", "weight": 5},
    {"url": "http://proxy3.example.com:8080", "weight": 3},
    {"url": "http://proxy4.example.com:8080", "weight": 2}
]

balancer = ProxyLoadBalancer(
    proxies=proxies,
    algorithm="weighted",
    rate_limit=60  # 프록시당 분당 60 요청
)

# 상품 목록 파싱
product_urls = [f"https://www.wildberries.ru/catalog/{i}/detail.aspx" for i in range(1000)]

results = []
for url in product_urls:
    try:
        response = balancer.make_request(url)
        # 응답 처리
        results.append({"url": url, "status": "success", "data": response.text})
    except Exception as e:
        results.append({"url": url, "status": "error", "error": str(e)})
    
    # 100 요청마다 통계 출력
    if len(results) % 100 == 0:
        stats = balancer.get_stats()
        for stat in stats:
            print(f"{stat['proxy']}: {stat['success_rate']}% 성공, "
                  f"{stat['avg_response_time']}s 평균 응답")

# 최종 통계
print("\n=== 최종 통계 ===")
for stat in balancer.get_stats():
    print(f"{stat['proxy']}:")
    print(f"  총 요청 수: {stat['total_requests']}")
    print(f"  성공률: {stat['success_rate']}%")
    print(f"  평균 응답 시간: {stat['avg_response_time']}s")
    print(f"  상태: {stat['status']}")

Node.js에서는 axios 라이브러리와 node-rate-limiter를 사용하여 유사한 아키텍처를 구현할 수 있습니다:

const axios = require('axios');
const { RateLimiter } = require('limiter');

class ProxyLoadBalancer {
  constructor(proxies, algorithm = 'round_robin', rateLimit = 10) {
    this.proxies = proxies;
    this.algorithm = algorithm;
    this.currentIndex = 0;
    this.connections = new Map();
    this.limiters = new Map();
    this.metrics = new Map();
    
    // 속도 제한기 초기화
    proxies.forEach(proxy => {
      this.limiters.set(proxy.url, new RateLimiter({ 
        tokensPerInterval: rateLimit, 
        interval: 'second' 
      }));
      this.connections.set(proxy.url, 0);
      this.metrics.set(proxy.url, {
        total: 0,
        success: 0,
        failed: 0,
        responseTimes: []
      });
    });
  }
  
  getNextProxy() {
    if (this.algorithm === 'round_robin') {
      const proxy = this.proxies[this.currentIndex].url;
      this.currentIndex = (this.currentIndex + 1) % this.proxies.length;
      return proxy;
    } else if (this.algorithm === 'least_connections') {
      let minConn = Infinity;
      let selectedProxy = null;
      
      this.connections.forEach((count, proxy) => {
        if (count < minConn) {
          minConn = count;
          selectedProxy = proxy;
        }
      });
      
      return selectedProxy;
    }
  }
  
  async makeRequest(url, options = {}) {
    const proxyUrl = this.getNextProxy();
    const limiter = this.limiters.get(proxyUrl);
    
    // 속도 제한의 가용성을 기다립니다.
    await limiter.removeTokens(1);
    
    // 연결 수 카운터 증가
    this.connections.set(proxyUrl, this.connections.get(proxyUrl) + 1);
    
    try {
      const startTime = Date.now();
      const response = await axios({
        url,
        proxy: this.parseProxyUrl(proxyUrl),
        timeout: options.timeout || 30000,
        ...options
      });
      
      const responseTime = (Date.now() - startTime) / 1000;
      this.recordMetrics(proxyUrl, responseTime, true);
      
      return response;
    } catch (error) {
      this.recordMetrics(proxyUrl, 0, false);
      throw error;
    } finally {
      this.connections.set(proxyUrl, this.connections.get(proxyUrl) - 1);
    }
  }
  
  parseProxyUrl(proxyUrl) {
    const url = new URL(proxyUrl);
    return {
      host: url.hostname,
      port: parseInt(url.port)
    };
  }
  
  recordMetrics(proxyUrl, responseTime, success) {
    const m = this.metrics.get(proxyUrl);
    m.total++;
    
    if (success) {
      m.success++;
      m.responseTimes.push(responseTime);
      if (m.responseTimes.length > 1000) {
        m.responseTimes.shift();
      }
    } else {
      m.failed++;
    }
  }
  
  getStats() {
    const stats = [];
    
    this.proxies.forEach(proxy => {
      const m = this.metrics.get(proxy.url);
      const avgResponseTime = m.responseTimes.length > 0
        ? m.responseTimes.reduce((a, b) => a + b, 0) / m.responseTimes.length
        : 0;
      
      const successRate = m.total > 0 ? (m.success / m.total * 100) : 0;
      
      stats.push({
        proxy: proxy.url,
        totalRequests: m.total,
        successRate: successRate.toFixed(2),
        avgResponseTime: avgResponseTime.toFixed(3),
        activeConnections: this.connections.get(proxy.url)
      });
    });
    
    return stats;
  }
}

// 사용 예
const proxies = [
  { url: 'http://proxy1.example.com:8080', weight: 5 },
  { url: 'http://proxy2.example.com:8080', weight: 5 }
];

const balancer = new ProxyLoadBalancer(proxies, 'round_robin', 60);

async function parseProducts() {
  const urls = Array.from({ length: 1000 }, (_, i) => 
    `https://www.wildberries.ru/catalog/${i}/detail.aspx`
  );
  
  for (const url of urls) {
    try {
      const response = await balancer.makeRequest(url);
      console.log(`성공: ${url}`);
    } catch (error) {
      console.error(`오류: ${url} - ${error.message}`);
    }
  }
  
  console.log('\n=== 통계 ===');
  console.log(balancer.getStats());
}

parseProducts();

특정 작업을 위한 최적화: 파싱, API, 자동화

다양한 작업은 서로 다른 부하 분산 전략을 요구합니다. 마켓플레이스(Wildberries, Ozon, Avito)를 파싱할 때는 요청 수에 따른 회전 및 지리적 분산 전략이 최적입니다. 다양한 러시아 도시의 주거용 프록시를 사용하고, 50-100 요청마다 프록시를 변경하며, 요청 간에 무작위 지연(1-3초)을 추가하여 사람의 행동을 모방합니다.

소셜 미디어 API(Instagram, Facebook, VK)와 작업할 때는 하나의 세션 내에서 IP 주소의 안정성이 중요합니다. IP 해시 알고리즘이나 스티키 세션(sticky sessions)을 사용하여 하나의 계정에 대한 모든 요청이 동일한 프록시를 통해 이루어지도록 합니다. 이는 의심스러운 지리적 변경을 방지하여 계정 차단을 예방합니다. 모바일 프록시를 사용하는 것이 좋습니다. 소셜 미디어는 모바일 IP를 주거용 또는 데이터 센터보다 더 신뢰합니다.

# Instagram을 위한 스티키 세션 예시
class StickySessionBalancer:
    def __init__(self, proxies):
        self.proxies = proxies
        self.session_map = {}  # {account_id: proxy_url}
        self.proxy_usage = defaultdict(int)
    
    def get_proxy_for_account(self, account_id):
        """계정에 대한 고정 프록시를 반환합니다."""
        if account_id in self.session_map:
            return self.session_map[account_id]
        
        # 가장 적게 사용된 프록시 선택
        proxy = min(self.proxies, key=lambda p: self.proxy_usage[p])
        self.session_map[account_id] = proxy
        self.proxy_usage[proxy] += 1
        
        return proxy
    
    def release_account(self, account_id):
        """계정 작업이 완료되면 프록시를 해제합니다."""
        if account_id in self.session_map:
            proxy = self.session_map[account_id]
            self.proxy_usage[proxy] -= 1
            del self.session_map[account_id]

이 예제를 통해 다양한 작업에 대한 부하 분산 전략을 이해하고 구현할 수 있습니다. 각 플랫폼의 요구 사항에 맞게 조정하여 최적의 성능을 달성하세요.

```