Node.js 앱 런타임 측정 문제

Mar 07 2023
Node.js 애플리케이션의 런타임 측정은 성능 최적화의 중요한 측면입니다.
Burst Copied의 Chris Gonzalez 사진

Node.js 애플리케이션의 런타임 측정은 성능 최적화의 중요한 측면입니다. 애플리케이션의 여러 부분을 실행하는 데 걸리는 시간을 측정하여 병목 현상을 식별하고 코드를 최적화하여 성능을 향상시킬 수 있습니다.

함수 실행 시간을 어떻게 측정할 수 있습니까?

node.js에서 시간을 측정하는 방법에는 여러 가지가 있습니다. 이를 수행하는 가장 높은 해상도의 방법은 process.hrtime() 또는 Perf_hooks 를 사용하는 것입니다 . 둘 다 시스템 시계에 의존하지 않기 때문에 Date 클래스를 사용하는 것보다 낫습니다. 그러나 여전히 그들 사이에는 몇 가지 차이점이 있습니다.

process.hrtime()은 나노초 수준의 해상도를 제공하고 perf_hooks는 마이크로초 수준의 해상도를 제공합니다. 이는 process.hrtime() 이 일반적으로 더 정확하지만 호출 비용이 더 많이 든다는 것을 의미합니다.

이제 다음 논리의 실행 시간을 측정하는 예를 들어 보겠습니다. 1000ms 후에 해결되는 비동기 논리(이것이 일부 HTTP 호출이라고 가정할 수 있음)가 있습니다. 이 약속을 이행하는 데 걸리는 시간을 측정하기 위해 perf_hooks를 사용할 것입니다.

const {performance} = require('perf_hooks');
const SOME_BIG_NUMBER = 4000000000;

async function asyncLogic(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const start = performance.now();
asyncLogic(1000).then(() => {
    const time = performance.now() - start;
    console.log(`Time taken: ${time}ms`);
});

let x = 0;
for (let i = 0; i < SOME_BIG_NUMBER; i++) {
    x += Math.sqrt(SOME_BIG_NUMBER);
}

Time taken: 3948ms

이벤트 루프 및 이벤트 큐

JavaScript 런타임 모델은 이벤트 루프를 기반으로 하며, 코드 실행을 담당하고 비동기 로직을 ​​사용할 수 있게 해주는 부분입니다.

JavaScript 코드를 실행하면 다음과 같은 일이 발생합니다.

  1. JavaScript는 프로그램에 나타나는 순서대로 모든 동기 코드를 실행합니다.
  2. 비동기 코드(예: asyncLogic 함수)가 있는 경우 JavaScript는 코드를 즉시 실행하는 대신 이벤트 대기열 에 추가합니다.
  3. 이벤트 루프는 실행 대기 중인 이벤트가 있는지 확인하기 위해 이벤트 큐를 지속적으로 확인합니다 .
  4. 이벤트를 실행할 준비가 되면 이벤트 루프는 이벤트 큐에서 실행할 수 있는 호출 스택 으로 이벤트를 이동합니다.
  5. 이벤트가 실행되면 이벤트 루프가 더 많은 이벤트에 대한 이벤트 대기열을 확인하기 위해 돌아가기 전에 추가 동기 코드가 실행됩니다.

그래서 우리 예제에서 시간 측정이 4000ms인 이유입니다.

  1. 이벤트 큐에 추가된 asyncLogic 함수를 실행합니다.
  2. 그런 다음 실행하는 데 약 4000ms가 걸리는 동기식 논리를 실행합니다.
  3. 이벤트 루프는 asyncLogic() 콜백을 이벤트 큐에서 호출 스택으로 이동합니다.
  4. 그런 다음 콜백 함수를 실행하고 종료 시간을 계산합니다.

이벤트 루프 지연

따라서 애플리케이션 성능을 모니터링할 때 이벤트 루프의 지연, 이 콜백이 실행될 때까지 이벤트 큐에 새 콜백을 추가하는 사이에 이벤트 루프에 걸리는 시간과 같이 주의해야 하는 중요한 메트릭이 있습니다.

다음 코드를 사용하여 이벤트 루프 지연을 측정할 수 있는 루프를 쉽게 만들 수 있습니다.

function measureEventLoopLag() {
    let lastLoopTime = performance.now();
    setTimeout(() => {
        const delay = performance.now() - lastLoopTime - 1000;
        lastLoopTime = performance.now();
        console.log(`Event Loop lag is: ${delay} ms`)
        measureEventLoopLag();
    }, 1000)
}

런타임 개선

코드에 4000ms가 걸리는 HTTP 호출이 있고 실행하는 데 약 4000ms가 걸리는 느린 루프도 있다고 가정해 보겠습니다. 이전 예제에서 본 것처럼 이 런타임을 측정하면 전체 코드를 실행하는 데 4000ms가 걸린다는 것을 알 수 있습니다.

코드를 리팩토링한 후 느린 루프 실행 시간을 50% 줄입니다. 코드를 다시 실행할 때 측정에 따른 전체 런타임은 실제로 변경되지 않았습니다. 비동기 흐름이 완료될 때까지 기다렸기 때문에 전체 흐름을 완료하는 데 여전히 약 4000ms를 기다렸습니다. 리팩토링이 실제로 도움이 되지 않은 것 같습니다.

실제로 CPU 측면에서 살펴보면 큰 차이를 만들었습니다. HTTP 호출은 CPU를 많이 사용하지 않으며 대부분 응답을 기다립니다. 따라서 대부분의 경우 CPU는 느린 루프를 실행하는 데 사용됩니다. 코드를 리팩토링하고 50%로 줄였을 때 전체 시간은 변경되지 않았지만 CPU에 상당한 영향을 미쳤습니다.

결론

우리의 예에서는 모니터링할 로직이 매우 작았습니다. 실행할 로직이 많은 복잡한 시스템이 있으면 상황이 정말 복잡해집니다. 이 경우 로직의 작은 부분을 측정하여 성능 저하를 더 잘 모니터링해야 합니다.

애플리케이션 실행 시간을 모니터링할 때 유의해야 할 두 가지 중요한 사항이 있습니다.

  • 비동기 함수 시간 측정은 동기 코드(이벤트 루프 지연)의 영향을 받을 수 있습니다.
  • 비동기 및 동기 로직이 모두 있는 측정 함수는 런타임 개선(또는 감소)을 숨길 수 있습니다.
  • ~30ms보다 높은 이벤트 루프 지연은 애플리케이션에 정말 좋지 않습니다.