1688 제품 이미지 일괄 다운로드: 대역폭 최대 활용 교훈
문제 상황: 이미지 다운로드에 대한 무자비한 접근
우리는 매일 약 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);
}
}
}
오래된 스크립트의 문제점
- 동시성 제어 없음 – 외부 루프가 대량의 HTTP 요청을 동시에 발생시킵니다.
- 재시도 메커니즘 부재 – 다운로드에 실패하면 단순히 건너뛰어 제품 이미지가 누락됩니다.
- 대역폭 제한 없음 – 약 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
}
}
}
}
핵심 개선 사항
| 개선 항목 | 설명 |
|---|---|
| 동시성 제어 | concurrency를 10으로 설정해 즉각적인 대역폭 포화 현상을 방지합니다. |
| 토큰 버킷 속도 제한 | 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/타오바오 구매, 주문 관리, 국제 배송을 지원하는 대구오 시스템.