Skip to content
isdnetworks
Go back

순서를 바꾸니 기다리는 시간이 달라졌다

상세 화면이 2초 걸렸다. 부르는 것을 줄일 수 없었는데 순서를 바꾸니 빨라졌다.

Table of contents

Open Table of contents

증상 — 순서대로 부르던 네 호출

화면 하나를 그리는 데 외부를 넷 부르고 있었다.

$product = $this->productApi->get($no);        // 320ms
$stock   = $this->stockApi->get($no);          // 180ms
$review  = $this->reviewApi->summary($no);     // 640ms
$related = $this->recommendApi->related($no);  // 820ms

합이 1,960밀리초이고 하나를 기다렸다가 다음을 부르는 구조였다.

curl_getinfo 로 각 호출에 걸린 시간을 재 보니 그 합이 전체 시간과 거의 같았다. 화면 쪽 코드가 느린 것이 아니라 기다림이 그대로 쌓인 것이었다.

넷 사이에 의존이 있는지부터 봤다. relatedproduct 의 분류를 쓰고 나머지 셋은 서로 관계가 없었다.

독립인 것을 같이 불렀다

관계가 없는 셋을 함께 띄웠다.

$promises = [
    'product' => $this->productApi->getAsync($no),
    'stock'   => $this->stockApi->getAsync($no),
    'review'  => $this->reviewApi->summaryAsync($no),
];
$results = Promise\settle($promises)->wait();

$related = $this->recommendApi->related($no, $results['product']['value']['category']);

셋을 같이 부르면 가장 느린 것만큼만 걸린다.

전  320 + 180 + 640 + 820 = 1,960ms
후  max(320, 180, 640) + 820 = 1,460ms

500밀리초가 줄었는데 부르는 개수는 그대로다.

stockreview 를 기다리던 시간이 product 를 기다리는 동안에 겹쳐 들어간 것이다. 줄인 것은 일의 양이 아니라 기다림이 겹치지 않던 부분이었다.

남은 820밀리초가 여전히 컸다. recommendApi 호출이 뒤에 혼자 붙어 있어서 그 시간은 겹칠 자리가 없었다.

의존이 진짜인지 다시 봤다

related 가 분류를 실제로 무엇에 쓰는지 코드를 열어 봤다.

public function related(int $no, string $category): array {
    return $this->call('/recommend/related', ['no' => $no, 'cate' => $category]);
}

cate 를 그대로 넘기기만 할 뿐 이쪽에서 쓰는 데는 없었다.

상대 쪽에 물어보니 cate 는 없어도 되는 값이었다. 있으면 추천 정확도가 조금 오르는 정도라고 했다.

$promises = [
    'product' => ..., 'stock' => ..., 'review' => ...,
    'related' => $this->recommendApi->relatedAsync($no),
];

넷을 다 같이 부르니 820밀리초로 떨어졌다.

의존이 있다고 본 것이 실제로는 없었다. 코드만 읽었으면 cate 를 넘기는 줄과 $results['product'] 를 꺼내는 줄을 보고 필수라고 판단했을 것이다.

변경 내용 — 화면 위쪽과 아래쪽

넷이 화면에서 나오는 자리도 서로 달랐다.

위   상품 정보, 재고
중   리뷰 요약
아래 관련 상품

관련 상품은 스크롤을 내려야 보이므로 처음 응답에 없어도 된다.

// 첫 응답에는 위쪽 것만
$data = ['product' => ..., 'stock' => ...];

// 아래쪽은 화면이 뜬 뒤 따로 가져간다
GET /product/{no}/related

첫 화면이 320밀리초에 뜨고 나머지는 뒤에 채워진다.

한 번에 전부 주려던 것을 GET /product/{no}/related 를 따로 두어 두 번으로 나눈 것뿐이다. 사람이 기다리는 시간은 화면이 뜰 때까지이지 자료가 다 올 때까지가 아니었다.

대응 — 실패와 시간 제한

넷 중 하나가 실패했을 때의 처리도 갈랐다.

foreach ($results as $k => $r) {
    if ($r['state'] !== 'fulfilled') {
        if ($k === 'product') { throw new NotFound(); }   // 이건 없으면 화면이 안 된다
        $data[$k] = null;                                  // 나머지는 비워 둔다
    }
}

product 가 없으면 화면이 성립하지 않고 나머지는 비어도 화면이 뜬다.

전에는 하나만 실패해도 전체가 오류였다. reviewApi 가 잠깐 안 될 때 상세 화면이 통째로 안 열렸다.

시간 제한도 같은 기준으로 갈랐다.

$this->productApi->timeout(2.0);
$this->reviewApi->timeout(0.5);
$this->recommendApi->timeout(0.5);

productApi 는 2초까지 기다리고 나머지는 0.5초에서 끊는다.

전체를 한 값으로 두면 길게 줄 때는 없어도 되는 것 때문에 화면이 늦어지고 짧게 줄 때는 필수인 것이 먼저 끊긴다. 둘을 가르고 나서야 양쪽을 다 만족시킬 수 있었다.

검증 — 중앙값과 느린 구간

바꾼 뒤에 실제 사용자 응답 시간을 봤다.

전   중앙값 1,940ms   상위 5% 4,200ms
후   중앙값   340ms   상위 5% 1,100ms

access log 의 응답 시간으로 중앙값과 상위 구간을 나란히 놓고 봤다.

평균은 아주 느린 몇 건에 끌려가서 좋아졌는지 나빠졌는지가 흐려진다. 중앙값은 보통의 요청이 어떤지를 보여주고 상위 5퍼센트는 가장 나쁜 경우를 보여준다.

상위 5퍼센트가 4.2초에서 1.1초가 된 것이 컸다. 답답하다고 말이 나오는 쪽은 340ms 가 아니라 이쪽이다.

정리


Share this post on:

Previous Post
돌리기 전에 읽는 이행 스크립트
Next Post
스무 곳에 있던 같은 등록 기능