상품 등록 배치가 100건에 4분 넘게 걸렸다. MySQL 쪽은 빨랐고 느린 곳은 이미지 확인이었다.
Table of contents
Open Table of contents
원인 — 건마다 이미지를 받고 있었다
상품마다 대표 이미지 하나와 추가 이미지 서너 장이 있다. 크기를 확인하려고 각각을 받아 왔다.
foreach ($products as $p) {
$images = array_merge([$p['main_img']], $p['sub_imgs']);
foreach ($images as $url) {
$bin = file_get_contents($url);
$size = getimagesizefromstring($bin);
if ($size[0] != 600 || $size[1] != 600) {
$needResize[] = $url;
}
}
}
100건에 이미지가 400~500장이다. 한 장에 0.4초면 그것만 3분이다.
바깥 반복문 100번이 안쪽 반복문 4~5번을 곱해 수백 번이 된다. 배치가 느린 것이 아니라 요청 수가 많은 것이었고 줄일 곳은 요청 수와 한 번의 비용 둘이었다.
앞부분만 받으면 됐다
크기를 알려면 이미지 전체를 받아야 하나 봤더니 그렇지 않았다. 이미지 파일의 크기 정보는 앞부분에 있어서 앞 몇 킬로바이트만 받아도 알 수 있다.
$ctx = stream_context_create(['http' => ['timeout' => 3]]);
$fp = fopen($url, 'rb', false, $ctx);
$head = fread($fp, 32768);
fclose($fp);
$size = getimagesizefromstring($head);
fread 로 32KB 만 읽고 getimagesizefromstring 에 넘긴다. 받는 양이 줄어드니 시간도 줄고 상대에게도 부담이 덜했다.
다만 형식에 따라 앞부분만으로 안 되는 것이 있어서 실패하면 전체를 받게 했다. 무엇을 확인하려는지 분명히 하니 받을 것도 줄었다.
같은 것을 여러 번 받고 있었다
로그를 찍어 보니 같은 주소를 두 번 이상 받는 경우가 있었다. 상품이 달라도 이미지가 같은 것들이다.
$checked = [];
foreach ($images as $url) {
if (isset($checked[$url])) {
$size = $checked[$url];
} else {
$size = fetchSize($url);
$checked[$url] = $size;
}
}
$checked 에 기억해 두니 100건 중 30건쯤이 중복이었다.
배치가 매일 도는데 이미 확인한 이미지를 다음 날 또 확인하는 것도 있었다. 실행 안에서만 기억하는 것으로는 부족해서 크기를 테이블에 넣었다.
CREATE TABLE image_size (
url_hash CHAR(32) NOT NULL,
url VARCHAR(500) NOT NULL,
width INT NOT NULL,
height INT NOT NULL,
chk_date DATETIME NOT NULL,
PRIMARY KEY (url_hash)
);
주소를 그대로 키로 쓰면 길어서 md5 해시를 썼다.
$hash = md5($url);
$row = $this->db->get_where('image_size', ['url_hash' => $hash])->row();
if ($row) {
$size = [$row->width, $row->height];
} else {
$size = fetchSize($url);
$this->db->insert('image_size', [...]);
}
이미지가 바뀌었을 수 있으니 chk_date 를 두고 오래된 것은 다시 받게 했다.
남은 호출과 측정
그래도 처음 보는 이미지는 받아야 한다. 이건 줄일 수 없어서 순차로 부르지 않고 여러 개를 동시에 부르게 했다.
$mh = curl_multi_init();
$handles = [];
foreach (array_slice($urls, 0, 10) as $u) {
$ch = curl_init($u);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_RANGE, '0-32767');
curl_multi_add_handle($mh, $ch);
$handles[$u] = $ch;
}
curl_multi_init 으로 10개씩 동시에 부르고 CURLOPT_RANGE 로 앞부분만 달라고 했다. 몇 개까지 동시에 부를지는 상대 부담을 보고 늘려 가며 정했다.
고칠 때마다 시간을 쟀다.
처음 4분 12초
앞부분만 받기 2분 05초
중복 제거 1분 28초
결과 저장(둘째 날부터) 0분 22초
동시 호출 10개 0분 14초
어느 조치가 얼마나 줄였는지가 보인다. 한꺼번에 다 고치면 무엇이 효과가 있었는지 모르고 다음에 비슷한 상황에서 어디부터 손댈지도 모른다.
정리
- 반복문 안에서 외부를 부르면 바깥 건수와 안쪽 건수가 곱해진다
- 느린 것이 배치가 아니라 요청 수일 수 있다
- 줄일 곳은 요청 수와 한 번의 비용 둘이다
- 필요한 정보가 앞부분에만 있으면 전체를 안 받아도 된다
- 같은 대상을 여러 번 부르는지 확인한다. 한 번 확인한 것은 기억해 둔다
- 반복 실행되는 작업이면 결과를 저장하고
chk_date를 같이 둔다 - 남은 호출은
curl_multi_init으로 묶되 상대 부담을 보고 개수를 정한다 - 조치마다 시간을 재면 무엇이 효과가 있었는지 남는다