1688 제품 이미지 일괄 다운로드: 대역폭 최대 활용 교훈

발행: (2026년 5월 27일 PM 12:32 GMT+9)
7 분 소요
원문: Dev.to

문제 상황: 이미지 다운로드에 대한 무자비한 접근

우리는 매일 약 3,000개의 1688 제품을 동기화해야 했으며, 메인 이미지와 상세 이미지를 포함해 제품당 평균 5장의 이미지를 다루었습니다. 초기 구현은 직관적이었지만 조악했습니다:

// Old brute‑force download script
function downloadAllImages($productIds) {
    foreach ($productIds as $id) {
        $images = get1688ProductImages($id); // Call 1688 API to get image URL list
        foreach ($images as $url) {
            $content = file_get_contents($url); // Synchronous blocking download
            file_put_contents("/images/$id/".basename($url), $content);
        }
    }
}

오래된 스크립트의 문제점

  1. 동시성 제어 없음 – 외부 루프가 대량의 HTTP 요청을 동시에 발생시킵니다.
  2. 재시도 메커니즘 부재 – 다운로드에 실패하면 단순히 건너뛰어 제품 이미지가 누락됩니다.
  3. 대역폭 제한 없음 – 약 200개의 동시 요청이 각각 평균 2 MB를 전송해 즉시 ~400 MB의 대역폭을 소모합니다.

영향: 주문 처리, 물류 조회 등 모든 다른 비즈니스 작업이 18 분 동안 중단되었습니다. 우리는 프로세스를 수동으로 강제 종료하고 2 시간을 들여 실패한 이미지를 다시 다운로드해야 했습니다.


해결책: 속도 제한 및 큐를 갖춘 다운로더

우리는 Guzzle의 비동기 기능을 활용해 다운로드 프로그램을 재설계하고, 대역폭 제어와 재시도 메커니즘을 추가했습니다.

1단계 – Guzzle 요청 풀을 이용한 동시성 제한

2단계 – 대역폭 제어를 위한 토큰 버킷 알고리즘

// New rate‑limited downloader
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;

class ThrottledImageDownloader {
    private $client;
    private $concurrency = 10;                 // Maximum concurrency
    private $bandwidthLimit = 50 * 1024 * 1024; // 50 MB/s bandwidth limit
    private $tokens;
    private $lastRefillTime;

    public function __construct() {
        $this->client = new Client(['timeout' => 30]);
        $this->tokens = $this->bandwidthLimit;
        $this->lastRefillTime = microtime(true);
    }

    // Token bucket algorithm for bandwidth control
    private function consumeBandwidth($bytes) {
        $now = microtime(true);
        $elapsed = $now - $this->lastRefillTime;
        $this->tokens = min(
            $this->bandwidthLimit,
            $this->tokens + $elapsed * $this->bandwidthLimit
        );
        $this->lastRefillTime = $now;

        if ($this->tokens < $bytes) {
            $sleepTime = ($bytes - $this->tokens) / $this->bandwidthLimit;
            usleep($sleepTime * 1e6);
            $this->tokens = 0;
        } else {
            $this->tokens -= $bytes;
        }
    }

    public function downloadBatch(array $imageUrls) {
        $requests = function ($urls) {
            foreach ($urls as $url) {
                yield new Request('GET', $url);
            }
        };

        $pool = new Pool($this->client, $requests($imageUrls), [
            'concurrency' => $this->concurrency,
            'fulfilled'   => function ($response, $index) use ($imageUrls) {
                $content = $response->getBody()->getContents();
                $this->consumeBandwidth(strlen($content));

                // Save image logic
                $filename = basename($imageUrls[$index]);
                file_put_contents("/images/$filename", $content);
            },
            'rejected'    => function ($reason, $index) use ($imageUrls) {
                // Retry on failure, up to 3 times
                $this->retryDownload($imageUrls[$index], 3);
            },
        ]);

        $pool->promise()->wait();
    }

    private function retryDownload($url, $maxRetries) {
        for ($i = 0; $i < $maxRetries; $i++) {
            try {
                $response = $this->client->get($url);
                $content  = $response->getBody()->getContents();
                $filename = basename($url);
                file_put_contents("/images/$filename", $content);
                return;
            } catch (\Exception $e) {
                if ($i === $maxRetries - 1) {
                    // Log failure
                    error_log("Failed to download $url after $maxRetries attempts");
                }
                sleep(pow(2, $i)); // Exponential backoff
            }
        }
    }
}

핵심 개선 사항

개선 항목설명
동시성 제어concurrency10으로 설정해 즉각적인 대역폭 포화 현상을 방지합니다.
토큰 버킷 속도 제한consumeBandwidth()가 다운로드 속도가 50 MB/s를 초과하지 않도록 보장합니다.
지수 백오프 재시도실패 시 2ⁱ 초씩 대기하며 최대 3번까지 재시도합니다.

교훈: 대역폭 재난에서 안정적인 동기화까지

지표오래된 스크립트새로운 스크립트
15,000 이미지 다운로드 시간12 분18 분
최대 대역폭 사용량~500 Mbps (포화)45‑50 Mbps (안정)
다른 서비스에 미친 영향18 분 완전 중단영향 없음
실패한 다운로드다수, 수동 재실행 필요자동 재시도로 0

추가 최적화: 증분 다운로드 및 캐싱

우리는 기존 이미지를 다시 다운로드하지 않도록 간단한 파일 해시 검사를 추가했습니다:

// Incremental check – only download new images
function needsDownload($url, $localPath) {
    if (!file_exists($localPath)) {
        return true;
    }
    // Check if remote file has been updated via HEAD request
    $headers = get_headers($url, 1);
    $remoteEtag = $headers['ETag'] ?? null;
    $localEtag = md5_file($localPath);

    return $remoteEtag && $remoteEtag !== $localEtag;
}

변경된 이미지만 가져오게 함으로써 대역폭 소비를 더욱 줄이고 전체 동기화 효율을 높였습니다.

$moteSize = $headers['Content-Length'] ?? 0;
$localSize = filesize($localPath);
return $remoteSize != $localSize;
}

이 최적화를 적용한 뒤 일일 증분 동기화 시간은 18분에서 3~5분으로 감소했으며, 이는 전체 제품 이미지 중 10 % 정도만 매일 업데이트되기 때문입니다.

요약
서드파티 리소스를 대량으로 다운로드할 때 “빠르면 좋다”는 가정을 하면 안 됩니다. 무차별적인 동시 다운로드는 효율적으로 보일 수 있지만 시스템 안정성을 크게 해칩니다. 속도 제한, 재시도 메커니즘, 증분 검사가 신뢰할 수 있는 다운로드 시스템을 구성하는 핵심 요소입니다. 아직도 보호 장치 없이 file_get_contents만 사용하고 있다면 업그레이드할 때입니다.

여러분의 시스템에서도 외부 리소스를 대량으로 처리하면서 비슷한 문제를 겪은 적이 있나요? 해결 방안을 공유해 주세요.

저자 소개

taocarts와 함께 크로스보더 구매 솔루션을 구축하고 있습니다 — 1688/타오바오 구매, 주문 관리, 국제 배송을 지원하는 대구오 시스템.

0 조회
Back to Blog

관련 글

더 보기 »