Skip to content
isdnetworks
Go back

반복문 안의 외부 요청

상품 등록 배치가 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초

어느 조치가 얼마나 줄였는지가 보인다. 한꺼번에 다 고치면 무엇이 효과가 있었는지 모르고 다음에 비슷한 상황에서 어디부터 손댈지도 모른다.

정리


Share this post on:

Previous Post
코드와 데이터가 다른 길로 갔다
Next Post
컬럼이 무슨 뜻인지 물어보고 나서 조건을 짰다